From 91edb23715aa11ddeded4f5df9f4b7a0b1f7388c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 25 Mar 2024 09:14:24 +0000 Subject: [PATCH] Deployed 8028df0f to main with MkDocs 1.5.3 and mike 2.0.0 --- main/404.html | 4 +- main/advanced_usages/as-python-lib/index.html | 4 +- main/advanced_usages/caching/index.html | 4 +- main/advanced_usages/custom-tests/index.html | 4 +- main/api/catalog/index.html | 4 +- main/api/device/index.html | 4 +- main/api/inventory.models.input/index.html | 4 +- main/api/inventory/index.html | 4 +- main/api/models/index.html | 4 +- main/api/report_manager/index.html | 4 +- main/api/result_manager/index.html | 4 +- main/api/result_manager_models/index.html | 4 +- main/api/tests.aaa/index.html | 4 +- main/api/tests.bfd/index.html | 4 +- main/api/tests.configuration/index.html | 4 +- main/api/tests.connectivity/index.html | 4 +- main/api/tests.field_notices/index.html | 4 +- main/api/tests.greent/index.html | 4 +- main/api/tests.hardware/index.html | 4 +- main/api/tests.interfaces/index.html | 58 +++++++++--------- main/api/tests.lanz/index.html | 4 +- main/api/tests.logging/index.html | 4 +- main/api/tests.mlag/index.html | 4 +- main/api/tests.multicast/index.html | 4 +- main/api/tests.profiles/index.html | 4 +- main/api/tests.ptp/index.html | 4 +- main/api/tests.routing.bgp/index.html | 4 +- main/api/tests.routing.generic/index.html | 4 +- main/api/tests.routing.ospf/index.html | 4 +- main/api/tests.security/index.html | 4 +- main/api/tests.services/index.html | 4 +- main/api/tests.snmp/index.html | 4 +- main/api/tests.software/index.html | 4 +- main/api/tests.stp/index.html | 4 +- main/api/tests.system/index.html | 4 +- main/api/tests.vlan/index.html | 4 +- main/api/tests.vxlan/index.html | 4 +- main/api/tests/index.html | 4 +- main/api/types/index.html | 4 +- main/assets/stylesheets/main.10ba22f1.min.css | 1 - .../stylesheets/main.10ba22f1.min.css.map | 1 - main/assets/stylesheets/main.7e359304.min.css | 1 + .../stylesheets/main.7e359304.min.css.map | 1 + main/cli/check/index.html | 4 +- main/cli/debug/index.html | 4 +- main/cli/exec/index.html | 4 +- main/cli/get-inventory-information/index.html | 4 +- main/cli/inv-from-ansible/index.html | 4 +- main/cli/inv-from-cvp/index.html | 4 +- main/cli/nrfu/index.html | 4 +- main/cli/overview/index.html | 4 +- main/cli/tag-management/index.html | 4 +- main/contribution/index.html | 4 +- main/faq/index.html | 4 +- main/getting-started/index.html | 4 +- main/index.html | 4 +- main/requirements-and-installation/index.html | 4 +- main/search/search_index.json | 2 +- main/sitemap.xml.gz | Bin 127 -> 127 bytes main/usage-inventory-catalog/index.html | 4 +- 60 files changed, 137 insertions(+), 139 deletions(-) delete mode 100644 main/assets/stylesheets/main.10ba22f1.min.css delete mode 100644 main/assets/stylesheets/main.10ba22f1.min.css.map create mode 100644 main/assets/stylesheets/main.7e359304.min.css create mode 100644 main/assets/stylesheets/main.7e359304.min.css.map diff --git a/main/404.html b/main/404.html index eeeb833a5..ec7147547 100644 --- a/main/404.html +++ b/main/404.html @@ -16,7 +16,7 @@ - + @@ -24,7 +24,7 @@ - + diff --git a/main/advanced_usages/as-python-lib/index.html b/main/advanced_usages/as-python-lib/index.html index 26653f761..a2d41bb62 100644 --- a/main/advanced_usages/as-python-lib/index.html +++ b/main/advanced_usages/as-python-lib/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/advanced_usages/caching/index.html b/main/advanced_usages/caching/index.html index 2bcf39fa7..93fd6d6f2 100644 --- a/main/advanced_usages/caching/index.html +++ b/main/advanced_usages/caching/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/advanced_usages/custom-tests/index.html b/main/advanced_usages/custom-tests/index.html index a6a1a732c..5d0f00893 100644 --- a/main/advanced_usages/custom-tests/index.html +++ b/main/advanced_usages/custom-tests/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/catalog/index.html b/main/api/catalog/index.html index e9b64ff4d..42d8744ee 100644 --- a/main/api/catalog/index.html +++ b/main/api/catalog/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/device/index.html b/main/api/device/index.html index 0190ceee8..464ef5229 100644 --- a/main/api/device/index.html +++ b/main/api/device/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/inventory.models.input/index.html b/main/api/inventory.models.input/index.html index 71317858f..a0bc90c1c 100644 --- a/main/api/inventory.models.input/index.html +++ b/main/api/inventory.models.input/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/inventory/index.html b/main/api/inventory/index.html index ecf850e1a..52ba89263 100644 --- a/main/api/inventory/index.html +++ b/main/api/inventory/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/models/index.html b/main/api/models/index.html index 697ca8abf..8dbbd9fcb 100644 --- a/main/api/models/index.html +++ b/main/api/models/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/report_manager/index.html b/main/api/report_manager/index.html index 25e3539d7..cb58f8db7 100644 --- a/main/api/report_manager/index.html +++ b/main/api/report_manager/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/result_manager/index.html b/main/api/result_manager/index.html index 80d87800d..1c93e5b2f 100644 --- a/main/api/result_manager/index.html +++ b/main/api/result_manager/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/result_manager_models/index.html b/main/api/result_manager_models/index.html index 0f41420b1..1e38082c0 100644 --- a/main/api/result_manager_models/index.html +++ b/main/api/result_manager_models/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.aaa/index.html b/main/api/tests.aaa/index.html index c6d1f58cf..213eb8d54 100644 --- a/main/api/tests.aaa/index.html +++ b/main/api/tests.aaa/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.bfd/index.html b/main/api/tests.bfd/index.html index 5d39411d9..d14631bab 100644 --- a/main/api/tests.bfd/index.html +++ b/main/api/tests.bfd/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.configuration/index.html b/main/api/tests.configuration/index.html index e7455321b..67c902b32 100644 --- a/main/api/tests.configuration/index.html +++ b/main/api/tests.configuration/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.connectivity/index.html b/main/api/tests.connectivity/index.html index 9f24f05b1..e17cb78ff 100644 --- a/main/api/tests.connectivity/index.html +++ b/main/api/tests.connectivity/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.field_notices/index.html b/main/api/tests.field_notices/index.html index 9225ce3bc..ee32cafdc 100644 --- a/main/api/tests.field_notices/index.html +++ b/main/api/tests.field_notices/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.greent/index.html b/main/api/tests.greent/index.html index 3637a3a32..a34bf35fd 100644 --- a/main/api/tests.greent/index.html +++ b/main/api/tests.greent/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.hardware/index.html b/main/api/tests.hardware/index.html index 0337935f1..ca5e4149a 100644 --- a/main/api/tests.hardware/index.html +++ b/main/api/tests.hardware/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.interfaces/index.html b/main/api/tests.interfaces/index.html index b7348a83b..cc89c9b5e 100644 --- a/main/api/tests.interfaces/index.html +++ b/main/api/tests.interfaces/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -2795,7 +2795,8 @@

Inputs Source code in anta/tests/interfaces.py -
534
+	
533
+534
 535
 536
 537
@@ -2840,8 +2841,7 @@ 

Inputs576 577 578 -579 -580

class VerifyIPProxyARP(AntaTest):
+579
class VerifyIPProxyARP(AntaTest):
     """Verifies if Proxy-ARP is enabled for the provided list of interface(s).
 
     Expected Results
@@ -2944,7 +2944,8 @@ 

Source code in anta/tests/interfaces.py -
348
+	
347
+348
 349
 350
 351
@@ -2976,8 +2977,7 @@ 

377 378 379 -380 -381

class VerifyIllegalLACP(AntaTest):
+380
class VerifyIllegalLACP(AntaTest):
     """Verifies there are no illegal LACP packets in all port channels.
 
     Expected Results
@@ -3518,7 +3518,8 @@ 

InterfaceDetail
Source code in anta/tests/interfaces.py -
648
+	
647
+648
 649
 650
 651
@@ -3609,8 +3610,7 @@ 

InterfaceDetail736 737 738 -739 -740

class VerifyInterfaceIPv4(AntaTest):
+739
class VerifyInterfaceIPv4(AntaTest):
     """Verifies if an interface is configured with a correct primary and list of optional secondary IPv4 addresses.
 
     Expected Results
@@ -4326,7 +4326,8 @@ 

Inputs
Source code in anta/tests/interfaces.py -
743
+	
742
+743
 744
 745
 746
@@ -4362,8 +4363,7 @@ 

Inputs776 777 778 -779 -780

class VerifyIpVirtualRouterMac(AntaTest):
+779
class VerifyIpVirtualRouterMac(AntaTest):
     """Verifies the IP virtual router MAC address.
 
     Expected Results
@@ -4523,7 +4523,8 @@ 

Inputs Source code in anta/tests/interfaces.py -
583
+	
582
+583
 584
 585
 586
@@ -4584,8 +4585,7 @@ 

Inputs641 642 643 -644 -645

class VerifyL2MTU(AntaTest):
+644
class VerifyL2MTU(AntaTest):
     """Verifies the global layer 2 Maximum Transfer Unit (MTU) for all L2 interfaces.
 
     Test that L2 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces.
@@ -4769,7 +4769,8 @@ 

Inputs Source code in anta/tests/interfaces.py -
470
+	
469
+470
 471
 472
 473
@@ -4829,8 +4830,7 @@ 

Inputs527 528 529 -530 -531

class VerifyL3MTU(AntaTest):
+530
class VerifyL3MTU(AntaTest):
     """Verifies the global layer 3 Maximum Transfer Unit (MTU) for all L3 interfaces.
 
     Test that L3 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces.
@@ -4979,7 +4979,8 @@ 

Inputs Source code in anta/tests/interfaces.py -
384
+	
383
+384
 385
 386
 387
@@ -5025,8 +5026,7 @@ 

Inputs427 428 429 -430 -431

class VerifyLoopbackCount(AntaTest):
+430
class VerifyLoopbackCount(AntaTest):
     """Verifies that the device has the expected number of loopback interfaces and all are operational.
 
     Expected Results
@@ -5162,8 +5162,7 @@ 

341 342 343 -344 -345

class VerifyPortChannels(AntaTest):
+344
class VerifyPortChannels(AntaTest):
     """Verifies there are no inactive ports in all port channels.
 
     Expected Results
@@ -5184,7 +5183,6 @@ 

categories: ClassVar[list[str]] = ["interfaces"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show port-channel")] - @skip_on_platforms(["cEOSLab", "vEOS-lab"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyPortChannels.""" @@ -5253,7 +5251,8 @@

Source code in anta/tests/interfaces.py -
434
+	
433
+434
 435
 436
 437
@@ -5285,8 +5284,7 @@ 

463 464 465 -466 -467

class VerifySVI(AntaTest):
+466
class VerifySVI(AntaTest):
     """Verifies the status of all SVIs.
 
     Expected Results
@@ -5432,7 +5430,7 @@ 

categories: ClassVar[list[str]] = ["interfaces"] commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command="show storm-control")] - @skip_on_platforms(["cEOSLab", "vEOS-lab"]) + @skip_on_platforms(["cEOSLab", "vEOS-lab", "cEOSCloudLab"]) @AntaTest.anta_test def test(self) -> None: """Main test function for VerifyStormControlDrops.""" diff --git a/main/api/tests.lanz/index.html b/main/api/tests.lanz/index.html index 27a73a16d..8aceb0a87 100644 --- a/main/api/tests.lanz/index.html +++ b/main/api/tests.lanz/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.logging/index.html b/main/api/tests.logging/index.html index b262dd36c..e43840e8a 100644 --- a/main/api/tests.logging/index.html +++ b/main/api/tests.logging/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.mlag/index.html b/main/api/tests.mlag/index.html index 1a2012fd9..27b8ea370 100644 --- a/main/api/tests.mlag/index.html +++ b/main/api/tests.mlag/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.multicast/index.html b/main/api/tests.multicast/index.html index 53e5fb164..a6fdedc99 100644 --- a/main/api/tests.multicast/index.html +++ b/main/api/tests.multicast/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.profiles/index.html b/main/api/tests.profiles/index.html index e13839b01..9964315f4 100644 --- a/main/api/tests.profiles/index.html +++ b/main/api/tests.profiles/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.ptp/index.html b/main/api/tests.ptp/index.html index d49a7854d..bf3cc41ef 100644 --- a/main/api/tests.ptp/index.html +++ b/main/api/tests.ptp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.routing.bgp/index.html b/main/api/tests.routing.bgp/index.html index a0c17e986..be6173eb1 100644 --- a/main/api/tests.routing.bgp/index.html +++ b/main/api/tests.routing.bgp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.routing.generic/index.html b/main/api/tests.routing.generic/index.html index c7b706857..3a6754bf0 100644 --- a/main/api/tests.routing.generic/index.html +++ b/main/api/tests.routing.generic/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.routing.ospf/index.html b/main/api/tests.routing.ospf/index.html index 2f0436852..08e8cc7f2 100644 --- a/main/api/tests.routing.ospf/index.html +++ b/main/api/tests.routing.ospf/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.security/index.html b/main/api/tests.security/index.html index 08b252ce6..561e4a3ae 100644 --- a/main/api/tests.security/index.html +++ b/main/api/tests.security/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.services/index.html b/main/api/tests.services/index.html index f95c89cb8..68c775233 100644 --- a/main/api/tests.services/index.html +++ b/main/api/tests.services/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.snmp/index.html b/main/api/tests.snmp/index.html index e5974e588..8b5f07747 100644 --- a/main/api/tests.snmp/index.html +++ b/main/api/tests.snmp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.software/index.html b/main/api/tests.software/index.html index 199b0723c..2d98eab5d 100644 --- a/main/api/tests.software/index.html +++ b/main/api/tests.software/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.stp/index.html b/main/api/tests.stp/index.html index 3727cabc4..cca19f7fc 100644 --- a/main/api/tests.stp/index.html +++ b/main/api/tests.stp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.system/index.html b/main/api/tests.system/index.html index 0f243523d..61e25eb0e 100644 --- a/main/api/tests.system/index.html +++ b/main/api/tests.system/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.vlan/index.html b/main/api/tests.vlan/index.html index fa672ec76..a8442c79f 100644 --- a/main/api/tests.vlan/index.html +++ b/main/api/tests.vlan/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests.vxlan/index.html b/main/api/tests.vxlan/index.html index b385ab9ee..26c7fa84f 100644 --- a/main/api/tests.vxlan/index.html +++ b/main/api/tests.vxlan/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/tests/index.html b/main/api/tests/index.html index dccfe5904..3c1b733ac 100644 --- a/main/api/tests/index.html +++ b/main/api/tests/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/api/types/index.html b/main/api/types/index.html index 317ddb2d1..aefd4587e 100644 --- a/main/api/types/index.html +++ b/main/api/types/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/assets/stylesheets/main.10ba22f1.min.css b/main/assets/stylesheets/main.10ba22f1.min.css deleted file mode 100644 index 1659959e4..000000000 --- a/main/assets/stylesheets/main.10ba22f1.min.css +++ /dev/null @@ -1 +0,0 @@ -@charset "UTF-8";html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none;box-sizing:border-box}*,:after,:before{box-sizing:inherit}@media (prefers-reduced-motion){*,:after,:before{transition:none!important}}body{margin:0}a,button,input,label{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}hr{border:0;box-sizing:initial;display:block;height:.05rem;overflow:visible;padding:0}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:initial;border-spacing:0}td,th{font-weight:400;vertical-align:top}button{background:#0000;border:0;font-family:inherit;font-size:inherit;margin:0;padding:0}input{border:0;outline:none}:root{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-scheme=default]{color-scheme:light}[data-md-color-scheme=default] img[src$="#gh-dark-mode-only"],[data-md-color-scheme=default] img[src$="#only-dark"]{display:none}:root,[data-md-color-scheme=default]{--md-hue:225deg;--md-default-fg-color:#000000de;--md-default-fg-color--light:#0000008a;--md-default-fg-color--lighter:#00000052;--md-default-fg-color--lightest:#00000012;--md-default-bg-color:#fff;--md-default-bg-color--light:#ffffffb3;--md-default-bg-color--lighter:#ffffff4d;--md-default-bg-color--lightest:#ffffff1f;--md-code-fg-color:#36464e;--md-code-bg-color:#f5f5f5;--md-code-hl-color:#4287ff;--md-code-hl-color--light:#4287ff1a;--md-code-hl-number-color:#d52a2a;--md-code-hl-special-color:#db1457;--md-code-hl-function-color:#a846b9;--md-code-hl-constant-color:#6e59d9;--md-code-hl-keyword-color:#3f6ec6;--md-code-hl-string-color:#1c7d4d;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-del-color:#f5503d26;--md-typeset-ins-color:#0bd57026;--md-typeset-kbd-color:#fafafa;--md-typeset-kbd-accent-color:#fff;--md-typeset-kbd-border-color:#b8b8b8;--md-typeset-mark-color:#ffff0080;--md-typeset-table-color:#0000001f;--md-typeset-table-color--light:rgba(0,0,0,.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-warning-fg-color:#000000de;--md-warning-bg-color:#ff9;--md-footer-fg-color:#fff;--md-footer-fg-color--light:#ffffffb3;--md-footer-fg-color--lighter:#ffffff73;--md-footer-bg-color:#000000de;--md-footer-bg-color--dark:#00000052;--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;--md-shadow-z2:0 0.2rem 0.5rem #0000001a,0 0 0.05rem #00000040;--md-shadow-z3:0 0.2rem 0.5rem #0003,0 0 0.05rem #00000059}.md-icon svg{fill:currentcolor;display:block;height:1.2rem;width:1.2rem}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-text-font-family:var(--md-text-font,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;--md-code-font-family:var(--md-code-font,_),SFMono-Regular,Consolas,Menlo,monospace}aside,body,input{font-feature-settings:"kern","liga";color:var(--md-typeset-color);font-family:var(--md-text-font-family)}code,kbd,pre{font-feature-settings:"kern";font-family:var(--md-code-font-family)}:root{--md-typeset-table-sort-icon:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--asc:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--desc:url('data:image/svg+xml;charset=utf-8,')}.md-typeset{-webkit-print-color-adjust:exact;color-adjust:exact;font-size:.8rem;line-height:1.6}@media print{.md-typeset{font-size:.68rem}}.md-typeset blockquote,.md-typeset dl,.md-typeset figure,.md-typeset ol,.md-typeset pre,.md-typeset ul{margin-bottom:1em;margin-top:1em}.md-typeset h1{color:var(--md-default-fg-color--light);font-size:2em;line-height:1.3;margin:0 0 1.25em}.md-typeset h1,.md-typeset h2{font-weight:300;letter-spacing:-.01em}.md-typeset h2{font-size:1.5625em;line-height:1.4;margin:1.6em 0 .64em}.md-typeset h3{font-size:1.25em;font-weight:400;letter-spacing:-.01em;line-height:1.5;margin:1.6em 0 .8em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{font-weight:700;letter-spacing:-.01em;margin:1em 0}.md-typeset h5,.md-typeset h6{color:var(--md-default-fg-color--light);font-size:.8em;font-weight:700;letter-spacing:-.01em;margin:1.25em 0}.md-typeset h5{text-transform:uppercase}.md-typeset hr{border-bottom:.05rem solid var(--md-default-fg-color--lightest);display:flow-root;margin:1.5em 0}.md-typeset a{color:var(--md-typeset-a-color);word-break:break-word}.md-typeset a,.md-typeset a:before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset a:focus code,.md-typeset a:hover code{background-color:var(--md-accent-fg-color--transparent)}.md-typeset a code{color:currentcolor;transition:background-color 125ms}.md-typeset a.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset code,.md-typeset kbd,.md-typeset pre{color:var(--md-code-fg-color);direction:ltr;font-variant-ligatures:none}@media print{.md-typeset code,.md-typeset kbd,.md-typeset pre{white-space:pre-wrap}}.md-typeset code{background-color:var(--md-code-bg-color);border-radius:.1rem;-webkit-box-decoration-break:clone;box-decoration-break:clone;font-size:.85em;padding:0 .2941176471em;word-break:break-word}.md-typeset code:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset pre{display:flow-root;line-height:1.4;position:relative}.md-typeset pre>code{-webkit-box-decoration-break:slice;box-decoration-break:slice;box-shadow:none;display:block;margin:0;outline-color:var(--md-accent-fg-color);overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-typeset pre>code::-webkit-scrollbar{height:.2rem;width:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}.md-typeset kbd{background-color:var(--md-typeset-kbd-color);border-radius:.1rem;box-shadow:0 .1rem 0 .05rem var(--md-typeset-kbd-border-color),0 .1rem 0 var(--md-typeset-kbd-border-color),0 -.1rem .2rem var(--md-typeset-kbd-accent-color) inset;color:var(--md-default-fg-color);display:inline-block;font-size:.75em;padding:0 .6666666667em;vertical-align:text-top;word-break:break-word}.md-typeset mark{background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone;color:inherit;word-break:break-word}.md-typeset abbr{border-bottom:.05rem dotted var(--md-default-fg-color--light);cursor:help;text-decoration:none}.md-typeset small{opacity:.75}[dir=ltr] .md-typeset sub,[dir=ltr] .md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-right:.078125em}[dir=ltr] .md-typeset blockquote{padding-left:.6rem}[dir=rtl] .md-typeset blockquote{padding-right:.6rem}[dir=ltr] .md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{border-right:.2rem solid var(--md-default-fg-color--lighter)}.md-typeset blockquote{color:var(--md-default-fg-color--light);margin-left:0;margin-right:0}.md-typeset ul{list-style-type:disc}[dir=ltr] .md-typeset ol,[dir=ltr] .md-typeset ul{margin-left:.625em}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-right:.625em}.md-typeset ol,.md-typeset ul{padding:0}.md-typeset ol:not([hidden]),.md-typeset ul:not([hidden]){display:flow-root}.md-typeset ol ol,.md-typeset ul ol{list-style-type:lower-alpha}.md-typeset ol ol ol,.md-typeset ul ol ol{list-style-type:lower-roman}[dir=ltr] .md-typeset ol li,[dir=ltr] .md-typeset ul li{margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-right:1.25em}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em}.md-typeset ol li blockquote,.md-typeset ol li p,.md-typeset ul li blockquote,.md-typeset ul li p{margin:.5em 0}.md-typeset ol li:last-child,.md-typeset ul li:last-child{margin-bottom:0}[dir=ltr] .md-typeset ol li ol,[dir=ltr] .md-typeset ol li ul,[dir=ltr] .md-typeset ul li ol,[dir=ltr] .md-typeset ul li ul{margin-left:.625em}[dir=rtl] .md-typeset ol li ol,[dir=rtl] .md-typeset ol li ul,[dir=rtl] .md-typeset ul li ol,[dir=rtl] .md-typeset ul li ul{margin-right:.625em}.md-typeset ol li ol,.md-typeset ol li ul,.md-typeset ul li ol,.md-typeset ul li ul{margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset dd{margin-left:1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em}.md-typeset dd{margin-bottom:1.5em;margin-top:1em}.md-typeset img,.md-typeset svg,.md-typeset video{height:auto;max-width:100%}.md-typeset img[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child{margin-top:0}.md-typeset figure{display:flow-root;margin:1em auto;max-width:100%;text-align:center;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.md-typeset figure img{display:block;margin:0 auto}.md-typeset figcaption{font-style:italic;margin:1em auto;max-width:24rem}.md-typeset iframe{color-scheme:light;max-width:100%}.md-typeset table:not([class]){background-color:var(--md-default-bg-color);border:.05rem solid var(--md-typeset-table-color);border-radius:.1rem;display:inline-block;font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) td>:first-child,.md-typeset table:not([class]) th>:first-child{margin-top:0}.md-typeset table:not([class]) td>:last-child,.md-typeset table:not([class]) th>:last-child{margin-bottom:0}.md-typeset table:not([class]) td:not([align]),.md-typeset table:not([class]) th:not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) td:not([align]),[dir=rtl] .md-typeset table:not([class]) th:not([align]){text-align:right}.md-typeset table:not([class]) th{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) td{border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) tbody tr{transition:background-color 125ms}.md-typeset table:not([class]) tbody tr:hover{background-color:var(--md-typeset-table-color--light);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}[dir=ltr] .md-typeset table th[role=columnheader]:after{margin-left:.5em}[dir=rtl] .md-typeset table th[role=columnheader]:after{margin-right:.5em}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-typeset-table-sort-icon);mask-image:var(--md-typeset-table-sort-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset table th[role=columnheader]:hover:after{background-color:var(--md-default-fg-color--lighter)}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--asc);mask-image:var(--md-typeset-table-sort-icon--asc)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--desc);mask-image:var(--md-typeset-table-sort-icon--desc)}.md-typeset__scrollwrap{margin:1em -.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;margin:0;overflow:hidden;width:100%}@media screen and (max-width:44.984375em){.md-content__inner>pre{margin:1em -.8rem}.md-content__inner>pre code{border-radius:0}}.md-typeset .md-author{border-radius:100%;display:block;flex-shrink:0;height:1.6rem;overflow:hidden;position:relative;transition:color 125ms,transform 125ms;width:1.6rem}.md-typeset .md-author img{display:block}.md-typeset .md-author--more{background:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--lighter);font-size:.6rem;font-weight:700;line-height:1.6rem;text-align:center}.md-typeset .md-author--long{height:2.4rem;width:2.4rem}.md-typeset a.md-author{transform:scale(1)}.md-typeset a.md-author img{filter:grayscale(100%) opacity(75%);transition:filter 125ms}.md-typeset a.md-author:focus,.md-typeset a.md-author:hover{transform:scale(1.1);z-index:1}.md-typeset a.md-author:focus img,.md-typeset a.md-author:hover img{filter:grayscale(0)}.md-banner{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color);overflow:auto}@media print{.md-banner{display:none}}.md-banner--warning{background-color:var(--md-warning-bg-color);color:var(--md-warning-fg-color)}.md-banner__inner{font-size:.7rem;margin:.6rem auto;padding:0 .8rem}[dir=ltr] .md-banner__button{float:right}[dir=rtl] .md-banner__button{float:left}.md-banner__button{color:inherit;cursor:pointer;transition:opacity .25s}.no-js .md-banner__button{display:none}.md-banner__button:hover{opacity:.7}html{font-size:125%;height:100%;overflow-x:hidden}@media screen and (min-width:100em){html{font-size:137.5%}}@media screen and (min-width:125em){html{font-size:150%}}body{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;font-size:.5rem;min-height:100%;position:relative;width:100%}@media print{body{display:block}}@media screen and (max-width:59.984375em){body[data-md-scrolllock]{position:fixed}}.md-grid{margin-left:auto;margin-right:auto;max-width:61rem}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{overflow:hidden;text-overflow:ellipsis}.md-toggle{display:none}.md-option{height:0;opacity:0;position:absolute;width:0}.md-option:checked+label:not([hidden]){display:block}.md-option.focus-visible+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-skip{background-color:var(--md-default-fg-color);border-radius:.1rem;color:var(--md-default-bg-color);font-size:.64rem;margin:.5rem;opacity:0;outline-color:var(--md-accent-fg-color);padding:.3rem .5rem;position:fixed;transform:translateY(.4rem);z-index:-1}.md-skip:focus{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 175ms 75ms;z-index:10}@page{margin:25mm}:root{--md-clipboard-icon:url('data:image/svg+xml;charset=utf-8,')}.md-clipboard{border-radius:.1rem;color:var(--md-default-fg-color--lightest);cursor:pointer;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;position:absolute;right:.5em;top:.5em;transition:color .25s;width:1.5em;z-index:1}@media print{.md-clipboard{display:none}}.md-clipboard:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}:hover>.md-clipboard{color:var(--md-default-fg-color--light)}.md-clipboard:focus,.md-clipboard:hover{color:var(--md-accent-fg-color)}.md-clipboard:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-clipboard--inline{cursor:pointer}.md-clipboard--inline code{transition:color .25s,background-color .25s}.md-clipboard--inline:focus code,.md-clipboard--inline:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}@keyframes consent{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes overlay{0%{opacity:0}to{opacity:1}}.md-consent__overlay{animation:overlay .25s both;-webkit-backdrop-filter:blur(.1rem);backdrop-filter:blur(.1rem);background-color:#0000008a;height:100%;opacity:1;position:fixed;top:0;width:100%;z-index:5}.md-consent__inner{animation:consent .5s cubic-bezier(.1,.7,.1,1) both;background-color:var(--md-default-bg-color);border:0;border-radius:.1rem;bottom:0;box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;max-height:100%;overflow:auto;padding:0;position:fixed;width:100%;z-index:5}.md-consent__form{padding:.8rem}.md-consent__settings{display:none;margin:1em 0}input:checked+.md-consent__settings{display:block}.md-consent__controls{margin-bottom:.8rem}.md-typeset .md-consent__controls .md-button{display:inline}@media screen and (max-width:44.984375em){.md-typeset .md-consent__controls .md-button{display:block;margin-top:.4rem;text-align:center;width:100%}}.md-consent label{cursor:pointer}.md-content{flex-grow:1;min-width:0}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width:76.25em){[dir=ltr] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=ltr] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner,[dir=rtl] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-right:1.2rem}[dir=rtl] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}[dir=ltr] .md-content__button{float:right}[dir=rtl] .md-content__button{float:left}[dir=ltr] .md-content__button{margin-left:.4rem}[dir=rtl] .md-content__button{margin-right:.4rem}.md-content__button{margin:.4rem 0;padding:0}@media print{.md-content__button{display:none}}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}[dir=ltr] .md-dialog{right:.8rem}[dir=rtl] .md-dialog{left:.8rem}.md-dialog{background-color:var(--md-default-fg-color);border-radius:.1rem;bottom:.8rem;box-shadow:var(--md-shadow-z3);min-width:11.1rem;opacity:0;padding:.4rem .6rem;pointer-events:none;position:fixed;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:4}@media print{.md-dialog{display:none}}.md-dialog--active{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(.075,.85,.175,1),opacity .4s}.md-dialog__inner{color:var(--md-default-bg-color);font-size:.7rem}.md-feedback{margin:2em 0 1em;text-align:center}.md-feedback fieldset{border:none;margin:0;padding:0}.md-feedback__title{font-weight:700;margin:1em auto}.md-feedback__inner{position:relative}.md-feedback__list{display:flex;flex-wrap:wrap;place-content:baseline center;position:relative}.md-feedback__list:hover .md-icon:not(:disabled){color:var(--md-default-fg-color--lighter)}:disabled .md-feedback__list{min-height:1.8rem}.md-feedback__icon{color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;margin:0 .1rem;transition:color 125ms}.md-feedback__icon:not(:disabled).md-icon:hover{color:var(--md-accent-fg-color)}.md-feedback__icon:disabled{color:var(--md-default-fg-color--lightest);pointer-events:none}.md-feedback__note{opacity:0;position:relative;transform:translateY(.4rem);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-feedback__note>*{margin:0 auto;max-width:16rem}:disabled .md-feedback__note{opacity:1;transform:translateY(0)}.md-footer{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{justify-content:space-between;overflow:auto;padding:.2rem}.md-footer__inner:not([hidden]){display:flex}.md-footer__link{align-items:end;display:flex;flex-grow:0.01;margin-bottom:.4rem;margin-top:1rem;max-width:100%;outline-color:var(--md-accent-fg-color);overflow:hidden;transition:opacity .25s}.md-footer__link:focus,.md-footer__link:hover{opacity:.7}[dir=rtl] .md-footer__link svg{transform:scaleX(-1)}@media screen and (max-width:44.984375em){.md-footer__link--prev{flex-shrink:0}.md-footer__link--prev .md-footer__title{display:none}}[dir=ltr] .md-footer__link--next{margin-left:auto}[dir=rtl] .md-footer__link--next{margin-right:auto}.md-footer__link--next{text-align:right}[dir=rtl] .md-footer__link--next{text-align:left}.md-footer__title{flex-grow:1;font-size:.9rem;margin-bottom:.7rem;max-width:calc(100% - 2.4rem);padding:0 1rem;white-space:nowrap}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.64rem;opacity:.7}.md-footer-meta{background-color:var(--md-footer-bg-color--dark)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a{color:var(--md-footer-fg-color--light)}html .md-footer-meta.md-typeset a:focus,html .md-footer-meta.md-typeset a:hover{color:var(--md-footer-fg-color)}.md-copyright{color:var(--md-footer-fg-color--lighter);font-size:.64rem;margin:auto .6rem;padding:.4rem 0;width:100%}@media screen and (min-width:45em){.md-copyright{width:auto}}.md-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-social{display:inline-flex;gap:.2rem;margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-social{padding:.6rem 0}}.md-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-social__link:before{line-height:1.9}.md-social__link svg{fill:currentcolor;max-height:.8rem;vertical-align:-25%}.md-typeset .md-button{border:.1rem solid;border-radius:.1rem;color:var(--md-primary-fg-color);cursor:pointer;display:inline-block;font-weight:700;padding:.625em 2em;transition:color 125ms,background-color 125ms,border-color 125ms}.md-typeset .md-button--primary{background-color:var(--md-primary-fg-color);border-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-typeset .md-button:focus,.md-typeset .md-button:hover{background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[dir=ltr] .md-typeset .md-input{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .md-input,[dir=rtl] .md-typeset .md-input{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .md-input{border-top-left-radius:.1rem}.md-typeset .md-input{border-bottom:.1rem solid var(--md-default-fg-color--lighter);box-shadow:var(--md-shadow-z1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:border .25s,box-shadow .25s}.md-typeset .md-input:focus,.md-typeset .md-input:hover{border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input--stretch{width:100%}.md-header{background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem #0000,0 .2rem .4rem #0000;color:var(--md-primary-bg-color);display:block;left:0;position:sticky;right:0;top:0;z-index:4}@media print{.md-header{display:none}}.md-header[hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1),box-shadow .25s}.md-header--shadow{box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;transition:transform .25s cubic-bezier(.1,.7,.1,1),box-shadow .25s}.md-header__inner{align-items:center;display:flex;padding:0 .2rem}.md-header__button{color:currentcolor;cursor:pointer;margin:.2rem;outline-color:var(--md-accent-fg-color);padding:.4rem;position:relative;transition:opacity .25s;vertical-align:middle;z-index:1}.md-header__button:hover{opacity:.7}.md-header__button:not([hidden]){display:inline-block}.md-header__button:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-header__button.md-logo{margin:.2rem;padding:.4rem}@media screen and (max-width:76.234375em){.md-header__button.md-logo{display:none}}.md-header__button.md-logo img,.md-header__button.md-logo svg{fill:currentcolor;display:block;height:1.2rem;width:auto}@media screen and (min-width:60em){.md-header__button[for=__search]{display:none}}.no-js .md-header__button[for=__search]{display:none}[dir=rtl] .md-header__button[for=__search] svg{transform:scaleX(-1)}@media screen and (min-width:76.25em){.md-header__button[for=__drawer]{display:none}}.md-header__topic{display:flex;max-width:100%;position:absolute;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;white-space:nowrap}.md-header__topic+.md-header__topic{opacity:0;pointer-events:none;transform:translateX(1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__topic+.md-header__topic{transform:translateX(-1.25rem)}.md-header__topic:first-child{font-weight:700}[dir=ltr] .md-header__title{margin-left:1rem;margin-right:.4rem}[dir=rtl] .md-header__title{margin-left:.4rem;margin-right:1rem}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;line-height:2.4rem}.md-header__title--active .md-header__topic{opacity:0;pointer-events:none;transform:translateX(-1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__title--active .md-header__topic{transform:translateX(1.25rem)}.md-header__title--active .md-header__topic+.md-header__topic{opacity:1;pointer-events:auto;transform:translateX(0);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;z-index:0}.md-header__title>.md-header__ellipsis{height:100%;position:relative;width:100%}.md-header__option{display:flex;flex-shrink:0;max-width:100%;transition:max-width 0ms .25s,opacity .25s .25s;white-space:nowrap}[data-md-toggle=search]:checked~.md-header .md-header__option{max-width:0;opacity:0;transition:max-width 0ms,opacity 0ms}.md-header__option>input{bottom:0}.md-header__source{display:none}@media screen and (min-width:60em){[dir=ltr] .md-header__source{margin-left:1rem}[dir=rtl] .md-header__source{margin-right:1rem}.md-header__source{display:block;max-width:11.7rem;width:11.7rem}}@media screen and (min-width:76.25em){[dir=ltr] .md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}.md-meta{color:var(--md-default-fg-color--light);font-size:.7rem;line-height:1.3}.md-meta__list{display:inline-flex;flex-wrap:wrap;list-style:none;margin:0;padding:0}.md-meta__item:not(:last-child):after{content:"·";margin-left:.2rem;margin-right:.2rem}.md-meta__link{color:var(--md-typeset-a-color)}.md-meta__link:focus,.md-meta__link:hover{color:var(--md-accent-fg-color)}.md-draft{background-color:#ff1744;border-radius:.125em;color:#fff;display:inline-block;font-weight:700;padding-left:.5714285714em;padding-right:.5714285714em}:root{--md-nav-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-nav-icon--next:url('data:image/svg+xml;charset=utf-8,');--md-toc-icon:url('data:image/svg+xml;charset=utf-8,')}.md-nav{font-size:.7rem;line-height:1.3}.md-nav__title{color:var(--md-default-fg-color--light);display:block;font-weight:700;overflow:hidden;padding:0 .6rem;text-overflow:ellipsis}.md-nav__title .md-nav__button{display:none}.md-nav__title .md-nav__button img{height:100%;width:auto}.md-nav__title .md-nav__button.md-logo img,.md-nav__title .md-nav__button.md-logo svg{fill:currentcolor;display:block;height:2.4rem;max-width:100%;object-fit:contain;width:auto}.md-nav__list{list-style:none;margin:0;padding:0}.md-nav__link{align-items:flex-start;display:flex;gap:.4rem;margin-top:.625em;scroll-snap-align:start;transition:color 125ms}.md-nav__link--passed{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active,.md-nav__item .md-nav__link--active code{color:var(--md-typeset-a-color)}.md-nav__link .md-ellipsis{position:relative}[dir=ltr] .md-nav__link .md-icon:last-child{margin-left:auto}[dir=rtl] .md-nav__link .md-icon:last-child{margin-right:auto}.md-nav__link svg{fill:currentcolor;flex-shrink:0;height:1.3em}.md-nav__link[for]:focus,.md-nav__link[for]:hover,.md-nav__link[href]:focus,.md-nav__link[href]:hover{color:var(--md-accent-fg-color);cursor:pointer}.md-nav__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-nav--primary .md-nav__link[for=__toc]{display:none}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{background-color:currentcolor;display:block;height:100%;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);width:100%}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__container>.md-nav__link{margin-top:0}.md-nav__container>.md-nav__link:first-child{flex-grow:1;min-width:0}.md-nav__icon{flex-shrink:0}.md-nav__source{display:none}@media screen and (max-width:76.234375em){.md-nav--primary,.md-nav--primary .md-nav{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;height:100%;left:0;position:absolute;right:0;top:0;z-index:1}.md-nav--primary .md-nav__item,.md-nav--primary .md-nav__title{font-size:.8rem;line-height:1.5}.md-nav--primary .md-nav__title{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);cursor:pointer;height:5.6rem;line-height:2.4rem;padding:3rem .8rem .2rem;position:relative;white-space:nowrap}[dir=ltr] .md-nav--primary .md-nav__title .md-nav__icon{left:.4rem}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{right:.4rem}.md-nav--primary .md-nav__title .md-nav__icon{display:block;height:1.2rem;margin:.2rem;position:absolute;top:.4rem;width:1.2rem}.md-nav--primary .md-nav__title .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--prev);mask-image:var(--md-nav-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}.md-nav--primary .md-nav__title~.md-nav__list{background-color:var(--md-default-bg-color);box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest) inset;overflow-y:auto;scroll-snap-type:y mandatory;touch-action:pan-y}.md-nav--primary .md-nav__title~.md-nav__list>:first-child{border-top:0}.md-nav--primary .md-nav__title[for=__drawer]{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);font-weight:700}.md-nav--primary .md-nav__title .md-logo{display:block;left:.2rem;margin:.2rem;padding:.4rem;position:absolute;right:.2rem;top:.2rem}.md-nav--primary .md-nav__list{flex:1}.md-nav--primary .md-nav__item{border-top:.05rem solid var(--md-default-fg-color--lightest)}.md-nav--primary .md-nav__item--active>.md-nav__link{color:var(--md-typeset-a-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:focus,.md-nav--primary .md-nav__item--active>.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link{margin-top:0;padding:.6rem .8rem}.md-nav--primary .md-nav__link svg{margin-top:.1em}.md-nav--primary .md-nav__link>.md-nav__link{padding:0}[dir=ltr] .md-nav--primary .md-nav__link .md-nav__icon{margin-right:-.2rem}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{margin-left:-.2rem}.md-nav--primary .md-nav__link .md-nav__icon{font-size:1.2rem;height:1.2rem;width:1.2rem}.md-nav--primary .md-nav__link .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-nav--primary .md-nav__icon:after{transform:scale(-1)}.md-nav--primary .md-nav--secondary .md-nav{background-color:initial;position:static}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:1.4rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-right:1.4rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-right:2rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:2.6rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-right:2.6rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:3.2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-right:3.2rem}.md-nav--secondary{background-color:initial}.md-nav__toggle~.md-nav{display:flex;opacity:0;transform:translateX(100%);transition:transform .25s cubic-bezier(.8,0,.6,1),opacity 125ms 50ms}[dir=rtl] .md-nav__toggle~.md-nav{transform:translateX(-100%)}.md-nav__toggle:checked~.md-nav{opacity:1;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 125ms 125ms}.md-nav__toggle:checked~.md-nav>.md-nav__list{-webkit-backface-visibility:hidden;backface-visibility:hidden}}@media screen and (max-width:59.984375em){.md-nav--primary .md-nav__link[for=__toc]{display:flex}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--primary .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:flex}.md-nav__source{background-color:var(--md-primary-fg-color--dark);color:var(--md-primary-bg-color);display:block;padding:0 .2rem}}@media screen and (min-width:60em) and (max-width:76.234375em){.md-nav--integrated .md-nav__link[for=__toc]{display:flex}.md-nav--integrated .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--integrated .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--integrated .md-nav__link[for=__toc]~.md-nav{display:flex}}@media screen and (min-width:60em){.md-nav{margin-bottom:-.4rem}.md-nav--secondary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--secondary .md-nav__title[for=__toc]{scroll-snap-align:start}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--secondary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--secondary .md-nav__list{padding-right:.6rem}.md-nav--secondary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--secondary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--secondary .md-nav__item>.md-nav__link{margin-left:.4rem}}@media screen and (min-width:76.25em){.md-nav{margin-bottom:-.4rem;transition:max-height .25s cubic-bezier(.86,0,.07,1)}.md-nav--primary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--primary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--primary .md-nav__list{padding-right:.6rem}.md-nav--primary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--primary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--primary .md-nav__item>.md-nav__link{margin-left:.4rem}.md-nav__toggle~.md-nav{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .25s,visibility 0ms .25s;visibility:collapse}.md-nav__toggle~.md-nav>.md-nav__list{overflow:hidden}.md-nav__toggle.md-toggle--indeterminate~.md-nav,.md-nav__toggle:checked~.md-nav{grid-template-rows:1fr;opacity:1;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .15s .1s,visibility 0ms;visibility:visible}.md-nav__toggle.md-toggle--indeterminate~.md-nav{transition:none}.md-nav__item--nested>.md-nav>.md-nav__title{display:none}.md-nav__item--section{display:block;margin:1.25em 0}.md-nav__item--section:last-child{margin-bottom:0}.md-nav__item--section>.md-nav__link{font-weight:700}.md-nav__item--section>.md-nav__link[for]{color:var(--md-default-fg-color--light)}.md-nav__item--section>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav__item--section>.md-nav__link .md-icon,.md-nav__item--section>.md-nav__link>[for]{display:none}[dir=ltr] .md-nav__item--section>.md-nav{margin-left:-.6rem}[dir=rtl] .md-nav__item--section>.md-nav{margin-right:-.6rem}.md-nav__item--section>.md-nav{display:block;opacity:1;visibility:visible}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav__icon{border-radius:100%;height:.9rem;transition:background-color .25s;width:.9rem}.md-nav__icon:hover{background-color:var(--md-accent-fg-color--transparent)}.md-nav__icon:after{background-color:currentcolor;border-radius:100%;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:transform .25s;vertical-align:-.1rem;width:100%}[dir=rtl] .md-nav__icon:after{transform:rotate(180deg)}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon:after,.md-nav__item--nested .md-toggle--indeterminate~.md-nav__link .md-nav__icon:after{transform:rotate(90deg)}.md-nav--lifted>.md-nav__list>.md-nav__item,.md-nav--lifted>.md-nav__title{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active{display:block}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);margin-top:0;position:sticky;top:0;z-index:1}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active.md-nav__item--section{margin:0}[dir=ltr] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav:not(.md-nav--secondary){margin-left:-.6rem}[dir=rtl] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav:not(.md-nav--secondary){margin-right:-.6rem}.md-nav--lifted>.md-nav__list>.md-nav__item>[for]{color:var(--md-default-fg-color--light)}.md-nav--lifted .md-nav[data-md-level="1"]{grid-template-rows:1fr;opacity:1;visibility:visible}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-left:.05rem solid var(--md-primary-fg-color)}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-right:.05rem solid var(--md-primary-fg-color)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{display:block;margin-bottom:1.25em;opacity:1;visibility:visible}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__list{overflow:visible;padding-bottom:0}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__title{display:none}}.md-pagination{font-size:.8rem;font-weight:700;gap:.4rem}.md-pagination,.md-pagination>*{align-items:center;display:flex;justify-content:center}.md-pagination>*{border-radius:.2rem;height:1.8rem;min-width:1.8rem;text-align:center}.md-pagination__current{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light)}.md-pagination__link{transition:color 125ms,background-color 125ms}.md-pagination__link:focus,.md-pagination__link:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-pagination__link:focus svg,.md-pagination__link:hover svg{color:var(--md-accent-fg-color)}.md-pagination__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-pagination__link svg{fill:currentcolor;color:var(--md-default-fg-color--lighter);display:block;max-height:100%;width:1.2rem}.md-post__back{border-bottom:.05rem solid var(--md-default-fg-color--lightest);margin-bottom:1.2rem;padding-bottom:1.2rem}@media screen and (max-width:76.234375em){.md-post__back{display:none}}[dir=rtl] .md-post__back svg{transform:scaleX(-1)}.md-post__authors{display:flex;flex-direction:column;gap:.6rem;margin:0 .6rem 1.2rem}.md-post .md-post__meta a{transition:color 125ms}.md-post .md-post__meta a:focus,.md-post .md-post__meta a:hover{color:var(--md-accent-fg-color)}.md-post__title{color:var(--md-default-fg-color--light);font-weight:700}.md-post--excerpt{margin-bottom:3.2rem}.md-post--excerpt .md-post__header{align-items:center;display:flex;gap:.6rem;min-height:1.6rem}.md-post--excerpt .md-post__authors{align-items:center;display:inline-flex;flex-direction:row;gap:.2rem;margin:0;min-height:2.4rem}[dir=ltr] .md-post--excerpt .md-post__meta .md-meta__list{margin-right:.4rem}[dir=rtl] .md-post--excerpt .md-post__meta .md-meta__list{margin-left:.4rem}.md-post--excerpt .md-post__content>:first-child{--md-scroll-margin:6rem;margin-top:0}.md-post>.md-nav--secondary{margin:1em 0}.md-profile{align-items:center;display:flex;font-size:.7rem;gap:.6rem;line-height:1.4;width:100%}.md-profile__description{flex-grow:1}.md-content--post{display:flex}@media screen and (max-width:76.234375em){.md-content--post{flex-flow:column-reverse}}.md-content--post>.md-content__inner{min-width:0}@media screen and (min-width:76.25em){[dir=ltr] .md-content--post>.md-content__inner{margin-left:1.2rem}[dir=rtl] .md-content--post>.md-content__inner{margin-right:1.2rem}}@media screen and (max-width:76.234375em){.md-sidebar.md-sidebar--post{padding:0;position:static;width:100%}.md-sidebar.md-sidebar--post .md-sidebar__scrollwrap{overflow:visible}.md-sidebar.md-sidebar--post .md-sidebar__inner{padding:0}.md-sidebar.md-sidebar--post .md-post__meta{margin-left:.6rem;margin-right:.6rem}.md-sidebar.md-sidebar--post .md-nav__item{border:none;display:inline}.md-sidebar.md-sidebar--post .md-nav__list{display:inline-flex;flex-wrap:wrap;gap:.6rem;padding-bottom:.6rem;padding-top:.6rem}.md-sidebar.md-sidebar--post .md-nav__link{padding:0}.md-sidebar.md-sidebar--post .md-nav{height:auto;margin-bottom:0;position:static}}:root{--md-progress-value:0;--md-progress-delay:400ms}.md-progress{background:var(--md-primary-bg-color);height:.075rem;opacity:min(clamp(0,var(--md-progress-value),1),clamp(0,100 - var(--md-progress-value),1));position:fixed;top:0;transform:scaleX(calc(var(--md-progress-value)*1%));transform-origin:left;transition:transform .5s cubic-bezier(.19,1,.22,1),opacity .25s var(--md-progress-delay);width:100%;z-index:4}:root{--md-search-result-icon:url('data:image/svg+xml;charset=utf-8,')}.md-search{position:relative}@media screen and (min-width:60em){.md-search{padding:.2rem 0}}.no-js .md-search{display:none}.md-search__overlay{opacity:0;z-index:1}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__overlay{left:-2.2rem}[dir=rtl] .md-search__overlay{right:-2.2rem}.md-search__overlay{background-color:var(--md-default-bg-color);border-radius:1rem;height:2rem;overflow:hidden;pointer-events:none;position:absolute;top:-1rem;transform-origin:center;transition:transform .3s .1s,opacity .2s .2s;width:2rem}[data-md-toggle=search]:checked~.md-header .md-search__overlay{opacity:1;transition:transform .4s,opacity .1s}}@media screen and (min-width:60em){[dir=ltr] .md-search__overlay{left:0}[dir=rtl] .md-search__overlay{right:0}.md-search__overlay{background-color:#0000008a;cursor:pointer;height:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0}[data-md-toggle=search]:checked~.md-header .md-search__overlay{height:200vh;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@media screen and (max-width:29.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(45)}}@media screen and (min-width:30em) and (max-width:44.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(60)}}@media screen and (min-width:45em) and (max-width:59.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(75)}}.md-search__inner{-webkit-backface-visibility:hidden;backface-visibility:hidden}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__inner{left:0}[dir=rtl] .md-search__inner{right:0}.md-search__inner{height:0;opacity:0;overflow:hidden;position:fixed;top:0;transform:translateX(5%);transition:width 0ms .3s,height 0ms .3s,transform .15s cubic-bezier(.4,0,.2,1) .15s,opacity .15s .15s;width:0;z-index:2}[dir=rtl] .md-search__inner{transform:translateX(-5%)}[data-md-toggle=search]:checked~.md-header .md-search__inner{height:100%;opacity:1;transform:translateX(0);transition:width 0ms 0ms,height 0ms 0ms,transform .15s cubic-bezier(.1,.7,.1,1) .15s,opacity .15s .15s;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__inner{float:right}[dir=rtl] .md-search__inner{float:left}.md-search__inner{padding:.1rem 0;position:relative;transition:width .25s cubic-bezier(.1,.7,.1,1);width:11.7rem}}@media screen and (min-width:60em) and (max-width:76.234375em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:23.4rem}}@media screen and (min-width:76.25em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:34.4rem}}.md-search__form{background-color:var(--md-default-bg-color);box-shadow:0 0 .6rem #0000;height:2.4rem;position:relative;transition:color .25s,background-color .25s;z-index:2}@media screen and (min-width:60em){.md-search__form{background-color:#00000042;border-radius:.1rem;height:1.8rem}.md-search__form:hover{background-color:#ffffff1f}}[data-md-toggle=search]:checked~.md-header .md-search__form{background-color:var(--md-default-bg-color);border-radius:.1rem .1rem 0 0;box-shadow:0 0 .6rem #00000012;color:var(--md-default-fg-color)}[dir=ltr] .md-search__input{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__input{padding-left:2.2rem;padding-right:3.6rem}.md-search__input{background:#0000;font-size:.9rem;height:100%;position:relative;text-overflow:ellipsis;width:100%;z-index:2}.md-search__input::placeholder{transition:color .25s}.md-search__input::placeholder,.md-search__input~.md-search__icon{color:var(--md-default-fg-color--light)}.md-search__input::-ms-clear{display:none}@media screen and (max-width:59.984375em){.md-search__input{font-size:.9rem;height:2.4rem;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__input{padding-left:2.2rem}[dir=rtl] .md-search__input{padding-right:2.2rem}.md-search__input{color:inherit;font-size:.8rem}.md-search__input::placeholder{color:var(--md-primary-bg-color--light)}.md-search__input+.md-search__icon{color:var(--md-primary-bg-color)}[data-md-toggle=search]:checked~.md-header .md-search__input{text-overflow:clip}[data-md-toggle=search]:checked~.md-header .md-search__input+.md-search__icon{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input::placeholder{color:#0000}}.md-search__icon{cursor:pointer;display:inline-block;height:1.2rem;transition:color .25s,opacity .25s;width:1.2rem}.md-search__icon:hover{opacity:.7}[dir=ltr] .md-search__icon[for=__search]{left:.5rem}[dir=rtl] .md-search__icon[for=__search]{right:.5rem}.md-search__icon[for=__search]{position:absolute;top:.3rem;z-index:2}[dir=rtl] .md-search__icon[for=__search] svg{transform:scaleX(-1)}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__icon[for=__search]{left:.8rem}[dir=rtl] .md-search__icon[for=__search]{right:.8rem}.md-search__icon[for=__search]{top:.6rem}.md-search__icon[for=__search] svg:first-child{display:none}}@media screen and (min-width:60em){.md-search__icon[for=__search]{pointer-events:none}.md-search__icon[for=__search] svg:last-child{display:none}}[dir=ltr] .md-search__options{right:.5rem}[dir=rtl] .md-search__options{left:.5rem}.md-search__options{pointer-events:none;position:absolute;top:.3rem;z-index:2}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__options{right:.8rem}[dir=rtl] .md-search__options{left:.8rem}.md-search__options{top:.6rem}}[dir=ltr] .md-search__options>.md-icon{margin-left:.2rem}[dir=rtl] .md-search__options>.md-icon{margin-right:.2rem}.md-search__options>.md-icon{color:var(--md-default-fg-color--light);opacity:0;transform:scale(.75);transition:transform .15s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-search__options>.md-icon:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>.md-icon{opacity:1;pointer-events:auto;transform:scale(1)}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>.md-icon:hover{opacity:.7}[dir=ltr] .md-search__suggest{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__suggest{padding-left:2.2rem;padding-right:3.6rem}.md-search__suggest{align-items:center;color:var(--md-default-fg-color--lighter);display:flex;font-size:.9rem;height:100%;opacity:0;position:absolute;top:0;transition:opacity 50ms;white-space:nowrap;width:100%}@media screen and (min-width:60em){[dir=ltr] .md-search__suggest{padding-left:2.2rem}[dir=rtl] .md-search__suggest{padding-right:2.2rem}.md-search__suggest{font-size:.8rem}}[data-md-toggle=search]:checked~.md-header .md-search__suggest{opacity:1;transition:opacity .3s .1s}[dir=ltr] .md-search__output{border-bottom-left-radius:.1rem}[dir=ltr] .md-search__output,[dir=rtl] .md-search__output{border-bottom-right-radius:.1rem}[dir=rtl] .md-search__output{border-bottom-left-radius:.1rem}.md-search__output{overflow:hidden;position:absolute;width:100%;z-index:1}@media screen and (max-width:59.984375em){.md-search__output{bottom:0;top:2.4rem}}@media screen and (min-width:60em){.md-search__output{opacity:0;top:1.9rem;transition:opacity .4s}[data-md-toggle=search]:checked~.md-header .md-search__output{box-shadow:var(--md-shadow-z3);opacity:1}}.md-search__scrollwrap{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);height:100%;overflow-y:auto;touch-action:pan-y}@media (-webkit-max-device-pixel-ratio:1),(max-resolution:1dppx){.md-search__scrollwrap{transform:translateZ(0)}}@media screen and (min-width:60em) and (max-width:76.234375em){.md-search__scrollwrap{width:23.4rem}}@media screen and (min-width:76.25em){.md-search__scrollwrap{width:34.4rem}}@media screen and (min-width:60em){.md-search__scrollwrap{max-height:0;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin}[data-md-toggle=search]:checked~.md-header .md-search__scrollwrap{max-height:75vh}.md-search__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-search__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-search__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-search__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}}.md-search-result{color:var(--md-default-fg-color);word-break:break-word}.md-search-result__meta{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.8rem;padding:0 .8rem;scroll-snap-align:start}@media screen and (min-width:60em){[dir=ltr] .md-search-result__meta{padding-left:2.2rem}[dir=rtl] .md-search-result__meta{padding-right:2.2rem}}.md-search-result__list{list-style:none;margin:0;padding:0;-webkit-user-select:none;user-select:none}.md-search-result__item{box-shadow:0 -.05rem var(--md-default-fg-color--lightest)}.md-search-result__item:first-child{box-shadow:none}.md-search-result__link{display:block;outline:none;scroll-snap-align:start;transition:background-color .25s}.md-search-result__link:focus,.md-search-result__link:hover{background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:last-child p:last-child{margin-bottom:.6rem}.md-search-result__more>summary{cursor:pointer;display:block;outline:none;position:sticky;scroll-snap-align:start;top:0;z-index:1}.md-search-result__more>summary::marker{display:none}.md-search-result__more>summary::-webkit-details-marker{display:none}.md-search-result__more>summary>div{color:var(--md-typeset-a-color);font-size:.64rem;padding:.75em .8rem;transition:color .25s,background-color .25s}@media screen and (min-width:60em){[dir=ltr] .md-search-result__more>summary>div{padding-left:2.2rem}[dir=rtl] .md-search-result__more>summary>div{padding-right:2.2rem}}.md-search-result__more>summary:focus>div,.md-search-result__more>summary:hover>div{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more[open]>summary{background-color:var(--md-default-bg-color)}.md-search-result__article{overflow:hidden;padding:0 .8rem;position:relative}@media screen and (min-width:60em){[dir=ltr] .md-search-result__article{padding-left:2.2rem}[dir=rtl] .md-search-result__article{padding-right:2.2rem}}[dir=ltr] .md-search-result__icon{left:0}[dir=rtl] .md-search-result__icon{right:0}.md-search-result__icon{color:var(--md-default-fg-color--light);height:1.2rem;margin:.5rem;position:absolute;width:1.2rem}@media screen and (max-width:59.984375em){.md-search-result__icon{display:none}}.md-search-result__icon:after{background-color:currentcolor;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-search-result-icon);mask-image:var(--md-search-result-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-search-result__icon:after{transform:scaleX(-1)}.md-search-result .md-typeset{color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.6}.md-search-result .md-typeset h1{color:var(--md-default-fg-color);font-size:.8rem;font-weight:400;line-height:1.4;margin:.55rem 0}.md-search-result .md-typeset h1 mark{text-decoration:none}.md-search-result .md-typeset h2{color:var(--md-default-fg-color);font-size:.64rem;font-weight:700;line-height:1.6;margin:.5em 0}.md-search-result .md-typeset h2 mark{text-decoration:none}.md-search-result__terms{color:var(--md-default-fg-color);display:block;font-size:.64rem;font-style:italic;margin:.5em 0}.md-search-result mark{background-color:initial;color:var(--md-accent-fg-color);text-decoration:underline}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);left:50%;margin-top:.2rem;max-height:0;opacity:0;position:absolute;top:calc(100% - .2rem);transform:translate3d(-50%,.3rem,0);transition:transform .25s 375ms,opacity .25s .25s,max-height 0ms .5s}.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{max-height:10rem;opacity:1;transform:translate3d(-50%,0,0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}.md-select__inner:after{border-bottom:.2rem solid #0000;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid #0000;border-right:.2rem solid #0000;border-top:0;content:"";height:0;left:50%;margin-left:-.2rem;margin-top:-.2rem;position:absolute;top:0;width:0}.md-select__list{border-radius:.1rem;font-size:.8rem;list-style-type:none;margin:0;max-height:inherit;overflow:auto;padding:0}.md-select__item{line-height:1.8rem}[dir=ltr] .md-select__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}.md-select__link:focus,.md-select__link:hover{color:var(--md-accent-fg-color)}.md-select__link:focus{background-color:var(--md-default-fg-color--lightest)}.md-sidebar{align-self:flex-start;flex-shrink:0;padding:1.2rem 0;position:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.234375em){[dir=ltr] .md-sidebar--primary{left:-12.1rem}[dir=rtl] .md-sidebar--primary{right:-12.1rem}.md-sidebar--primary{background-color:var(--md-default-bg-color);display:block;height:100%;position:fixed;top:0;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),box-shadow .25s;width:12.1rem;z-index:5}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:var(--md-shadow-z3);transform:translateX(12.1rem)}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.1rem)}.md-sidebar--primary .md-sidebar__scrollwrap{bottom:0;left:0;margin:0;overflow:hidden;position:absolute;right:0;scroll-snap-type:none;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}.md-header--lifted~.md-container .md-sidebar{top:4.8rem}}.md-sidebar--secondary{display:none;order:2}@media screen and (min-width:60em){.md-sidebar--secondary{height:0}.no-js .md-sidebar--secondary{height:auto}.md-sidebar--secondary:not([hidden]){display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{scrollbar-gutter:stable;-webkit-backface-visibility:hidden;backface-visibility:hidden;margin:0 .2rem;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap:focus-within,.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb:hover,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@supports selector(::-webkit-scrollbar){.md-sidebar__scrollwrap{scrollbar-gutter:auto}[dir=ltr] .md-sidebar__inner{padding-right:calc(100% - 11.5rem)}[dir=rtl] .md-sidebar__inner{padding-left:calc(100% - 11.5rem)}}@media screen and (max-width:76.234375em){.md-overlay{background-color:#0000008a;height:0;opacity:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0;z-index:5}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@keyframes facts{0%{height:0}to{height:.65rem}}@keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}:root{--md-source-forks-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-repositories-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-stars-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-source{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;font-size:.65rem;line-height:1.2;outline-color:var(--md-accent-fg-color);transition:opacity .25s;white-space:nowrap}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;height:2.4rem;vertical-align:middle;width:2rem}[dir=ltr] .md-source__icon svg{margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem}.md-source__icon svg{margin-top:.6rem}[dir=ltr] .md-source__icon+.md-source__repository{padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{padding-right:2rem}[dir=ltr] .md-source__icon+.md-source__repository{margin-left:-2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem}[dir=ltr] .md-source__repository{margin-left:.6rem}[dir=rtl] .md-source__repository{margin-right:.6rem}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{display:flex;font-size:.55rem;gap:.4rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0;width:100%}.md-source__repository--active .md-source__facts{animation:facts .25s ease-in}.md-source__fact{overflow:hidden;text-overflow:ellipsis}.md-source__repository--active .md-source__fact{animation:fact .4s ease-out}[dir=ltr] .md-source__fact:before{margin-right:.1rem}[dir=rtl] .md-source__fact:before{margin-left:.1rem}.md-source__fact:before{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-top;width:.6rem}.md-source__fact:nth-child(1n+2){flex-shrink:0}.md-source__fact--version:before{-webkit-mask-image:var(--md-source-version-icon);mask-image:var(--md-source-version-icon)}.md-source__fact--stars:before{-webkit-mask-image:var(--md-source-stars-icon);mask-image:var(--md-source-stars-icon)}.md-source__fact--forks:before{-webkit-mask-image:var(--md-source-forks-icon);mask-image:var(--md-source-forks-icon)}.md-source__fact--repositories:before{-webkit-mask-image:var(--md-source-repositories-icon);mask-image:var(--md-source-repositories-icon)}.md-source-file{margin:1em 0}[dir=ltr] .md-source-file__fact{margin-right:.6rem}[dir=rtl] .md-source-file__fact{margin-left:.6rem}.md-source-file__fact{align-items:center;color:var(--md-default-fg-color--light);display:inline-flex;font-size:.68rem;gap:.3rem}.md-source-file__fact .md-icon{flex-shrink:0;margin-bottom:.05rem}[dir=ltr] .md-source-file__fact .md-author{float:left}[dir=rtl] .md-source-file__fact .md-author{float:right}.md-source-file__fact .md-author{margin-right:.2rem}.md-source-file__fact svg{width:.9rem}:root{--md-status:url('data:image/svg+xml;charset=utf-8,');--md-status--new:url('data:image/svg+xml;charset=utf-8,');--md-status--deprecated:url('data:image/svg+xml;charset=utf-8,');--md-status--encrypted:url('data:image/svg+xml;charset=utf-8,')}.md-status:after{background-color:var(--md-default-fg-color--light);content:"";display:inline-block;height:1.125em;-webkit-mask-image:var(--md-status);mask-image:var(--md-status);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-bottom;width:1.125em}.md-status:hover:after{background-color:currentcolor}.md-status--new:after{-webkit-mask-image:var(--md-status--new);mask-image:var(--md-status--new)}.md-status--deprecated:after{-webkit-mask-image:var(--md-status--deprecated);mask-image:var(--md-status--deprecated)}.md-status--encrypted:after{-webkit-mask-image:var(--md-status--encrypted);mask-image:var(--md-status--encrypted)}.md-tabs{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);display:block;line-height:1.3;overflow:auto;width:100%;z-index:3}@media print{.md-tabs{display:none}}@media screen and (max-width:76.234375em){.md-tabs{display:none}}.md-tabs[hidden]{pointer-events:none}[dir=ltr] .md-tabs__list{margin-left:.2rem}[dir=rtl] .md-tabs__list{margin-right:.2rem}.md-tabs__list{contain:content;display:flex;list-style:none;margin:0;overflow:auto;padding:0;scrollbar-width:none;white-space:nowrap}.md-tabs__list::-webkit-scrollbar{display:none}.md-tabs__item{height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__item--active .md-tabs__link{color:inherit;opacity:1}.md-tabs__link{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:flex;font-size:.7rem;margin-top:.8rem;opacity:.7;outline-color:var(--md-accent-fg-color);outline-offset:.2rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s}.md-tabs__link:focus,.md-tabs__link:hover{color:inherit;opacity:1}[dir=ltr] .md-tabs__link svg{margin-right:.4rem}[dir=rtl] .md-tabs__link svg{margin-left:.4rem}.md-tabs__link svg{fill:currentcolor;height:1.3em}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:.1s}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:.12s}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:.14s}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:.16s}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:.18s}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:.2s}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:.22s}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:.24s}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:.26s}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:.28s}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:.3s}.md-tabs[hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}:root{--md-tag-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .md-tags:not([hidden]){display:inline-flex;flex-wrap:wrap;gap:.5em;margin-bottom:.75em;margin-top:-.125em}.md-typeset .md-tag{align-items:center;background:var(--md-default-fg-color--lightest);border-radius:2.4rem;display:inline-flex;font-size:.64rem;font-size:min(.8em,.64rem);font-weight:700;gap:.5em;letter-spacing:normal;line-height:1.6;padding:.3125em .78125em}.md-typeset .md-tag[href]{-webkit-tap-highlight-color:transparent;color:inherit;outline:none;transition:color 125ms,background-color 125ms}.md-typeset .md-tag[href]:focus,.md-typeset .md-tag[href]:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[id]>.md-typeset .md-tag{vertical-align:text-top}.md-typeset .md-tag-icon:before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-tag-icon);mask-image:var(--md-tag-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset .md-tag-icon[href]:focus:before,.md-typeset .md-tag-icon[href]:hover:before{background-color:var(--md-accent-bg-color)}@keyframes pulse{0%{transform:scale(.95)}75%{transform:scale(1)}to{transform:scale(.95)}}:root{--md-annotation-bg-icon:url('data:image/svg+xml;charset=utf-8,');--md-annotation-icon:url('data:image/svg+xml;charset=utf-8,');--md-tooltip-width:20rem}.md-tooltip{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);font-family:var(--md-text-font-family);left:clamp(var(--md-tooltip-0,0rem) + .8rem,var(--md-tooltip-x),100vw + var(--md-tooltip-0,0rem) + .8rem - var(--md-tooltip-width) - 2 * .8rem);max-width:calc(100vw - 1.6rem);opacity:0;position:absolute;top:var(--md-tooltip-y);transform:translateY(-.4rem);transition:transform 0ms .25s,opacity .25s,z-index .25s;width:var(--md-tooltip-width);z-index:0}.md-tooltip--active{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip--inline{font-weight:700;-webkit-user-select:none;user-select:none;width:auto}.md-tooltip--inline:not(.md-tooltip--active){transform:translateY(.2rem) scale(.9)}.md-tooltip--inline .md-tooltip__inner{font-size:.5rem;padding:.2rem .4rem}[hidden]+.md-tooltip--inline{display:none}.focus-visible>.md-tooltip,.md-tooltip:target{outline:var(--md-accent-fg-color) auto}.md-tooltip__inner{font-size:.64rem;padding:.8rem}.md-tooltip__inner.md-typeset>:first-child{margin-top:0}.md-tooltip__inner.md-typeset>:last-child{margin-bottom:0}.md-annotation{font-weight:400;outline:none;vertical-align:text-bottom;white-space:normal}[dir=rtl] .md-annotation{direction:rtl}code .md-annotation{font-family:var(--md-code-font-family);font-size:inherit}.md-annotation:not([hidden]){display:inline-block;line-height:1.25}.md-annotation__index{border-radius:.01px;cursor:pointer;display:inline-block;margin-left:.4ch;margin-right:.4ch;outline:none;overflow:hidden;position:relative;-webkit-user-select:none;user-select:none;vertical-align:text-top;z-index:0}.md-annotation .md-annotation__index{transition:z-index .25s}@media screen{.md-annotation__index{width:2.2ch}[data-md-visible]>.md-annotation__index{animation:pulse 2s infinite}.md-annotation__index:before{background:var(--md-default-bg-color);-webkit-mask-image:var(--md-annotation-bg-icon);mask-image:var(--md-annotation-bg-icon)}.md-annotation__index:after,.md-annotation__index:before{content:"";height:2.2ch;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:-.1ch;width:2.2ch;z-index:-1}.md-annotation__index:after{background-color:var(--md-default-fg-color--lighter);-webkit-mask-image:var(--md-annotation-icon);mask-image:var(--md-annotation-icon);transform:scale(1.0001);transition:background-color .25s,transform .25s}.md-tooltip--active+.md-annotation__index:after{transform:rotate(45deg)}.md-tooltip--active+.md-annotation__index:after,:hover>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}}.md-tooltip--active+.md-annotation__index{animation-play-state:paused;transition-duration:0ms;z-index:2}.md-annotation__index [data-md-annotation-id]{display:inline-block}@media print{.md-annotation__index [data-md-annotation-id]{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);font-weight:700;padding:0 .6ch;white-space:nowrap}.md-annotation__index [data-md-annotation-id]:after{content:attr(data-md-annotation-id)}}.md-typeset .md-annotation-list{counter-reset:xxx;list-style:none}.md-typeset .md-annotation-list li{position:relative}[dir=ltr] .md-typeset .md-annotation-list li:before{left:-2.125em}[dir=rtl] .md-typeset .md-annotation-list li:before{right:-2.125em}.md-typeset .md-annotation-list li:before{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);content:counter(xxx);counter-increment:xxx;font-size:.8875em;font-weight:700;height:2ch;line-height:1.25;min-width:2ch;padding:0 .6ch;position:absolute;text-align:center;top:.25em}[dir=ltr] .md-top{margin-left:50%}[dir=rtl] .md-top{margin-right:50%}.md-top{background-color:var(--md-default-bg-color);border-radius:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:block;font-size:.7rem;outline:none;padding:.4rem .8rem;position:fixed;top:3.2rem;transform:translate(-50%);transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms;z-index:2}@media print{.md-top{display:none}}[dir=rtl] .md-top{transform:translate(50%)}.md-top[hidden]{opacity:0;pointer-events:none;transform:translate(-50%,.2rem);transition-duration:0ms}[dir=rtl] .md-top[hidden]{transform:translate(50%,.2rem)}.md-top:focus,.md-top:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top svg{display:inline-block;vertical-align:-.5em}@keyframes hoverfix{0%{pointer-events:none}}:root{--md-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-version{flex-shrink:0;font-size:.8rem;height:2.4rem}[dir=ltr] .md-version__current{margin-left:1.4rem;margin-right:.4rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current{color:inherit;cursor:pointer;outline:none;position:relative;top:.05rem}[dir=ltr] .md-version__current:after{margin-left:.4rem}[dir=rtl] .md-version__current:after{margin-right:.4rem}.md-version__current:after{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.4rem}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:3}.md-version:focus-within .md-version__list,.md-version:hover .md-version__list{max-height:10rem;opacity:1;transition:max-height 0ms,opacity .25s}@media (hover:none),(pointer:coarse){.md-version:hover .md-version__list{animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{animation:none}}.md-version__item{line-height:1.8rem}[dir=ltr] .md-version__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}.md-version__link:focus,.md-version__link:hover{color:var(--md-accent-fg-color)}.md-version__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-admonition-icon--note:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--abstract:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--tip:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--success:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--question:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--warning:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--failure:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--danger:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--bug:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--example:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--quote:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .admonition,.md-typeset details{background-color:var(--md-admonition-bg-color);border:.075rem solid #448aff;border-radius:.2rem;box-shadow:var(--md-shadow-z1);color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .6rem;page-break-inside:avoid;transition:box-shadow 125ms}@media print{.md-typeset .admonition,.md-typeset details{box-shadow:none}}.md-typeset .admonition:focus-within,.md-typeset details:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .admonition>*,.md-typeset details>*{box-sizing:border-box}.md-typeset .admonition .admonition,.md-typeset .admonition details,.md-typeset details .admonition,.md-typeset details details{margin-bottom:1em;margin-top:1em}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{padding-left:2rem;padding-right:.6rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{padding-left:.6rem;padding-right:2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-left-width:.2rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-right-width:.2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset .admonition-title,.md-typeset summary{background-color:#448aff1a;border:none;font-weight:700;margin:0 -.6rem;padding-bottom:.4rem;padding-top:.4rem;position:relative}html .md-typeset .admonition-title:last-child,html .md-typeset summary:last-child{margin-bottom:0}[dir=ltr] .md-typeset .admonition-title:before,[dir=ltr] .md-typeset summary:before{left:.6rem}[dir=rtl] .md-typeset .admonition-title:before,[dir=rtl] .md-typeset summary:before{right:.6rem}.md-typeset .admonition-title:before,.md-typeset summary:before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;width:1rem}.md-typeset .admonition-title code,.md-typeset summary code{box-shadow:0 0 0 .05rem var(--md-default-fg-color--lightest)}.md-typeset .admonition.note,.md-typeset details.note{border-color:#448aff}.md-typeset .admonition.note:focus-within,.md-typeset details.note:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .note>.admonition-title,.md-typeset .note>summary{background-color:#448aff1a}.md-typeset .note>.admonition-title:before,.md-typeset .note>summary:before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note)}.md-typeset .note>.admonition-title:after,.md-typeset .note>summary:after{color:#448aff}.md-typeset .admonition.abstract,.md-typeset details.abstract{border-color:#00b0ff}.md-typeset .admonition.abstract:focus-within,.md-typeset details.abstract:focus-within{box-shadow:0 0 0 .2rem #00b0ff1a}.md-typeset .abstract>.admonition-title,.md-typeset .abstract>summary{background-color:#00b0ff1a}.md-typeset .abstract>.admonition-title:before,.md-typeset .abstract>summary:before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract)}.md-typeset .abstract>.admonition-title:after,.md-typeset .abstract>summary:after{color:#00b0ff}.md-typeset .admonition.info,.md-typeset details.info{border-color:#00b8d4}.md-typeset .admonition.info:focus-within,.md-typeset details.info:focus-within{box-shadow:0 0 0 .2rem #00b8d41a}.md-typeset .info>.admonition-title,.md-typeset .info>summary{background-color:#00b8d41a}.md-typeset .info>.admonition-title:before,.md-typeset .info>summary:before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info)}.md-typeset .info>.admonition-title:after,.md-typeset .info>summary:after{color:#00b8d4}.md-typeset .admonition.tip,.md-typeset details.tip{border-color:#00bfa5}.md-typeset .admonition.tip:focus-within,.md-typeset details.tip:focus-within{box-shadow:0 0 0 .2rem #00bfa51a}.md-typeset .tip>.admonition-title,.md-typeset .tip>summary{background-color:#00bfa51a}.md-typeset .tip>.admonition-title:before,.md-typeset .tip>summary:before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip)}.md-typeset .tip>.admonition-title:after,.md-typeset .tip>summary:after{color:#00bfa5}.md-typeset .admonition.success,.md-typeset details.success{border-color:#00c853}.md-typeset .admonition.success:focus-within,.md-typeset details.success:focus-within{box-shadow:0 0 0 .2rem #00c8531a}.md-typeset .success>.admonition-title,.md-typeset .success>summary{background-color:#00c8531a}.md-typeset .success>.admonition-title:before,.md-typeset .success>summary:before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success)}.md-typeset .success>.admonition-title:after,.md-typeset .success>summary:after{color:#00c853}.md-typeset .admonition.question,.md-typeset details.question{border-color:#64dd17}.md-typeset .admonition.question:focus-within,.md-typeset details.question:focus-within{box-shadow:0 0 0 .2rem #64dd171a}.md-typeset .question>.admonition-title,.md-typeset .question>summary{background-color:#64dd171a}.md-typeset .question>.admonition-title:before,.md-typeset .question>summary:before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question)}.md-typeset .question>.admonition-title:after,.md-typeset .question>summary:after{color:#64dd17}.md-typeset .admonition.warning,.md-typeset details.warning{border-color:#ff9100}.md-typeset .admonition.warning:focus-within,.md-typeset details.warning:focus-within{box-shadow:0 0 0 .2rem #ff91001a}.md-typeset .warning>.admonition-title,.md-typeset .warning>summary{background-color:#ff91001a}.md-typeset .warning>.admonition-title:before,.md-typeset .warning>summary:before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning)}.md-typeset .warning>.admonition-title:after,.md-typeset .warning>summary:after{color:#ff9100}.md-typeset .admonition.failure,.md-typeset details.failure{border-color:#ff5252}.md-typeset .admonition.failure:focus-within,.md-typeset details.failure:focus-within{box-shadow:0 0 0 .2rem #ff52521a}.md-typeset .failure>.admonition-title,.md-typeset .failure>summary{background-color:#ff52521a}.md-typeset .failure>.admonition-title:before,.md-typeset .failure>summary:before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure)}.md-typeset .failure>.admonition-title:after,.md-typeset .failure>summary:after{color:#ff5252}.md-typeset .admonition.danger,.md-typeset details.danger{border-color:#ff1744}.md-typeset .admonition.danger:focus-within,.md-typeset details.danger:focus-within{box-shadow:0 0 0 .2rem #ff17441a}.md-typeset .danger>.admonition-title,.md-typeset .danger>summary{background-color:#ff17441a}.md-typeset .danger>.admonition-title:before,.md-typeset .danger>summary:before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger)}.md-typeset .danger>.admonition-title:after,.md-typeset .danger>summary:after{color:#ff1744}.md-typeset .admonition.bug,.md-typeset details.bug{border-color:#f50057}.md-typeset .admonition.bug:focus-within,.md-typeset details.bug:focus-within{box-shadow:0 0 0 .2rem #f500571a}.md-typeset .bug>.admonition-title,.md-typeset .bug>summary{background-color:#f500571a}.md-typeset .bug>.admonition-title:before,.md-typeset .bug>summary:before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug)}.md-typeset .bug>.admonition-title:after,.md-typeset .bug>summary:after{color:#f50057}.md-typeset .admonition.example,.md-typeset details.example{border-color:#7c4dff}.md-typeset .admonition.example:focus-within,.md-typeset details.example:focus-within{box-shadow:0 0 0 .2rem #7c4dff1a}.md-typeset .example>.admonition-title,.md-typeset .example>summary{background-color:#7c4dff1a}.md-typeset .example>.admonition-title:before,.md-typeset .example>summary:before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example)}.md-typeset .example>.admonition-title:after,.md-typeset .example>summary:after{color:#7c4dff}.md-typeset .admonition.quote,.md-typeset details.quote{border-color:#9e9e9e}.md-typeset .admonition.quote:focus-within,.md-typeset details.quote:focus-within{box-shadow:0 0 0 .2rem #9e9e9e1a}.md-typeset .quote>.admonition-title,.md-typeset .quote>summary{background-color:#9e9e9e1a}.md-typeset .quote>.admonition-title:before,.md-typeset .quote>summary:before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote)}.md-typeset .quote>.admonition-title:after,.md-typeset .quote>summary:after{color:#9e9e9e}:root{--md-footnotes-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}[dir=ltr] .md-typeset .footnote>ol{margin-left:0}[dir=rtl] .md-typeset .footnote>ol{margin-right:0}.md-typeset .footnote>ol>li{transition:color 125ms}.md-typeset .footnote>ol>li:target{color:var(--md-default-fg-color)}.md-typeset .footnote>ol>li:focus-within .footnote-backref{opacity:1;transform:translateX(0);transition:none}.md-typeset .footnote>ol>li:hover .footnote-backref,.md-typeset .footnote>ol>li:target .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li>:first-child{margin-top:0}.md-typeset .footnote-ref{font-size:.75em;font-weight:700}html .md-typeset .footnote-ref{outline-offset:.1rem}.md-typeset [id^="fnref:"]:target>.footnote-ref{outline:auto}.md-typeset .footnote-backref{color:var(--md-typeset-a-color);display:inline-block;font-size:0;opacity:0;transform:translateX(.25rem);transition:color .25s,transform .25s .25s,opacity 125ms .25s;vertical-align:text-bottom}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);opacity:1;transform:translateX(0)}}[dir=rtl] .md-typeset .footnote-backref{transform:translateX(-.25rem)}.md-typeset .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-backref:before{background-color:currentcolor;content:"";display:inline-block;height:.8rem;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.8rem}[dir=rtl] .md-typeset .footnote-backref:before svg{transform:scaleX(-1)}[dir=ltr] .md-typeset .headerlink{margin-left:.5rem}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;opacity:0;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}.md-typeset .headerlink:focus,.md-typeset :hover>.headerlink,.md-typeset :target>.headerlink{opacity:1;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset .headerlink:hover,.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset :target{--md-scroll-margin:3.6rem;--md-scroll-offset:0rem;scroll-margin-top:calc(var(--md-scroll-margin) - var(--md-scroll-offset))}@media screen and (min-width:76.25em){.md-header--lifted~.md-container .md-typeset :target{--md-scroll-margin:6rem}}.md-typeset h1:target,.md-typeset h2:target,.md-typeset h3:target{--md-scroll-offset:0.2rem}.md-typeset h4:target{--md-scroll-offset:0.15rem}.md-typeset div.arithmatex{overflow:auto}@media screen and (max-width:44.984375em){.md-typeset div.arithmatex{margin:0 -.8rem}.md-typeset div.arithmatex>*{width:-webkit-min-content;width:min-content}}.md-typeset div.arithmatex>*{margin-left:auto!important;margin-right:auto!important;padding:0 .8rem;touch-action:auto}.md-typeset div.arithmatex>* mjx-container{margin:0!important}.md-typeset div.arithmatex mjx-assistive-mml{height:0}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset del.critic,.md-typeset ins.critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{-webkit-box-decoration-break:clone;box-decoration-break:clone;color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment:before{content:"/* "}.md-typeset .critic.comment:after{content:" */"}.md-typeset .critic.block{box-shadow:none;display:block;margin:1em 0;overflow:auto;padding-left:.8rem;padding-right:.8rem}.md-typeset .critic.block>:first-child{margin-top:.5em}.md-typeset .critic.block>:last-child{margin-bottom:.5em}:root{--md-details-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset details{display:flow-root;overflow:visible;padding-top:0}.md-typeset details[open]>summary:after{transform:rotate(90deg)}.md-typeset details:not([open]){box-shadow:none;padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem}[dir=ltr] .md-typeset summary{padding-right:1.8rem}[dir=rtl] .md-typeset summary{padding-left:1.8rem}[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset summary{cursor:pointer;display:block;min-height:1rem;overflow:hidden}.md-typeset summary.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[dir=ltr] .md-typeset summary:after{right:.4rem}[dir=rtl] .md-typeset summary:after{left:.4rem}.md-typeset summary:after{background-color:currentcolor;content:"";height:1rem;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{transform:rotate(180deg)}.md-typeset summary::marker{display:none}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset .emojione,.md-typeset .gemoji,.md-typeset .twemoji{--md-icon-size:1.125em;display:inline-flex;height:var(--md-icon-size);vertical-align:text-top}.md-typeset .emojione svg,.md-typeset .gemoji svg,.md-typeset .twemoji svg{fill:currentcolor;max-height:100%;width:var(--md-icon-size)}.md-typeset .lg,.md-typeset .xl,.md-typeset .xxl,.md-typeset .xxxl{vertical-align:text-bottom}.md-typeset .middle{vertical-align:middle}.md-typeset .lg{--md-icon-size:1.5em}.md-typeset .xl{--md-icon-size:2.25em}.md-typeset .xxl{--md-icon-size:3em}.md-typeset .xxxl{--md-icon-size:4em}.highlight .o,.highlight .ow{color:var(--md-code-hl-operator-color)}.highlight .p{color:var(--md-code-hl-punctuation-color)}.highlight .cpf,.highlight .l,.highlight .s,.highlight .s1,.highlight .s2,.highlight .sb,.highlight .sc,.highlight .si,.highlight .ss{color:var(--md-code-hl-string-color)}.highlight .cp,.highlight .se,.highlight .sh,.highlight .sr,.highlight .sx{color:var(--md-code-hl-special-color)}.highlight .il,.highlight .m,.highlight .mb,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:var(--md-code-hl-number-color)}.highlight .k,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr,.highlight .kt{color:var(--md-code-hl-keyword-color)}.highlight .kc,.highlight .n{color:var(--md-code-hl-name-color)}.highlight .bp,.highlight .nb,.highlight .no{color:var(--md-code-hl-constant-color)}.highlight .nc,.highlight .ne,.highlight .nf,.highlight .nn{color:var(--md-code-hl-function-color)}.highlight .nd,.highlight .ni,.highlight .nl,.highlight .nt{color:var(--md-code-hl-keyword-color)}.highlight .c,.highlight .c1,.highlight .ch,.highlight .cm,.highlight .cs,.highlight .sd{color:var(--md-code-hl-comment-color)}.highlight .na,.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi{color:var(--md-code-hl-variable-color)}.highlight .ge,.highlight .gh,.highlight .go,.highlight .gp,.highlight .gr,.highlight .gs,.highlight .gt,.highlight .gu{color:var(--md-code-hl-generic-color)}.highlight .gd,.highlight .gi{border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight .gd{background-color:var(--md-typeset-del-color)}.highlight .gi{background-color:var(--md-typeset-ins-color)}.highlight .hll{background-color:var(--md-code-hl-color--light);box-shadow:2px 0 0 0 var(--md-code-hl-color) inset;display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em}.highlight span.filename{background-color:var(--md-code-bg-color);border-bottom:.05rem solid var(--md-default-fg-color--lightest);border-top-left-radius:.1rem;border-top-right-radius:.1rem;display:flow-root;font-size:.85em;font-weight:700;margin-top:1em;padding:.6617647059em 1.1764705882em;position:relative}.highlight span.filename+pre{margin-top:0}.highlight span.filename+pre>code{border-top-left-radius:0;border-top-right-radius:0}.highlight [data-linenos]:before{background-color:var(--md-code-bg-color);box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;color:var(--md-default-fg-color--light);content:attr(data-linenos);float:left;left:-1.1764705882em;margin-left:-1.1764705882em;margin-right:1.1764705882em;padding-left:1.1764705882em;position:sticky;-webkit-user-select:none;user-select:none;z-index:3}.highlight code a[id]{position:absolute;visibility:hidden}.highlight code[data-md-copying] .hll{display:contents}.highlight code[data-md-copying] .md-annotation{display:none}.highlighttable{display:flow-root}.highlighttable tbody,.highlighttable td{display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable th.filename{flex-grow:1;padding:0;text-align:left}.highlighttable th.filename span.filename{margin-top:0}.highlighttable .linenos{background-color:var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-top-left-radius:.1rem;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;user-select:none}.highlighttable .linenodiv{box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;padding-right:.5882352941em}.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.highlighttable .code{flex:1;min-width:0}.linenodiv a{color:inherit}.md-typeset .highlighttable{direction:ltr;margin:1em 0}.md-typeset .highlighttable>tbody>tr>.code>div>pre>code{border-bottom-left-radius:0;border-top-left-radius:0}.md-typeset .highlight+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset .highlight+.result:after{clear:both;content:"";display:block}@media screen and (max-width:44.984375em){.md-content__inner>.highlight{margin:1em -.8rem}.md-content__inner>.highlight>.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.code>div>pre>code,.md-content__inner>.highlight>.highlighttable>tbody>tr>.filename span.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.linenos,.md-content__inner>.highlight>pre>code{border-radius:0}.md-content__inner>.highlight+.result{border-left-width:0;border-radius:0;border-right-width:0;margin-left:-.8rem;margin-right:-.8rem}}.md-typeset .keys kbd:after,.md-typeset .keys kbd:before{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys span{color:var(--md-default-fg-color--light);padding:0 .2em}.md-typeset .keys .key-alt:before,.md-typeset .keys .key-left-alt:before,.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before,.md-typeset .keys .key-left-command:before,.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before,.md-typeset .keys .key-left-control:before,.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-meta:before,.md-typeset .keys .key-meta:before,.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-option:before,.md-typeset .keys .key-option:before,.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-shift:before,.md-typeset .keys .key-right-shift:before,.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-super:before,.md-typeset .keys .key-right-super:before,.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-windows:before,.md-typeset .keys .key-right-windows:before,.md-typeset .keys .key-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-arrow-down:before{content:"↓";padding-right:.4em}.md-typeset .keys .key-arrow-left:before{content:"←";padding-right:.4em}.md-typeset .keys .key-arrow-right:before{content:"→";padding-right:.4em}.md-typeset .keys .key-arrow-up:before{content:"↑";padding-right:.4em}.md-typeset .keys .key-backspace:before{content:"⌫";padding-right:.4em}.md-typeset .keys .key-backtab:before{content:"⇤";padding-right:.4em}.md-typeset .keys .key-caps-lock:before{content:"⇪";padding-right:.4em}.md-typeset .keys .key-clear:before{content:"⌧";padding-right:.4em}.md-typeset .keys .key-context-menu:before{content:"☰";padding-right:.4em}.md-typeset .keys .key-delete:before{content:"⌦";padding-right:.4em}.md-typeset .keys .key-eject:before{content:"⏏";padding-right:.4em}.md-typeset .keys .key-end:before{content:"⤓";padding-right:.4em}.md-typeset .keys .key-escape:before{content:"⎋";padding-right:.4em}.md-typeset .keys .key-home:before{content:"⤒";padding-right:.4em}.md-typeset .keys .key-insert:before{content:"⎀";padding-right:.4em}.md-typeset .keys .key-page-down:before{content:"⇟";padding-right:.4em}.md-typeset .keys .key-page-up:before{content:"⇞";padding-right:.4em}.md-typeset .keys .key-print-screen:before{content:"⎙";padding-right:.4em}.md-typeset .keys .key-tab:after{content:"⇥";padding-left:.4em}.md-typeset .keys .key-num-enter:after{content:"⌤";padding-left:.4em}.md-typeset .keys .key-enter:after{content:"⏎";padding-left:.4em}:root{--md-tabbed-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-tabbed-icon--next:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .tabbed-set{border-radius:.1rem;display:flex;flex-flow:column wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:target{--md-scroll-offset:0.625em}.md-typeset .tabbed-set>input.focus-visible~.tabbed-labels:before{background-color:var(--md-accent-fg-color)}.md-typeset .tabbed-labels{-ms-overflow-style:none;box-shadow:0 -.05rem var(--md-default-fg-color--lightest) inset;display:flex;max-width:100%;overflow:auto;scrollbar-width:none}@media print{.md-typeset .tabbed-labels{display:contents}}@media screen{.js .md-typeset .tabbed-labels{position:relative}.js .md-typeset .tabbed-labels:before{background:var(--md-default-fg-color);bottom:0;content:"";display:block;height:2px;left:0;position:absolute;transform:translateX(var(--md-indicator-x));transition:width 225ms,background-color .25s,transform .25s;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--md-indicator-width)}}.md-typeset .tabbed-labels::-webkit-scrollbar{display:none}.md-typeset .tabbed-labels>label{border-bottom:.1rem solid #0000;border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;font-size:.64rem;font-weight:700;padding:.78125em 1.25em .625em;scroll-margin-inline-start:1rem;transition:background-color .25s,color .25s;white-space:nowrap;width:auto}@media print{.md-typeset .tabbed-labels>label:first-child{order:1}.md-typeset .tabbed-labels>label:nth-child(2){order:2}.md-typeset .tabbed-labels>label:nth-child(3){order:3}.md-typeset .tabbed-labels>label:nth-child(4){order:4}.md-typeset .tabbed-labels>label:nth-child(5){order:5}.md-typeset .tabbed-labels>label:nth-child(6){order:6}.md-typeset .tabbed-labels>label:nth-child(7){order:7}.md-typeset .tabbed-labels>label:nth-child(8){order:8}.md-typeset .tabbed-labels>label:nth-child(9){order:9}.md-typeset .tabbed-labels>label:nth-child(10){order:10}.md-typeset .tabbed-labels>label:nth-child(11){order:11}.md-typeset .tabbed-labels>label:nth-child(12){order:12}.md-typeset .tabbed-labels>label:nth-child(13){order:13}.md-typeset .tabbed-labels>label:nth-child(14){order:14}.md-typeset .tabbed-labels>label:nth-child(15){order:15}.md-typeset .tabbed-labels>label:nth-child(16){order:16}.md-typeset .tabbed-labels>label:nth-child(17){order:17}.md-typeset .tabbed-labels>label:nth-child(18){order:18}.md-typeset .tabbed-labels>label:nth-child(19){order:19}.md-typeset .tabbed-labels>label:nth-child(20){order:20}}.md-typeset .tabbed-labels>label:hover{color:var(--md-default-fg-color)}.md-typeset .tabbed-labels>label>[href]:first-child{color:inherit}.md-typeset .tabbed-labels--linked>label{padding:0}.md-typeset .tabbed-labels--linked>label>a{display:block;padding:.78125em 1.25em .625em}.md-typeset .tabbed-content{width:100%}@media print{.md-typeset .tabbed-content{display:contents}}.md-typeset .tabbed-block{display:none}@media print{.md-typeset .tabbed-block{display:block}.md-typeset .tabbed-block:first-child{order:1}.md-typeset .tabbed-block:nth-child(2){order:2}.md-typeset .tabbed-block:nth-child(3){order:3}.md-typeset .tabbed-block:nth-child(4){order:4}.md-typeset .tabbed-block:nth-child(5){order:5}.md-typeset .tabbed-block:nth-child(6){order:6}.md-typeset .tabbed-block:nth-child(7){order:7}.md-typeset .tabbed-block:nth-child(8){order:8}.md-typeset .tabbed-block:nth-child(9){order:9}.md-typeset .tabbed-block:nth-child(10){order:10}.md-typeset .tabbed-block:nth-child(11){order:11}.md-typeset .tabbed-block:nth-child(12){order:12}.md-typeset .tabbed-block:nth-child(13){order:13}.md-typeset .tabbed-block:nth-child(14){order:14}.md-typeset .tabbed-block:nth-child(15){order:15}.md-typeset .tabbed-block:nth-child(16){order:16}.md-typeset .tabbed-block:nth-child(17){order:17}.md-typeset .tabbed-block:nth-child(18){order:18}.md-typeset .tabbed-block:nth-child(19){order:19}.md-typeset .tabbed-block:nth-child(20){order:20}}.md-typeset .tabbed-block>.highlight:first-child>pre,.md-typeset .tabbed-block>pre:first-child{margin:0}.md-typeset .tabbed-block>.highlight:first-child>pre>code,.md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child>.filename{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable{margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.filename span.filename,.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.linenos{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.code>div>pre>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child+.result{margin-top:-.125em}.md-typeset .tabbed-block>.tabbed-set{margin:0}.md-typeset .tabbed-button{align-self:center;border-radius:100%;color:var(--md-default-fg-color--light);cursor:pointer;display:block;height:.9rem;margin-top:.1rem;pointer-events:auto;transition:background-color .25s;width:.9rem}.md-typeset .tabbed-button:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset .tabbed-button:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-tabbed-icon--prev);mask-image:var(--md-tabbed-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color .25s,transform .25s;width:100%}.md-typeset .tabbed-control{background:linear-gradient(to right,var(--md-default-bg-color) 60%,#0000);display:flex;height:1.9rem;justify-content:start;pointer-events:none;position:absolute;transition:opacity 125ms;width:1.2rem}[dir=rtl] .md-typeset .tabbed-control{transform:rotate(180deg)}.md-typeset .tabbed-control[hidden]{opacity:0}.md-typeset .tabbed-control--next{background:linear-gradient(to left,var(--md-default-bg-color) 60%,#0000);justify-content:end;right:0}.md-typeset .tabbed-control--next .tabbed-button:after{-webkit-mask-image:var(--md-tabbed-icon--next);mask-image:var(--md-tabbed-icon--next)}@media screen and (max-width:44.984375em){[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels{margin:0 -.8rem;max-width:100vw;scroll-padding-inline-start:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels:after{content:""}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-right:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-left:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-right:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{width:2rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-left:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-right:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-left:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{width:2rem}}@media screen{.md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){color:var(--md-default-fg-color)}.md-typeset .no-js .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .no-js .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .no-js .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .no-js .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .no-js .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .no-js .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .no-js .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .no-js .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .no-js .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .no-js .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .no-js .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .no-js .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .no-js .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .no-js .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .no-js .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .no-js .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .no-js .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .no-js .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .no-js .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .no-js .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.no-js .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.no-js .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.no-js .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.no-js .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.no-js .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.no-js .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.no-js .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.no-js .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.no-js .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.no-js .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.no-js .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.no-js .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.no-js .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.no-js .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.no-js .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.no-js .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.no-js .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.no-js .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.no-js .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.no-js .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){border-color:var(--md-default-fg-color)}}.md-typeset .tabbed-set>input:first-child.focus-visible~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10).focus-visible~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11).focus-visible~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12).focus-visible~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13).focus-visible~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14).focus-visible~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15).focus-visible~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16).focus-visible~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17).focus-visible~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18).focus-visible~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19).focus-visible~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2).focus-visible~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20).focus-visible~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3).focus-visible~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4).focus-visible~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5).focus-visible~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6).focus-visible~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7).focus-visible~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8).focus-visible~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9).focus-visible~.tabbed-labels>:nth-child(9){color:var(--md-accent-fg-color)}.md-typeset .tabbed-set>input:first-child:checked~.tabbed-content>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-content>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-content>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-content>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-content>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-content>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-content>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-content>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-content>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-content>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-content>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-content>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-content>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-content>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-content>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-content>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-content>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-content>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-content>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-content>:nth-child(9){display:block}:root{--md-tasklist-icon:url('data:image/svg+xml;charset=utf-8,');--md-tasklist-icon--checked:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .task-list-item{list-style-type:none;position:relative}[dir=ltr] .md-typeset .task-list-item [type=checkbox]{left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}[dir=ltr] .md-typeset .task-list-indicator:before{left:-1.5em}[dir=rtl] .md-typeset .task-list-indicator:before{right:-1.5em}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lightest);content:"";height:1.25em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.15em;width:1.25em}.md-typeset [type=checkbox]:checked+.task-list-indicator:before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}:root>*{--md-mermaid-font-family:var(--md-text-font-family),sans-serif;--md-mermaid-edge-color:var(--md-code-fg-color);--md-mermaid-node-bg-color:var(--md-accent-fg-color--transparent);--md-mermaid-node-fg-color:var(--md-accent-fg-color);--md-mermaid-label-bg-color:var(--md-default-bg-color);--md-mermaid-label-fg-color:var(--md-code-fg-color);--md-mermaid-sequence-actor-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actor-fg-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-actor-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-actor-line-color:var(--md-default-fg-color--lighter);--md-mermaid-sequence-actorman-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actorman-line-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-box-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-box-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-label-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-label-fg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-loop-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-loop-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-loop-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-message-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-message-line-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-note-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-border-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-number-bg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-number-fg-color:var(--md-accent-bg-color)}.mermaid{line-height:normal;margin:1em 0}.md-typeset .grid{grid-gap:.4rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr));margin:1em 0}.md-typeset .grid.cards>ol,.md-typeset .grid.cards>ul{display:contents}.md-typeset .grid.cards>ol>li,.md-typeset .grid.cards>ul>li,.md-typeset .grid>.card{border:.05rem solid var(--md-default-fg-color--lightest);border-radius:.1rem;display:block;margin:0;padding:.8rem;transition:border .25s,box-shadow .25s}.md-typeset .grid.cards>ol>li:focus-within,.md-typeset .grid.cards>ol>li:hover,.md-typeset .grid.cards>ul>li:focus-within,.md-typeset .grid.cards>ul>li:hover,.md-typeset .grid>.card:focus-within,.md-typeset .grid>.card:hover{border-color:#0000;box-shadow:var(--md-shadow-z2)}.md-typeset .grid.cards>ol>li>hr,.md-typeset .grid.cards>ul>li>hr,.md-typeset .grid>.card>hr{margin-bottom:1em;margin-top:1em}.md-typeset .grid.cards>ol>li>:first-child,.md-typeset .grid.cards>ul>li>:first-child,.md-typeset .grid>.card>:first-child{margin-top:0}.md-typeset .grid.cards>ol>li>:last-child,.md-typeset .grid.cards>ul>li>:last-child,.md-typeset .grid>.card>:last-child{margin-bottom:0}.md-typeset .grid>*,.md-typeset .grid>.admonition,.md-typeset .grid>.highlight>*,.md-typeset .grid>.highlighttable,.md-typeset .grid>.md-typeset details,.md-typeset .grid>details,.md-typeset .grid>pre{margin-bottom:0;margin-top:0}.md-typeset .grid>.highlight>pre:only-child,.md-typeset .grid>.highlight>pre>code,.md-typeset .grid>.highlighttable,.md-typeset .grid>.highlighttable>tbody,.md-typeset .grid>.highlighttable>tbody>tr,.md-typeset .grid>.highlighttable>tbody>tr>.code,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre>code{height:100%}.md-typeset .grid>.tabbed-set{margin-bottom:0;margin-top:0}@media screen and (min-width:45em){[dir=ltr] .md-typeset .inline{float:left}[dir=rtl] .md-typeset .inline{float:right}[dir=ltr] .md-typeset .inline{margin-right:.8rem}[dir=rtl] .md-typeset .inline{margin-left:.8rem}.md-typeset .inline{margin-bottom:.8rem;margin-top:0;width:11.7rem}[dir=ltr] .md-typeset .inline.end{float:right}[dir=rtl] .md-typeset .inline.end{float:left}[dir=ltr] .md-typeset .inline.end{margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{margin-left:0;margin-right:.8rem}} \ No newline at end of file diff --git a/main/assets/stylesheets/main.10ba22f1.min.css.map b/main/assets/stylesheets/main.10ba22f1.min.css.map deleted file mode 100644 index 087936565..000000000 --- a/main/assets/stylesheets/main.10ba22f1.min.css.map +++ /dev/null @@ -1 +0,0 @@ -{"version":3,"sources":["src/templates/assets/stylesheets/main/components/_meta.scss","../../../../src/templates/assets/stylesheets/main.scss","src/templates/assets/stylesheets/main/_resets.scss","src/templates/assets/stylesheets/main/_colors.scss","src/templates/assets/stylesheets/main/_icons.scss","src/templates/assets/stylesheets/main/_typeset.scss","src/templates/assets/stylesheets/utilities/_break.scss","src/templates/assets/stylesheets/main/components/_author.scss","src/templates/assets/stylesheets/main/components/_banner.scss","src/templates/assets/stylesheets/main/components/_base.scss","src/templates/assets/stylesheets/main/components/_clipboard.scss","src/templates/assets/stylesheets/main/components/_consent.scss","src/templates/assets/stylesheets/main/components/_content.scss","src/templates/assets/stylesheets/main/components/_dialog.scss","src/templates/assets/stylesheets/main/components/_feedback.scss","src/templates/assets/stylesheets/main/components/_footer.scss","src/templates/assets/stylesheets/main/components/_form.scss","src/templates/assets/stylesheets/main/components/_header.scss","node_modules/material-design-color/material-color.scss","src/templates/assets/stylesheets/main/components/_nav.scss","src/templates/assets/stylesheets/main/components/_pagination.scss","src/templates/assets/stylesheets/main/components/_post.scss","src/templates/assets/stylesheets/main/components/_progress.scss","src/templates/assets/stylesheets/main/components/_search.scss","src/templates/assets/stylesheets/main/components/_select.scss","src/templates/assets/stylesheets/main/components/_sidebar.scss","src/templates/assets/stylesheets/main/components/_source.scss","src/templates/assets/stylesheets/main/components/_status.scss","src/templates/assets/stylesheets/main/components/_tabs.scss","src/templates/assets/stylesheets/main/components/_tag.scss","src/templates/assets/stylesheets/main/components/_tooltip.scss","src/templates/assets/stylesheets/main/components/_top.scss","src/templates/assets/stylesheets/main/components/_version.scss","src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss","src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss","src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss","src/templates/assets/stylesheets/main/integrations/_mermaid.scss","src/templates/assets/stylesheets/main/modifiers/_grid.scss","src/templates/assets/stylesheets/main/modifiers/_inline.scss"],"names":[],"mappings":"AA0CE,gBCixCF,CC/xCA,KAEE,6BAAA,CAAA,0BAAA,CAAA,qBAAA,CADA,qBDzBF,CC8BA,iBAGE,kBD3BF,CC8BE,gCANF,iBAOI,yBDzBF,CACF,CC6BA,KACE,QD1BF,CC8BA,qBAIE,uCD3BF,CC+BA,EACE,aAAA,CACA,oBD5BF,CCgCA,GAME,QAAA,CALA,kBAAA,CACA,aAAA,CACA,aAAA,CAEA,gBAAA,CADA,SD3BF,CCiCA,MACE,aD9BF,CCkCA,QAEE,eD/BF,CCmCA,IACE,iBDhCF,CCoCA,MAEE,uBAAA,CADA,gBDhCF,CCqCA,MAEE,eAAA,CACA,kBDlCF,CCsCA,OAKE,gBAAA,CACA,QAAA,CAHA,mBAAA,CACA,iBAAA,CAFA,QAAA,CADA,SD9BF,CCuCA,MACE,QAAA,CACA,YDpCF,CErDA,MAIE,6BAAA,CACA,oCAAA,CACA,mCAAA,CACA,0BAAA,CACA,sCAAA,CAGA,4BAAA,CACA,2CAAA,CACA,yBAAA,CACA,qCFmDF,CE7CA,+BAIE,kBF6CF,CE1CE,oHAEE,YF4CJ,CEnCA,qCAIE,eAAA,CAGA,+BAAA,CACA,sCAAA,CACA,wCAAA,CACA,yCAAA,CACA,0BAAA,CACA,sCAAA,CACA,wCAAA,CACA,yCAAA,CAGA,0BAAA,CACA,0BAAA,CAGA,0BAAA,CACA,mCAAA,CAGA,iCAAA,CACA,kCAAA,CACA,mCAAA,CACA,mCAAA,CACA,kCAAA,CACA,iCAAA,CACA,+CAAA,CACA,6DAAA,CACA,gEAAA,CACA,4DAAA,CACA,4DAAA,CACA,6DAAA,CAGA,6CAAA,CAGA,+CAAA,CAGA,gCAAA,CACA,gCAAA,CAGA,8BAAA,CACA,kCAAA,CACA,qCAAA,CAGA,iCAAA,CAGA,kCAAA,CACA,gDAAA,CAGA,mDAAA,CACA,mDAAA,CAGA,+BAAA,CACA,0BAAA,CAGA,yBAAA,CACA,qCAAA,CACA,uCAAA,CACA,8BAAA,CACA,oCAAA,CAGA,8DAAA,CAKA,8DAAA,CAKA,0DFKF,CG9HE,aAIE,iBAAA,CAHA,aAAA,CAEA,aAAA,CADA,YHmIJ,CIxIA,KACE,kCAAA,CACA,iCAAA,CAGA,uGAAA,CAKA,mFJyIF,CInIA,iBAIE,mCAAA,CACA,6BAAA,CAFA,sCJwIF,CIlIA,aAIE,4BAAA,CADA,sCJsIF,CI7HA,MACE,0NAAA,CACA,mNAAA,CACA,oNJgIF,CIzHA,YAGE,gCAAA,CAAA,kBAAA,CAFA,eAAA,CACA,eJ6HF,CIxHE,aAPF,YAQI,gBJ2HF,CACF,CIxHE,uGAME,iBAAA,CAAA,cJ0HJ,CItHE,eAKE,uCAAA,CAHA,aAAA,CAEA,eAAA,CAHA,iBJ6HJ,CIpHE,8BAPE,eAAA,CAGA,qBJ+HJ,CI3HE,eAEE,kBAAA,CAEA,eAAA,CAHA,oBJ0HJ,CIlHE,eAEE,gBAAA,CACA,eAAA,CAEA,qBAAA,CADA,eAAA,CAHA,mBJwHJ,CIhHE,kBACE,eJkHJ,CI9GE,eAEE,eAAA,CACA,qBAAA,CAFA,YJkHJ,CI5GE,8BAKE,uCAAA,CAFA,cAAA,CACA,eAAA,CAEA,qBAAA,CAJA,eJkHJ,CI1GE,eACE,wBJ4GJ,CIxGE,eAGE,+DAAA,CAFA,iBAAA,CACA,cJ2GJ,CItGE,cACE,+BAAA,CACA,qBJwGJ,CIrGI,mCAEE,sBJsGN,CIlGI,wCACE,+BJoGN,CIjGM,kDACE,uDJmGR,CI9FI,mBACE,kBAAA,CACA,iCJgGN,CI5FI,4BACE,uCAAA,CACA,oBJ8FN,CIzFE,iDAIE,6BAAA,CACA,aAAA,CAFA,2BJ6FJ,CIxFI,aARF,iDASI,oBJ6FJ,CACF,CIzFE,iBAIE,wCAAA,CACA,mBAAA,CACA,kCAAA,CAAA,0BAAA,CAJA,eAAA,CADA,uBAAA,CAEA,qBJ8FJ,CIxFI,qCAEE,uCAAA,CADA,YJ2FN,CIrFE,gBAEE,iBAAA,CACA,eAAA,CAFA,iBJyFJ,CIpFI,qBASE,kCAAA,CAAA,0BAAA,CADA,eAAA,CAPA,aAAA,CAEA,QAAA,CAIA,uCAAA,CAHA,aAAA,CAFA,oCAAA,CASA,yDAAA,CADA,oBAAA,CAJA,iBAAA,CADA,iBJ4FN,CInFM,2BACE,+CJqFR,CIjFM,wCAEE,YAAA,CADA,WJoFR,CI/EM,8CACE,oDJiFR,CI9EQ,oDACE,0CJgFV,CIzEE,gBAOE,4CAAA,CACA,mBAAA,CACA,mKACE,CANF,gCAAA,CAHA,oBAAA,CAEA,eAAA,CADA,uBAAA,CAIA,uBAAA,CADA,qBJ+EJ,CIpEE,iBAGE,6CAAA,CACA,kCAAA,CAAA,0BAAA,CAHA,aAAA,CACA,qBJwEJ,CIlEE,iBAGE,6DAAA,CADA,WAAA,CADA,oBJsEJ,CIhEE,kBACE,WJkEJ,CI9DE,oDAEE,qBJgEJ,CIlEE,oDAEE,sBJgEJ,CI5DE,iCACE,kBJiEJ,CIlEE,iCACE,mBJiEJ,CIlEE,iCAIE,2DJ8DJ,CIlEE,iCAIE,4DJ8DJ,CIlEE,uBAGE,uCAAA,CADA,aAAA,CAAA,cJgEJ,CI1DE,eACE,oBJ4DJ,CIxDE,kDAGE,kBJ0DJ,CI7DE,kDAGE,mBJ0DJ,CI7DE,8BAEE,SJ2DJ,CIvDI,0DACE,iBJ0DN,CItDI,oCACE,2BJyDN,CItDM,0CACE,2BJyDR,CIpDI,wDACE,kBJwDN,CIzDI,wDACE,mBJwDN,CIzDI,oCAEE,kBJuDN,CIpDM,kGAEE,aJwDR,CIpDM,0DACE,eJuDR,CInDM,4HAEE,kBJsDR,CIxDM,4HAEE,mBJsDR,CIxDM,oFACE,kBAAA,CAAA,eJuDR,CIhDE,yBAEE,mBJkDJ,CIpDE,yBAEE,oBJkDJ,CIpDE,eACE,mBAAA,CAAA,cJmDJ,CI9CE,kDAIE,WAAA,CADA,cJiDJ,CIzCI,4BAEE,oBJ2CN,CIvCI,6BAEE,oBJyCN,CIrCI,kCACE,YJuCN,CIlCE,mBACE,iBAAA,CAGA,eAAA,CADA,cAAA,CAEA,iBAAA,CAHA,yBAAA,CAAA,sBAAA,CAAA,iBJuCJ,CIjCI,uBACE,aAAA,CACA,aJmCN,CI9BE,uBAGE,iBAAA,CADA,eAAA,CADA,eJkCJ,CI5BE,mBAME,kBAAA,CALA,cJ+BJ,CItBE,+BAME,2CAAA,CACA,iDAAA,CACA,mBAAA,CAPA,oBAAA,CAGA,gBAAA,CAFA,cAAA,CACA,aAAA,CAEA,iBJ2BJ,CIrBI,aAXF,+BAYI,aJwBJ,CACF,CInBI,iCACE,gBJqBN,CIdM,8FACE,YJgBR,CIZM,4FACE,eJcR,CITI,8FACE,eJWN,CIRM,kHACE,gBJUR,CILI,kCAGE,eAAA,CAFA,cAAA,CACA,sBAAA,CAEA,kBJON,CIHI,kCAGE,qDAAA,CAFA,sBAAA,CACA,kBJMN,CIDI,wCACE,iCJGN,CIAM,8CACE,qDAAA,CACA,sDJER,CIGI,iCACE,iBJDN,CIME,wCACE,cJJJ,CIOI,wDAIE,gBJCN,CILI,wDAIE,iBJCN,CILI,8CAME,UAAA,CALA,oBAAA,CAEA,YAAA,CAKA,oDAAA,CAAA,4CAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CAHA,iCAAA,CAFA,0BAAA,CAHA,WJGN,CISI,oDACE,oDJPN,CIWI,mEACE,kDAAA,CACA,yDAAA,CAAA,iDJTN,CIaI,oEACE,kDAAA,CACA,0DAAA,CAAA,kDJXN,CIgBE,wBACE,iBAAA,CACA,eAAA,CACA,iBJdJ,CIkBE,mBACE,oBAAA,CAEA,kBAAA,CADA,eJfJ,CImBI,aANF,mBAOI,aJhBJ,CACF,CImBI,8BACE,aAAA,CAEA,QAAA,CACA,eAAA,CAFA,UJfN,CKpVI,0CDkXF,uBACE,iBJ1BF,CI6BE,4BACE,eJ3BJ,CACF,CMnhBE,uBAOE,kBAAA,CALA,aAAA,CACA,aAAA,CAEA,aAAA,CACA,eAAA,CALA,iBAAA,CAOA,sCACE,CALF,YNyhBJ,CMhhBI,2BACE,aNkhBN,CM9gBI,6BAME,+CAAA,CAFA,yCAAA,CAHA,eAAA,CACA,eAAA,CACA,kBAAA,CAEA,iBNihBN,CM5gBI,6BAEE,aAAA,CADA,YN+gBN,CMzgBE,wBACE,kBN2gBJ,CMxgBI,4BACE,mCAAA,CACA,uBN0gBN,CMtgBI,4DAEE,oBAAA,CADA,SNygBN,CMrgBM,oEACE,mBNugBR,CO7jBA,WAGE,0CAAA,CADA,+BAAA,CADA,aPkkBF,CO7jBE,aANF,WAOI,YPgkBF,CACF,CO7jBE,oBAEE,2CAAA,CADA,gCPgkBJ,CO3jBE,kBAGE,eAAA,CADA,iBAAA,CADA,eP+jBJ,COzjBE,6BACE,WP8jBJ,CO/jBE,6BACE,UP8jBJ,CO/jBE,mBAEE,aAAA,CACA,cAAA,CACA,uBP2jBJ,COxjBI,0BACE,YP0jBN,COtjBI,yBACE,UPwjBN,CQ7lBA,KASE,cAAA,CARA,WAAA,CACA,iBRimBF,CK7bI,oCGtKJ,KAaI,gBR0lBF,CACF,CKlcI,oCGtKJ,KAkBI,cR0lBF,CACF,CQrlBA,KASE,2CAAA,CAPA,YAAA,CACA,qBAAA,CAKA,eAAA,CAHA,eAAA,CAJA,iBAAA,CAGA,UR2lBF,CQnlBE,aAZF,KAaI,aRslBF,CACF,CKncI,0CGhJF,yBAII,cRmlBJ,CACF,CQ1kBA,SAEE,gBAAA,CAAA,iBAAA,CADA,eR8kBF,CQzkBA,cACE,YAAA,CACA,qBAAA,CACA,WR4kBF,CQzkBE,aANF,cAOI,aR4kBF,CACF,CQxkBA,SACE,WR2kBF,CQxkBE,gBACE,YAAA,CACA,WAAA,CACA,iBR0kBJ,CQrkBA,aACE,eAAA,CACA,sBRwkBF,CQ/jBA,WACE,YRkkBF,CQ7jBA,WAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,ORkkBF,CQ7jBE,uCACE,aR+jBJ,CQ3jBE,+BAEE,uCAAA,CADA,kBR8jBJ,CQxjBA,SASE,2CAAA,CACA,mBAAA,CAFA,gCAAA,CADA,gBAAA,CADA,YAAA,CAMA,SAAA,CADA,uCAAA,CANA,mBAAA,CAJA,cAAA,CAYA,2BAAA,CATA,URkkBF,CQtjBE,eAEE,SAAA,CAIA,uBAAA,CAHA,oEACE,CAHF,UR2jBJ,CQ7iBA,MACE,WRgjBF,CSzsBA,MACE,+PT2sBF,CSrsBA,cASE,mBAAA,CAFA,0CAAA,CACA,cAAA,CAFA,YAAA,CAIA,uCAAA,CACA,oBAAA,CAVA,iBAAA,CAEA,UAAA,CADA,QAAA,CAUA,qBAAA,CAPA,WAAA,CADA,STgtBF,CSrsBE,aAfF,cAgBI,YTwsBF,CACF,CSrsBE,kCAEE,uCAAA,CADA,YTwsBJ,CSnsBE,qBACE,uCTqsBJ,CSjsBE,wCACE,+BTmsBJ,CS9rBE,oBAME,6BAAA,CADA,UAAA,CAJA,aAAA,CAEA,cAAA,CACA,aAAA,CAGA,2CAAA,CAAA,mCAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CARA,aTwsBJ,CS5rBE,sBACE,cT8rBJ,CS3rBI,2BACE,2CT6rBN,CSvrBI,kEAEE,uDAAA,CADA,+BT0rBN,CUhwBA,mBACE,GACE,SAAA,CACA,0BVmwBF,CUhwBA,GACE,SAAA,CACA,uBVkwBF,CACF,CU9vBA,mBACE,GACE,SVgwBF,CU7vBA,GACE,SV+vBF,CACF,CUpvBE,qBASE,2BAAA,CADA,mCAAA,CAAA,2BAAA,CAFA,0BAAA,CADA,WAAA,CAEA,SAAA,CANA,cAAA,CACA,KAAA,CAEA,UAAA,CADA,SV4vBJ,CUlvBE,mBAcE,mDAAA,CANA,2CAAA,CACA,QAAA,CACA,mBAAA,CARA,QAAA,CASA,kDACE,CAPF,eAAA,CAEA,aAAA,CADA,SAAA,CALA,cAAA,CAGA,UAAA,CADA,SV6vBJ,CU9uBE,kBACE,aVgvBJ,CU5uBE,sBACE,YAAA,CACA,YV8uBJ,CU3uBI,oCACE,aV6uBN,CUxuBE,sBACE,mBV0uBJ,CUvuBI,6CACE,cVyuBN,CKnoBI,0CKvGA,6CAKI,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,UV2uBN,CACF,CUpuBE,kBACE,cVsuBJ,CWv0BA,YACE,WAAA,CAIA,WXu0BF,CWp0BE,mBAEE,qBAAA,CADA,iBXu0BJ,CK1qBI,sCMtJE,4EACE,kBXm0BN,CW/zBI,0JACE,mBXi0BN,CWl0BI,8EACE,kBXi0BN,CACF,CW5zBI,0BAGE,UAAA,CAFA,aAAA,CACA,YX+zBN,CW1zBI,+BACE,eX4zBN,CWtzBE,8BACE,WX2zBJ,CW5zBE,8BACE,UX2zBJ,CW5zBE,8BAIE,iBXwzBJ,CW5zBE,8BAIE,kBXwzBJ,CW5zBE,oBAGE,cAAA,CADA,SX0zBJ,CWrzBI,aAPF,oBAQI,YXwzBJ,CACF,CWrzBI,gCACE,yCXuzBN,CWnzBI,wBACE,cAAA,CACA,kBXqzBN,CWlzBM,kCACE,oBXozBR,CYr3BA,qBAeE,WZs3BF,CYr4BA,qBAeE,UZs3BF,CYr4BA,WAOE,2CAAA,CACA,mBAAA,CANA,YAAA,CAOA,8BAAA,CALA,iBAAA,CAMA,SAAA,CALA,mBAAA,CACA,mBAAA,CALA,cAAA,CAaA,0BAAA,CAHA,wCACE,CATF,SZk4BF,CYn3BE,aAlBF,WAmBI,YZs3BF,CACF,CYn3BE,mBAEE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,kEZs3BJ,CY/2BE,kBAEE,gCAAA,CADA,eZk3BJ,Cap5BA,aACE,gBAAA,CACA,iBbu5BF,Cap5BE,sBAGE,WAAA,CADA,QAAA,CADA,Sbw5BJ,Cal5BE,oBAEE,eAAA,CADA,ebq5BJ,Cah5BE,oBACE,iBbk5BJ,Ca94BE,mBAEE,YAAA,CACA,cAAA,CACA,6BAAA,CAHA,iBbm5BJ,Ca74BI,iDACE,yCb+4BN,Ca34BI,6BACE,iBb64BN,Cax4BE,mBAGE,uCAAA,CACA,cAAA,CAHA,aAAA,CACA,cAAA,CAGA,sBb04BJ,Cav4BI,gDACE,+Bby4BN,Car4BI,4BACE,0CAAA,CACA,mBbu4BN,Cal4BE,mBAEE,SAAA,CADA,iBAAA,CAKA,2BAAA,CAHA,8Dbq4BJ,Ca/3BI,qBAEE,aAAA,CADA,ebk4BN,Ca73BI,6BACE,SAAA,CACA,uBb+3BN,Cc78BA,WAEE,0CAAA,CADA,+Bdi9BF,Cc78BE,aALF,WAMI,Ydg9BF,CACF,Cc78BE,kBACE,6BAAA,CAEA,aAAA,CADA,adg9BJ,Cc58BI,gCACE,Yd88BN,Ccz8BE,iBAOE,eAAA,CANA,YAAA,CAKA,cAAA,CAGA,mBAAA,CAAA,eAAA,CADA,cAAA,CAGA,uCAAA,CADA,eAAA,CAEA,uBdu8BJ,Ccp8BI,8CACE,Uds8BN,Ccl8BI,+BACE,oBdo8BN,CKtzBI,0CSvIE,uBACE,adg8BN,Cc77BM,yCACE,Yd+7BR,CACF,Cc17BI,iCACE,gBd67BN,Cc97BI,iCACE,iBd67BN,Cc97BI,uBAEE,gBd47BN,Ccz7BM,iCACE,ed27BR,Ccr7BE,kBACE,WAAA,CAIA,eAAA,CADA,mBAAA,CAFA,6BAAA,CACA,cAAA,CAGA,kBdu7BJ,Ccn7BE,mBAEE,YAAA,CADA,ads7BJ,Ccj7BE,sBACE,gBAAA,CACA,Udm7BJ,Cc96BA,gBACE,gDdi7BF,Cc96BE,uBACE,YAAA,CACA,cAAA,CACA,6BAAA,CACA,adg7BJ,Cc56BE,kCACE,sCd86BJ,Cc36BI,gFACE,+Bd66BN,Ccr6BA,cAKE,wCAAA,CADA,gBAAA,CADA,iBAAA,CADA,eAAA,CADA,Ud46BF,CKh4BI,mCS7CJ,cASI,Udw6BF,CACF,Ccp6BE,yBACE,sCds6BJ,Cc/5BA,WACE,mBAAA,CACA,SAAA,CAEA,cAAA,CADA,qBdm6BF,CK/4BI,mCSvBJ,WAQI,edk6BF,CACF,Cc/5BE,iBACE,oBAAA,CAEA,aAAA,CACA,iBAAA,CAFA,Ydm6BJ,Cc95BI,wBACE,edg6BN,Cc55BI,qBAGE,iBAAA,CAFA,gBAAA,CACA,mBd+5BN,CerkCE,uBAME,kBAAA,CACA,mBAAA,CAHA,gCAAA,CACA,cAAA,CAJA,oBAAA,CAEA,eAAA,CADA,kBAAA,CAMA,gEfwkCJ,CelkCI,gCAEE,2CAAA,CACA,uCAAA,CAFA,gCfskCN,CehkCI,0DAEE,0CAAA,CACA,sCAAA,CAFA,+BfokCN,Ce7jCE,gCAKE,4BfkkCJ,CevkCE,gEAME,6BfikCJ,CevkCE,gCAME,4BfikCJ,CevkCE,sBAIE,6DAAA,CAGA,8BAAA,CAJA,eAAA,CAFA,aAAA,CACA,eAAA,CAMA,sCf+jCJ,Ce1jCI,wDACE,6CAAA,CACA,8Bf4jCN,CexjCI,+BACE,Uf0jCN,CgB7mCA,WAOE,2CAAA,CAGA,8CACE,CALF,gCAAA,CADA,aAAA,CAHA,MAAA,CADA,eAAA,CACA,OAAA,CACA,KAAA,CACA,ShBonCF,CgBzmCE,aAfF,WAgBI,YhB4mCF,CACF,CgBzmCE,mBAIE,2BAAA,CAHA,iEhB4mCJ,CgBrmCE,mBACE,kDACE,CAEF,kEhBqmCJ,CgB/lCE,kBAEE,kBAAA,CADA,YAAA,CAEA,ehBimCJ,CgB7lCE,mBAKE,kBAAA,CAEA,cAAA,CAHA,YAAA,CAIA,uCAAA,CALA,aAAA,CAFA,iBAAA,CAQA,uBAAA,CAHA,qBAAA,CAJA,ShBsmCJ,CgB5lCI,yBACE,UhB8lCN,CgB1lCI,iCACE,oBhB4lCN,CgBxlCI,uCAEE,uCAAA,CADA,YhB2lCN,CgBtlCI,2BAEE,YAAA,CADA,ahBylCN,CK3+BI,0CW/GA,2BAMI,YhBwlCN,CACF,CgBrlCM,8DAIE,iBAAA,CAHA,aAAA,CAEA,aAAA,CADA,UhBylCR,CKzgCI,mCWzEA,iCAII,YhBklCN,CACF,CgB/kCM,wCACE,YhBilCR,CgB7kCM,+CACE,oBhB+kCR,CKphCI,sCWtDA,iCAII,YhB0kCN,CACF,CgBrkCE,kBAEE,YAAA,CACA,cAAA,CAFA,iBAAA,CAIA,8DACE,CAFF,kBhBwkCJ,CgBlkCI,oCAGE,SAAA,CADA,mBAAA,CAKA,6BAAA,CAHA,8DACE,CAJF,UhBwkCN,CgB/jCM,8CACE,8BhBikCR,CgB5jCI,8BACE,ehB8jCN,CgBzjCE,4BAGE,gBAAA,CAAA,kBhB6jCJ,CgBhkCE,4BAGE,iBAAA,CAAA,iBhB6jCJ,CgBhkCE,kBACE,WAAA,CAGA,eAAA,CAFA,aAAA,CAGA,kBhB2jCJ,CgBxjCI,4CAGE,SAAA,CADA,mBAAA,CAKA,8BAAA,CAHA,8DACE,CAJF,UhB8jCN,CgBrjCM,sDACE,6BhBujCR,CgBnjCM,8DAGE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,8DACE,CAJF,ShByjCR,CgB9iCI,uCAGE,WAAA,CAFA,iBAAA,CACA,UhBijCN,CgB3iCE,mBACE,YAAA,CACA,aAAA,CACA,cAAA,CAEA,+CACE,CAFF,kBhB8iCJ,CgBxiCI,8DACE,WAAA,CACA,SAAA,CACA,oChB0iCN,CgBjiCI,yBACE,QhBmiCN,CgB9hCE,mBACE,YhBgiCJ,CK5lCI,mCW2DF,6BAQI,gBhBgiCJ,CgBxiCA,6BAQI,iBhBgiCJ,CgBxiCA,mBAKI,aAAA,CAEA,iBAAA,CADA,ahBkiCJ,CACF,CKpmCI,sCW2DF,6BAaI,kBhBgiCJ,CgB7iCA,6BAaI,mBhBgiCJ,CACF,CD/wCA,SAGE,uCAAA,CAFA,eAAA,CACA,eCmxCF,CD/wCE,eACE,mBAAA,CACA,cAAA,CAGA,eAAA,CADA,QAAA,CADA,SCmxCJ,CD7wCE,sCAEE,WAAA,CADA,iBAAA,CAAA,kBCgxCJ,CD3wCE,eACE,+BC6wCJ,CD1wCI,0CACE,+BC4wCN,CDtwCA,UAKE,wBkBaa,ClBZb,oBAAA,CAFA,UAAA,CAHA,oBAAA,CAEA,eAAA,CADA,0BAAA,CAAA,2BC6wCF,CkB/yCA,MACE,0MAAA,CACA,gMAAA,CACA,yNlBkzCF,CkB5yCA,QACE,eAAA,CACA,elB+yCF,CkB5yCE,eAKE,uCAAA,CAJA,aAAA,CAGA,eAAA,CADA,eAAA,CADA,eAAA,CAIA,sBlB8yCJ,CkB3yCI,+BACE,YlB6yCN,CkB1yCM,mCAEE,WAAA,CADA,UlB6yCR,CkBryCQ,sFAME,iBAAA,CALA,aAAA,CAGA,aAAA,CADA,cAAA,CAEA,kBAAA,CAHA,UlB2yCV,CkBhyCE,cAGE,eAAA,CADA,QAAA,CADA,SlBoyCJ,CkB9xCE,cAGE,sBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBAAA,CAEA,uBAAA,CADA,sBlBiyCJ,CkB7xCI,sBACE,uClB+xCN,CkBxxCM,6EAEE,+BlB0xCR,CkBrxCI,2BAIE,iBlBoxCN,CkBhxCI,4CACE,gBlBkxCN,CkBnxCI,4CACE,iBlBkxCN,CkB9wCI,kBAGE,iBAAA,CAFA,aAAA,CACA,YlBixCN,CkB5wCI,sGACE,+BAAA,CACA,clB8wCN,CkB1wCI,4BACE,uCAAA,CACA,oBlB4wCN,CkBxwCI,0CACE,YlB0wCN,CkBvwCM,yDAKE,6BAAA,CAJA,aAAA,CAEA,WAAA,CACA,qCAAA,CAAA,6BAAA,CAFA,UlB4wCR,CkBrwCM,kDACE,YlBuwCR,CkBjwCE,iCACE,YlBmwCJ,CkBhwCI,6CACE,WAAA,CAGA,WlBgwCN,CkB3vCE,cACE,alB6vCJ,CkBzvCE,gBACE,YlB2vCJ,CKztCI,0Ca3BA,0CASE,2CAAA,CAHA,YAAA,CACA,qBAAA,CACA,WAAA,CALA,MAAA,CADA,iBAAA,CACA,OAAA,CACA,KAAA,CACA,SlB0vCJ,CkB/uCI,+DACE,eAAA,CACA,elBivCN,CkB7uCI,gCAQE,qDAAA,CAHA,uCAAA,CAEA,cAAA,CALA,aAAA,CAEA,kBAAA,CADA,wBAAA,CAFA,iBAAA,CAKA,kBlBivCN,CkB5uCM,wDAGE,UlBkvCR,CkBrvCM,wDAGE,WlBkvCR,CkBrvCM,8CAIE,aAAA,CAEA,aAAA,CACA,YAAA,CANA,iBAAA,CACA,SAAA,CAGA,YlBgvCR,CkB3uCQ,oDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CAPA,UlBovCV,CkBxuCM,8CAGE,2CAAA,CACA,gEACE,CAJF,eAAA,CAKA,4BAAA,CAJA,kBlB6uCR,CkBtuCQ,2DACE,YlBwuCV,CkBnuCM,8CAGE,2CAAA,CADA,gCAAA,CADA,elBuuCR,CkBjuCM,yCAIE,aAAA,CAFA,UAAA,CAIA,YAAA,CADA,aAAA,CAJA,iBAAA,CACA,WAAA,CACA,SlBsuCR,CkB9tCI,+BACE,MlBguCN,CkB5tCI,+BACE,4DlB8tCN,CkB3tCM,qDACE,+BlB6tCR,CkB1tCQ,sHACE,+BlB4tCV,CkBttCI,+BAEE,YAAA,CADA,mBlBytCN,CkBrtCM,mCACE,elButCR,CkBntCM,6CACE,SlBqtCR,CkBjtCM,uDAGE,mBlBotCR,CkBvtCM,uDAGE,kBlBotCR,CkBvtCM,6CAIE,gBAAA,CAFA,aAAA,CADA,YlBstCR,CkBhtCQ,mDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CAPA,UlBytCV,CkBzsCM,+CACE,mBlB2sCR,CkBnsCM,4CAEE,wBAAA,CADA,elBssCR,CkBlsCQ,oEACE,mBlBosCV,CkBrsCQ,oEACE,oBlBosCV,CkBhsCQ,4EACE,iBlBksCV,CkBnsCQ,4EACE,kBlBksCV,CkB9rCQ,oFACE,mBlBgsCV,CkBjsCQ,oFACE,oBlBgsCV,CkB5rCQ,4FACE,mBlB8rCV,CkB/rCQ,4FACE,oBlB8rCV,CkBvrCE,mBACE,wBlByrCJ,CkBrrCE,wBACE,YAAA,CACA,SAAA,CAIA,0BAAA,CAHA,oElBwrCJ,CkBlrCI,kCACE,2BlBorCN,CkB/qCE,gCACE,SAAA,CAIA,uBAAA,CAHA,qElBkrCJ,CkB5qCI,8CAEE,kCAAA,CAAA,0BlB6qCN,CACF,CK52CI,0CauMA,0CACE,YlBwqCJ,CkBrqCI,yDACE,UlBuqCN,CkBnqCI,wDACE,YlBqqCN,CkBjqCI,kDACE,YlBmqCN,CkB9pCE,gBAIE,iDAAA,CADA,gCAAA,CAFA,aAAA,CACA,elBkqCJ,CACF,CKz6CM,+DagRF,6CACE,YlB4pCJ,CkBzpCI,4DACE,UlB2pCN,CkBvpCI,2DACE,YlBypCN,CkBrpCI,qDACE,YlBupCN,CACF,CKj6CI,mCa7JJ,QA6aI,oBlBqpCF,CkB/oCI,kCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SlBipCN,CkB5oCM,6CACE,uBlB8oCR,CkB1oCM,gDACE,YlB4oCR,CkBvoCI,2CACE,kBlB0oCN,CkB3oCI,2CACE,mBlB0oCN,CkB3oCI,iCAEE,oBlByoCN,CkBloCI,yDACE,kBlBooCN,CkBroCI,yDACE,iBlBooCN,CACF,CK17CI,sCa7JJ,QAydI,oBAAA,CACA,oDlBkoCF,CkB5nCI,gCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SlB8nCN,CkBznCM,8CACE,uBlB2nCR,CkBvnCM,8CACE,YlBynCR,CkBpnCI,yCACE,kBlBunCN,CkBxnCI,yCACE,mBlBunCN,CkBxnCI,+BAEE,oBlBsnCN,CkB/mCI,uDACE,kBlBinCN,CkBlnCI,uDACE,iBlBinCN,CkB5mCE,wBACE,YAAA,CACA,sBAAA,CAEA,SAAA,CACA,6FACE,CAHF,mBlBgnCJ,CkBxmCI,sCACE,elB0mCN,CkBrmCE,iFACE,sBAAA,CAEA,SAAA,CACA,4FACE,CAHF,kBlBymCJ,CkBhmCE,iDACE,elBkmCJ,CkB9lCE,6CACE,YlBgmCJ,CkB5lCE,uBACE,aAAA,CACA,elB8lCJ,CkB3lCI,kCACE,elB6lCN,CkBzlCI,qCACE,elB2lCN,CkBxlCM,0CACE,uClB0lCR,CkBtlCM,6DACE,mBlBwlCR,CkBplCM,yFAEE,YlBslCR,CkBjlCI,yCAEE,kBlBqlCN,CkBvlCI,yCAEE,mBlBqlCN,CkBvlCI,+BACE,aAAA,CAGA,SAAA,CADA,kBlBolCN,CkBhlCM,2DACE,SlBklCR,CkB5kCE,cAGE,kBAAA,CADA,YAAA,CAEA,gCAAA,CAHA,WlBilCJ,CkB3kCI,oBACE,uDlB6kCN,CkBzkCI,oBAME,6BAAA,CACA,kBAAA,CAFA,UAAA,CAJA,oBAAA,CAEA,WAAA,CAMA,2CAAA,CAAA,mCAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CAJA,yBAAA,CAJA,qBAAA,CAFA,UlBqlCN,CkBxkCM,8BACE,wBlB0kCR,CkBtkCM,kKAEE,uBlBukCR,CkBzjCI,2EACE,YlB8jCN,CkB3jCM,oDACE,alB6jCR,CkB1jCQ,kEAKE,qCAAA,CACA,qDAAA,CAFA,YAAA,CAHA,eAAA,CACA,KAAA,CACA,SlB+jCV,CkBzjCU,0FACE,mBlB2jCZ,CkBtjCQ,0EACE,QlBwjCV,CkBnjCM,sFACE,kBlBqjCR,CkBtjCM,sFACE,mBlBqjCR,CkBjjCM,kDACE,uClBmjCR,CkB7iCI,2CACE,sBAAA,CAEA,SAAA,CADA,kBlBgjCN,CkBviCI,qFAIE,mDlB0iCN,CkB9iCI,qFAIE,oDlB0iCN,CkB9iCI,2EACE,aAAA,CACA,oBAAA,CAGA,SAAA,CAFA,kBlB2iCN,CkBtiCM,yFAEE,gBAAA,CADA,gBlByiCR,CkBpiCM,0FACE,YlBsiCR,CACF,CmB1vDA,eAKE,eAAA,CACA,eAAA,CAJA,SnBiwDF,CmB1vDE,gCANA,kBAAA,CAFA,YAAA,CAGA,sBnBwwDF,CmBnwDE,iBAOE,mBAAA,CAFA,aAAA,CADA,gBAAA,CAEA,iBnB6vDJ,CmBxvDE,wBAEE,qDAAA,CADA,uCnB2vDJ,CmBtvDE,qBACE,6CnBwvDJ,CmBnvDI,sDAEE,uDAAA,CADA,+BnBsvDN,CmBlvDM,8DACE,+BnBovDR,CmB/uDI,mCACE,uCAAA,CACA,oBnBivDN,CmB7uDI,yBAKE,iBAAA,CADA,yCAAA,CAHA,aAAA,CAEA,eAAA,CADA,YnBkvDN,CoBlyDE,eAGE,+DAAA,CADA,oBAAA,CADA,qBpBuyDJ,CKlnDI,0CetLF,eAOI,YpBqyDJ,CACF,CoB/xDM,6BACE,oBpBiyDR,CoB3xDE,kBACE,YAAA,CACA,qBAAA,CACA,SAAA,CACA,qBpB6xDJ,CoBtxDI,0BACE,sBpBwxDN,CoBrxDM,gEACE,+BpBuxDR,CoBjxDE,gBAEE,uCAAA,CADA,epBoxDJ,CoB/wDE,kBACE,oBpBixDJ,CoB9wDI,mCAGE,kBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBpBgxDN,CoB5wDI,oCAIE,kBAAA,CAHA,mBAAA,CACA,kBAAA,CACA,SAAA,CAGA,QAAA,CADA,iBpB+wDN,CoB1wDI,0DACE,kBpB4wDN,CoB7wDI,0DACE,iBpB4wDN,CoBxwDI,iDACE,uBAAA,CAEA,YpBywDN,CoBpwDE,4BACE,YpBswDJ,CoB/vDA,YAGE,kBAAA,CAFA,YAAA,CAIA,eAAA,CAHA,SAAA,CAIA,eAAA,CAFA,UpBowDF,CoB/vDE,yBACE,WpBiwDJ,CoB1vDA,kBACE,YpB6vDF,CKrrDI,0CezEJ,kBAKI,wBpB6vDF,CACF,CoB1vDE,qCACE,WpB4vDJ,CKhtDI,sCe7CF,+CAKI,kBpB4vDJ,CoBjwDA,+CAKI,mBpB4vDJ,CACF,CKlsDI,0CerDJ,6BAMI,SAAA,CAFA,eAAA,CACA,UpByvDF,CoBtvDE,qDACE,gBpBwvDJ,CoBrvDE,gDACE,SpBuvDJ,CoBpvDE,4CACE,iBAAA,CAAA,kBpBsvDJ,CoBnvDE,2CAEE,WAAA,CADA,cpBsvDJ,CoBlvDE,2CACE,mBAAA,CACA,cAAA,CACA,SAAA,CACA,oBAAA,CAAA,iBpBovDJ,CoBjvDE,2CACE,SpBmvDJ,CoBhvDE,qCAEE,WAAA,CACA,eAAA,CAFA,epBovDJ,CACF,CqB95DA,MACE,qBAAA,CACA,yBrBi6DF,CqB35DA,aAME,qCAAA,CADA,cAAA,CAEA,0FACE,CAPF,cAAA,CACA,KAAA,CAaA,mDAAA,CACA,qBAAA,CAJA,wFACE,CATF,UAAA,CADA,SrBq6DF,CsBh7DA,MACE,igBtBm7DF,CsB76DA,WACE,iBtBg7DF,CKlxDI,mCiB/JJ,WAKI,etBg7DF,CACF,CsB76DE,kBACE,YtB+6DJ,CsB36DE,oBAEE,SAAA,CADA,StB86DJ,CK3wDI,0CiBpKF,8BAkBI,YtB26DJ,CsB77DA,8BAkBI,atB26DJ,CsB77DA,oBAYI,2CAAA,CACA,kBAAA,CAJA,WAAA,CACA,eAAA,CACA,mBAAA,CALA,iBAAA,CACA,SAAA,CAUA,uBAAA,CAHA,4CACE,CAPF,UtBq7DJ,CsBx6DI,+DACE,SAAA,CACA,oCtB06DN,CACF,CKjzDI,mCiBjJF,8BAyCI,MtBo6DJ,CsB78DA,8BAyCI,OtBo6DJ,CsB78DA,oBAoCI,0BAAA,CADA,cAAA,CADA,QAAA,CAHA,cAAA,CACA,KAAA,CAKA,sDACE,CALF,OtB46DJ,CsBj6DI,+DAME,YAAA,CACA,SAAA,CACA,4CACE,CARF,UtBs6DN,CACF,CKhzDI,0CiBxGA,+DAII,mBtBw5DN,CACF,CK91DM,+DiB/DF,+DASI,mBtBw5DN,CACF,CKn2DM,+DiB/DF,+DAcI,mBtBw5DN,CACF,CsBn5DE,kBAEE,kCAAA,CAAA,0BtBo5DJ,CKl0DI,0CiBpFF,4BAmBI,MtBg5DJ,CsBn6DA,4BAmBI,OtBg5DJ,CsBn6DA,kBAUI,QAAA,CAEA,SAAA,CADA,eAAA,CALA,cAAA,CACA,KAAA,CAWA,wBAAA,CALA,qGACE,CALF,OAAA,CADA,StB25DJ,CsB74DI,4BACE,yBtB+4DN,CsB34DI,6DAEE,WAAA,CACA,SAAA,CAMA,uBAAA,CALA,sGACE,CAJF,UtBi5DN,CACF,CK72DI,mCiBjEF,4BA2CI,WtB24DJ,CsBt7DA,4BA2CI,UtB24DJ,CsBt7DA,kBA6CI,eAAA,CAHA,iBAAA,CAIA,8CAAA,CAFA,atB04DJ,CACF,CK54DM,+DiBOF,6DAII,atBq4DN,CACF,CK33DI,sCiBfA,6DASI,atBq4DN,CACF,CsBh4DE,iBAIE,2CAAA,CACA,0BAAA,CAFA,aAAA,CAFA,iBAAA,CAKA,2CACE,CALF,StBs4DJ,CKx4DI,mCiBAF,iBAaI,0BAAA,CACA,mBAAA,CAFA,atBk4DJ,CsB73DI,uBACE,0BtB+3DN,CACF,CsB33DI,4DAEE,2CAAA,CACA,6BAAA,CACA,8BAAA,CAHA,gCtBg4DN,CsBx3DE,4BAKE,mBAAA,CAAA,oBtB63DJ,CsBl4DE,4BAKE,mBAAA,CAAA,oBtB63DJ,CsBl4DE,kBAQE,gBAAA,CAFA,eAAA,CAFA,WAAA,CAHA,iBAAA,CAMA,sBAAA,CAJA,UAAA,CADA,StBg4DJ,CsBv3DI,+BACE,qBtBy3DN,CsBr3DI,kEAEE,uCtBs3DN,CsBl3DI,6BACE,YtBo3DN,CKx5DI,0CiBaF,kBA8BI,eAAA,CADA,aAAA,CADA,UtBq3DJ,CACF,CKl7DI,mCiBgCF,4BAmCI,mBtBq3DJ,CsBx5DA,4BAmCI,oBtBq3DJ,CsBx5DA,kBAqCI,aAAA,CADA,etBo3DJ,CsBh3DI,+BACE,uCtBk3DN,CsB92DI,mCACE,gCtBg3DN,CsB52DI,6DACE,kBtB82DN,CsB32DM,8EACE,uCtB62DR,CsBz2DM,0EACE,WtB22DR,CACF,CsBr2DE,iBAIE,cAAA,CAHA,oBAAA,CAEA,aAAA,CAEA,kCACE,CAJF,YtB02DJ,CsBl2DI,uBACE,UtBo2DN,CsBh2DI,yCAGE,UtBm2DN,CsBt2DI,yCAGE,WtBm2DN,CsBt2DI,+BACE,iBAAA,CACA,SAAA,CAEA,StBk2DN,CsB/1DM,6CACE,oBtBi2DR,CKx8DI,0CiB+FA,yCAcI,UtBg2DN,CsB92DE,yCAcI,WtBg2DN,CsB92DE,+BAaI,StBi2DN,CsB71DM,+CACE,YtB+1DR,CACF,CKp+DI,mCiBkHA,+BAwBI,mBtB81DN,CsB31DM,8CACE,YtB61DR,CACF,CsBv1DE,8BAGE,WtB21DJ,CsB91DE,8BAGE,UtB21DJ,CsB91DE,oBAKE,mBAAA,CAJA,iBAAA,CACA,SAAA,CAEA,StB01DJ,CKh+DI,0CiBkIF,8BAUI,WtBy1DJ,CsBn2DA,8BAUI,UtBy1DJ,CsBn2DA,oBASI,StB01DJ,CACF,CsBt1DI,uCACE,iBtB41DN,CsB71DI,uCACE,kBtB41DN,CsB71DI,6BAEE,uCAAA,CACA,SAAA,CAIA,oBAAA,CAHA,+DtBy1DN,CsBn1DM,iDAEE,uCAAA,CADA,YtBs1DR,CsBj1DM,gGAGE,SAAA,CADA,mBAAA,CAEA,kBtBk1DR,CsB/0DQ,sGACE,UtBi1DV,CsB10DE,8BAOE,mBAAA,CAAA,oBtBi1DJ,CsBx1DE,8BAOE,mBAAA,CAAA,oBtBi1DJ,CsBx1DE,oBAIE,kBAAA,CAKA,yCAAA,CANA,YAAA,CAKA,eAAA,CAFA,WAAA,CAKA,SAAA,CAVA,iBAAA,CACA,KAAA,CAUA,uBAAA,CAFA,kBAAA,CALA,UtBm1DJ,CK1hEI,mCiBkMF,8BAgBI,mBtB60DJ,CsB71DA,8BAgBI,oBtB60DJ,CsB71DA,oBAiBI,etB40DJ,CACF,CsBz0DI,+DACE,SAAA,CACA,0BtB20DN,CsBt0DE,6BAKE,+BtBy0DJ,CsB90DE,0DAME,gCtBw0DJ,CsB90DE,6BAME,+BtBw0DJ,CsB90DE,mBAIE,eAAA,CAHA,iBAAA,CAEA,UAAA,CADA,StB40DJ,CKzhEI,0CiB2MF,mBAWI,QAAA,CADA,UtBy0DJ,CACF,CKljEI,mCiB8NF,mBAiBI,SAAA,CADA,UAAA,CAEA,sBtBw0DJ,CsBr0DI,8DACE,8BAAA,CACA,StBu0DN,CACF,CsBl0DE,uBASE,kCAAA,CAAA,0BAAA,CAFA,2CAAA,CANA,WAAA,CACA,eAAA,CAIA,kBtBm0DJ,CsB7zDI,iEAZF,uBAaI,uBtBg0DJ,CACF,CK/lEM,+DiBiRJ,uBAkBI,atBg0DJ,CACF,CK9kEI,sCiB2PF,uBAuBI,atBg0DJ,CACF,CKnlEI,mCiB2PF,uBA4BI,YAAA,CAEA,yDAAA,CADA,oBtBi0DJ,CsB7zDI,kEACE,etB+zDN,CsB3zDI,6BACE,+CtB6zDN,CsBzzDI,0CAEE,YAAA,CADA,WtB4zDN,CsBvzDI,gDACE,oDtByzDN,CsBtzDM,sDACE,0CtBwzDR,CACF,CsBjzDA,kBACE,gCAAA,CACA,qBtBozDF,CsBjzDE,wBAKE,qDAAA,CADA,uCAAA,CAFA,gBAAA,CACA,kBAAA,CAFA,eAAA,CAKA,uBtBmzDJ,CKvnEI,mCiB8TF,kCAUI,mBtBmzDJ,CsB7zDA,kCAUI,oBtBmzDJ,CACF,CsB/yDE,wBAGE,eAAA,CADA,QAAA,CADA,SAAA,CAIA,wBAAA,CAAA,gBtBgzDJ,CsB5yDE,wBACE,yDtB8yDJ,CsB3yDI,oCACE,etB6yDN,CsBxyDE,wBACE,aAAA,CACA,YAAA,CAEA,uBAAA,CADA,gCtB2yDJ,CsBvyDI,4DACE,uDtByyDN,CsBryDI,gDACE,mBtBuyDN,CsBlyDE,gCAKE,cAAA,CADA,aAAA,CAEA,YAAA,CALA,eAAA,CAMA,uBAAA,CALA,KAAA,CACA,StBwyDJ,CsBjyDI,wCACE,YtBmyDN,CsB9xDI,wDACE,YtBgyDN,CsB5xDI,oCAGE,+BAAA,CADA,gBAAA,CADA,mBAAA,CAGA,2CtB8xDN,CKzqEI,mCiBuYA,8CAUI,mBtB4xDN,CsBtyDE,8CAUI,oBtB4xDN,CACF,CsBxxDI,oFAEE,uDAAA,CADA,+BtB2xDN,CsBrxDE,sCACE,2CtBuxDJ,CsBlxDE,2BAGE,eAAA,CADA,eAAA,CADA,iBtBsxDJ,CK1rEI,mCiBmaF,qCAOI,mBtBoxDJ,CsB3xDA,qCAOI,oBtBoxDJ,CACF,CsBhxDE,kCAEE,MtBsxDJ,CsBxxDE,kCAEE,OtBsxDJ,CsBxxDE,wBAME,uCAAA,CAFA,aAAA,CACA,YAAA,CAJA,iBAAA,CAEA,YtBqxDJ,CKprEI,0CiB4ZF,wBAUI,YtBkxDJ,CACF,CsB/wDI,8BAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,+CAAA,CAAA,uCAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CAPA,UtBwxDN,CsB9wDM,wCACE,oBtBgxDR,CsB1wDE,8BAGE,uCAAA,CAFA,gBAAA,CACA,etB6wDJ,CsBzwDI,iCAKE,gCAAA,CAHA,eAAA,CACA,eAAA,CACA,eAAA,CAHA,etB+wDN,CsBxwDM,sCACE,oBtB0wDR,CsBrwDI,iCAKE,gCAAA,CAHA,gBAAA,CACA,eAAA,CACA,eAAA,CAHA,atB2wDN,CsBpwDM,sCACE,oBtBswDR,CsBhwDE,yBAKE,gCAAA,CAJA,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,atBqwDJ,CsB9vDE,uBAGE,wBAAA,CAFA,+BAAA,CACA,yBtBiwDJ,CuBr6EA,WACE,iBAAA,CACA,SvBw6EF,CuBr6EE,kBAOE,2CAAA,CACA,mBAAA,CACA,8BAAA,CAHA,gCAAA,CAHA,QAAA,CAEA,gBAAA,CADA,YAAA,CAMA,SAAA,CATA,iBAAA,CACA,sBAAA,CAaA,mCAAA,CAJA,oEvBw6EJ,CuBj6EI,6EACE,gBAAA,CACA,SAAA,CAKA,+BAAA,CAJA,8EvBo6EN,CuB55EI,wBAWE,+BAAA,CAAA,8CAAA,CAFA,6BAAA,CAAA,8BAAA,CACA,YAAA,CAFA,UAAA,CAHA,QAAA,CAFA,QAAA,CAIA,kBAAA,CADA,iBAAA,CALA,iBAAA,CACA,KAAA,CAEA,OvBq6EN,CuBz5EE,iBAOE,mBAAA,CAFA,eAAA,CACA,oBAAA,CAHA,QAAA,CAFA,kBAAA,CAGA,aAAA,CAFA,SvBg6EJ,CuBv5EE,iBACE,kBvBy5EJ,CuBr5EE,2BAGE,kBAAA,CAAA,oBvB25EJ,CuB95EE,2BAGE,mBAAA,CAAA,mBvB25EJ,CuB95EE,iBAIE,cAAA,CAHA,aAAA,CAIA,YAAA,CAIA,uBAAA,CAHA,2CACE,CALF,UvB45EJ,CuBl5EI,8CACE,+BvBo5EN,CuBh5EI,uBACE,qDvBk5EN,CwBt+EA,YAIE,qBAAA,CADA,aAAA,CAGA,gBAAA,CALA,eAAA,CACA,UAAA,CAGA,axB0+EF,CwBt+EE,aATF,YAUI,YxBy+EF,CACF,CK3zEI,0CmB3KF,+BAeI,axBo+EJ,CwBn/EA,+BAeI,cxBo+EJ,CwBn/EA,qBAUI,2CAAA,CAHA,aAAA,CAEA,WAAA,CALA,cAAA,CACA,KAAA,CASA,uBAAA,CAHA,iEACE,CAJF,aAAA,CAFA,SxB6+EJ,CwBj+EI,mEACE,8BAAA,CACA,6BxBm+EN,CwBh+EM,6EACE,8BxBk+ER,CwB79EI,6CAEE,QAAA,CAAA,MAAA,CACA,QAAA,CAEA,eAAA,CAJA,iBAAA,CACA,OAAA,CAEA,qBAAA,CAFA,KxBk+EN,CACF,CK12EI,sCmBtKJ,YAuDI,QxB69EF,CwB19EE,mBACE,WxB49EJ,CwBx9EE,6CACE,UxB09EJ,CACF,CwBt9EE,uBACE,YAAA,CACA,OxBw9EJ,CKz3EI,mCmBjGF,uBAMI,QxBw9EJ,CwBr9EI,8BACE,WxBu9EN,CwBn9EI,qCACE,axBq9EN,CwBj9EI,+CACE,kBxBm9EN,CACF,CwB98EE,wBAUE,uBAAA,CANA,kCAAA,CAAA,0BAAA,CAHA,cAAA,CACA,eAAA,CASA,yDAAA,CAFA,oBxB68EJ,CwBx8EI,2CAEE,YAAA,CADA,WxB28EN,CwBt8EI,mEACE,+CxBw8EN,CwBr8EM,qHACE,oDxBu8ER,CwBp8EQ,iIACE,0CxBs8EV,CwBv7EE,wCAGE,wBACE,qBxBu7EJ,CwBn7EE,6BACE,kCxBq7EJ,CwBt7EE,6BACE,iCxBq7EJ,CACF,CKj5EI,0CmB5BF,YAME,0BAAA,CADA,QAAA,CAEA,SAAA,CANA,cAAA,CACA,KAAA,CAMA,sDACE,CALF,OAAA,CADA,SxBs7EF,CwB36EE,4CAEE,WAAA,CACA,SAAA,CACA,4CACE,CAJF,UxBg7EJ,CACF,CyB7lFA,iBACE,GACE,QzB+lFF,CyB5lFA,GACE,azB8lFF,CACF,CyB1lFA,gBACE,GACE,SAAA,CACA,0BzB4lFF,CyBzlFA,IACE,SzB2lFF,CyBxlFA,GACE,SAAA,CACA,uBzB0lFF,CACF,CyBllFA,MACE,+eAAA,CACA,ygBAAA,CACA,mmBAAA,CACA,sfzBolFF,CyB9kFA,WAOE,kCAAA,CAAA,0BAAA,CANA,aAAA,CACA,gBAAA,CACA,eAAA,CAEA,uCAAA,CAGA,uBAAA,CAJA,kBzBolFF,CyB7kFE,iBACE,UzB+kFJ,CyB3kFE,iBACE,oBAAA,CAEA,aAAA,CACA,qBAAA,CAFA,UzB+kFJ,CyB1kFI,+BACE,iBzB6kFN,CyB9kFI,+BACE,kBzB6kFN,CyB9kFI,qBAEE,gBzB4kFN,CyBxkFI,kDACE,iBzB2kFN,CyB5kFI,kDACE,kBzB2kFN,CyB5kFI,kDAEE,iBzB0kFN,CyB5kFI,kDAEE,kBzB0kFN,CyBrkFE,iCAGE,iBzB0kFJ,CyB7kFE,iCAGE,kBzB0kFJ,CyB7kFE,uBACE,oBAAA,CACA,6BAAA,CAEA,eAAA,CACA,sBAAA,CACA,qBzBukFJ,CyBnkFE,kBACE,YAAA,CAMA,gBAAA,CALA,SAAA,CAMA,oBAAA,CAHA,gBAAA,CAIA,WAAA,CAHA,eAAA,CAFA,SAAA,CADA,UzB2kFJ,CyBlkFI,iDACE,4BzBokFN,CyB/jFE,iBACE,eAAA,CACA,sBzBikFJ,CyB9jFI,gDACE,2BzBgkFN,CyB5jFI,kCAIE,kBzBokFN,CyBxkFI,kCAIE,iBzBokFN,CyBxkFI,wBAOE,6BAAA,CADA,UAAA,CALA,oBAAA,CAEA,YAAA,CAKA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CALA,uBAAA,CAHA,WzBskFN,CyB1jFI,iCACE,azB4jFN,CyBxjFI,iCACE,gDAAA,CAAA,wCzB0jFN,CyBtjFI,+BACE,8CAAA,CAAA,sCzBwjFN,CyBpjFI,+BACE,8CAAA,CAAA,sCzBsjFN,CyBljFI,sCACE,qDAAA,CAAA,6CzBojFN,CyB9iFA,gBACE,YzBijFF,CyB9iFE,gCAIE,kBzBkjFJ,CyBtjFE,gCAIE,iBzBkjFJ,CyBtjFE,sBAGE,kBAAA,CAGA,uCAAA,CALA,mBAAA,CAIA,gBAAA,CAHA,SzBojFJ,CyB7iFI,+BACE,aAAA,CACA,oBzB+iFN,CyB3iFI,2CACE,UzB8iFN,CyB/iFI,2CACE,WzB8iFN,CyB/iFI,iCAEE,kBzB6iFN,CyBziFI,0BACE,WzB2iFN,C0BluFA,MACE,mSAAA,CACA,oVAAA,CACA,mOAAA,CACA,qZ1BquFF,C0B5tFE,iBAME,kDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,cAAA,CAIA,mCAAA,CAAA,2BAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CANA,0BAAA,CAFA,a1BuuFJ,C0B3tFE,uBACE,6B1B6tFJ,C0BztFE,sBACE,wCAAA,CAAA,gC1B2tFJ,C0BvtFE,6BACE,+CAAA,CAAA,uC1BytFJ,C0BrtFE,4BACE,8CAAA,CAAA,sC1ButFJ,C2BlwFA,SASE,2CAAA,CADA,gCAAA,CAJA,aAAA,CAGA,eAAA,CADA,aAAA,CADA,UAAA,CAFA,S3BywFF,C2BhwFE,aAZF,SAaI,Y3BmwFF,CACF,CKxlFI,0CsBzLJ,SAkBI,Y3BmwFF,CACF,C2BhwFE,iBACE,mB3BkwFJ,C2B9vFE,yBAIE,iB3BqwFJ,C2BzwFE,yBAIE,kB3BqwFJ,C2BzwFE,eAQE,eAAA,CAPA,YAAA,CAMA,eAAA,CAJA,QAAA,CAEA,aAAA,CAHA,SAAA,CAWA,oBAAA,CAPA,kB3BmwFJ,C2BzvFI,kCACE,Y3B2vFN,C2BtvFE,eACE,aAAA,CACA,kBAAA,CAAA,mB3BwvFJ,C2BrvFI,sCACE,aAAA,CACA,S3BuvFN,C2BjvFE,eAOE,kCAAA,CAAA,0BAAA,CANA,YAAA,CAEA,eAAA,CADA,gBAAA,CAMA,UAAA,CAJA,uCAAA,CACA,oBAAA,CAIA,8D3BkvFJ,C2B7uFI,0CACE,aAAA,CACA,S3B+uFN,C2B3uFI,6BAEE,kB3B8uFN,C2BhvFI,6BAEE,iB3B8uFN,C2BhvFI,mBAGE,iBAAA,CAFA,Y3B+uFN,C2BxuFM,2CACE,qB3B0uFR,C2B3uFM,2CACE,qB3B6uFR,C2B9uFM,2CACE,qB3BgvFR,C2BjvFM,2CACE,qB3BmvFR,C2BpvFM,2CACE,oB3BsvFR,C2BvvFM,2CACE,qB3ByvFR,C2B1vFM,2CACE,qB3B4vFR,C2B7vFM,2CACE,qB3B+vFR,C2BhwFM,4CACE,qB3BkwFR,C2BnwFM,4CACE,oB3BqwFR,C2BtwFM,4CACE,qB3BwwFR,C2BzwFM,4CACE,qB3B2wFR,C2B5wFM,4CACE,qB3B8wFR,C2B/wFM,4CACE,qB3BixFR,C2BlxFM,4CACE,oB3BoxFR,C2B9wFI,gCACE,SAAA,CAIA,yBAAA,CAHA,wC3BixFN,C4Bp3FA,MACE,wS5Bu3FF,C4B92FE,mCACE,mBAAA,CACA,cAAA,CACA,QAAA,CAEA,mBAAA,CADA,kB5Bk3FJ,C4B72FE,oBAGE,kBAAA,CAOA,+CAAA,CACA,oBAAA,CAVA,mBAAA,CAIA,gBAAA,CACA,0BAAA,CACA,eAAA,CALA,QAAA,CAOA,qBAAA,CADA,eAAA,CAJA,wB5Bs3FJ,C4B52FI,0BAGE,uCAAA,CAFA,aAAA,CACA,YAAA,CAEA,6C5B82FN,C4Bz2FM,gEAEE,0CAAA,CADA,+B5B42FR,C4Bt2FI,yBACE,uB5Bw2FN,C4Bh2FI,gCAME,oDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAKA,qCAAA,CAAA,6BAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CAJA,iCAAA,CAHA,0BAAA,CAFA,W5B22FN,C4B91FI,wFACE,0C5Bg2FN,C6B16FA,iBACE,GACE,oB7B66FF,C6B16FA,IACE,kB7B46FF,C6Bz6FA,GACE,oB7B26FF,CACF,C6Bn6FA,MACE,0NAAA,CACA,uPAAA,CACA,wB7Bq6FF,C6B/5FA,YA6BE,kCAAA,CAAA,0BAAA,CAVA,2CAAA,CACA,mBAAA,CACA,8BAAA,CAHA,gCAAA,CADA,sCAAA,CAdA,+IACE,CAYF,8BAAA,CAMA,SAAA,CArBA,iBAAA,CACA,uBAAA,CAyBA,4BAAA,CAJA,uDACE,CATF,6BAAA,CADA,S7Bm6FF,C6Bj5FE,oBAEE,SAAA,CAKA,uBAAA,CAJA,2EACE,CAHF,S7Bs5FJ,C6B54FE,oBAEE,eAAA,CACA,wBAAA,CAAA,gBAAA,CAFA,U7Bg5FJ,C6B34FI,6CACE,qC7B64FN,C6Bz4FI,uCAEE,eAAA,CADA,mB7B44FN,C6Bt4FI,6BACE,Y7Bw4FN,C6Bn4FE,8CACE,sC7Bq4FJ,C6Bj4FE,mBAEE,gBAAA,CADA,a7Bo4FJ,C6Bh4FI,2CACE,Y7Bk4FN,C6B93FI,0CACE,e7Bg4FN,C6Bx3FA,eACE,eAAA,CAGA,YAAA,CADA,0BAAA,CADA,kB7B63FF,C6Bx3FE,yBACE,a7B03FJ,C6Bt3FE,oBACE,sCAAA,CACA,iB7Bw3FJ,C6Bp3FE,6BACE,oBAAA,CAGA,gB7Bo3FJ,C6Bh3FE,sBAmBE,mBAAA,CAbA,cAAA,CAHA,oBAAA,CACA,gBAAA,CAAA,iBAAA,CAIA,YAAA,CAUA,eAAA,CAjBA,iBAAA,CAMA,wBAAA,CAAA,gBAAA,CAFA,uBAAA,CAHA,S7B03FJ,C6Bh3FI,qCACE,uB7Bk3FN,C6Bz2FI,cAtBF,sBAuBI,W7B42FJ,C6Bz2FI,wCACE,2B7B22FN,C6Bv2FI,6BAOE,qCAAA,CACA,+CAAA,CAAA,uC7B42FN,C6Bl2FI,yDAZE,UAAA,CADA,YAAA,CAIA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CACA,SAAA,CAEA,WAAA,CADA,U7Bg4FN,C6Bj3FI,4BAOE,oDAAA,CAMA,4CAAA,CAAA,oCAAA,CADA,uBAAA,CAJA,+C7By2FN,C6B91FM,gDACE,uB7Bg2FR,C6B51FM,mFACE,0C7B81FR,CACF,C6Bz1FI,0CAGE,2BAAA,CADA,uBAAA,CADA,S7B61FN,C6Bv1FI,8CACE,oB7By1FN,C6Bt1FM,aAJF,8CASI,8CAAA,CACA,iBAAA,CAHA,gCAAA,CADA,eAAA,CADA,cAAA,CAGA,kB7B21FN,C6Bt1FM,oDACE,mC7Bw1FR,CACF,C6B50FE,gCAEE,iBAAA,CADA,e7Bg1FJ,C6B50FI,mCACE,iB7B80FN,C6B30FM,oDAGE,a7By1FR,C6B51FM,oDAGE,c7By1FR,C6B51FM,0CAcE,8CAAA,CACA,iBAAA,CALA,gCAAA,CAEA,oBAAA,CACA,qBAAA,CANA,iBAAA,CACA,eAAA,CAHA,UAAA,CAIA,gBAAA,CALA,aAAA,CAEA,cAAA,CALA,iBAAA,CAUA,iBAAA,CATA,S7B01FR,C8BxmGA,kBAME,e9BonGF,C8B1nGA,kBAME,gB9BonGF,C8B1nGA,QAUE,2CAAA,CACA,oBAAA,CAEA,8BAAA,CALA,uCAAA,CACA,cAAA,CALA,aAAA,CAGA,eAAA,CAKA,YAAA,CAPA,mBAAA,CAJA,cAAA,CACA,UAAA,CAiBA,yBAAA,CALA,mGACE,CAZF,S9BunGF,C8BpmGE,aAtBF,QAuBI,Y9BumGF,CACF,C8BpmGE,kBACE,wB9BsmGJ,C8BlmGE,gBAEE,SAAA,CADA,mBAAA,CAGA,+BAAA,CADA,uB9BqmGJ,C8BjmGI,0BACE,8B9BmmGN,C8B9lGE,4BAEE,0CAAA,CADA,+B9BimGJ,C8B5lGE,YACE,oBAAA,CACA,oB9B8lGJ,C+BnpGA,oBACE,GACE,mB/BspGF,CACF,C+B9oGA,MACE,wf/BgpGF,C+B1oGA,YACE,aAAA,CAEA,eAAA,CADA,a/B8oGF,C+B1oGE,+BAOE,kBAAA,CAAA,kB/B2oGJ,C+BlpGE,+BAOE,iBAAA,CAAA,mB/B2oGJ,C+BlpGE,qBAQE,aAAA,CACA,cAAA,CACA,YAAA,CATA,iBAAA,CAKA,U/B4oGJ,C+BroGI,qCAIE,iB/B6oGN,C+BjpGI,qCAIE,kB/B6oGN,C+BjpGI,2BAME,6BAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,yCAAA,CAAA,iCAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CARA,W/B+oGN,C+BloGE,kBAUE,2CAAA,CACA,mBAAA,CACA,8BAAA,CAJA,gCAAA,CACA,oBAAA,CAHA,kBAAA,CAFA,YAAA,CASA,SAAA,CANA,aAAA,CAFA,SAAA,CAJA,iBAAA,CAgBA,4BAAA,CAfA,UAAA,CAYA,+CACE,CAZF,S/BgpGJ,C+B/nGI,+EACE,gBAAA,CACA,SAAA,CACA,sC/BioGN,C+B3nGI,qCAEE,oCACE,gC/B4nGN,C+BxnGI,2CACE,c/B0nGN,CACF,C+BrnGE,kBACE,kB/BunGJ,C+BnnGE,4BAGE,kBAAA,CAAA,oB/B0nGJ,C+B7nGE,4BAGE,mBAAA,CAAA,mB/B0nGJ,C+B7nGE,kBAKE,cAAA,CAJA,aAAA,CAKA,YAAA,CAIA,uBAAA,CAHA,2CACE,CAJF,kBAAA,CAFA,U/B2nGJ,C+BhnGI,gDACE,+B/BknGN,C+B9mGI,wBACE,qD/BgnGN,CgChtGA,MAEI,uWAAA,CAAA,8WAAA,CAAA,sPAAA,CAAA,8xBAAA,CAAA,0MAAA,CAAA,gbAAA,CAAA,gMAAA,CAAA,iQAAA,CAAA,0VAAA,CAAA,6aAAA,CAAA,8SAAA,CAAA,gMhCyuGJ,CgC7tGE,4CAME,8CAAA,CACA,4BAAA,CACA,mBAAA,CACA,8BAAA,CAJA,mCAAA,CAJA,iBAAA,CAGA,gBAAA,CADA,iBAAA,CADA,eAAA,CASA,uBAAA,CADA,2BhCiuGJ,CgC7tGI,aAdF,4CAeI,ehCguGJ,CACF,CgC7tGI,sEACE,gChC+tGN,CgC1tGI,gDACE,qBhC4tGN,CgCxtGI,gIAEE,iBAAA,CADA,chC2tGN,CgCttGI,4FACE,iBhCwtGN,CgCptGI,kFACE,ehCstGN,CgCltGI,0FACE,YhCotGN,CgChtGI,8EACE,mBhCktGN,CgC7sGE,sEAGE,iBAAA,CAAA,mBhCutGJ,CgC1tGE,sEAGE,kBAAA,CAAA,kBhCutGJ,CgC1tGE,sEASE,uBhCitGJ,CgC1tGE,sEASE,wBhCitGJ,CgC1tGE,sEAUE,4BhCgtGJ,CgC1tGE,4IAWE,6BhC+sGJ,CgC1tGE,sEAWE,4BhC+sGJ,CgC1tGE,kDAOE,0BAAA,CACA,WAAA,CAFA,eAAA,CADA,eAAA,CAHA,oBAAA,CAAA,iBAAA,CADA,iBhCytGJ,CgC5sGI,kFACE,ehC8sGN,CgC1sGI,oFAOE,UhCgtGN,CgCvtGI,oFAOE,WhCgtGN,CgCvtGI,gEAME,wBfkIU,CenIV,UAAA,CADA,WAAA,CAIA,kDAAA,CAAA,0CAAA,CACA,4BAAA,CAAA,oBAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CACA,UAAA,CACA,UhCotGN,CgCxsGI,4DACE,4DhC0sGN,CgC5rGE,sDACE,oBhC+rGJ,CgC5rGI,gFACE,gChC8rGN,CgCzrGE,8DACE,0BhC4rGJ,CgCzrGI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ChC2rGN,CgCvrGI,0EACE,ahCyrGN,CgC9sGE,8DACE,oBhCitGJ,CgC9sGI,wFACE,gChCgtGN,CgC3sGE,sEACE,0BhC8sGJ,CgC3sGI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ChC6sGN,CgCzsGI,kFACE,ahC2sGN,CgChuGE,sDACE,oBhCmuGJ,CgChuGI,gFACE,gChCkuGN,CgC7tGE,8DACE,0BhCguGJ,CgC7tGI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ChC+tGN,CgC3tGI,0EACE,ahC6tGN,CgClvGE,oDACE,oBhCqvGJ,CgClvGI,8EACE,gChCovGN,CgC/uGE,4DACE,0BhCkvGJ,CgC/uGI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yChCivGN,CgC7uGI,wEACE,ahC+uGN,CgCpwGE,4DACE,oBhCuwGJ,CgCpwGI,sFACE,gChCswGN,CgCjwGE,oEACE,0BhCowGJ,CgCjwGI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ChCmwGN,CgC/vGI,gFACE,ahCiwGN,CgCtxGE,8DACE,oBhCyxGJ,CgCtxGI,wFACE,gChCwxGN,CgCnxGE,sEACE,0BhCsxGJ,CgCnxGI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ChCqxGN,CgCjxGI,kFACE,ahCmxGN,CgCxyGE,4DACE,oBhC2yGJ,CgCxyGI,sFACE,gChC0yGN,CgCryGE,oEACE,0BhCwyGJ,CgCryGI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ChCuyGN,CgCnyGI,gFACE,ahCqyGN,CgC1zGE,4DACE,oBhC6zGJ,CgC1zGI,sFACE,gChC4zGN,CgCvzGE,oEACE,0BhC0zGJ,CgCvzGI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ChCyzGN,CgCrzGI,gFACE,ahCuzGN,CgC50GE,0DACE,oBhC+0GJ,CgC50GI,oFACE,gChC80GN,CgCz0GE,kEACE,0BhC40GJ,CgCz0GI,gFACE,wBAlBG,CAmBH,oDAAA,CAAA,4ChC20GN,CgCv0GI,8EACE,ahCy0GN,CgC91GE,oDACE,oBhCi2GJ,CgC91GI,8EACE,gChCg2GN,CgC31GE,4DACE,0BhC81GJ,CgC31GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yChC61GN,CgCz1GI,wEACE,ahC21GN,CgCh3GE,4DACE,oBhCm3GJ,CgCh3GI,sFACE,gChCk3GN,CgC72GE,oEACE,0BhCg3GJ,CgC72GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ChC+2GN,CgC32GI,gFACE,ahC62GN,CgCl4GE,wDACE,oBhCq4GJ,CgCl4GI,kFACE,gChCo4GN,CgC/3GE,gEACE,0BhCk4GJ,CgC/3GI,8EACE,wBAlBG,CAmBH,mDAAA,CAAA,2ChCi4GN,CgC73GI,4EACE,ahC+3GN,CiCniHA,MACE,wMjCsiHF,CiC7hHE,sBAEE,uCAAA,CADA,gBjCiiHJ,CiC7hHI,mCACE,ajC+hHN,CiChiHI,mCACE,cjC+hHN,CiC3hHM,4BACE,sBjC6hHR,CiC1hHQ,mCAC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","file":"main.css"} \ No newline at end of file diff --git a/main/assets/stylesheets/main.7e359304.min.css b/main/assets/stylesheets/main.7e359304.min.css new file mode 100644 index 000000000..e5a7e3a73 --- /dev/null +++ b/main/assets/stylesheets/main.7e359304.min.css @@ -0,0 +1 @@ +@charset "UTF-8";html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none;box-sizing:border-box}*,:after,:before{box-sizing:inherit}@media (prefers-reduced-motion){*,:after,:before{transition:none!important}}body{margin:0}a,button,input,label{-webkit-tap-highlight-color:transparent}a{color:inherit;text-decoration:none}hr{border:0;box-sizing:initial;display:block;height:.05rem;overflow:visible;padding:0}small{font-size:80%}sub,sup{line-height:1em}img{border-style:none}table{border-collapse:initial;border-spacing:0}td,th{font-weight:400;vertical-align:top}button{background:#0000;border:0;font-family:inherit;font-size:inherit;margin:0;padding:0}input{border:0;outline:none}:root{--md-primary-fg-color:#4051b5;--md-primary-fg-color--light:#5d6cc0;--md-primary-fg-color--dark:#303fa1;--md-primary-bg-color:#fff;--md-primary-bg-color--light:#ffffffb3;--md-accent-fg-color:#526cfe;--md-accent-fg-color--transparent:#526cfe1a;--md-accent-bg-color:#fff;--md-accent-bg-color--light:#ffffffb3}[data-md-color-scheme=default]{color-scheme:light}[data-md-color-scheme=default] img[src$="#gh-dark-mode-only"],[data-md-color-scheme=default] img[src$="#only-dark"]{display:none}:root,[data-md-color-scheme=default]{--md-hue:225deg;--md-default-fg-color:#000000de;--md-default-fg-color--light:#0000008a;--md-default-fg-color--lighter:#00000052;--md-default-fg-color--lightest:#00000012;--md-default-bg-color:#fff;--md-default-bg-color--light:#ffffffb3;--md-default-bg-color--lighter:#ffffff4d;--md-default-bg-color--lightest:#ffffff1f;--md-code-fg-color:#36464e;--md-code-bg-color:#f5f5f5;--md-code-hl-color:#4287ff;--md-code-hl-color--light:#4287ff1a;--md-code-hl-number-color:#d52a2a;--md-code-hl-special-color:#db1457;--md-code-hl-function-color:#a846b9;--md-code-hl-constant-color:#6e59d9;--md-code-hl-keyword-color:#3f6ec6;--md-code-hl-string-color:#1c7d4d;--md-code-hl-name-color:var(--md-code-fg-color);--md-code-hl-operator-color:var(--md-default-fg-color--light);--md-code-hl-punctuation-color:var(--md-default-fg-color--light);--md-code-hl-comment-color:var(--md-default-fg-color--light);--md-code-hl-generic-color:var(--md-default-fg-color--light);--md-code-hl-variable-color:var(--md-default-fg-color--light);--md-typeset-color:var(--md-default-fg-color);--md-typeset-a-color:var(--md-primary-fg-color);--md-typeset-del-color:#f5503d26;--md-typeset-ins-color:#0bd57026;--md-typeset-kbd-color:#fafafa;--md-typeset-kbd-accent-color:#fff;--md-typeset-kbd-border-color:#b8b8b8;--md-typeset-mark-color:#ffff0080;--md-typeset-table-color:#0000001f;--md-typeset-table-color--light:rgba(0,0,0,.035);--md-admonition-fg-color:var(--md-default-fg-color);--md-admonition-bg-color:var(--md-default-bg-color);--md-warning-fg-color:#000000de;--md-warning-bg-color:#ff9;--md-footer-fg-color:#fff;--md-footer-fg-color--light:#ffffffb3;--md-footer-fg-color--lighter:#ffffff73;--md-footer-bg-color:#000000de;--md-footer-bg-color--dark:#00000052;--md-shadow-z1:0 0.2rem 0.5rem #0000000d,0 0 0.05rem #0000001a;--md-shadow-z2:0 0.2rem 0.5rem #0000001a,0 0 0.05rem #00000040;--md-shadow-z3:0 0.2rem 0.5rem #0003,0 0 0.05rem #00000059}.md-icon svg{fill:currentcolor;display:block;height:1.2rem;width:1.2rem}body{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;--md-text-font-family:var(--md-text-font,_),-apple-system,BlinkMacSystemFont,Helvetica,Arial,sans-serif;--md-code-font-family:var(--md-code-font,_),SFMono-Regular,Consolas,Menlo,monospace}aside,body,input{font-feature-settings:"kern","liga";color:var(--md-typeset-color);font-family:var(--md-text-font-family)}code,kbd,pre{font-feature-settings:"kern";font-family:var(--md-code-font-family)}:root{--md-typeset-table-sort-icon:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--asc:url('data:image/svg+xml;charset=utf-8,');--md-typeset-table-sort-icon--desc:url('data:image/svg+xml;charset=utf-8,')}.md-typeset{-webkit-print-color-adjust:exact;color-adjust:exact;font-size:.8rem;line-height:1.6}@media print{.md-typeset{font-size:.68rem}}.md-typeset blockquote,.md-typeset dl,.md-typeset figure,.md-typeset ol,.md-typeset pre,.md-typeset ul{margin-bottom:1em;margin-top:1em}.md-typeset h1{color:var(--md-default-fg-color--light);font-size:2em;line-height:1.3;margin:0 0 1.25em}.md-typeset h1,.md-typeset h2{font-weight:300;letter-spacing:-.01em}.md-typeset h2{font-size:1.5625em;line-height:1.4;margin:1.6em 0 .64em}.md-typeset h3{font-size:1.25em;font-weight:400;letter-spacing:-.01em;line-height:1.5;margin:1.6em 0 .8em}.md-typeset h2+h3{margin-top:.8em}.md-typeset h4{font-weight:700;letter-spacing:-.01em;margin:1em 0}.md-typeset h5,.md-typeset h6{color:var(--md-default-fg-color--light);font-size:.8em;font-weight:700;letter-spacing:-.01em;margin:1.25em 0}.md-typeset h5{text-transform:uppercase}.md-typeset hr{border-bottom:.05rem solid var(--md-default-fg-color--lightest);display:flow-root;margin:1.5em 0}.md-typeset a{color:var(--md-typeset-a-color);word-break:break-word}.md-typeset a,.md-typeset a:before{transition:color 125ms}.md-typeset a:focus,.md-typeset a:hover{color:var(--md-accent-fg-color)}.md-typeset a:focus code,.md-typeset a:hover code{background-color:var(--md-accent-fg-color--transparent)}.md-typeset a code{color:currentcolor;transition:background-color 125ms}.md-typeset a.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset code,.md-typeset kbd,.md-typeset pre{color:var(--md-code-fg-color);direction:ltr;font-variant-ligatures:none}@media print{.md-typeset code,.md-typeset kbd,.md-typeset pre{white-space:pre-wrap}}.md-typeset code{background-color:var(--md-code-bg-color);border-radius:.1rem;-webkit-box-decoration-break:clone;box-decoration-break:clone;font-size:.85em;padding:0 .2941176471em;word-break:break-word}.md-typeset code:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-typeset pre{display:flow-root;line-height:1.4;position:relative}.md-typeset pre>code{-webkit-box-decoration-break:slice;box-decoration-break:slice;box-shadow:none;display:block;margin:0;outline-color:var(--md-accent-fg-color);overflow:auto;padding:.7720588235em 1.1764705882em;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin;touch-action:auto;word-break:normal}.md-typeset pre>code:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-typeset pre>code::-webkit-scrollbar{height:.2rem;width:.2rem}.md-typeset pre>code::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-typeset pre>code::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}.md-typeset kbd{background-color:var(--md-typeset-kbd-color);border-radius:.1rem;box-shadow:0 .1rem 0 .05rem var(--md-typeset-kbd-border-color),0 .1rem 0 var(--md-typeset-kbd-border-color),0 -.1rem .2rem var(--md-typeset-kbd-accent-color) inset;color:var(--md-default-fg-color);display:inline-block;font-size:.75em;padding:0 .6666666667em;vertical-align:text-top;word-break:break-word}.md-typeset mark{background-color:var(--md-typeset-mark-color);-webkit-box-decoration-break:clone;box-decoration-break:clone;color:inherit;word-break:break-word}.md-typeset abbr{border-bottom:.05rem dotted var(--md-default-fg-color--light);cursor:help;text-decoration:none}.md-typeset small{opacity:.75}[dir=ltr] .md-typeset sub,[dir=ltr] .md-typeset sup{margin-left:.078125em}[dir=rtl] .md-typeset sub,[dir=rtl] .md-typeset sup{margin-right:.078125em}[dir=ltr] .md-typeset blockquote{padding-left:.6rem}[dir=rtl] .md-typeset blockquote{padding-right:.6rem}[dir=ltr] .md-typeset blockquote{border-left:.2rem solid var(--md-default-fg-color--lighter)}[dir=rtl] .md-typeset blockquote{border-right:.2rem solid var(--md-default-fg-color--lighter)}.md-typeset blockquote{color:var(--md-default-fg-color--light);margin-left:0;margin-right:0}.md-typeset ul{list-style-type:disc}[dir=ltr] .md-typeset ol,[dir=ltr] .md-typeset ul{margin-left:.625em}[dir=rtl] .md-typeset ol,[dir=rtl] .md-typeset ul{margin-right:.625em}.md-typeset ol,.md-typeset ul{padding:0}.md-typeset ol:not([hidden]),.md-typeset ul:not([hidden]){display:flow-root}.md-typeset ol ol,.md-typeset ul ol{list-style-type:lower-alpha}.md-typeset ol ol ol,.md-typeset ul ol ol{list-style-type:lower-roman}[dir=ltr] .md-typeset ol li,[dir=ltr] .md-typeset ul li{margin-left:1.25em}[dir=rtl] .md-typeset ol li,[dir=rtl] .md-typeset ul li{margin-right:1.25em}.md-typeset ol li,.md-typeset ul li{margin-bottom:.5em}.md-typeset ol li blockquote,.md-typeset ol li p,.md-typeset ul li blockquote,.md-typeset ul li p{margin:.5em 0}.md-typeset ol li:last-child,.md-typeset ul li:last-child{margin-bottom:0}[dir=ltr] .md-typeset ol li ol,[dir=ltr] .md-typeset ol li ul,[dir=ltr] .md-typeset ul li ol,[dir=ltr] .md-typeset ul li ul{margin-left:.625em}[dir=rtl] .md-typeset ol li ol,[dir=rtl] .md-typeset ol li ul,[dir=rtl] .md-typeset ul li ol,[dir=rtl] .md-typeset ul li ul{margin-right:.625em}.md-typeset ol li ol,.md-typeset ol li ul,.md-typeset ul li ol,.md-typeset ul li ul{margin-bottom:.5em;margin-top:.5em}[dir=ltr] .md-typeset dd{margin-left:1.875em}[dir=rtl] .md-typeset dd{margin-right:1.875em}.md-typeset dd{margin-bottom:1.5em;margin-top:1em}.md-typeset img,.md-typeset svg,.md-typeset video{height:auto;max-width:100%}.md-typeset img[align=left]{margin:1em 1em 1em 0}.md-typeset img[align=right]{margin:1em 0 1em 1em}.md-typeset img[align]:only-child{margin-top:0}.md-typeset figure{display:flow-root;margin:1em auto;max-width:100%;text-align:center;width:-webkit-fit-content;width:-moz-fit-content;width:fit-content}.md-typeset figure img{display:block;margin:0 auto}.md-typeset figcaption{font-style:italic;margin:1em auto;max-width:24rem}.md-typeset iframe{max-width:100%}.md-typeset table:not([class]){background-color:var(--md-default-bg-color);border:.05rem solid var(--md-typeset-table-color);border-radius:.1rem;display:inline-block;font-size:.64rem;max-width:100%;overflow:auto;touch-action:auto}@media print{.md-typeset table:not([class]){display:table}}.md-typeset table:not([class])+*{margin-top:1.5em}.md-typeset table:not([class]) td>:first-child,.md-typeset table:not([class]) th>:first-child{margin-top:0}.md-typeset table:not([class]) td>:last-child,.md-typeset table:not([class]) th>:last-child{margin-bottom:0}.md-typeset table:not([class]) td:not([align]),.md-typeset table:not([class]) th:not([align]){text-align:left}[dir=rtl] .md-typeset table:not([class]) td:not([align]),[dir=rtl] .md-typeset table:not([class]) th:not([align]){text-align:right}.md-typeset table:not([class]) th{font-weight:700;min-width:5rem;padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) td{border-top:.05rem solid var(--md-typeset-table-color);padding:.9375em 1.25em;vertical-align:top}.md-typeset table:not([class]) tbody tr{transition:background-color 125ms}.md-typeset table:not([class]) tbody tr:hover{background-color:var(--md-typeset-table-color--light);box-shadow:0 .05rem 0 var(--md-default-bg-color) inset}.md-typeset table:not([class]) a{word-break:normal}.md-typeset table th[role=columnheader]{cursor:pointer}[dir=ltr] .md-typeset table th[role=columnheader]:after{margin-left:.5em}[dir=rtl] .md-typeset table th[role=columnheader]:after{margin-right:.5em}.md-typeset table th[role=columnheader]:after{content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-typeset-table-sort-icon);mask-image:var(--md-typeset-table-sort-icon);-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset table th[role=columnheader]:hover:after{background-color:var(--md-default-fg-color--lighter)}.md-typeset table th[role=columnheader][aria-sort=ascending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--asc);mask-image:var(--md-typeset-table-sort-icon--asc)}.md-typeset table th[role=columnheader][aria-sort=descending]:after{background-color:var(--md-default-fg-color--light);-webkit-mask-image:var(--md-typeset-table-sort-icon--desc);mask-image:var(--md-typeset-table-sort-icon--desc)}.md-typeset__scrollwrap{margin:1em -.8rem;overflow-x:auto;touch-action:auto}.md-typeset__table{display:inline-block;margin-bottom:.5em;padding:0 .8rem}@media print{.md-typeset__table{display:block}}html .md-typeset__table table{display:table;margin:0;overflow:hidden;width:100%}@media screen and (max-width:44.984375em){.md-content__inner>pre{margin:1em -.8rem}.md-content__inner>pre code{border-radius:0}}.md-typeset .md-author{border-radius:100%;display:block;flex-shrink:0;height:1.6rem;overflow:hidden;position:relative;transition:color 125ms,transform 125ms;width:1.6rem}.md-typeset .md-author img{display:block}.md-typeset .md-author--more{background:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--lighter);font-size:.6rem;font-weight:700;line-height:1.6rem;text-align:center}.md-typeset .md-author--long{height:2.4rem;width:2.4rem}.md-typeset a.md-author{transform:scale(1)}.md-typeset a.md-author img{filter:grayscale(100%) opacity(75%);transition:filter 125ms}.md-typeset a.md-author:focus,.md-typeset a.md-author:hover{transform:scale(1.1);z-index:1}.md-typeset a.md-author:focus img,.md-typeset a.md-author:hover img{filter:grayscale(0)}.md-banner{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color);overflow:auto}@media print{.md-banner{display:none}}.md-banner--warning{background-color:var(--md-warning-bg-color);color:var(--md-warning-fg-color)}.md-banner__inner{font-size:.7rem;margin:.6rem auto;padding:0 .8rem}[dir=ltr] .md-banner__button{float:right}[dir=rtl] .md-banner__button{float:left}.md-banner__button{color:inherit;cursor:pointer;transition:opacity .25s}.no-js .md-banner__button{display:none}.md-banner__button:hover{opacity:.7}html{font-size:125%;height:100%;overflow-x:hidden}@media screen and (min-width:100em){html{font-size:137.5%}}@media screen and (min-width:125em){html{font-size:150%}}body{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;font-size:.5rem;min-height:100%;position:relative;width:100%}@media print{body{display:block}}@media screen and (max-width:59.984375em){body[data-md-scrolllock]{position:fixed}}.md-grid{margin-left:auto;margin-right:auto;max-width:61rem}.md-container{display:flex;flex-direction:column;flex-grow:1}@media print{.md-container{display:block}}.md-main{flex-grow:1}.md-main__inner{display:flex;height:100%;margin-top:1.5rem}.md-ellipsis{overflow:hidden;text-overflow:ellipsis}.md-toggle{display:none}.md-option{height:0;opacity:0;position:absolute;width:0}.md-option:checked+label:not([hidden]){display:block}.md-option.focus-visible+label{outline-color:var(--md-accent-fg-color);outline-style:auto}.md-skip{background-color:var(--md-default-fg-color);border-radius:.1rem;color:var(--md-default-bg-color);font-size:.64rem;margin:.5rem;opacity:0;outline-color:var(--md-accent-fg-color);padding:.3rem .5rem;position:fixed;transform:translateY(.4rem);z-index:-1}.md-skip:focus{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 175ms 75ms;z-index:10}@page{margin:25mm}:root{--md-clipboard-icon:url('data:image/svg+xml;charset=utf-8,')}.md-clipboard{border-radius:.1rem;color:var(--md-default-fg-color--lightest);cursor:pointer;height:1.5em;outline-color:var(--md-accent-fg-color);outline-offset:.1rem;position:absolute;right:.5em;top:.5em;transition:color .25s;width:1.5em;z-index:1}@media print{.md-clipboard{display:none}}.md-clipboard:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}:hover>.md-clipboard{color:var(--md-default-fg-color--light)}.md-clipboard:focus,.md-clipboard:hover{color:var(--md-accent-fg-color)}.md-clipboard:after{background-color:currentcolor;content:"";display:block;height:1.125em;margin:0 auto;-webkit-mask-image:var(--md-clipboard-icon);mask-image:var(--md-clipboard-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:1.125em}.md-clipboard--inline{cursor:pointer}.md-clipboard--inline code{transition:color .25s,background-color .25s}.md-clipboard--inline:focus code,.md-clipboard--inline:hover code{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}@keyframes consent{0%{opacity:0;transform:translateY(100%)}to{opacity:1;transform:translateY(0)}}@keyframes overlay{0%{opacity:0}to{opacity:1}}.md-consent__overlay{animation:overlay .25s both;-webkit-backdrop-filter:blur(.1rem);backdrop-filter:blur(.1rem);background-color:#0000008a;height:100%;opacity:1;position:fixed;top:0;width:100%;z-index:5}.md-consent__inner{animation:consent .5s cubic-bezier(.1,.7,.1,1) both;background-color:var(--md-default-bg-color);border:0;border-radius:.1rem;bottom:0;box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;max-height:100%;overflow:auto;padding:0;position:fixed;width:100%;z-index:5}.md-consent__form{padding:.8rem}.md-consent__settings{display:none;margin:1em 0}input:checked+.md-consent__settings{display:block}.md-consent__controls{margin-bottom:.8rem}.md-typeset .md-consent__controls .md-button{display:inline}@media screen and (max-width:44.984375em){.md-typeset .md-consent__controls .md-button{display:block;margin-top:.4rem;text-align:center;width:100%}}.md-consent label{cursor:pointer}.md-content{flex-grow:1;min-width:0}.md-content__inner{margin:0 .8rem 1.2rem;padding-top:.6rem}@media screen and (min-width:76.25em){[dir=ltr] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}[dir=ltr] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner,[dir=rtl] .md-sidebar--primary:not([hidden])~.md-content>.md-content__inner{margin-right:1.2rem}[dir=rtl] .md-sidebar--secondary:not([hidden])~.md-content>.md-content__inner{margin-left:1.2rem}}.md-content__inner:before{content:"";display:block;height:.4rem}.md-content__inner>:last-child{margin-bottom:0}[dir=ltr] .md-content__button{float:right}[dir=rtl] .md-content__button{float:left}[dir=ltr] .md-content__button{margin-left:.4rem}[dir=rtl] .md-content__button{margin-right:.4rem}.md-content__button{margin:.4rem 0;padding:0}@media print{.md-content__button{display:none}}.md-typeset .md-content__button{color:var(--md-default-fg-color--lighter)}.md-content__button svg{display:inline;vertical-align:top}[dir=rtl] .md-content__button svg{transform:scaleX(-1)}[dir=ltr] .md-dialog{right:.8rem}[dir=rtl] .md-dialog{left:.8rem}.md-dialog{background-color:var(--md-default-fg-color);border-radius:.1rem;bottom:.8rem;box-shadow:var(--md-shadow-z3);min-width:11.1rem;opacity:0;padding:.4rem .6rem;pointer-events:none;position:fixed;transform:translateY(100%);transition:transform 0ms .4s,opacity .4s;z-index:4}@media print{.md-dialog{display:none}}.md-dialog--active{opacity:1;pointer-events:auto;transform:translateY(0);transition:transform .4s cubic-bezier(.075,.85,.175,1),opacity .4s}.md-dialog__inner{color:var(--md-default-bg-color);font-size:.7rem}.md-feedback{margin:2em 0 1em;text-align:center}.md-feedback fieldset{border:none;margin:0;padding:0}.md-feedback__title{font-weight:700;margin:1em auto}.md-feedback__inner{position:relative}.md-feedback__list{display:flex;flex-wrap:wrap;place-content:baseline center;position:relative}.md-feedback__list:hover .md-icon:not(:disabled){color:var(--md-default-fg-color--lighter)}:disabled .md-feedback__list{min-height:1.8rem}.md-feedback__icon{color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;margin:0 .1rem;transition:color 125ms}.md-feedback__icon:not(:disabled).md-icon:hover{color:var(--md-accent-fg-color)}.md-feedback__icon:disabled{color:var(--md-default-fg-color--lightest);pointer-events:none}.md-feedback__note{opacity:0;position:relative;transform:translateY(.4rem);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-feedback__note>*{margin:0 auto;max-width:16rem}:disabled .md-feedback__note{opacity:1;transform:translateY(0)}.md-footer{background-color:var(--md-footer-bg-color);color:var(--md-footer-fg-color)}@media print{.md-footer{display:none}}.md-footer__inner{justify-content:space-between;overflow:auto;padding:.2rem}.md-footer__inner:not([hidden]){display:flex}.md-footer__link{align-items:end;display:flex;flex-grow:0.01;margin-bottom:.4rem;margin-top:1rem;max-width:100%;outline-color:var(--md-accent-fg-color);overflow:hidden;transition:opacity .25s}.md-footer__link:focus,.md-footer__link:hover{opacity:.7}[dir=rtl] .md-footer__link svg{transform:scaleX(-1)}@media screen and (max-width:44.984375em){.md-footer__link--prev{flex-shrink:0}.md-footer__link--prev .md-footer__title{display:none}}[dir=ltr] .md-footer__link--next{margin-left:auto}[dir=rtl] .md-footer__link--next{margin-right:auto}.md-footer__link--next{text-align:right}[dir=rtl] .md-footer__link--next{text-align:left}.md-footer__title{flex-grow:1;font-size:.9rem;margin-bottom:.7rem;max-width:calc(100% - 2.4rem);padding:0 1rem;white-space:nowrap}.md-footer__button{margin:.2rem;padding:.4rem}.md-footer__direction{font-size:.64rem;opacity:.7}.md-footer-meta{background-color:var(--md-footer-bg-color--dark)}.md-footer-meta__inner{display:flex;flex-wrap:wrap;justify-content:space-between;padding:.2rem}html .md-footer-meta.md-typeset a{color:var(--md-footer-fg-color--light)}html .md-footer-meta.md-typeset a:focus,html .md-footer-meta.md-typeset a:hover{color:var(--md-footer-fg-color)}.md-copyright{color:var(--md-footer-fg-color--lighter);font-size:.64rem;margin:auto .6rem;padding:.4rem 0;width:100%}@media screen and (min-width:45em){.md-copyright{width:auto}}.md-copyright__highlight{color:var(--md-footer-fg-color--light)}.md-social{display:inline-flex;gap:.2rem;margin:0 .4rem;padding:.2rem 0 .6rem}@media screen and (min-width:45em){.md-social{padding:.6rem 0}}.md-social__link{display:inline-block;height:1.6rem;text-align:center;width:1.6rem}.md-social__link:before{line-height:1.9}.md-social__link svg{fill:currentcolor;max-height:.8rem;vertical-align:-25%}.md-typeset .md-button{border:.1rem solid;border-radius:.1rem;color:var(--md-primary-fg-color);cursor:pointer;display:inline-block;font-weight:700;padding:.625em 2em;transition:color 125ms,background-color 125ms,border-color 125ms}.md-typeset .md-button--primary{background-color:var(--md-primary-fg-color);border-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color)}.md-typeset .md-button:focus,.md-typeset .md-button:hover{background-color:var(--md-accent-fg-color);border-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[dir=ltr] .md-typeset .md-input{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .md-input,[dir=rtl] .md-typeset .md-input{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .md-input{border-top-left-radius:.1rem}.md-typeset .md-input{border-bottom:.1rem solid var(--md-default-fg-color--lighter);box-shadow:var(--md-shadow-z1);font-size:.8rem;height:1.8rem;padding:0 .6rem;transition:border .25s,box-shadow .25s}.md-typeset .md-input:focus,.md-typeset .md-input:hover{border-bottom-color:var(--md-accent-fg-color);box-shadow:var(--md-shadow-z2)}.md-typeset .md-input--stretch{width:100%}.md-header{background-color:var(--md-primary-fg-color);box-shadow:0 0 .2rem #0000,0 .2rem .4rem #0000;color:var(--md-primary-bg-color);display:block;left:0;position:sticky;right:0;top:0;z-index:4}@media print{.md-header{display:none}}.md-header[hidden]{transform:translateY(-100%);transition:transform .25s cubic-bezier(.8,0,.6,1),box-shadow .25s}.md-header--shadow{box-shadow:0 0 .2rem #0000001a,0 .2rem .4rem #0003;transition:transform .25s cubic-bezier(.1,.7,.1,1),box-shadow .25s}.md-header__inner{align-items:center;display:flex;padding:0 .2rem}.md-header__button{color:currentcolor;cursor:pointer;margin:.2rem;outline-color:var(--md-accent-fg-color);padding:.4rem;position:relative;transition:opacity .25s;vertical-align:middle;z-index:1}.md-header__button:hover{opacity:.7}.md-header__button:not([hidden]){display:inline-block}.md-header__button:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}.md-header__button.md-logo{margin:.2rem;padding:.4rem}@media screen and (max-width:76.234375em){.md-header__button.md-logo{display:none}}.md-header__button.md-logo img,.md-header__button.md-logo svg{fill:currentcolor;display:block;height:1.2rem;width:auto}@media screen and (min-width:60em){.md-header__button[for=__search]{display:none}}.no-js .md-header__button[for=__search]{display:none}[dir=rtl] .md-header__button[for=__search] svg{transform:scaleX(-1)}@media screen and (min-width:76.25em){.md-header__button[for=__drawer]{display:none}}.md-header__topic{display:flex;max-width:100%;position:absolute;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;white-space:nowrap}.md-header__topic+.md-header__topic{opacity:0;pointer-events:none;transform:translateX(1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__topic+.md-header__topic{transform:translateX(-1.25rem)}.md-header__topic:first-child{font-weight:700}[dir=ltr] .md-header__title{margin-left:1rem;margin-right:.4rem}[dir=rtl] .md-header__title{margin-left:.4rem;margin-right:1rem}.md-header__title{flex-grow:1;font-size:.9rem;height:2.4rem;line-height:2.4rem}.md-header__title--active .md-header__topic{opacity:0;pointer-events:none;transform:translateX(-1.25rem);transition:transform .4s cubic-bezier(1,.7,.1,.1),opacity .15s;z-index:-1}[dir=rtl] .md-header__title--active .md-header__topic{transform:translateX(1.25rem)}.md-header__title--active .md-header__topic+.md-header__topic{opacity:1;pointer-events:auto;transform:translateX(0);transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .15s;z-index:0}.md-header__title>.md-header__ellipsis{height:100%;position:relative;width:100%}.md-header__option{display:flex;flex-shrink:0;max-width:100%;transition:max-width 0ms .25s,opacity .25s .25s;white-space:nowrap}[data-md-toggle=search]:checked~.md-header .md-header__option{max-width:0;opacity:0;transition:max-width 0ms,opacity 0ms}.md-header__option>input{bottom:0}.md-header__source{display:none}@media screen and (min-width:60em){[dir=ltr] .md-header__source{margin-left:1rem}[dir=rtl] .md-header__source{margin-right:1rem}.md-header__source{display:block;max-width:11.7rem;width:11.7rem}}@media screen and (min-width:76.25em){[dir=ltr] .md-header__source{margin-left:1.4rem}[dir=rtl] .md-header__source{margin-right:1.4rem}}.md-meta{color:var(--md-default-fg-color--light);font-size:.7rem;line-height:1.3}.md-meta__list{display:inline-flex;flex-wrap:wrap;list-style:none;margin:0;padding:0}.md-meta__item:not(:last-child):after{content:"·";margin-left:.2rem;margin-right:.2rem}.md-meta__link{color:var(--md-typeset-a-color)}.md-meta__link:focus,.md-meta__link:hover{color:var(--md-accent-fg-color)}.md-draft{background-color:#ff1744;border-radius:.125em;color:#fff;display:inline-block;font-weight:700;padding-left:.5714285714em;padding-right:.5714285714em}:root{--md-nav-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-nav-icon--next:url('data:image/svg+xml;charset=utf-8,');--md-toc-icon:url('data:image/svg+xml;charset=utf-8,')}.md-nav{font-size:.7rem;line-height:1.3}.md-nav__title{color:var(--md-default-fg-color--light);display:block;font-weight:700;overflow:hidden;padding:0 .6rem;text-overflow:ellipsis}.md-nav__title .md-nav__button{display:none}.md-nav__title .md-nav__button img{height:100%;width:auto}.md-nav__title .md-nav__button.md-logo img,.md-nav__title .md-nav__button.md-logo svg{fill:currentcolor;display:block;height:2.4rem;max-width:100%;object-fit:contain;width:auto}.md-nav__list{list-style:none;margin:0;padding:0}.md-nav__link{align-items:flex-start;display:flex;gap:.4rem;margin-top:.625em;scroll-snap-align:start;transition:color 125ms}.md-nav__link--passed{color:var(--md-default-fg-color--light)}.md-nav__item .md-nav__link--active,.md-nav__item .md-nav__link--active code{color:var(--md-typeset-a-color)}.md-nav__link .md-ellipsis{position:relative}[dir=ltr] .md-nav__link .md-icon:last-child{margin-left:auto}[dir=rtl] .md-nav__link .md-icon:last-child{margin-right:auto}.md-nav__link svg{fill:currentcolor;flex-shrink:0;height:1.3em}.md-nav__link[for]:focus,.md-nav__link[for]:hover,.md-nav__link[href]:focus,.md-nav__link[href]:hover{color:var(--md-accent-fg-color);cursor:pointer}.md-nav__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-nav--primary .md-nav__link[for=__toc]{display:none}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{background-color:currentcolor;display:block;height:100%;-webkit-mask-image:var(--md-toc-icon);mask-image:var(--md-toc-icon);width:100%}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:none}.md-nav__container>.md-nav__link{margin-top:0}.md-nav__container>.md-nav__link:first-child{flex-grow:1;min-width:0}.md-nav__icon{flex-shrink:0}.md-nav__source{display:none}@media screen and (max-width:76.234375em){.md-nav--primary,.md-nav--primary .md-nav{background-color:var(--md-default-bg-color);display:flex;flex-direction:column;height:100%;left:0;position:absolute;right:0;top:0;z-index:1}.md-nav--primary .md-nav__item,.md-nav--primary .md-nav__title{font-size:.8rem;line-height:1.5}.md-nav--primary .md-nav__title{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);cursor:pointer;height:5.6rem;line-height:2.4rem;padding:3rem .8rem .2rem;position:relative;white-space:nowrap}[dir=ltr] .md-nav--primary .md-nav__title .md-nav__icon{left:.4rem}[dir=rtl] .md-nav--primary .md-nav__title .md-nav__icon{right:.4rem}.md-nav--primary .md-nav__title .md-nav__icon{display:block;height:1.2rem;margin:.2rem;position:absolute;top:.4rem;width:1.2rem}.md-nav--primary .md-nav__title .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--prev);mask-image:var(--md-nav-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}.md-nav--primary .md-nav__title~.md-nav__list{background-color:var(--md-default-bg-color);box-shadow:0 .05rem 0 var(--md-default-fg-color--lightest) inset;overflow-y:auto;scroll-snap-type:y mandatory;touch-action:pan-y}.md-nav--primary .md-nav__title~.md-nav__list>:first-child{border-top:0}.md-nav--primary .md-nav__title[for=__drawer]{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);font-weight:700}.md-nav--primary .md-nav__title .md-logo{display:block;left:.2rem;margin:.2rem;padding:.4rem;position:absolute;right:.2rem;top:.2rem}.md-nav--primary .md-nav__list{flex:1}.md-nav--primary .md-nav__item{border-top:.05rem solid var(--md-default-fg-color--lightest)}.md-nav--primary .md-nav__item--active>.md-nav__link{color:var(--md-typeset-a-color)}.md-nav--primary .md-nav__item--active>.md-nav__link:focus,.md-nav--primary .md-nav__item--active>.md-nav__link:hover{color:var(--md-accent-fg-color)}.md-nav--primary .md-nav__link{margin-top:0;padding:.6rem .8rem}.md-nav--primary .md-nav__link svg{margin-top:.1em}.md-nav--primary .md-nav__link>.md-nav__link{padding:0}[dir=ltr] .md-nav--primary .md-nav__link .md-nav__icon{margin-right:-.2rem}[dir=rtl] .md-nav--primary .md-nav__link .md-nav__icon{margin-left:-.2rem}.md-nav--primary .md-nav__link .md-nav__icon{font-size:1.2rem;height:1.2rem;width:1.2rem}.md-nav--primary .md-nav__link .md-nav__icon:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-nav--primary .md-nav__icon:after{transform:scale(-1)}.md-nav--primary .md-nav--secondary .md-nav{background-color:initial;position:static}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-left:1.4rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav__link{padding-right:1.4rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-left:2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav__link{padding-right:2rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-left:2.6rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav__link{padding-right:2.6rem}[dir=ltr] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-left:3.2rem}[dir=rtl] .md-nav--primary .md-nav--secondary .md-nav .md-nav .md-nav .md-nav .md-nav__link{padding-right:3.2rem}.md-nav--secondary{background-color:initial}.md-nav__toggle~.md-nav{display:flex;opacity:0;transform:translateX(100%);transition:transform .25s cubic-bezier(.8,0,.6,1),opacity 125ms 50ms}[dir=rtl] .md-nav__toggle~.md-nav{transform:translateX(-100%)}.md-nav__toggle:checked~.md-nav{opacity:1;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),opacity 125ms 125ms}.md-nav__toggle:checked~.md-nav>.md-nav__list{-webkit-backface-visibility:hidden;backface-visibility:hidden}}@media screen and (max-width:59.984375em){.md-nav--primary .md-nav__link[for=__toc]{display:flex}.md-nav--primary .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--primary .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--primary .md-nav__link[for=__toc]~.md-nav{display:flex}.md-nav__source{background-color:var(--md-primary-fg-color--dark);color:var(--md-primary-bg-color);display:block;padding:0 .2rem}}@media screen and (min-width:60em) and (max-width:76.234375em){.md-nav--integrated .md-nav__link[for=__toc]{display:flex}.md-nav--integrated .md-nav__link[for=__toc] .md-icon:after{content:""}.md-nav--integrated .md-nav__link[for=__toc]+.md-nav__link{display:none}.md-nav--integrated .md-nav__link[for=__toc]~.md-nav{display:flex}}@media screen and (min-width:60em){.md-nav{margin-bottom:-.4rem}.md-nav--secondary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--secondary .md-nav__title[for=__toc]{scroll-snap-align:start}.md-nav--secondary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--secondary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--secondary .md-nav__list{padding-right:.6rem}.md-nav--secondary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--secondary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--secondary .md-nav__item>.md-nav__link{margin-left:.4rem}}@media screen and (min-width:76.25em){.md-nav{margin-bottom:-.4rem;transition:max-height .25s cubic-bezier(.86,0,.07,1)}.md-nav--primary .md-nav__title{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);position:sticky;top:0;z-index:1}.md-nav--primary .md-nav__title[for=__drawer]{scroll-snap-align:start}.md-nav--primary .md-nav__title .md-nav__icon{display:none}[dir=ltr] .md-nav--primary .md-nav__list{padding-left:.6rem}[dir=rtl] .md-nav--primary .md-nav__list{padding-right:.6rem}.md-nav--primary .md-nav__list{padding-bottom:.4rem}[dir=ltr] .md-nav--primary .md-nav__item>.md-nav__link{margin-right:.4rem}[dir=rtl] .md-nav--primary .md-nav__item>.md-nav__link{margin-left:.4rem}.md-nav__toggle~.md-nav{display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .25s,visibility 0ms .25s;visibility:collapse}.md-nav__toggle~.md-nav>.md-nav__list{overflow:hidden}.md-nav__toggle.md-toggle--indeterminate~.md-nav,.md-nav__toggle:checked~.md-nav{grid-template-rows:1fr;opacity:1;transition:grid-template-rows .25s cubic-bezier(.86,0,.07,1),opacity .15s .1s,visibility 0ms;visibility:visible}.md-nav__toggle.md-toggle--indeterminate~.md-nav{transition:none}.md-nav__item--nested>.md-nav>.md-nav__title{display:none}.md-nav__item--section{display:block;margin:1.25em 0}.md-nav__item--section:last-child{margin-bottom:0}.md-nav__item--section>.md-nav__link{font-weight:700}.md-nav__item--section>.md-nav__link[for]{color:var(--md-default-fg-color--light)}.md-nav__item--section>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav__item--section>.md-nav__link .md-icon,.md-nav__item--section>.md-nav__link>[for]{display:none}[dir=ltr] .md-nav__item--section>.md-nav{margin-left:-.6rem}[dir=rtl] .md-nav__item--section>.md-nav{margin-right:-.6rem}.md-nav__item--section>.md-nav{display:block;opacity:1;visibility:visible}.md-nav__item--section>.md-nav>.md-nav__list>.md-nav__item{padding:0}.md-nav__icon{border-radius:100%;height:.9rem;transition:background-color .25s;width:.9rem}.md-nav__icon:hover{background-color:var(--md-accent-fg-color--transparent)}.md-nav__icon:after{background-color:currentcolor;border-radius:100%;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-nav-icon--next);mask-image:var(--md-nav-icon--next);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:transform .25s;vertical-align:-.1rem;width:100%}[dir=rtl] .md-nav__icon:after{transform:rotate(180deg)}.md-nav__item--nested .md-nav__toggle:checked~.md-nav__link .md-nav__icon:after,.md-nav__item--nested .md-toggle--indeterminate~.md-nav__link .md-nav__icon:after{transform:rotate(90deg)}.md-nav--lifted>.md-nav__list>.md-nav__item,.md-nav--lifted>.md-nav__title{display:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active{display:block}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link{background:var(--md-default-bg-color);box-shadow:0 0 .4rem .4rem var(--md-default-bg-color);margin-top:0;position:sticky;top:0;z-index:1}.md-nav--lifted>.md-nav__list>.md-nav__item--active>.md-nav__link:not(.md-nav__container){pointer-events:none}.md-nav--lifted>.md-nav__list>.md-nav__item--active.md-nav__item--section{margin:0}[dir=ltr] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav:not(.md-nav--secondary){margin-left:-.6rem}[dir=rtl] .md-nav--lifted>.md-nav__list>.md-nav__item>.md-nav:not(.md-nav--secondary){margin-right:-.6rem}.md-nav--lifted>.md-nav__list>.md-nav__item>[for]{color:var(--md-default-fg-color--light)}.md-nav--lifted .md-nav[data-md-level="1"]{grid-template-rows:1fr;opacity:1;visibility:visible}[dir=ltr] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-left:.05rem solid var(--md-primary-fg-color)}[dir=rtl] .md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{border-right:.05rem solid var(--md-primary-fg-color)}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary{display:block;margin-bottom:1.25em;opacity:1;visibility:visible}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__list{overflow:visible;padding-bottom:0}.md-nav--integrated>.md-nav__list>.md-nav__item--active .md-nav--secondary>.md-nav__title{display:none}}.md-pagination{font-size:.8rem;font-weight:700;gap:.4rem}.md-pagination,.md-pagination>*{align-items:center;display:flex;justify-content:center}.md-pagination>*{border-radius:.2rem;height:1.8rem;min-width:1.8rem;text-align:center}.md-pagination__current{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light)}.md-pagination__link{transition:color 125ms,background-color 125ms}.md-pagination__link:focus,.md-pagination__link:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-pagination__link:focus svg,.md-pagination__link:hover svg{color:var(--md-accent-fg-color)}.md-pagination__link.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-pagination__link svg{fill:currentcolor;color:var(--md-default-fg-color--lighter);display:block;max-height:100%;width:1.2rem}.md-post__back{border-bottom:.05rem solid var(--md-default-fg-color--lightest);margin-bottom:1.2rem;padding-bottom:1.2rem}@media screen and (max-width:76.234375em){.md-post__back{display:none}}[dir=rtl] .md-post__back svg{transform:scaleX(-1)}.md-post__authors{display:flex;flex-direction:column;gap:.6rem;margin:0 .6rem 1.2rem}.md-post .md-post__meta a{transition:color 125ms}.md-post .md-post__meta a:focus,.md-post .md-post__meta a:hover{color:var(--md-accent-fg-color)}.md-post__title{color:var(--md-default-fg-color--light);font-weight:700}.md-post--excerpt{margin-bottom:3.2rem}.md-post--excerpt .md-post__header{align-items:center;display:flex;gap:.6rem;min-height:1.6rem}.md-post--excerpt .md-post__authors{align-items:center;display:inline-flex;flex-direction:row;gap:.2rem;margin:0;min-height:2.4rem}[dir=ltr] .md-post--excerpt .md-post__meta .md-meta__list{margin-right:.4rem}[dir=rtl] .md-post--excerpt .md-post__meta .md-meta__list{margin-left:.4rem}.md-post--excerpt .md-post__content>:first-child{--md-scroll-margin:6rem;margin-top:0}.md-post>.md-nav--secondary{margin:1em 0}.md-profile{align-items:center;display:flex;font-size:.7rem;gap:.6rem;line-height:1.4;width:100%}.md-profile__description{flex-grow:1}.md-content--post{display:flex}@media screen and (max-width:76.234375em){.md-content--post{flex-flow:column-reverse}}.md-content--post>.md-content__inner{min-width:0}@media screen and (min-width:76.25em){[dir=ltr] .md-content--post>.md-content__inner{margin-left:1.2rem}[dir=rtl] .md-content--post>.md-content__inner{margin-right:1.2rem}}@media screen and (max-width:76.234375em){.md-sidebar.md-sidebar--post{padding:0;position:static;width:100%}.md-sidebar.md-sidebar--post .md-sidebar__scrollwrap{overflow:visible}.md-sidebar.md-sidebar--post .md-sidebar__inner{padding:0}.md-sidebar.md-sidebar--post .md-post__meta{margin-left:.6rem;margin-right:.6rem}.md-sidebar.md-sidebar--post .md-nav__item{border:none;display:inline}.md-sidebar.md-sidebar--post .md-nav__list{display:inline-flex;flex-wrap:wrap;gap:.6rem;padding-bottom:.6rem;padding-top:.6rem}.md-sidebar.md-sidebar--post .md-nav__link{padding:0}.md-sidebar.md-sidebar--post .md-nav{height:auto;margin-bottom:0;position:static}}:root{--md-progress-value:0;--md-progress-delay:400ms}.md-progress{background:var(--md-primary-bg-color);height:.075rem;opacity:min(clamp(0,var(--md-progress-value),1),clamp(0,100 - var(--md-progress-value),1));position:fixed;top:0;transform:scaleX(calc(var(--md-progress-value)*1%));transform-origin:left;transition:transform .5s cubic-bezier(.19,1,.22,1),opacity .25s var(--md-progress-delay);width:100%;z-index:4}:root{--md-search-result-icon:url('data:image/svg+xml;charset=utf-8,')}.md-search{position:relative}@media screen and (min-width:60em){.md-search{padding:.2rem 0}}.no-js .md-search{display:none}.md-search__overlay{opacity:0;z-index:1}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__overlay{left:-2.2rem}[dir=rtl] .md-search__overlay{right:-2.2rem}.md-search__overlay{background-color:var(--md-default-bg-color);border-radius:1rem;height:2rem;overflow:hidden;pointer-events:none;position:absolute;top:-1rem;transform-origin:center;transition:transform .3s .1s,opacity .2s .2s;width:2rem}[data-md-toggle=search]:checked~.md-header .md-search__overlay{opacity:1;transition:transform .4s,opacity .1s}}@media screen and (min-width:60em){[dir=ltr] .md-search__overlay{left:0}[dir=rtl] .md-search__overlay{right:0}.md-search__overlay{background-color:#0000008a;cursor:pointer;height:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0}[data-md-toggle=search]:checked~.md-header .md-search__overlay{height:200vh;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@media screen and (max-width:29.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(45)}}@media screen and (min-width:30em) and (max-width:44.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(60)}}@media screen and (min-width:45em) and (max-width:59.984375em){[data-md-toggle=search]:checked~.md-header .md-search__overlay{transform:scale(75)}}.md-search__inner{-webkit-backface-visibility:hidden;backface-visibility:hidden}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__inner{left:0}[dir=rtl] .md-search__inner{right:0}.md-search__inner{height:0;opacity:0;overflow:hidden;position:fixed;top:0;transform:translateX(5%);transition:width 0ms .3s,height 0ms .3s,transform .15s cubic-bezier(.4,0,.2,1) .15s,opacity .15s .15s;width:0;z-index:2}[dir=rtl] .md-search__inner{transform:translateX(-5%)}[data-md-toggle=search]:checked~.md-header .md-search__inner{height:100%;opacity:1;transform:translateX(0);transition:width 0ms 0ms,height 0ms 0ms,transform .15s cubic-bezier(.1,.7,.1,1) .15s,opacity .15s .15s;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__inner{float:right}[dir=rtl] .md-search__inner{float:left}.md-search__inner{padding:.1rem 0;position:relative;transition:width .25s cubic-bezier(.1,.7,.1,1);width:11.7rem}}@media screen and (min-width:60em) and (max-width:76.234375em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:23.4rem}}@media screen and (min-width:76.25em){[data-md-toggle=search]:checked~.md-header .md-search__inner{width:34.4rem}}.md-search__form{background-color:var(--md-default-bg-color);box-shadow:0 0 .6rem #0000;height:2.4rem;position:relative;transition:color .25s,background-color .25s;z-index:2}@media screen and (min-width:60em){.md-search__form{background-color:#00000042;border-radius:.1rem;height:1.8rem}.md-search__form:hover{background-color:#ffffff1f}}[data-md-toggle=search]:checked~.md-header .md-search__form{background-color:var(--md-default-bg-color);border-radius:.1rem .1rem 0 0;box-shadow:0 0 .6rem #00000012;color:var(--md-default-fg-color)}[dir=ltr] .md-search__input{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__input{padding-left:2.2rem;padding-right:3.6rem}.md-search__input{background:#0000;font-size:.9rem;height:100%;position:relative;text-overflow:ellipsis;width:100%;z-index:2}.md-search__input::placeholder{transition:color .25s}.md-search__input::placeholder,.md-search__input~.md-search__icon{color:var(--md-default-fg-color--light)}.md-search__input::-ms-clear{display:none}@media screen and (max-width:59.984375em){.md-search__input{font-size:.9rem;height:2.4rem;width:100%}}@media screen and (min-width:60em){[dir=ltr] .md-search__input{padding-left:2.2rem}[dir=rtl] .md-search__input{padding-right:2.2rem}.md-search__input{color:inherit;font-size:.8rem}.md-search__input::placeholder{color:var(--md-primary-bg-color--light)}.md-search__input+.md-search__icon{color:var(--md-primary-bg-color)}[data-md-toggle=search]:checked~.md-header .md-search__input{text-overflow:clip}[data-md-toggle=search]:checked~.md-header .md-search__input+.md-search__icon{color:var(--md-default-fg-color--light)}[data-md-toggle=search]:checked~.md-header .md-search__input::placeholder{color:#0000}}.md-search__icon{cursor:pointer;display:inline-block;height:1.2rem;transition:color .25s,opacity .25s;width:1.2rem}.md-search__icon:hover{opacity:.7}[dir=ltr] .md-search__icon[for=__search]{left:.5rem}[dir=rtl] .md-search__icon[for=__search]{right:.5rem}.md-search__icon[for=__search]{position:absolute;top:.3rem;z-index:2}[dir=rtl] .md-search__icon[for=__search] svg{transform:scaleX(-1)}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__icon[for=__search]{left:.8rem}[dir=rtl] .md-search__icon[for=__search]{right:.8rem}.md-search__icon[for=__search]{top:.6rem}.md-search__icon[for=__search] svg:first-child{display:none}}@media screen and (min-width:60em){.md-search__icon[for=__search]{pointer-events:none}.md-search__icon[for=__search] svg:last-child{display:none}}[dir=ltr] .md-search__options{right:.5rem}[dir=rtl] .md-search__options{left:.5rem}.md-search__options{pointer-events:none;position:absolute;top:.3rem;z-index:2}@media screen and (max-width:59.984375em){[dir=ltr] .md-search__options{right:.8rem}[dir=rtl] .md-search__options{left:.8rem}.md-search__options{top:.6rem}}[dir=ltr] .md-search__options>.md-icon{margin-left:.2rem}[dir=rtl] .md-search__options>.md-icon{margin-right:.2rem}.md-search__options>.md-icon{color:var(--md-default-fg-color--light);opacity:0;transform:scale(.75);transition:transform .15s cubic-bezier(.1,.7,.1,1),opacity .15s}.md-search__options>.md-icon:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>.md-icon{opacity:1;pointer-events:auto;transform:scale(1)}[data-md-toggle=search]:checked~.md-header .md-search__input:valid~.md-search__options>.md-icon:hover{opacity:.7}[dir=ltr] .md-search__suggest{padding-left:3.6rem;padding-right:2.2rem}[dir=rtl] .md-search__suggest{padding-left:2.2rem;padding-right:3.6rem}.md-search__suggest{align-items:center;color:var(--md-default-fg-color--lighter);display:flex;font-size:.9rem;height:100%;opacity:0;position:absolute;top:0;transition:opacity 50ms;white-space:nowrap;width:100%}@media screen and (min-width:60em){[dir=ltr] .md-search__suggest{padding-left:2.2rem}[dir=rtl] .md-search__suggest{padding-right:2.2rem}.md-search__suggest{font-size:.8rem}}[data-md-toggle=search]:checked~.md-header .md-search__suggest{opacity:1;transition:opacity .3s .1s}[dir=ltr] .md-search__output{border-bottom-left-radius:.1rem}[dir=ltr] .md-search__output,[dir=rtl] .md-search__output{border-bottom-right-radius:.1rem}[dir=rtl] .md-search__output{border-bottom-left-radius:.1rem}.md-search__output{overflow:hidden;position:absolute;width:100%;z-index:1}@media screen and (max-width:59.984375em){.md-search__output{bottom:0;top:2.4rem}}@media screen and (min-width:60em){.md-search__output{opacity:0;top:1.9rem;transition:opacity .4s}[data-md-toggle=search]:checked~.md-header .md-search__output{box-shadow:var(--md-shadow-z3);opacity:1}}.md-search__scrollwrap{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);height:100%;overflow-y:auto;touch-action:pan-y}@media (-webkit-max-device-pixel-ratio:1),(max-resolution:1dppx){.md-search__scrollwrap{transform:translateZ(0)}}@media screen and (min-width:60em) and (max-width:76.234375em){.md-search__scrollwrap{width:23.4rem}}@media screen and (min-width:76.25em){.md-search__scrollwrap{width:34.4rem}}@media screen and (min-width:60em){.md-search__scrollwrap{max-height:0;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin}[data-md-toggle=search]:checked~.md-header .md-search__scrollwrap{max-height:75vh}.md-search__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-search__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-search__scrollwrap::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-search__scrollwrap::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}}.md-search-result{color:var(--md-default-fg-color);word-break:break-word}.md-search-result__meta{background-color:var(--md-default-fg-color--lightest);color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.8rem;padding:0 .8rem;scroll-snap-align:start}@media screen and (min-width:60em){[dir=ltr] .md-search-result__meta{padding-left:2.2rem}[dir=rtl] .md-search-result__meta{padding-right:2.2rem}}.md-search-result__list{list-style:none;margin:0;padding:0;-webkit-user-select:none;user-select:none}.md-search-result__item{box-shadow:0 -.05rem var(--md-default-fg-color--lightest)}.md-search-result__item:first-child{box-shadow:none}.md-search-result__link{display:block;outline:none;scroll-snap-align:start;transition:background-color .25s}.md-search-result__link:focus,.md-search-result__link:hover{background-color:var(--md-accent-fg-color--transparent)}.md-search-result__link:last-child p:last-child{margin-bottom:.6rem}.md-search-result__more>summary{cursor:pointer;display:block;outline:none;position:sticky;scroll-snap-align:start;top:0;z-index:1}.md-search-result__more>summary::marker{display:none}.md-search-result__more>summary::-webkit-details-marker{display:none}.md-search-result__more>summary>div{color:var(--md-typeset-a-color);font-size:.64rem;padding:.75em .8rem;transition:color .25s,background-color .25s}@media screen and (min-width:60em){[dir=ltr] .md-search-result__more>summary>div{padding-left:2.2rem}[dir=rtl] .md-search-result__more>summary>div{padding-right:2.2rem}}.md-search-result__more>summary:focus>div,.md-search-result__more>summary:hover>div{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-search-result__more[open]>summary{background-color:var(--md-default-bg-color)}.md-search-result__article{overflow:hidden;padding:0 .8rem;position:relative}@media screen and (min-width:60em){[dir=ltr] .md-search-result__article{padding-left:2.2rem}[dir=rtl] .md-search-result__article{padding-right:2.2rem}}[dir=ltr] .md-search-result__icon{left:0}[dir=rtl] .md-search-result__icon{right:0}.md-search-result__icon{color:var(--md-default-fg-color--light);height:1.2rem;margin:.5rem;position:absolute;width:1.2rem}@media screen and (max-width:59.984375em){.md-search-result__icon{display:none}}.md-search-result__icon:after{background-color:currentcolor;content:"";display:inline-block;height:100%;-webkit-mask-image:var(--md-search-result-icon);mask-image:var(--md-search-result-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:100%}[dir=rtl] .md-search-result__icon:after{transform:scaleX(-1)}.md-search-result .md-typeset{color:var(--md-default-fg-color--light);font-size:.64rem;line-height:1.6}.md-search-result .md-typeset h1{color:var(--md-default-fg-color);font-size:.8rem;font-weight:400;line-height:1.4;margin:.55rem 0}.md-search-result .md-typeset h1 mark{text-decoration:none}.md-search-result .md-typeset h2{color:var(--md-default-fg-color);font-size:.64rem;font-weight:700;line-height:1.6;margin:.5em 0}.md-search-result .md-typeset h2 mark{text-decoration:none}.md-search-result__terms{color:var(--md-default-fg-color);display:block;font-size:.64rem;font-style:italic;margin:.5em 0}.md-search-result mark{background-color:initial;color:var(--md-accent-fg-color);text-decoration:underline}.md-select{position:relative;z-index:1}.md-select__inner{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);left:50%;margin-top:.2rem;max-height:0;opacity:0;position:absolute;top:calc(100% - .2rem);transform:translate3d(-50%,.3rem,0);transition:transform .25s 375ms,opacity .25s .25s,max-height 0ms .5s}.md-select:focus-within .md-select__inner,.md-select:hover .md-select__inner{max-height:10rem;opacity:1;transform:translate3d(-50%,0,0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,max-height 0ms}.md-select__inner:after{border-bottom:.2rem solid #0000;border-bottom-color:var(--md-default-bg-color);border-left:.2rem solid #0000;border-right:.2rem solid #0000;border-top:0;content:"";height:0;left:50%;margin-left:-.2rem;margin-top:-.2rem;position:absolute;top:0;width:0}.md-select__list{border-radius:.1rem;font-size:.8rem;list-style-type:none;margin:0;max-height:inherit;overflow:auto;padding:0}.md-select__item{line-height:1.8rem}[dir=ltr] .md-select__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-select__link{padding-left:1.2rem;padding-right:.6rem}.md-select__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:background-color .25s,color .25s;width:100%}.md-select__link:focus,.md-select__link:hover{color:var(--md-accent-fg-color)}.md-select__link:focus{background-color:var(--md-default-fg-color--lightest)}.md-sidebar{align-self:flex-start;flex-shrink:0;padding:1.2rem 0;position:sticky;top:2.4rem;width:12.1rem}@media print{.md-sidebar{display:none}}@media screen and (max-width:76.234375em){[dir=ltr] .md-sidebar--primary{left:-12.1rem}[dir=rtl] .md-sidebar--primary{right:-12.1rem}.md-sidebar--primary{background-color:var(--md-default-bg-color);display:block;height:100%;position:fixed;top:0;transform:translateX(0);transition:transform .25s cubic-bezier(.4,0,.2,1),box-shadow .25s;width:12.1rem;z-index:5}[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{box-shadow:var(--md-shadow-z3);transform:translateX(12.1rem)}[dir=rtl] [data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary{transform:translateX(-12.1rem)}.md-sidebar--primary .md-sidebar__scrollwrap{bottom:0;left:0;margin:0;overflow:hidden;position:absolute;right:0;scroll-snap-type:none;top:0}}@media screen and (min-width:76.25em){.md-sidebar{height:0}.no-js .md-sidebar{height:auto}.md-header--lifted~.md-container .md-sidebar{top:4.8rem}}.md-sidebar--secondary{display:none;order:2}@media screen and (min-width:60em){.md-sidebar--secondary{height:0}.no-js .md-sidebar--secondary{height:auto}.md-sidebar--secondary:not([hidden]){display:block}.md-sidebar--secondary .md-sidebar__scrollwrap{touch-action:pan-y}}.md-sidebar__scrollwrap{scrollbar-gutter:stable;-webkit-backface-visibility:hidden;backface-visibility:hidden;margin:0 .2rem;overflow-y:auto;scrollbar-color:var(--md-default-fg-color--lighter) #0000;scrollbar-width:thin}.md-sidebar__scrollwrap::-webkit-scrollbar{height:.2rem;width:.2rem}.md-sidebar__scrollwrap:focus-within,.md-sidebar__scrollwrap:hover{scrollbar-color:var(--md-accent-fg-color) #0000}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-sidebar__scrollwrap:focus-within::-webkit-scrollbar-thumb:hover,.md-sidebar__scrollwrap:hover::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}@supports selector(::-webkit-scrollbar){.md-sidebar__scrollwrap{scrollbar-gutter:auto}[dir=ltr] .md-sidebar__inner{padding-right:calc(100% - 11.5rem)}[dir=rtl] .md-sidebar__inner{padding-left:calc(100% - 11.5rem)}}@media screen and (max-width:76.234375em){.md-overlay{background-color:#0000008a;height:0;opacity:0;position:fixed;top:0;transition:width 0ms .25s,height 0ms .25s,opacity .25s;width:0;z-index:5}[data-md-toggle=drawer]:checked~.md-overlay{height:100%;opacity:1;transition:width 0ms,height 0ms,opacity .25s;width:100%}}@keyframes facts{0%{height:0}to{height:.65rem}}@keyframes fact{0%{opacity:0;transform:translateY(100%)}50%{opacity:0}to{opacity:1;transform:translateY(0)}}:root{--md-source-forks-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-repositories-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-stars-icon:url('data:image/svg+xml;charset=utf-8,');--md-source-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-source{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:block;font-size:.65rem;line-height:1.2;outline-color:var(--md-accent-fg-color);transition:opacity .25s;white-space:nowrap}.md-source:hover{opacity:.7}.md-source__icon{display:inline-block;height:2.4rem;vertical-align:middle;width:2rem}[dir=ltr] .md-source__icon svg{margin-left:.6rem}[dir=rtl] .md-source__icon svg{margin-right:.6rem}.md-source__icon svg{margin-top:.6rem}[dir=ltr] .md-source__icon+.md-source__repository{padding-left:2rem}[dir=rtl] .md-source__icon+.md-source__repository{padding-right:2rem}[dir=ltr] .md-source__icon+.md-source__repository{margin-left:-2rem}[dir=rtl] .md-source__icon+.md-source__repository{margin-right:-2rem}[dir=ltr] .md-source__repository{margin-left:.6rem}[dir=rtl] .md-source__repository{margin-right:.6rem}.md-source__repository{display:inline-block;max-width:calc(100% - 1.2rem);overflow:hidden;text-overflow:ellipsis;vertical-align:middle}.md-source__facts{display:flex;font-size:.55rem;gap:.4rem;list-style-type:none;margin:.1rem 0 0;opacity:.75;overflow:hidden;padding:0;width:100%}.md-source__repository--active .md-source__facts{animation:facts .25s ease-in}.md-source__fact{overflow:hidden;text-overflow:ellipsis}.md-source__repository--active .md-source__fact{animation:fact .4s ease-out}[dir=ltr] .md-source__fact:before{margin-right:.1rem}[dir=rtl] .md-source__fact:before{margin-left:.1rem}.md-source__fact:before{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-top;width:.6rem}.md-source__fact:nth-child(1n+2){flex-shrink:0}.md-source__fact--version:before{-webkit-mask-image:var(--md-source-version-icon);mask-image:var(--md-source-version-icon)}.md-source__fact--stars:before{-webkit-mask-image:var(--md-source-stars-icon);mask-image:var(--md-source-stars-icon)}.md-source__fact--forks:before{-webkit-mask-image:var(--md-source-forks-icon);mask-image:var(--md-source-forks-icon)}.md-source__fact--repositories:before{-webkit-mask-image:var(--md-source-repositories-icon);mask-image:var(--md-source-repositories-icon)}.md-source-file{margin:1em 0}[dir=ltr] .md-source-file__fact{margin-right:.6rem}[dir=rtl] .md-source-file__fact{margin-left:.6rem}.md-source-file__fact{align-items:center;color:var(--md-default-fg-color--light);display:inline-flex;font-size:.68rem;gap:.3rem}.md-source-file__fact .md-icon{flex-shrink:0;margin-bottom:.05rem}[dir=ltr] .md-source-file__fact .md-author{float:left}[dir=rtl] .md-source-file__fact .md-author{float:right}.md-source-file__fact .md-author{margin-right:.2rem}.md-source-file__fact svg{width:.9rem}:root{--md-status:url('data:image/svg+xml;charset=utf-8,');--md-status--new:url('data:image/svg+xml;charset=utf-8,');--md-status--deprecated:url('data:image/svg+xml;charset=utf-8,');--md-status--encrypted:url('data:image/svg+xml;charset=utf-8,')}.md-status:after{background-color:var(--md-default-fg-color--light);content:"";display:inline-block;height:1.125em;-webkit-mask-image:var(--md-status);mask-image:var(--md-status);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;vertical-align:text-bottom;width:1.125em}.md-status:hover:after{background-color:currentcolor}.md-status--new:after{-webkit-mask-image:var(--md-status--new);mask-image:var(--md-status--new)}.md-status--deprecated:after{-webkit-mask-image:var(--md-status--deprecated);mask-image:var(--md-status--deprecated)}.md-status--encrypted:after{-webkit-mask-image:var(--md-status--encrypted);mask-image:var(--md-status--encrypted)}.md-tabs{background-color:var(--md-primary-fg-color);color:var(--md-primary-bg-color);display:block;line-height:1.3;overflow:auto;width:100%;z-index:3}@media print{.md-tabs{display:none}}@media screen and (max-width:76.234375em){.md-tabs{display:none}}.md-tabs[hidden]{pointer-events:none}[dir=ltr] .md-tabs__list{margin-left:.2rem}[dir=rtl] .md-tabs__list{margin-right:.2rem}.md-tabs__list{contain:content;display:flex;list-style:none;margin:0;overflow:auto;padding:0;scrollbar-width:none;white-space:nowrap}.md-tabs__list::-webkit-scrollbar{display:none}.md-tabs__item{height:2.4rem;padding-left:.6rem;padding-right:.6rem}.md-tabs__item--active .md-tabs__link{color:inherit;opacity:1}.md-tabs__link{-webkit-backface-visibility:hidden;backface-visibility:hidden;display:flex;font-size:.7rem;margin-top:.8rem;opacity:.7;outline-color:var(--md-accent-fg-color);outline-offset:.2rem;transition:transform .4s cubic-bezier(.1,.7,.1,1),opacity .25s}.md-tabs__link:focus,.md-tabs__link:hover{color:inherit;opacity:1}[dir=ltr] .md-tabs__link svg{margin-right:.4rem}[dir=rtl] .md-tabs__link svg{margin-left:.4rem}.md-tabs__link svg{fill:currentcolor;height:1.3em}.md-tabs__item:nth-child(2) .md-tabs__link{transition-delay:20ms}.md-tabs__item:nth-child(3) .md-tabs__link{transition-delay:40ms}.md-tabs__item:nth-child(4) .md-tabs__link{transition-delay:60ms}.md-tabs__item:nth-child(5) .md-tabs__link{transition-delay:80ms}.md-tabs__item:nth-child(6) .md-tabs__link{transition-delay:.1s}.md-tabs__item:nth-child(7) .md-tabs__link{transition-delay:.12s}.md-tabs__item:nth-child(8) .md-tabs__link{transition-delay:.14s}.md-tabs__item:nth-child(9) .md-tabs__link{transition-delay:.16s}.md-tabs__item:nth-child(10) .md-tabs__link{transition-delay:.18s}.md-tabs__item:nth-child(11) .md-tabs__link{transition-delay:.2s}.md-tabs__item:nth-child(12) .md-tabs__link{transition-delay:.22s}.md-tabs__item:nth-child(13) .md-tabs__link{transition-delay:.24s}.md-tabs__item:nth-child(14) .md-tabs__link{transition-delay:.26s}.md-tabs__item:nth-child(15) .md-tabs__link{transition-delay:.28s}.md-tabs__item:nth-child(16) .md-tabs__link{transition-delay:.3s}.md-tabs[hidden] .md-tabs__link{opacity:0;transform:translateY(50%);transition:transform 0ms .1s,opacity .1s}:root{--md-tag-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .md-tags:not([hidden]){display:inline-flex;flex-wrap:wrap;gap:.5em;margin-bottom:.75em;margin-top:-.125em}.md-typeset .md-tag{align-items:center;background:var(--md-default-fg-color--lightest);border-radius:2.4rem;display:inline-flex;font-size:.64rem;font-size:min(.8em,.64rem);font-weight:700;gap:.5em;letter-spacing:normal;line-height:1.6;padding:.3125em .78125em}.md-typeset .md-tag[href]{-webkit-tap-highlight-color:transparent;color:inherit;outline:none;transition:color 125ms,background-color 125ms}.md-typeset .md-tag[href]:focus,.md-typeset .md-tag[href]:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}[id]>.md-typeset .md-tag{vertical-align:text-top}.md-typeset .md-tag-icon:before{background-color:var(--md-default-fg-color--lighter);content:"";display:inline-block;height:1.2em;-webkit-mask-image:var(--md-tag-icon);mask-image:var(--md-tag-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color 125ms;vertical-align:text-bottom;width:1.2em}.md-typeset .md-tag-icon[href]:focus:before,.md-typeset .md-tag-icon[href]:hover:before{background-color:var(--md-accent-bg-color)}@keyframes pulse{0%{transform:scale(.95)}75%{transform:scale(1)}to{transform:scale(.95)}}:root{--md-annotation-bg-icon:url('data:image/svg+xml;charset=utf-8,');--md-annotation-icon:url('data:image/svg+xml;charset=utf-8,');--md-tooltip-width:20rem}.md-tooltip{-webkit-backface-visibility:hidden;backface-visibility:hidden;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);font-family:var(--md-text-font-family);left:clamp(var(--md-tooltip-0,0rem) + .8rem,var(--md-tooltip-x),100vw + var(--md-tooltip-0,0rem) + .8rem - var(--md-tooltip-width) - 2 * .8rem);max-width:calc(100vw - 1.6rem);opacity:0;position:absolute;top:var(--md-tooltip-y);transform:translateY(-.4rem);transition:transform 0ms .25s,opacity .25s,z-index .25s;width:var(--md-tooltip-width);z-index:0}.md-tooltip--active{opacity:1;transform:translateY(0);transition:transform .25s cubic-bezier(.1,.7,.1,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip--inline{font-weight:700;-webkit-user-select:none;user-select:none;width:auto}.md-tooltip--inline:not(.md-tooltip--active){transform:translateY(.2rem) scale(.9)}.md-tooltip--inline .md-tooltip__inner{font-size:.5rem;padding:.2rem .4rem}[hidden]+.md-tooltip--inline{display:none}.focus-visible>.md-tooltip,.md-tooltip:target{outline:var(--md-accent-fg-color) auto}.md-tooltip__inner{font-size:.64rem;padding:.8rem}.md-tooltip__inner.md-typeset>:first-child{margin-top:0}.md-tooltip__inner.md-typeset>:last-child{margin-bottom:0}.md-annotation{font-weight:400;outline:none;vertical-align:text-bottom;white-space:normal}[dir=rtl] .md-annotation{direction:rtl}code .md-annotation{font-family:var(--md-code-font-family);font-size:inherit}.md-annotation:not([hidden]){display:inline-block;line-height:1.25}.md-annotation__index{border-radius:.01px;cursor:pointer;display:inline-block;margin-left:.4ch;margin-right:.4ch;outline:none;overflow:hidden;position:relative;-webkit-user-select:none;user-select:none;vertical-align:text-top;z-index:0}.md-annotation .md-annotation__index{transition:z-index .25s}@media screen{.md-annotation__index{width:2.2ch}[data-md-visible]>.md-annotation__index{animation:pulse 2s infinite}.md-annotation__index:before{background:var(--md-default-bg-color);-webkit-mask-image:var(--md-annotation-bg-icon);mask-image:var(--md-annotation-bg-icon)}.md-annotation__index:after,.md-annotation__index:before{content:"";height:2.2ch;-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:-.1ch;width:2.2ch;z-index:-1}.md-annotation__index:after{background-color:var(--md-default-fg-color--lighter);-webkit-mask-image:var(--md-annotation-icon);mask-image:var(--md-annotation-icon);transform:scale(1.0001);transition:background-color .25s,transform .25s}.md-tooltip--active+.md-annotation__index:after{transform:rotate(45deg)}.md-tooltip--active+.md-annotation__index:after,:hover>.md-annotation__index:after{background-color:var(--md-accent-fg-color)}}.md-tooltip--active+.md-annotation__index{animation-play-state:paused;transition-duration:0ms;z-index:2}.md-annotation__index [data-md-annotation-id]{display:inline-block}@media print{.md-annotation__index [data-md-annotation-id]{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);font-weight:700;padding:0 .6ch;white-space:nowrap}.md-annotation__index [data-md-annotation-id]:after{content:attr(data-md-annotation-id)}}.md-typeset .md-annotation-list{counter-reset:xxx;list-style:none}.md-typeset .md-annotation-list li{position:relative}[dir=ltr] .md-typeset .md-annotation-list li:before{left:-2.125em}[dir=rtl] .md-typeset .md-annotation-list li:before{right:-2.125em}.md-typeset .md-annotation-list li:before{background:var(--md-default-fg-color--lighter);border-radius:2ch;color:var(--md-default-bg-color);content:counter(xxx);counter-increment:xxx;font-size:.8875em;font-weight:700;height:2ch;line-height:1.25;min-width:2ch;padding:0 .6ch;position:absolute;text-align:center;top:.25em}[dir=ltr] .md-top{margin-left:50%}[dir=rtl] .md-top{margin-right:50%}.md-top{background-color:var(--md-default-bg-color);border-radius:1.6rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color--light);cursor:pointer;display:block;font-size:.7rem;outline:none;padding:.4rem .8rem;position:fixed;top:3.2rem;transform:translate(-50%);transition:color 125ms,background-color 125ms,transform 125ms cubic-bezier(.4,0,.2,1),opacity 125ms;z-index:2}@media print{.md-top{display:none}}[dir=rtl] .md-top{transform:translate(50%)}.md-top[hidden]{opacity:0;pointer-events:none;transform:translate(-50%,.2rem);transition-duration:0ms}[dir=rtl] .md-top[hidden]{transform:translate(50%,.2rem)}.md-top:focus,.md-top:hover{background-color:var(--md-accent-fg-color);color:var(--md-accent-bg-color)}.md-top svg{display:inline-block;vertical-align:-.5em}@keyframes hoverfix{0%{pointer-events:none}}:root{--md-version-icon:url('data:image/svg+xml;charset=utf-8,')}.md-version{flex-shrink:0;font-size:.8rem;height:2.4rem}[dir=ltr] .md-version__current{margin-left:1.4rem;margin-right:.4rem}[dir=rtl] .md-version__current{margin-left:.4rem;margin-right:1.4rem}.md-version__current{color:inherit;cursor:pointer;outline:none;position:relative;top:.05rem}[dir=ltr] .md-version__current:after{margin-left:.4rem}[dir=rtl] .md-version__current:after{margin-right:.4rem}.md-version__current:after{background-color:currentcolor;content:"";display:inline-block;height:.6rem;-webkit-mask-image:var(--md-version-icon);mask-image:var(--md-version-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.4rem}.md-version__list{background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);color:var(--md-default-fg-color);list-style-type:none;margin:.2rem .8rem;max-height:0;opacity:0;overflow:auto;padding:0;position:absolute;scroll-snap-type:y mandatory;top:.15rem;transition:max-height 0ms .5s,opacity .25s .25s;z-index:3}.md-version:focus-within .md-version__list,.md-version:hover .md-version__list{max-height:10rem;opacity:1;transition:max-height 0ms,opacity .25s}@media (hover:none),(pointer:coarse){.md-version:hover .md-version__list{animation:hoverfix .25s forwards}.md-version:focus-within .md-version__list{animation:none}}.md-version__item{line-height:1.8rem}[dir=ltr] .md-version__link{padding-left:.6rem;padding-right:1.2rem}[dir=rtl] .md-version__link{padding-left:1.2rem;padding-right:.6rem}.md-version__link{cursor:pointer;display:block;outline:none;scroll-snap-align:start;transition:color .25s,background-color .25s;white-space:nowrap;width:100%}.md-version__link:focus,.md-version__link:hover{color:var(--md-accent-fg-color)}.md-version__link:focus{background-color:var(--md-default-fg-color--lightest)}:root{--md-admonition-icon--note:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--abstract:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--info:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--tip:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--success:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--question:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--warning:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--failure:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--danger:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--bug:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--example:url('data:image/svg+xml;charset=utf-8,');--md-admonition-icon--quote:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .admonition,.md-typeset details{background-color:var(--md-admonition-bg-color);border:.075rem solid #448aff;border-radius:.2rem;box-shadow:var(--md-shadow-z1);color:var(--md-admonition-fg-color);display:flow-root;font-size:.64rem;margin:1.5625em 0;padding:0 .6rem;page-break-inside:avoid;transition:box-shadow 125ms}@media print{.md-typeset .admonition,.md-typeset details{box-shadow:none}}.md-typeset .admonition:focus-within,.md-typeset details:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .admonition>*,.md-typeset details>*{box-sizing:border-box}.md-typeset .admonition .admonition,.md-typeset .admonition details,.md-typeset details .admonition,.md-typeset details details{margin-bottom:1em;margin-top:1em}.md-typeset .admonition .md-typeset__scrollwrap,.md-typeset details .md-typeset__scrollwrap{margin:1em -.6rem}.md-typeset .admonition .md-typeset__table,.md-typeset details .md-typeset__table{padding:0 .6rem}.md-typeset .admonition>.tabbed-set:only-child,.md-typeset details>.tabbed-set:only-child{margin-top:0}html .md-typeset .admonition>:last-child,html .md-typeset details>:last-child{margin-bottom:.6rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{padding-left:2rem;padding-right:.6rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{padding-left:.6rem;padding-right:2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-left-width:.2rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-right-width:.2rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset .admonition-title,[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset .admonition-title,[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset .admonition-title,.md-typeset summary{background-color:#448aff1a;border:none;font-weight:700;margin:0 -.6rem;padding-bottom:.4rem;padding-top:.4rem;position:relative}html .md-typeset .admonition-title:last-child,html .md-typeset summary:last-child{margin-bottom:0}[dir=ltr] .md-typeset .admonition-title:before,[dir=ltr] .md-typeset summary:before{left:.6rem}[dir=rtl] .md-typeset .admonition-title:before,[dir=rtl] .md-typeset summary:before{right:.6rem}.md-typeset .admonition-title:before,.md-typeset summary:before{background-color:#448aff;content:"";height:1rem;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;width:1rem}.md-typeset .admonition-title code,.md-typeset summary code{box-shadow:0 0 0 .05rem var(--md-default-fg-color--lightest)}.md-typeset .admonition.note,.md-typeset details.note{border-color:#448aff}.md-typeset .admonition.note:focus-within,.md-typeset details.note:focus-within{box-shadow:0 0 0 .2rem #448aff1a}.md-typeset .note>.admonition-title,.md-typeset .note>summary{background-color:#448aff1a}.md-typeset .note>.admonition-title:before,.md-typeset .note>summary:before{background-color:#448aff;-webkit-mask-image:var(--md-admonition-icon--note);mask-image:var(--md-admonition-icon--note)}.md-typeset .note>.admonition-title:after,.md-typeset .note>summary:after{color:#448aff}.md-typeset .admonition.abstract,.md-typeset details.abstract{border-color:#00b0ff}.md-typeset .admonition.abstract:focus-within,.md-typeset details.abstract:focus-within{box-shadow:0 0 0 .2rem #00b0ff1a}.md-typeset .abstract>.admonition-title,.md-typeset .abstract>summary{background-color:#00b0ff1a}.md-typeset .abstract>.admonition-title:before,.md-typeset .abstract>summary:before{background-color:#00b0ff;-webkit-mask-image:var(--md-admonition-icon--abstract);mask-image:var(--md-admonition-icon--abstract)}.md-typeset .abstract>.admonition-title:after,.md-typeset .abstract>summary:after{color:#00b0ff}.md-typeset .admonition.info,.md-typeset details.info{border-color:#00b8d4}.md-typeset .admonition.info:focus-within,.md-typeset details.info:focus-within{box-shadow:0 0 0 .2rem #00b8d41a}.md-typeset .info>.admonition-title,.md-typeset .info>summary{background-color:#00b8d41a}.md-typeset .info>.admonition-title:before,.md-typeset .info>summary:before{background-color:#00b8d4;-webkit-mask-image:var(--md-admonition-icon--info);mask-image:var(--md-admonition-icon--info)}.md-typeset .info>.admonition-title:after,.md-typeset .info>summary:after{color:#00b8d4}.md-typeset .admonition.tip,.md-typeset details.tip{border-color:#00bfa5}.md-typeset .admonition.tip:focus-within,.md-typeset details.tip:focus-within{box-shadow:0 0 0 .2rem #00bfa51a}.md-typeset .tip>.admonition-title,.md-typeset .tip>summary{background-color:#00bfa51a}.md-typeset .tip>.admonition-title:before,.md-typeset .tip>summary:before{background-color:#00bfa5;-webkit-mask-image:var(--md-admonition-icon--tip);mask-image:var(--md-admonition-icon--tip)}.md-typeset .tip>.admonition-title:after,.md-typeset .tip>summary:after{color:#00bfa5}.md-typeset .admonition.success,.md-typeset details.success{border-color:#00c853}.md-typeset .admonition.success:focus-within,.md-typeset details.success:focus-within{box-shadow:0 0 0 .2rem #00c8531a}.md-typeset .success>.admonition-title,.md-typeset .success>summary{background-color:#00c8531a}.md-typeset .success>.admonition-title:before,.md-typeset .success>summary:before{background-color:#00c853;-webkit-mask-image:var(--md-admonition-icon--success);mask-image:var(--md-admonition-icon--success)}.md-typeset .success>.admonition-title:after,.md-typeset .success>summary:after{color:#00c853}.md-typeset .admonition.question,.md-typeset details.question{border-color:#64dd17}.md-typeset .admonition.question:focus-within,.md-typeset details.question:focus-within{box-shadow:0 0 0 .2rem #64dd171a}.md-typeset .question>.admonition-title,.md-typeset .question>summary{background-color:#64dd171a}.md-typeset .question>.admonition-title:before,.md-typeset .question>summary:before{background-color:#64dd17;-webkit-mask-image:var(--md-admonition-icon--question);mask-image:var(--md-admonition-icon--question)}.md-typeset .question>.admonition-title:after,.md-typeset .question>summary:after{color:#64dd17}.md-typeset .admonition.warning,.md-typeset details.warning{border-color:#ff9100}.md-typeset .admonition.warning:focus-within,.md-typeset details.warning:focus-within{box-shadow:0 0 0 .2rem #ff91001a}.md-typeset .warning>.admonition-title,.md-typeset .warning>summary{background-color:#ff91001a}.md-typeset .warning>.admonition-title:before,.md-typeset .warning>summary:before{background-color:#ff9100;-webkit-mask-image:var(--md-admonition-icon--warning);mask-image:var(--md-admonition-icon--warning)}.md-typeset .warning>.admonition-title:after,.md-typeset .warning>summary:after{color:#ff9100}.md-typeset .admonition.failure,.md-typeset details.failure{border-color:#ff5252}.md-typeset .admonition.failure:focus-within,.md-typeset details.failure:focus-within{box-shadow:0 0 0 .2rem #ff52521a}.md-typeset .failure>.admonition-title,.md-typeset .failure>summary{background-color:#ff52521a}.md-typeset .failure>.admonition-title:before,.md-typeset .failure>summary:before{background-color:#ff5252;-webkit-mask-image:var(--md-admonition-icon--failure);mask-image:var(--md-admonition-icon--failure)}.md-typeset .failure>.admonition-title:after,.md-typeset .failure>summary:after{color:#ff5252}.md-typeset .admonition.danger,.md-typeset details.danger{border-color:#ff1744}.md-typeset .admonition.danger:focus-within,.md-typeset details.danger:focus-within{box-shadow:0 0 0 .2rem #ff17441a}.md-typeset .danger>.admonition-title,.md-typeset .danger>summary{background-color:#ff17441a}.md-typeset .danger>.admonition-title:before,.md-typeset .danger>summary:before{background-color:#ff1744;-webkit-mask-image:var(--md-admonition-icon--danger);mask-image:var(--md-admonition-icon--danger)}.md-typeset .danger>.admonition-title:after,.md-typeset .danger>summary:after{color:#ff1744}.md-typeset .admonition.bug,.md-typeset details.bug{border-color:#f50057}.md-typeset .admonition.bug:focus-within,.md-typeset details.bug:focus-within{box-shadow:0 0 0 .2rem #f500571a}.md-typeset .bug>.admonition-title,.md-typeset .bug>summary{background-color:#f500571a}.md-typeset .bug>.admonition-title:before,.md-typeset .bug>summary:before{background-color:#f50057;-webkit-mask-image:var(--md-admonition-icon--bug);mask-image:var(--md-admonition-icon--bug)}.md-typeset .bug>.admonition-title:after,.md-typeset .bug>summary:after{color:#f50057}.md-typeset .admonition.example,.md-typeset details.example{border-color:#7c4dff}.md-typeset .admonition.example:focus-within,.md-typeset details.example:focus-within{box-shadow:0 0 0 .2rem #7c4dff1a}.md-typeset .example>.admonition-title,.md-typeset .example>summary{background-color:#7c4dff1a}.md-typeset .example>.admonition-title:before,.md-typeset .example>summary:before{background-color:#7c4dff;-webkit-mask-image:var(--md-admonition-icon--example);mask-image:var(--md-admonition-icon--example)}.md-typeset .example>.admonition-title:after,.md-typeset .example>summary:after{color:#7c4dff}.md-typeset .admonition.quote,.md-typeset details.quote{border-color:#9e9e9e}.md-typeset .admonition.quote:focus-within,.md-typeset details.quote:focus-within{box-shadow:0 0 0 .2rem #9e9e9e1a}.md-typeset .quote>.admonition-title,.md-typeset .quote>summary{background-color:#9e9e9e1a}.md-typeset .quote>.admonition-title:before,.md-typeset .quote>summary:before{background-color:#9e9e9e;-webkit-mask-image:var(--md-admonition-icon--quote);mask-image:var(--md-admonition-icon--quote)}.md-typeset .quote>.admonition-title:after,.md-typeset .quote>summary:after{color:#9e9e9e}:root{--md-footnotes-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .footnote{color:var(--md-default-fg-color--light);font-size:.64rem}[dir=ltr] .md-typeset .footnote>ol{margin-left:0}[dir=rtl] .md-typeset .footnote>ol{margin-right:0}.md-typeset .footnote>ol>li{transition:color 125ms}.md-typeset .footnote>ol>li:target{color:var(--md-default-fg-color)}.md-typeset .footnote>ol>li:focus-within .footnote-backref{opacity:1;transform:translateX(0);transition:none}.md-typeset .footnote>ol>li:hover .footnote-backref,.md-typeset .footnote>ol>li:target .footnote-backref{opacity:1;transform:translateX(0)}.md-typeset .footnote>ol>li>:first-child{margin-top:0}.md-typeset .footnote-ref{font-size:.75em;font-weight:700}html .md-typeset .footnote-ref{outline-offset:.1rem}.md-typeset [id^="fnref:"]:target>.footnote-ref{outline:auto}.md-typeset .footnote-backref{color:var(--md-typeset-a-color);display:inline-block;font-size:0;opacity:0;transform:translateX(.25rem);transition:color .25s,transform .25s .25s,opacity 125ms .25s;vertical-align:text-bottom}@media print{.md-typeset .footnote-backref{color:var(--md-typeset-a-color);opacity:1;transform:translateX(0)}}[dir=rtl] .md-typeset .footnote-backref{transform:translateX(-.25rem)}.md-typeset .footnote-backref:hover{color:var(--md-accent-fg-color)}.md-typeset .footnote-backref:before{background-color:currentcolor;content:"";display:inline-block;height:.8rem;-webkit-mask-image:var(--md-footnotes-icon);mask-image:var(--md-footnotes-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;width:.8rem}[dir=rtl] .md-typeset .footnote-backref:before svg{transform:scaleX(-1)}[dir=ltr] .md-typeset .headerlink{margin-left:.5rem}[dir=rtl] .md-typeset .headerlink{margin-right:.5rem}.md-typeset .headerlink{color:var(--md-default-fg-color--lighter);display:inline-block;opacity:0;transition:color .25s,opacity 125ms}@media print{.md-typeset .headerlink{display:none}}.md-typeset .headerlink:focus,.md-typeset :hover>.headerlink,.md-typeset :target>.headerlink{opacity:1;transition:color .25s,opacity 125ms}.md-typeset .headerlink:focus,.md-typeset .headerlink:hover,.md-typeset :target>.headerlink{color:var(--md-accent-fg-color)}.md-typeset :target{--md-scroll-margin:3.6rem;--md-scroll-offset:0rem;scroll-margin-top:calc(var(--md-scroll-margin) - var(--md-scroll-offset))}@media screen and (min-width:76.25em){.md-header--lifted~.md-container .md-typeset :target{--md-scroll-margin:6rem}}.md-typeset h1:target,.md-typeset h2:target,.md-typeset h3:target{--md-scroll-offset:0.2rem}.md-typeset h4:target{--md-scroll-offset:0.15rem}.md-typeset div.arithmatex{overflow:auto}@media screen and (max-width:44.984375em){.md-typeset div.arithmatex{margin:0 -.8rem}.md-typeset div.arithmatex>*{width:-webkit-min-content;width:min-content}}.md-typeset div.arithmatex>*{margin-left:auto!important;margin-right:auto!important;padding:0 .8rem;touch-action:auto}.md-typeset div.arithmatex>* mjx-container{margin:0!important}.md-typeset div.arithmatex mjx-assistive-mml{height:0}.md-typeset del.critic{background-color:var(--md-typeset-del-color)}.md-typeset del.critic,.md-typeset ins.critic{-webkit-box-decoration-break:clone;box-decoration-break:clone}.md-typeset ins.critic{background-color:var(--md-typeset-ins-color)}.md-typeset .critic.comment{-webkit-box-decoration-break:clone;box-decoration-break:clone;color:var(--md-code-hl-comment-color)}.md-typeset .critic.comment:before{content:"/* "}.md-typeset .critic.comment:after{content:" */"}.md-typeset .critic.block{box-shadow:none;display:block;margin:1em 0;overflow:auto;padding-left:.8rem;padding-right:.8rem}.md-typeset .critic.block>:first-child{margin-top:.5em}.md-typeset .critic.block>:last-child{margin-bottom:.5em}:root{--md-details-icon:url('data:image/svg+xml;charset=utf-8,')}.md-typeset details{display:flow-root;overflow:visible;padding-top:0}.md-typeset details[open]>summary:after{transform:rotate(90deg)}.md-typeset details:not([open]){box-shadow:none;padding-bottom:0}.md-typeset details:not([open])>summary{border-radius:.1rem}[dir=ltr] .md-typeset summary{padding-right:1.8rem}[dir=rtl] .md-typeset summary{padding-left:1.8rem}[dir=ltr] .md-typeset summary{border-top-left-radius:.1rem}[dir=ltr] .md-typeset summary,[dir=rtl] .md-typeset summary{border-top-right-radius:.1rem}[dir=rtl] .md-typeset summary{border-top-left-radius:.1rem}.md-typeset summary{cursor:pointer;display:block;min-height:1rem;overflow:hidden}.md-typeset summary.focus-visible{outline-color:var(--md-accent-fg-color);outline-offset:.2rem}.md-typeset summary:not(.focus-visible){-webkit-tap-highlight-color:transparent;outline:none}[dir=ltr] .md-typeset summary:after{right:.4rem}[dir=rtl] .md-typeset summary:after{left:.4rem}.md-typeset summary:after{background-color:currentcolor;content:"";height:1rem;-webkit-mask-image:var(--md-details-icon);mask-image:var(--md-details-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.625em;transform:rotate(0deg);transition:transform .25s;width:1rem}[dir=rtl] .md-typeset summary:after{transform:rotate(180deg)}.md-typeset summary::marker{display:none}.md-typeset summary::-webkit-details-marker{display:none}.md-typeset .emojione,.md-typeset .gemoji,.md-typeset .twemoji{--md-icon-size:1.125em;display:inline-flex;height:var(--md-icon-size);vertical-align:text-top}.md-typeset .emojione svg,.md-typeset .gemoji svg,.md-typeset .twemoji svg{fill:currentcolor;max-height:100%;width:var(--md-icon-size)}.md-typeset .lg,.md-typeset .xl,.md-typeset .xxl,.md-typeset .xxxl{vertical-align:text-bottom}.md-typeset .middle{vertical-align:middle}.md-typeset .lg{--md-icon-size:1.5em}.md-typeset .xl{--md-icon-size:2.25em}.md-typeset .xxl{--md-icon-size:3em}.md-typeset .xxxl{--md-icon-size:4em}.highlight .o,.highlight .ow{color:var(--md-code-hl-operator-color)}.highlight .p{color:var(--md-code-hl-punctuation-color)}.highlight .cpf,.highlight .l,.highlight .s,.highlight .s1,.highlight .s2,.highlight .sb,.highlight .sc,.highlight .si,.highlight .ss{color:var(--md-code-hl-string-color)}.highlight .cp,.highlight .se,.highlight .sh,.highlight .sr,.highlight .sx{color:var(--md-code-hl-special-color)}.highlight .il,.highlight .m,.highlight .mb,.highlight .mf,.highlight .mh,.highlight .mi,.highlight .mo{color:var(--md-code-hl-number-color)}.highlight .k,.highlight .kd,.highlight .kn,.highlight .kp,.highlight .kr,.highlight .kt{color:var(--md-code-hl-keyword-color)}.highlight .kc,.highlight .n{color:var(--md-code-hl-name-color)}.highlight .bp,.highlight .nb,.highlight .no{color:var(--md-code-hl-constant-color)}.highlight .nc,.highlight .ne,.highlight .nf,.highlight .nn{color:var(--md-code-hl-function-color)}.highlight .nd,.highlight .ni,.highlight .nl,.highlight .nt{color:var(--md-code-hl-keyword-color)}.highlight .c,.highlight .c1,.highlight .ch,.highlight .cm,.highlight .cs,.highlight .sd{color:var(--md-code-hl-comment-color)}.highlight .na,.highlight .nv,.highlight .vc,.highlight .vg,.highlight .vi{color:var(--md-code-hl-variable-color)}.highlight .ge,.highlight .gh,.highlight .go,.highlight .gp,.highlight .gr,.highlight .gs,.highlight .gt,.highlight .gu{color:var(--md-code-hl-generic-color)}.highlight .gd,.highlight .gi{border-radius:.1rem;margin:0 -.125em;padding:0 .125em}.highlight .gd{background-color:var(--md-typeset-del-color)}.highlight .gi{background-color:var(--md-typeset-ins-color)}.highlight .hll{background-color:var(--md-code-hl-color--light);box-shadow:2px 0 0 0 var(--md-code-hl-color) inset;display:block;margin:0 -1.1764705882em;padding:0 1.1764705882em}.highlight span.filename{background-color:var(--md-code-bg-color);border-bottom:.05rem solid var(--md-default-fg-color--lightest);border-top-left-radius:.1rem;border-top-right-radius:.1rem;display:flow-root;font-size:.85em;font-weight:700;margin-top:1em;padding:.6617647059em 1.1764705882em;position:relative}.highlight span.filename+pre{margin-top:0}.highlight span.filename+pre>code{border-top-left-radius:0;border-top-right-radius:0}.highlight [data-linenos]:before{background-color:var(--md-code-bg-color);box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;color:var(--md-default-fg-color--light);content:attr(data-linenos);float:left;left:-1.1764705882em;margin-left:-1.1764705882em;margin-right:1.1764705882em;padding-left:1.1764705882em;position:sticky;-webkit-user-select:none;user-select:none;z-index:3}.highlight code a[id]{position:absolute;visibility:hidden}.highlight code[data-md-copying] .hll{display:contents}.highlight code[data-md-copying] .md-annotation{display:none}.highlighttable{display:flow-root}.highlighttable tbody,.highlighttable td{display:block;padding:0}.highlighttable tr{display:flex}.highlighttable pre{margin:0}.highlighttable th.filename{flex-grow:1;padding:0;text-align:left}.highlighttable th.filename span.filename{margin-top:0}.highlighttable .linenos{background-color:var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-top-left-radius:.1rem;font-size:.85em;padding:.7720588235em 0 .7720588235em 1.1764705882em;-webkit-user-select:none;user-select:none}.highlighttable .linenodiv{box-shadow:-.05rem 0 var(--md-default-fg-color--lightest) inset;padding-right:.5882352941em}.highlighttable .linenodiv pre{color:var(--md-default-fg-color--light);text-align:right}.highlighttable .code{flex:1;min-width:0}.linenodiv a{color:inherit}.md-typeset .highlighttable{direction:ltr;margin:1em 0}.md-typeset .highlighttable>tbody>tr>.code>div>pre>code{border-bottom-left-radius:0;border-top-left-radius:0}.md-typeset .highlight+.result{border:.05rem solid var(--md-code-bg-color);border-bottom-left-radius:.1rem;border-bottom-right-radius:.1rem;border-top-width:.1rem;margin-top:-1.125em;overflow:visible;padding:0 1em}.md-typeset .highlight+.result:after{clear:both;content:"";display:block}@media screen and (max-width:44.984375em){.md-content__inner>.highlight{margin:1em -.8rem}.md-content__inner>.highlight>.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.code>div>pre>code,.md-content__inner>.highlight>.highlighttable>tbody>tr>.filename span.filename,.md-content__inner>.highlight>.highlighttable>tbody>tr>.linenos,.md-content__inner>.highlight>pre>code{border-radius:0}.md-content__inner>.highlight+.result{border-left-width:0;border-radius:0;border-right-width:0;margin-left:-.8rem;margin-right:-.8rem}}.md-typeset .keys kbd:after,.md-typeset .keys kbd:before{-moz-osx-font-smoothing:initial;-webkit-font-smoothing:initial;color:inherit;margin:0;position:relative}.md-typeset .keys span{color:var(--md-default-fg-color--light);padding:0 .2em}.md-typeset .keys .key-alt:before,.md-typeset .keys .key-left-alt:before,.md-typeset .keys .key-right-alt:before{content:"⎇";padding-right:.4em}.md-typeset .keys .key-command:before,.md-typeset .keys .key-left-command:before,.md-typeset .keys .key-right-command:before{content:"⌘";padding-right:.4em}.md-typeset .keys .key-control:before,.md-typeset .keys .key-left-control:before,.md-typeset .keys .key-right-control:before{content:"⌃";padding-right:.4em}.md-typeset .keys .key-left-meta:before,.md-typeset .keys .key-meta:before,.md-typeset .keys .key-right-meta:before{content:"◆";padding-right:.4em}.md-typeset .keys .key-left-option:before,.md-typeset .keys .key-option:before,.md-typeset .keys .key-right-option:before{content:"⌥";padding-right:.4em}.md-typeset .keys .key-left-shift:before,.md-typeset .keys .key-right-shift:before,.md-typeset .keys .key-shift:before{content:"⇧";padding-right:.4em}.md-typeset .keys .key-left-super:before,.md-typeset .keys .key-right-super:before,.md-typeset .keys .key-super:before{content:"❖";padding-right:.4em}.md-typeset .keys .key-left-windows:before,.md-typeset .keys .key-right-windows:before,.md-typeset .keys .key-windows:before{content:"⊞";padding-right:.4em}.md-typeset .keys .key-arrow-down:before{content:"↓";padding-right:.4em}.md-typeset .keys .key-arrow-left:before{content:"←";padding-right:.4em}.md-typeset .keys .key-arrow-right:before{content:"→";padding-right:.4em}.md-typeset .keys .key-arrow-up:before{content:"↑";padding-right:.4em}.md-typeset .keys .key-backspace:before{content:"⌫";padding-right:.4em}.md-typeset .keys .key-backtab:before{content:"⇤";padding-right:.4em}.md-typeset .keys .key-caps-lock:before{content:"⇪";padding-right:.4em}.md-typeset .keys .key-clear:before{content:"⌧";padding-right:.4em}.md-typeset .keys .key-context-menu:before{content:"☰";padding-right:.4em}.md-typeset .keys .key-delete:before{content:"⌦";padding-right:.4em}.md-typeset .keys .key-eject:before{content:"⏏";padding-right:.4em}.md-typeset .keys .key-end:before{content:"⤓";padding-right:.4em}.md-typeset .keys .key-escape:before{content:"⎋";padding-right:.4em}.md-typeset .keys .key-home:before{content:"⤒";padding-right:.4em}.md-typeset .keys .key-insert:before{content:"⎀";padding-right:.4em}.md-typeset .keys .key-page-down:before{content:"⇟";padding-right:.4em}.md-typeset .keys .key-page-up:before{content:"⇞";padding-right:.4em}.md-typeset .keys .key-print-screen:before{content:"⎙";padding-right:.4em}.md-typeset .keys .key-tab:after{content:"⇥";padding-left:.4em}.md-typeset .keys .key-num-enter:after{content:"⌤";padding-left:.4em}.md-typeset .keys .key-enter:after{content:"⏎";padding-left:.4em}:root{--md-tabbed-icon--prev:url('data:image/svg+xml;charset=utf-8,');--md-tabbed-icon--next:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .tabbed-set{border-radius:.1rem;display:flex;flex-flow:column wrap;margin:1em 0;position:relative}.md-typeset .tabbed-set>input{height:0;opacity:0;position:absolute;width:0}.md-typeset .tabbed-set>input:target{--md-scroll-offset:0.625em}.md-typeset .tabbed-set>input.focus-visible~.tabbed-labels:before{background-color:var(--md-accent-fg-color)}.md-typeset .tabbed-labels{-ms-overflow-style:none;box-shadow:0 -.05rem var(--md-default-fg-color--lightest) inset;display:flex;max-width:100%;overflow:auto;scrollbar-width:none}@media print{.md-typeset .tabbed-labels{display:contents}}@media screen{.js .md-typeset .tabbed-labels{position:relative}.js .md-typeset .tabbed-labels:before{background:var(--md-default-fg-color);bottom:0;content:"";display:block;height:2px;left:0;position:absolute;transform:translateX(var(--md-indicator-x));transition:width 225ms,background-color .25s,transform .25s;transition-timing-function:cubic-bezier(.4,0,.2,1);width:var(--md-indicator-width)}}.md-typeset .tabbed-labels::-webkit-scrollbar{display:none}.md-typeset .tabbed-labels>label{border-bottom:.1rem solid #0000;border-radius:.1rem .1rem 0 0;color:var(--md-default-fg-color--light);cursor:pointer;flex-shrink:0;font-size:.64rem;font-weight:700;padding:.78125em 1.25em .625em;scroll-margin-inline-start:1rem;transition:background-color .25s,color .25s;white-space:nowrap;width:auto}@media print{.md-typeset .tabbed-labels>label:first-child{order:1}.md-typeset .tabbed-labels>label:nth-child(2){order:2}.md-typeset .tabbed-labels>label:nth-child(3){order:3}.md-typeset .tabbed-labels>label:nth-child(4){order:4}.md-typeset .tabbed-labels>label:nth-child(5){order:5}.md-typeset .tabbed-labels>label:nth-child(6){order:6}.md-typeset .tabbed-labels>label:nth-child(7){order:7}.md-typeset .tabbed-labels>label:nth-child(8){order:8}.md-typeset .tabbed-labels>label:nth-child(9){order:9}.md-typeset .tabbed-labels>label:nth-child(10){order:10}.md-typeset .tabbed-labels>label:nth-child(11){order:11}.md-typeset .tabbed-labels>label:nth-child(12){order:12}.md-typeset .tabbed-labels>label:nth-child(13){order:13}.md-typeset .tabbed-labels>label:nth-child(14){order:14}.md-typeset .tabbed-labels>label:nth-child(15){order:15}.md-typeset .tabbed-labels>label:nth-child(16){order:16}.md-typeset .tabbed-labels>label:nth-child(17){order:17}.md-typeset .tabbed-labels>label:nth-child(18){order:18}.md-typeset .tabbed-labels>label:nth-child(19){order:19}.md-typeset .tabbed-labels>label:nth-child(20){order:20}}.md-typeset .tabbed-labels>label:hover{color:var(--md-default-fg-color)}.md-typeset .tabbed-labels>label>[href]:first-child{color:inherit}.md-typeset .tabbed-labels--linked>label{padding:0}.md-typeset .tabbed-labels--linked>label>a{display:block;padding:.78125em 1.25em .625em}.md-typeset .tabbed-content{width:100%}@media print{.md-typeset .tabbed-content{display:contents}}.md-typeset .tabbed-block{display:none}@media print{.md-typeset .tabbed-block{display:block}.md-typeset .tabbed-block:first-child{order:1}.md-typeset .tabbed-block:nth-child(2){order:2}.md-typeset .tabbed-block:nth-child(3){order:3}.md-typeset .tabbed-block:nth-child(4){order:4}.md-typeset .tabbed-block:nth-child(5){order:5}.md-typeset .tabbed-block:nth-child(6){order:6}.md-typeset .tabbed-block:nth-child(7){order:7}.md-typeset .tabbed-block:nth-child(8){order:8}.md-typeset .tabbed-block:nth-child(9){order:9}.md-typeset .tabbed-block:nth-child(10){order:10}.md-typeset .tabbed-block:nth-child(11){order:11}.md-typeset .tabbed-block:nth-child(12){order:12}.md-typeset .tabbed-block:nth-child(13){order:13}.md-typeset .tabbed-block:nth-child(14){order:14}.md-typeset .tabbed-block:nth-child(15){order:15}.md-typeset .tabbed-block:nth-child(16){order:16}.md-typeset .tabbed-block:nth-child(17){order:17}.md-typeset .tabbed-block:nth-child(18){order:18}.md-typeset .tabbed-block:nth-child(19){order:19}.md-typeset .tabbed-block:nth-child(20){order:20}}.md-typeset .tabbed-block>.highlight:first-child>pre,.md-typeset .tabbed-block>pre:first-child{margin:0}.md-typeset .tabbed-block>.highlight:first-child>pre>code,.md-typeset .tabbed-block>pre:first-child>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child>.filename{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable{margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.filename span.filename,.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.linenos{border-top-left-radius:0;border-top-right-radius:0;margin:0}.md-typeset .tabbed-block>.highlight:first-child>.highlighttable>tbody>tr>.code>div>pre>code{border-top-left-radius:0;border-top-right-radius:0}.md-typeset .tabbed-block>.highlight:first-child+.result{margin-top:-.125em}.md-typeset .tabbed-block>.tabbed-set{margin:0}.md-typeset .tabbed-button{align-self:center;border-radius:100%;color:var(--md-default-fg-color--light);cursor:pointer;display:block;height:.9rem;margin-top:.1rem;pointer-events:auto;transition:background-color .25s;width:.9rem}.md-typeset .tabbed-button:hover{background-color:var(--md-accent-fg-color--transparent);color:var(--md-accent-fg-color)}.md-typeset .tabbed-button:after{background-color:currentcolor;content:"";display:block;height:100%;-webkit-mask-image:var(--md-tabbed-icon--prev);mask-image:var(--md-tabbed-icon--prev);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;transition:background-color .25s,transform .25s;width:100%}.md-typeset .tabbed-control{background:linear-gradient(to right,var(--md-default-bg-color) 60%,#0000);display:flex;height:1.9rem;justify-content:start;pointer-events:none;position:absolute;transition:opacity 125ms;width:1.2rem}[dir=rtl] .md-typeset .tabbed-control{transform:rotate(180deg)}.md-typeset .tabbed-control[hidden]{opacity:0}.md-typeset .tabbed-control--next{background:linear-gradient(to left,var(--md-default-bg-color) 60%,#0000);justify-content:end;right:0}.md-typeset .tabbed-control--next .tabbed-button:after{-webkit-mask-image:var(--md-tabbed-icon--next);mask-image:var(--md-tabbed-icon--next)}@media screen and (max-width:44.984375em){[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels{padding-right:.8rem}.md-content__inner>.tabbed-set .tabbed-labels{margin:0 -.8rem;max-width:100vw;scroll-padding-inline-start:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels:after{padding-left:.8rem}.md-content__inner>.tabbed-set .tabbed-labels:after{content:""}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-left:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{padding-right:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-left:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{margin-right:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--prev{width:2rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-right:.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{padding-left:.8rem}[dir=ltr] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-right:-.8rem}[dir=rtl] .md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{margin-left:-.8rem}.md-content__inner>.tabbed-set .tabbed-labels~.tabbed-control--next{width:2rem}}@media screen{.md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){color:var(--md-default-fg-color)}.md-typeset .no-js .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.md-typeset .no-js .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.md-typeset .no-js .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.md-typeset .no-js .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.md-typeset .no-js .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.md-typeset .no-js .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.md-typeset .no-js .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.md-typeset .no-js .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.md-typeset .no-js .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.md-typeset .no-js .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.md-typeset .no-js .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.md-typeset .no-js .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.md-typeset .no-js .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.md-typeset .no-js .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.md-typeset .no-js .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.md-typeset .no-js .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.md-typeset .no-js .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.md-typeset .no-js .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.md-typeset .no-js .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.md-typeset .no-js .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),.no-js .md-typeset .tabbed-set>input:first-child:checked~.tabbed-labels>:first-child,.no-js .md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),.no-js .md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),.no-js .md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),.no-js .md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),.no-js .md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),.no-js .md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),.no-js .md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),.no-js .md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),.no-js .md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),.no-js .md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),.no-js .md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),.no-js .md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20),.no-js .md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),.no-js .md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),.no-js .md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),.no-js .md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),.no-js .md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),.no-js .md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),.no-js .md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9){border-color:var(--md-default-fg-color)}}.md-typeset .tabbed-set>input:first-child.focus-visible~.tabbed-labels>:first-child,.md-typeset .tabbed-set>input:nth-child(10).focus-visible~.tabbed-labels>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11).focus-visible~.tabbed-labels>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12).focus-visible~.tabbed-labels>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13).focus-visible~.tabbed-labels>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14).focus-visible~.tabbed-labels>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15).focus-visible~.tabbed-labels>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16).focus-visible~.tabbed-labels>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17).focus-visible~.tabbed-labels>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18).focus-visible~.tabbed-labels>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19).focus-visible~.tabbed-labels>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2).focus-visible~.tabbed-labels>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20).focus-visible~.tabbed-labels>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3).focus-visible~.tabbed-labels>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4).focus-visible~.tabbed-labels>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5).focus-visible~.tabbed-labels>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6).focus-visible~.tabbed-labels>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7).focus-visible~.tabbed-labels>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8).focus-visible~.tabbed-labels>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9).focus-visible~.tabbed-labels>:nth-child(9){color:var(--md-accent-fg-color)}.md-typeset .tabbed-set>input:first-child:checked~.tabbed-content>:first-child,.md-typeset .tabbed-set>input:nth-child(10):checked~.tabbed-content>:nth-child(10),.md-typeset .tabbed-set>input:nth-child(11):checked~.tabbed-content>:nth-child(11),.md-typeset .tabbed-set>input:nth-child(12):checked~.tabbed-content>:nth-child(12),.md-typeset .tabbed-set>input:nth-child(13):checked~.tabbed-content>:nth-child(13),.md-typeset .tabbed-set>input:nth-child(14):checked~.tabbed-content>:nth-child(14),.md-typeset .tabbed-set>input:nth-child(15):checked~.tabbed-content>:nth-child(15),.md-typeset .tabbed-set>input:nth-child(16):checked~.tabbed-content>:nth-child(16),.md-typeset .tabbed-set>input:nth-child(17):checked~.tabbed-content>:nth-child(17),.md-typeset .tabbed-set>input:nth-child(18):checked~.tabbed-content>:nth-child(18),.md-typeset .tabbed-set>input:nth-child(19):checked~.tabbed-content>:nth-child(19),.md-typeset .tabbed-set>input:nth-child(2):checked~.tabbed-content>:nth-child(2),.md-typeset .tabbed-set>input:nth-child(20):checked~.tabbed-content>:nth-child(20),.md-typeset .tabbed-set>input:nth-child(3):checked~.tabbed-content>:nth-child(3),.md-typeset .tabbed-set>input:nth-child(4):checked~.tabbed-content>:nth-child(4),.md-typeset .tabbed-set>input:nth-child(5):checked~.tabbed-content>:nth-child(5),.md-typeset .tabbed-set>input:nth-child(6):checked~.tabbed-content>:nth-child(6),.md-typeset .tabbed-set>input:nth-child(7):checked~.tabbed-content>:nth-child(7),.md-typeset .tabbed-set>input:nth-child(8):checked~.tabbed-content>:nth-child(8),.md-typeset .tabbed-set>input:nth-child(9):checked~.tabbed-content>:nth-child(9){display:block}:root{--md-tasklist-icon:url('data:image/svg+xml;charset=utf-8,');--md-tasklist-icon--checked:url('data:image/svg+xml;charset=utf-8,')}.md-typeset .task-list-item{list-style-type:none;position:relative}[dir=ltr] .md-typeset .task-list-item [type=checkbox]{left:-2em}[dir=rtl] .md-typeset .task-list-item [type=checkbox]{right:-2em}.md-typeset .task-list-item [type=checkbox]{position:absolute;top:.45em}.md-typeset .task-list-control [type=checkbox]{opacity:0;z-index:-1}[dir=ltr] .md-typeset .task-list-indicator:before{left:-1.5em}[dir=rtl] .md-typeset .task-list-indicator:before{right:-1.5em}.md-typeset .task-list-indicator:before{background-color:var(--md-default-fg-color--lightest);content:"";height:1.25em;-webkit-mask-image:var(--md-tasklist-icon);mask-image:var(--md-tasklist-icon);-webkit-mask-position:center;mask-position:center;-webkit-mask-repeat:no-repeat;mask-repeat:no-repeat;-webkit-mask-size:contain;mask-size:contain;position:absolute;top:.15em;width:1.25em}.md-typeset [type=checkbox]:checked+.task-list-indicator:before{background-color:#00e676;-webkit-mask-image:var(--md-tasklist-icon--checked);mask-image:var(--md-tasklist-icon--checked)}:root>*{--md-mermaid-font-family:var(--md-text-font-family),sans-serif;--md-mermaid-edge-color:var(--md-code-fg-color);--md-mermaid-node-bg-color:var(--md-accent-fg-color--transparent);--md-mermaid-node-fg-color:var(--md-accent-fg-color);--md-mermaid-label-bg-color:var(--md-default-bg-color);--md-mermaid-label-fg-color:var(--md-code-fg-color);--md-mermaid-sequence-actor-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actor-fg-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-actor-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-actor-line-color:var(--md-default-fg-color--lighter);--md-mermaid-sequence-actorman-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-actorman-line-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-box-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-box-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-label-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-label-fg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-loop-bg-color:var(--md-mermaid-node-bg-color);--md-mermaid-sequence-loop-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-loop-border-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-message-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-message-line-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-bg-color:var(--md-mermaid-label-bg-color);--md-mermaid-sequence-note-fg-color:var(--md-mermaid-edge-color);--md-mermaid-sequence-note-border-color:var(--md-mermaid-label-fg-color);--md-mermaid-sequence-number-bg-color:var(--md-mermaid-node-fg-color);--md-mermaid-sequence-number-fg-color:var(--md-accent-bg-color)}.mermaid{line-height:normal;margin:1em 0}.md-typeset .grid{grid-gap:.4rem;display:grid;grid-template-columns:repeat(auto-fit,minmax(min(100%,16rem),1fr));margin:1em 0}.md-typeset .grid.cards>ol,.md-typeset .grid.cards>ul{display:contents}.md-typeset .grid.cards>ol>li,.md-typeset .grid.cards>ul>li,.md-typeset .grid>.card{border:.05rem solid var(--md-default-fg-color--lightest);border-radius:.1rem;display:block;margin:0;padding:.8rem;transition:border .25s,box-shadow .25s}.md-typeset .grid.cards>ol>li:focus-within,.md-typeset .grid.cards>ol>li:hover,.md-typeset .grid.cards>ul>li:focus-within,.md-typeset .grid.cards>ul>li:hover,.md-typeset .grid>.card:focus-within,.md-typeset .grid>.card:hover{border-color:#0000;box-shadow:var(--md-shadow-z2)}.md-typeset .grid.cards>ol>li>hr,.md-typeset .grid.cards>ul>li>hr,.md-typeset .grid>.card>hr{margin-bottom:1em;margin-top:1em}.md-typeset .grid.cards>ol>li>:first-child,.md-typeset .grid.cards>ul>li>:first-child,.md-typeset .grid>.card>:first-child{margin-top:0}.md-typeset .grid.cards>ol>li>:last-child,.md-typeset .grid.cards>ul>li>:last-child,.md-typeset .grid>.card>:last-child{margin-bottom:0}.md-typeset .grid>*,.md-typeset .grid>.admonition,.md-typeset .grid>.highlight>*,.md-typeset .grid>.highlighttable,.md-typeset .grid>.md-typeset details,.md-typeset .grid>details,.md-typeset .grid>pre{margin-bottom:0;margin-top:0}.md-typeset .grid>.highlight>pre:only-child,.md-typeset .grid>.highlight>pre>code,.md-typeset .grid>.highlighttable,.md-typeset .grid>.highlighttable>tbody,.md-typeset .grid>.highlighttable>tbody>tr,.md-typeset .grid>.highlighttable>tbody>tr>.code,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre,.md-typeset .grid>.highlighttable>tbody>tr>.code>.highlight>pre>code{height:100%}.md-typeset .grid>.tabbed-set{margin-bottom:0;margin-top:0}@media screen and (min-width:45em){[dir=ltr] .md-typeset .inline{float:left}[dir=rtl] .md-typeset .inline{float:right}[dir=ltr] .md-typeset .inline{margin-right:.8rem}[dir=rtl] .md-typeset .inline{margin-left:.8rem}.md-typeset .inline{margin-bottom:.8rem;margin-top:0;width:11.7rem}[dir=ltr] .md-typeset .inline.end{float:right}[dir=rtl] .md-typeset .inline.end{float:left}[dir=ltr] .md-typeset .inline.end{margin-left:.8rem;margin-right:0}[dir=rtl] .md-typeset .inline.end{margin-left:0;margin-right:.8rem}} \ No newline at end of file diff --git a/main/assets/stylesheets/main.7e359304.min.css.map b/main/assets/stylesheets/main.7e359304.min.css.map new file mode 100644 index 000000000..15b8fcf1e --- /dev/null +++ b/main/assets/stylesheets/main.7e359304.min.css.map @@ -0,0 +1 @@ +{"version":3,"sources":["src/templates/assets/stylesheets/main/components/_meta.scss","../../../../src/templates/assets/stylesheets/main.scss","src/templates/assets/stylesheets/main/_resets.scss","src/templates/assets/stylesheets/main/_colors.scss","src/templates/assets/stylesheets/main/_icons.scss","src/templates/assets/stylesheets/main/_typeset.scss","src/templates/assets/stylesheets/utilities/_break.scss","src/templates/assets/stylesheets/main/components/_author.scss","src/templates/assets/stylesheets/main/components/_banner.scss","src/templates/assets/stylesheets/main/components/_base.scss","src/templates/assets/stylesheets/main/components/_clipboard.scss","src/templates/assets/stylesheets/main/components/_consent.scss","src/templates/assets/stylesheets/main/components/_content.scss","src/templates/assets/stylesheets/main/components/_dialog.scss","src/templates/assets/stylesheets/main/components/_feedback.scss","src/templates/assets/stylesheets/main/components/_footer.scss","src/templates/assets/stylesheets/main/components/_form.scss","src/templates/assets/stylesheets/main/components/_header.scss","node_modules/material-design-color/material-color.scss","src/templates/assets/stylesheets/main/components/_nav.scss","src/templates/assets/stylesheets/main/components/_pagination.scss","src/templates/assets/stylesheets/main/components/_post.scss","src/templates/assets/stylesheets/main/components/_progress.scss","src/templates/assets/stylesheets/main/components/_search.scss","src/templates/assets/stylesheets/main/components/_select.scss","src/templates/assets/stylesheets/main/components/_sidebar.scss","src/templates/assets/stylesheets/main/components/_source.scss","src/templates/assets/stylesheets/main/components/_status.scss","src/templates/assets/stylesheets/main/components/_tabs.scss","src/templates/assets/stylesheets/main/components/_tag.scss","src/templates/assets/stylesheets/main/components/_tooltip.scss","src/templates/assets/stylesheets/main/components/_top.scss","src/templates/assets/stylesheets/main/components/_version.scss","src/templates/assets/stylesheets/main/extensions/markdown/_admonition.scss","src/templates/assets/stylesheets/main/extensions/markdown/_footnotes.scss","src/templates/assets/stylesheets/main/extensions/markdown/_toc.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_arithmatex.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_critic.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_details.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_emoji.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_highlight.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_keys.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_tabbed.scss","src/templates/assets/stylesheets/main/extensions/pymdownx/_tasklist.scss","src/templates/assets/stylesheets/main/integrations/_mermaid.scss","src/templates/assets/stylesheets/main/modifiers/_grid.scss","src/templates/assets/stylesheets/main/modifiers/_inline.scss"],"names":[],"mappings":"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E,oEACE,0BhCmwGJ,CgChwGI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ChCkwGN,CgC9vGI,gFACE,ahCgwGN,CgCrxGE,8DACE,oBhCwxGJ,CgCrxGI,wFACE,gChCuxGN,CgClxGE,sEACE,0BhCqxGJ,CgClxGI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ChCoxGN,CgChxGI,kFACE,ahCkxGN,CgCvyGE,4DACE,oBhC0yGJ,CgCvyGI,sFACE,gChCyyGN,CgCpyGE,oEACE,0BhCuyGJ,CgCpyGI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ChCsyGN,CgClyGI,gFACE,ahCoyGN,CgCzzGE,4DACE,oBhC4zGJ,CgCzzGI,sFACE,gChC2zGN,CgCtzGE,oEACE,0BhCyzGJ,CgCtzGI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ChCwzGN,CgCpzGI,gFACE,ahCszGN,CgC30GE,0DACE,oBhC80GJ,CgC30GI,oFACE,gChC60GN,CgCx0GE,kEACE,0BhC20GJ,CgCx0GI,gFACE,wBAlBG,CAmBH,oDAAA,CAAA,4ChC00GN,CgCt0GI,8EACE,ahCw0GN,CgC71GE,oDACE,oBhCg2GJ,CgC71GI,8EACE,gChC+1GN,CgC11GE,4DACE,0BhC61GJ,CgC11GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yChC41GN,CgCx1GI,wEACE,ahC01GN,CgC/2GE,4DACE,oBhCk3GJ,CgC/2GI,sFACE,gChCi3GN,CgC52GE,oEACE,0BhC+2GJ,CgC52GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ChC82GN,CgC12GI,gFACE,ahC42GN,CgCj4GE,wDACE,oBhCo4GJ,CgCj4GI,kFACE,gChCm4GN,CgC93GE,gEACE,0BhCi4GJ,CgC93GI,8EACE,wBAlBG,CAmBH,mDAAA,CAAA,2ChCg4GN,CgC53GI,4EACE,ahC83GN,CiCliHA,MACE,wMjCqiHF,CiC5hHE,sBAEE,uCAAA,CADA,gBjCgiHJ,CiC5hHI,mCACE,ajC8hHN,CiC/hHI,mCACE,cjC8hHN,CiC1hHM,4BACE,sBjC4hHR,CiCzhHQ,mCACE,gCjC2hHV,Ci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","file":"main.css"} \ No newline at end of file diff --git a/main/cli/check/index.html b/main/cli/check/index.html index e67a55c9b..daef01654 100644 --- a/main/cli/check/index.html +++ b/main/cli/check/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/cli/debug/index.html b/main/cli/debug/index.html index c43c98692..c53f6ac15 100644 --- a/main/cli/debug/index.html +++ b/main/cli/debug/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/cli/exec/index.html b/main/cli/exec/index.html index 635682666..af4c66daf 100644 --- a/main/cli/exec/index.html +++ b/main/cli/exec/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/cli/get-inventory-information/index.html b/main/cli/get-inventory-information/index.html index 37704c17b..3c26e438c 100644 --- a/main/cli/get-inventory-information/index.html +++ b/main/cli/get-inventory-information/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/cli/inv-from-ansible/index.html b/main/cli/inv-from-ansible/index.html index da7d2b9da..2a7d5315e 100644 --- a/main/cli/inv-from-ansible/index.html +++ b/main/cli/inv-from-ansible/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/cli/inv-from-cvp/index.html b/main/cli/inv-from-cvp/index.html index 8e980c9b7..54f2927fd 100644 --- a/main/cli/inv-from-cvp/index.html +++ b/main/cli/inv-from-cvp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/cli/nrfu/index.html b/main/cli/nrfu/index.html index d87854ceb..5143813a7 100644 --- a/main/cli/nrfu/index.html +++ b/main/cli/nrfu/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/cli/overview/index.html b/main/cli/overview/index.html index 6b168835e..5752b15aa 100644 --- a/main/cli/overview/index.html +++ b/main/cli/overview/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/cli/tag-management/index.html b/main/cli/tag-management/index.html index 09e0cc986..55032ff22 100644 --- a/main/cli/tag-management/index.html +++ b/main/cli/tag-management/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/contribution/index.html b/main/contribution/index.html index e3e4ab878..3ce8ce15d 100644 --- a/main/contribution/index.html +++ b/main/contribution/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/faq/index.html b/main/faq/index.html index 35971a3ac..35d52fe4b 100644 --- a/main/faq/index.html +++ b/main/faq/index.html @@ -18,7 +18,7 @@ - + @@ -26,7 +26,7 @@ - + diff --git a/main/getting-started/index.html b/main/getting-started/index.html index a6ef7896d..8773662fb 100644 --- a/main/getting-started/index.html +++ b/main/getting-started/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/index.html b/main/index.html index b10edb012..deaa69f8e 100644 --- a/main/index.html +++ b/main/index.html @@ -18,7 +18,7 @@ - + @@ -26,7 +26,7 @@ - + diff --git a/main/requirements-and-installation/index.html b/main/requirements-and-installation/index.html index ef7e8855c..323484641 100644 --- a/main/requirements-and-installation/index.html +++ b/main/requirements-and-installation/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + diff --git a/main/search/search_index.json b/main/search/search_index.json index 3facb82d2..36098e6fe 100644 --- a/main/search/search_index.json +++ b/main/search/search_index.json @@ -1 +1 @@ -{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":""},{"location":"#arista-network-test-automation-anta-framework","title":"Arista Network Test Automation (ANTA) Framework","text":"Code License GitHub PyPi

ANTA is Python framework that automates tests for Arista devices.

  • ANTA provides a set of tests to validate the state of your network
  • ANTA can be used to:
    • Automate NRFU (Network Ready For Use) test on a preproduction network
    • Automate tests on a live network (periodically or on demand)
  • ANTA can be used with:
    • The ANTA CLI
    • As a Python library in your own application

# Install ANTA CLI\n$ pip install anta\n\n# Run ANTA CLI\n$ anta --help\nUsage: anta [OPTIONS] COMMAND [ARGS]...\n\n  Arista Network Test Automation (ANTA) CLI\n\nOptions:\n  --version                       Show the version and exit.\n  --log-file FILE                 Send the logs to a file. If logging level is\n                                  DEBUG, only INFO or higher will be sent to\n                                  stdout.  [env var: ANTA_LOG_FILE]\n  -l, --log-level [CRITICAL|ERROR|WARNING|INFO|DEBUG]\n                                  ANTA logging level  [env var:\n                                  ANTA_LOG_LEVEL; default: INFO]\n  --help                          Show this message and exit.\n\nCommands:\n  check  Commands to validate configuration files\n  debug  Commands to execute EOS commands on remote devices\n  exec   Commands to execute various scripts on EOS devices\n  get    Commands to get information from or generate inventories\n  nrfu   Run ANTA tests on devices\n

[!WARNING] The ANTA CLI options have changed after version 0.11 and have moved away from the top level anta and are now required at their respective commands (e.g. anta nrfu). This breaking change occurs after users feedback on making the CLI more intuitive. This change should not affect user experience when using environment variables.

"},{"location":"#documentation","title":"Documentation","text":"

The documentation is published on ANTA package website. Also, a demo repository is available to facilitate your journey with ANTA.

"},{"location":"#contribution-guide","title":"Contribution guide","text":"

Contributions are welcome. Please refer to the contribution guide

"},{"location":"#credits","title":"Credits","text":"

Thank you to Ang\u00e9lique Phillipps, Colin MacGiollaE\u00e1in, Khelil Sator, Matthieu Tache, Onur Gashi, Paul Lavelle, Guillaume Mulocher and Thomas Grimonet for their contributions and guidances.

"},{"location":"contribution/","title":"Contributions","text":""},{"location":"contribution/#how-to-contribute-to-anta","title":"How to contribute to ANTA","text":"

Contribution model is based on a fork-model. Don\u2019t push to arista-netdevops-community/anta directly. Always do a branch in your forked repository and create a PR.

To help development, open your PR as soon as possible even in draft mode. It helps other to know on what you are working on and avoid duplicate PRs.

"},{"location":"contribution/#create-a-development-environement","title":"Create a development environement","text":"

Run the following commands to create an ANTA development environement:

# Clone repository\n$ git clone https://github.com/arista-netdevops-community/anta.git\n$ cd anta\n\n# Install ANTA in editable mode and its development tools\n$ pip install -e .[dev]\n\n# Verify installation\n$ pip list -e\nPackage Version Editable project location\n------- ------- -------------------------\nanta    0.13.0   /mnt/lab/projects/anta\n

Then, tox is configued with few environments to run CI locally:

$ tox list -d\ndefault environments:\nclean  -> Erase previous coverage reports\nlint   -> Check the code style\ntype   -> Check typing\npy38   -> Run pytest with py38\npy39   -> Run pytest with py39\npy310  -> Run pytest with py310\npy311  -> Run pytest with py311\nreport -> Generate coverage report\n
"},{"location":"contribution/#code-linting","title":"Code linting","text":"
tox -e lint\n[...]\nlint: commands[0]> black --check --diff --color .\nAll done! \u2728 \ud83c\udf70 \u2728\n104 files would be left unchanged.\nlint: commands[1]> isort --check --diff --color .\nSkipped 7 files\nlint: commands[2]> flake8 --max-line-length=165 --config=/dev/null anta\nlint: commands[3]> flake8 --max-line-length=165 --config=/dev/null tests\nlint: commands[4]> pylint anta\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\n.pkg: _exit> python /Users/guillaumemulocher/.pyenv/versions/3.8.13/envs/anta/lib/python3.8/site-packages/pyproject_api/_backend.py True setuptools.build_meta\n  lint: OK (19.26=setup[5.83]+cmd[1.50,0.76,1.19,1.20,8.77] seconds)\n  congratulations :) (19.56 seconds)\n
"},{"location":"contribution/#code-typing","title":"Code Typing","text":"
tox -e type\n\n[...]\ntype: commands[0]> mypy --config-file=pyproject.toml anta\nSuccess: no issues found in 52 source files\n.pkg: _exit> python /Users/guillaumemulocher/.pyenv/versions/3.8.13/envs/anta/lib/python3.8/site-packages/pyproject_api/_backend.py True setuptools.build_meta\n  type: OK (46.66=setup[24.20]+cmd[22.46] seconds)\n  congratulations :) (47.01 seconds)\n

NOTE: Typing is configured quite strictly, do not hesitate to reach out if you have any questions, struggles, nightmares.

"},{"location":"contribution/#unit-tests","title":"Unit tests","text":"

To keep high quality code, we require to provide a Pytest for every tests implemented in ANTA.

All submodule should have its own pytest section under tests/units/anta_tests/<submodule-name>.py.

"},{"location":"contribution/#how-to-write-a-unit-test-for-an-antatest-subclass","title":"How to write a unit test for an AntaTest subclass","text":"

The Python modules in the tests/units/anta_tests folder define test parameters for AntaTest subclasses unit tests. A generic test function is written for all unit tests in tests.lib.anta module. The pytest_generate_tests function definition in conftest.py is called during test collection. The pytest_generate_tests function will parametrize the generic test function based on the DATA data structure defined in tests.units.anta_tests modules. See https://docs.pytest.org/en/7.3.x/how-to/parametrize.html#basic-pytest-generate-tests-example

The DATA structure is a list of dictionaries used to parametrize the test. The list elements have the following keys: - name (str): Test name as displayed by Pytest. - test (AntaTest): An AntaTest subclass imported in the test module - e.g. VerifyUptime. - eos_data (list[dict]): List of data mocking EOS returned data to be passed to the test. - inputs (dict): Dictionary to instantiate the test inputs as defined in the class from test. - expected (dict): Expected test result structure, a dictionary containing a key result containing one of the allowed status (Literal['success', 'failure', 'unset', 'skipped', 'error']) and optionally a key messages which is a list(str) and each message is expected to be a substring of one of the actual messages in the TestResult object.

In order for your unit tests to be correctly collected, you need to import the generic test function even if not used in the Python module.

Test example for anta.tests.system.VerifyUptime AntaTest.

# Import the generic test function\nfrom tests.lib.anta import test  # noqa: F401\n\n# Import your AntaTest\nfrom anta.tests.system import VerifyUptime\n\n# Define test parameters\nDATA: list[dict[str, Any]] = [\n   {\n        # Arbitrary test name\n        \"name\": \"success\",\n        # Must be an AntaTest definition\n        \"test\": VerifyUptime,\n        # Data returned by EOS on which the AntaTest is tested\n        \"eos_data\": [{\"upTime\": 1186689.15, \"loadAvg\": [0.13, 0.12, 0.09], \"users\": 1, \"currentTime\": 1683186659.139859}],\n        # Dictionary to instantiate VerifyUptime.Input\n        \"inputs\": {\"minimum\": 666},\n        # Expected test result\n        \"expected\": {\"result\": \"success\"},\n    },\n    {\n        \"name\": \"failure\",\n        \"test\": VerifyUptime,\n        \"eos_data\": [{\"upTime\": 665.15, \"loadAvg\": [0.13, 0.12, 0.09], \"users\": 1, \"currentTime\": 1683186659.139859}],\n        \"inputs\": {\"minimum\": 666},\n        # If the test returns messages, it needs to be expected otherwise test will fail.\n        # NB: expected messages only needs to be included in messages returned by the test. Exact match is not required.\n        \"expected\": {\"result\": \"failure\", \"messages\": [\"Device uptime is 665.15 seconds\"]},\n    },\n]\n
"},{"location":"contribution/#git-pre-commit-hook","title":"Git Pre-commit hook","text":"
pip install pre-commit\npre-commit install\n

When running a commit or a pre-commit check:

\u276f echo \"import foobaz\" > test.py && git add test.py\n\u276f pre-commit\npylint...................................................................Failed\n- hook id: pylint\n- exit code: 22\n\n************* Module test\ntest.py:1:0: C0114: Missing module docstring (missing-module-docstring)\ntest.py:1:0: E0401: Unable to import 'foobaz' (import-error)\ntest.py:1:0: W0611: Unused import foobaz (unused-import)\n

NOTE: It could happen that pre-commit and tox disagree on something, in that case please open an issue on Github so we can take a look.. It is most probably wrong configuration on our side.

"},{"location":"contribution/#configure-mypypath","title":"Configure MYPYPATH","text":"

In some cases, mypy can complain about not having MYPYPATH configured in your shell. It is especially the case when you update both an anta test and its unit test. So you can configure this environment variable with:

# Option 1: use local folder\nexport MYPYPATH=.\n\n# Option 2: use absolute path\nexport MYPYPATH=/path/to/your/local/anta/repository\n
"},{"location":"contribution/#documentation","title":"Documentation","text":"

mkdocs is used to generate the documentation. A PR should always update the documentation to avoid documentation debt.

"},{"location":"contribution/#install-documentation-requirements","title":"Install documentation requirements","text":"

Run pip to install the documentation requirements from the root of the repo:

pip install -e .[doc]\n
"},{"location":"contribution/#testing-documentation","title":"Testing documentation","text":"

You can then check locally the documentation using the following command from the root of the repo:

mkdocs serve\n

By default, mkdocs listens to http://127.0.0.1:8000/, if you need to expose the documentation to another IP or port (for instance all IPs on port 8080), use the following command:

mkdocs serve --dev-addr=0.0.0.0:8080\n
"},{"location":"contribution/#build-class-diagram","title":"Build class diagram","text":"

To build class diagram to use in API documentation, you can use pyreverse part of pylint with graphviz installed for jpeg generation.

pyreverse anta --colorized -a1 -s1 -o jpeg -m true -k --output-directory docs/imgs/uml/ -c <FQDN anta class>\n

Image will be generated under docs/imgs/uml/ and can be inserted in your documentation.

"},{"location":"contribution/#checking-links","title":"Checking links","text":"

Writing documentation is crucial but managing links can be cumbersome. To be sure there is no dead links, you can use muffet with the following command:

muffet -c 2 --color=always http://127.0.0.1:8000 -e fonts.gstatic.com -b 8192\n
"},{"location":"contribution/#continuous-integration","title":"Continuous Integration","text":"

GitHub actions is used to test git pushes and pull requests. The workflows are defined in this directory. We can view the results here.

"},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#frequently-asked-questions-faq","title":"Frequently Asked Questions (FAQ)","text":""},{"location":"faq/#importerror-related-to-urllib3","title":"ImportError related to urllib3","text":"ImportError related to urllib3 when running ANTA

When running the anta --help command, some users might encounter the following error:

ImportError: urllib3 v2.0 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'OpenSSL 1.0.2k-fips  26 Jan 2017'. See: https://github.com/urllib3/urllib3/issues/2168\n

This error arises due to a compatibility issue between urllib3 v2.0 and older versions of OpenSSL.

"},{"location":"faq/#solution","title":"Solution","text":"
  1. Workaround: Downgrade urllib3

    If you need a quick fix, you can temporarily downgrade the urllib3 package:

    pip3 uninstall urllib3\n\npip3 install urllib3==1.26.15\n
  2. Recommended: Upgrade System or Libraries:

    As per the [urllib3 v2 migration guide](https://urllib3.readthedocs.io/en/latest/v2-migration-guide.html), the root cause of this error is an incompatibility with older OpenSSL versions. For example, users on RHEL7 might consider upgrading to RHEL8, which supports the required OpenSSL version.\n
"},{"location":"faq/#attributeerror-module-lib-has-no-attribute-openssl_add_all_algorithms","title":"AttributeError: module 'lib' has no attribute 'OpenSSL_add_all_algorithms'","text":"AttributeError: module 'lib' has no attribute 'OpenSSL_add_all_algorithms' when running ANTA

When running the anta commands after installation, some users might encounter the following error:

AttributeError: module 'lib' has no attribute 'OpenSSL_add_all_algorithms'\n

The error is a result of incompatibility between cryptography and pyopenssl when installing asyncssh which is a requirement of ANTA.

"},{"location":"faq/#solution_1","title":"Solution","text":"
  1. Upgrade pyopenssl

    pip install -U pyopenssl>22.0\n
"},{"location":"faq/#__nscfconstantstring-initialize-error-on-osx","title":"__NSCFConstantString initialize error on OSX","text":"__NSCFConstantString initialize error on OSX

This error occurs because of added security to restrict multithreading in macOS High Sierra and later versions of macOS. https://www.wefearchange.org/2018/11/forkmacos.rst.html

"},{"location":"faq/#solution_2","title":"Solution","text":"
  1. Set the following environment variable

    export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES\n
"},{"location":"faq/#still-facing-issues","title":"Still facing issues?","text":"

If you\u2019ve tried the above solutions and continue to experience problems, please report the issue in our GitHub repository.

"},{"location":"getting-started/","title":"Getting Started","text":""},{"location":"getting-started/#getting-started","title":"Getting Started","text":"

This section shows how to use ANTA with basic configuration. All examples are based on Arista Test Drive (ATD) topology you can access by reaching out to your preferred SE.

"},{"location":"getting-started/#installation","title":"Installation","text":"

The easiest way to intall ANTA package is to run Python (>=3.8) and its pip package to install:

pip install anta\n

For more details about how to install package, please see the requirements and intallation section.

"},{"location":"getting-started/#configure-arista-eos-devices","title":"Configure Arista EOS devices","text":"

For ANTA to be able to connect to your target devices, you need to configure your management interface

vrf instance MGMT\n!\ninterface Management0\n   description oob_management\n   vrf MGMT\n   ip address 192.168.0.10/24\n!\n

Then, configure access to eAPI:

!\nmanagement api http-commands\n   protocol https port 443\n   no shutdown\n   vrf MGMT\n      no shutdown\n   !\n!\n
"},{"location":"getting-started/#create-your-inventory","title":"Create your inventory","text":"

ANTA uses an inventory to list the target devices for the tests. You can create a file manually with this format:

anta_inventory:\n  hosts:\n  - host: 192.168.0.10\n    name: spine01\n    tags: ['fabric', 'spine']\n  - host: 192.168.0.11\n    name: spine02\n    tags: ['fabric', 'spine']\n  - host: 192.168.0.12\n    name: leaf01\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.13\n    name: leaf02\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.14\n    name: leaf03\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.15\n    name: leaf04\n    tags: ['fabric', 'leaf']\n

You can read more details about how to build your inventory here

"},{"location":"getting-started/#test-catalog","title":"Test Catalog","text":"

To test your network, ANTA relies on a test catalog to list all the tests to run against your inventory. A test catalog references python functions into a yaml file.

The structure to follow is like:

<anta_tests_submodule>:\n  - <anta_tests_submodule function name>:\n      <test function option>:\n        <test function option value>\n

You can read more details about how to build your catalog here

Here is an example for basic tests:

# Load anta.tests.software\nanta.tests.software:\n  - VerifyEOSVersion: # Verifies the device is running one of the allowed EOS version.\n      versions: # List of allowed EOS versions.\n        - 4.25.4M\n        - 4.26.1F\n        - '4.28.3M-28837868.4283M (engineering build)'\n  - VerifyTerminAttrVersion:\n      versions:\n        - v1.22.1\n\nanta.tests.system:\n  - VerifyUptime: # Verifies the device uptime is higher than a value.\n      minimum: 1\n  - VerifyNTP:\n  - VerifySyslog:\n\nanta.tests.mlag:\n  - VerifyMlagStatus:\n  - VerifyMlagInterfaces:\n  - VerifyMlagConfigSanity:\n\nanta.tests.configuration:\n  - VerifyZeroTouch: # Verifies ZeroTouch is disabled.\n  - VerifyRunningConfigDiffs:\n
"},{"location":"getting-started/#test-your-network","title":"Test your network","text":"

ANTA comes with a generic CLI entrypoint to run tests in your network. It requires an inventory file as well as a test catalog.

This entrypoint has multiple options to manage test coverage and reporting.

# Generic ANTA options\n$ anta\nUsage: anta [OPTIONS] COMMAND [ARGS]...\n\n  Arista Network Test Automation (ANTA) CLI\n\nOptions:\n  --version                       Show the version and exit.\n  --log-file FILE                 Send the logs to a file. If logging level is\n                                  DEBUG, only INFO or higher will be sent to\n                                  stdout.  [env var: ANTA_LOG_FILE]\n  -l, --log-level [CRITICAL|ERROR|WARNING|INFO|DEBUG]\n                                  ANTA logging level  [env var:\n                                  ANTA_LOG_LEVEL; default: INFO]\n  --help                          Show this message and exit.\n\nCommands:\n  check  Commands to validate configuration files\n  debug  Commands to execute EOS commands on remote devices\n  exec   Commands to execute various scripts on EOS devices\n  get    Commands to get information from or generate inventories\n  nrfu   Run ANTA tests on devices\n
# NRFU part of ANTA\nUsage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n  Run ANTA tests on devices\n\nOptions:\n  -u, --username TEXT     Username to connect to EOS  [env var: ANTA_USERNAME;\n                          required]\n  -p, --password TEXT     Password to connect to EOS that must be provided. It\n                          can be prompted using '--prompt' option.  [env var:\n                          ANTA_PASSWORD]\n  --enable-password TEXT  Password to access EOS Privileged EXEC mode. It can\n                          be prompted using '--prompt' option. Requires '--\n                          enable' option.  [env var: ANTA_ENABLE_PASSWORD]\n  --enable                Some commands may require EOS Privileged EXEC mode.\n                          This option tries to access this mode before sending\n                          a command to the device.  [env var: ANTA_ENABLE]\n  -P, --prompt            Prompt for passwords if they are not provided.  [env\n                          var: ANTA_PROMPT]\n  --timeout INTEGER       Global connection timeout  [env var: ANTA_TIMEOUT;\n                          default: 30]\n  --insecure              Disable SSH Host Key validation  [env var:\n                          ANTA_INSECURE]\n  --disable-cache         Disable cache globally  [env var:\n                          ANTA_DISABLE_CACHE]\n  -i, --inventory FILE    Path to the inventory YAML file  [env var:\n                          ANTA_INVENTORY; required]\n  -t, --tags TEXT         List of tags using comma as separator:\n                          tag1,tag2,tag3  [env var: ANTA_TAGS]\n  -c, --catalog FILE      Path to the test catalog YAML file  [env var:\n                          ANTA_CATALOG; required]\n  --ignore-status         Always exit with success  [env var:\n                          ANTA_NRFU_IGNORE_STATUS]\n  --ignore-error          Only report failures and not errors  [env var:\n                          ANTA_NRFU_IGNORE_ERROR]\n  --help                  Show this message and exit.\n\nCommands:\n  json        ANTA command to check network state with JSON result\n  table       ANTA command to check network states with table result\n  text        ANTA command to check network states with text result\n  tpl-report  ANTA command to check network state with templated report\n

To run the NRFU, you need to select an output format amongst [\u201cjson\u201d, \u201ctable\u201d, \u201ctext\u201d, \u201ctpl-report\u201d]. For a first usage, table is recommended. By default all test results for all devices are rendered but it can be changed to a report per test case or per host

"},{"location":"getting-started/#default-report-using-table","title":"Default report using table","text":"
anta nrfu \\\n    --username tom \\\n    --password arista123 \\\n    --enable \\\n    --enable-password t \\\n    --inventory .personal/inventory_atd.yml \\\n    --catalog .personal/tests-bases.yml \\\n    table --tags leaf\n\n\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 Running ANTA tests:                                  \u2502\n\u2502 - ANTA Inventory contains 6 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 10 tests                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n[10:17:24] INFO     Running ANTA tests...                                                                                                           runner.py:75\n  \u2022 Running NRFU Tests...100% \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 40/40 \u2022 0:00:02 \u2022 0:00:00\n\n                                                                       All tests results\n\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Device IP \u2503 Test Name                \u2503 Test Status \u2503 Message(s)       \u2503 Test description                                                     \u2503 Test category \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 leaf01    \u2502 VerifyEOSVersion         \u2502 success     \u2502                  \u2502 Verifies the device is running one of the allowed EOS version.       \u2502 software      \u2502\n\u2502 leaf01    \u2502 VerifyTerminAttrVersion  \u2502 success     \u2502                  \u2502 Verifies the device is running one of the allowed TerminAttr         \u2502 software      \u2502\n\u2502           \u2502                          \u2502             \u2502                  \u2502 version.                                                             \u2502               \u2502\n\u2502 leaf01    \u2502 VerifyUptime             \u2502 success     \u2502                  \u2502 Verifies the device uptime is higher than a value.                   \u2502 system        \u2502\n\u2502 leaf01    \u2502 VerifyNTP                \u2502 success     \u2502                  \u2502 Verifies NTP is synchronised.                                        \u2502 system        \u2502\n\u2502 leaf01    \u2502 VerifySyslog             \u2502 success     \u2502                  \u2502 Verifies the device had no syslog message with a severity of warning \u2502 system        \u2502\n\u2502           \u2502                          \u2502             \u2502                  \u2502 (or a more severe message) during the last 7 days.                   \u2502               \u2502\n\u2502 leaf01    \u2502 VerifyMlagStatus         \u2502 skipped     \u2502 MLAG is disabled \u2502 This test verifies the health status of the MLAG configuration.      \u2502 mlag          \u2502\n\u2502 leaf01    \u2502 VerifyMlagInterfaces     \u2502 skipped     \u2502 MLAG is disabled \u2502 This test verifies there are no inactive or active-partial MLAG      \u2502 mlag          \u2502\n[...]\n\u2502 leaf04    \u2502 VerifyMlagConfigSanity   \u2502 skipped     \u2502 MLAG is disabled \u2502 This test verifies there are no MLAG config-sanity inconsistencies.  \u2502 mlag          \u2502\n\u2502 leaf04    \u2502 VerifyZeroTouch          \u2502 success     \u2502                  \u2502 Verifies ZeroTouch is disabled.                                      \u2502 configuration \u2502\n\u2502 leaf04    \u2502 VerifyRunningConfigDiffs \u2502 success     \u2502                  \u2502                                                                      \u2502 configuration \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"getting-started/#report-in-text-mode","title":"Report in text mode","text":"
$ anta nrfu \\\n    --username tom \\\n    --password arista123 \\\n    --enable \\\n    --enable-password t \\\n    --inventory .personal/inventory_atd.yml \\\n    --catalog .personal/tests-bases.yml \\\n    text --tags leaf\n\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 Running ANTA tests:                                  \u2502\n\u2502 - ANTA Inventory contains 6 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 10 tests                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n[10:20:47] INFO     Running ANTA tests...                                                                                                           runner.py:75\n  \u2022 Running NRFU Tests...100% \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 40/40 \u2022 0:00:01 \u2022 0:00:00\nleaf01 :: VerifyEOSVersion :: SUCCESS\nleaf01 :: VerifyTerminAttrVersion :: SUCCESS\nleaf01 :: VerifyUptime :: SUCCESS\nleaf01 :: VerifyNTP :: SUCCESS\nleaf01 :: VerifySyslog :: SUCCESS\nleaf01 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\nleaf01 :: VerifyMlagInterfaces :: SKIPPED (MLAG is disabled)\nleaf01 :: VerifyMlagConfigSanity :: SKIPPED (MLAG is disabled)\n[...]\n
"},{"location":"getting-started/#report-in-json-format","title":"Report in JSON format","text":"
$ anta nrfu \\\n    --username tom \\\n    --password arista123 \\\n    --enable \\\n    --enable-password t \\\n    --inventory .personal/inventory_atd.yml \\\n    --catalog .personal/tests-bases.yml \\\n    json --tags leaf\n\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 Running ANTA tests:                                  \u2502\n\u2502 - ANTA Inventory contains 6 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 10 tests                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n[10:21:51] INFO     Running ANTA tests...                                                                                                           runner.py:75\n  \u2022 Running NRFU Tests...100% \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 40/40 \u2022 0:00:02 \u2022 0:00:00\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 JSON results of all tests                                                                                                                                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n[\n  {\n    \"name\": \"leaf01\",\n    \"test\": \"VerifyEOSVersion\",\n    \"categories\": [\n      \"software\"\n    ],\n    \"description\": \"Verifies the device is running one of the allowed EOS version.\",\n    \"result\": \"success\",\n    \"messages\": [],\n    \"custom_field\": \"None\",\n  },\n  {\n    \"name\": \"leaf01\",\n    \"test\": \"VerifyTerminAttrVersion\",\n    \"categories\": [\n      \"software\"\n    ],\n    \"description\": \"Verifies the device is running one of the allowed TerminAttr version.\",\n    \"result\": \"success\",\n    \"messages\": [],\n    \"custom_field\": \"None\",\n  },\n[...]\n]\n

You can find more information under the usage section of the website

"},{"location":"requirements-and-installation/","title":"Installation","text":""},{"location":"requirements-and-installation/#anta-requirements","title":"ANTA Requirements","text":""},{"location":"requirements-and-installation/#python-version","title":"Python version","text":"

Python 3 (>=3.8) is required:

python --version\nPython 3.9.9\n
"},{"location":"requirements-and-installation/#install-anta-package","title":"Install ANTA package","text":"

This installation will deploy tests collection, scripts and all their Python requirements.

The ANTA package and the cli require some packages that are not part of the Python standard library. They are indicated in the pyproject.toml file, under dependencies.

"},{"location":"requirements-and-installation/#install-from-pypi-server","title":"Install from Pypi server","text":"
pip install anta\n
"},{"location":"requirements-and-installation/#install-anta-from-github","title":"Install ANTA from github","text":"
pip install git+https://github.com/arista-netdevops-community/anta.git\n\n# You can even specify the branch, tag or commit:\npip install git+https://github.com/arista-netdevops-community/anta.git@<cool-feature-branch>\npip install git+https://github.com/arista-netdevops-community/anta.git@<cool-tag>\npip install git+https://github.com/arista-netdevops-community/anta.git@<more-or-less-cool-hash>\n
"},{"location":"requirements-and-installation/#check-installation","title":"Check installation","text":"

After installing ANTA, verify the installation with the following commands:

# Check ANTA has been installed in your python path\npip list | grep anta\n\n# Check scripts are in your $PATH\n# Path may differ but it means CLI is in your path\nwhich anta\n/home/tom/.pyenv/shims/anta\n

Warning

Before running the anta --version command, please be aware that some users have reported issues related to the urllib3 package. If you encounter an error at this step, please refer to our FAQ page for guidance on resolving it.

# Check ANTA version\nanta --version\nanta, version v0.13.0\n
"},{"location":"requirements-and-installation/#eos-requirements","title":"EOS Requirements","text":"

To get ANTA working, the targetted Arista EOS devices must have the following configuration (assuming you connect to the device using Management interface in MGMT VRF):

configure\n!\nvrf instance MGMT\n!\ninterface Management1\n   description oob_management\n   vrf MGMT\n   ip address 10.73.1.105/24\n!\nend\n

Enable eAPI on the MGMT vrf:

configure\n!\nmanagement api http-commands\n   protocol https port 443\n   no shutdown\n   vrf MGMT\n      no shutdown\n!\nend\n

Now the switch accepts on port 443 in the MGMT VRF HTTPS requests containing a list of CLI commands.

Run these EOS commands to verify:

show management http-server\nshow management api http-commands\n
"},{"location":"usage-inventory-catalog/","title":"Inventory & Tests catalog","text":""},{"location":"usage-inventory-catalog/#inventory-and-catalog","title":"Inventory and Catalog","text":"

The ANTA framework needs 2 important inputs from the user to run: a device inventory and a test catalog.

Both inputs can be defined in a file or programmatically.

"},{"location":"usage-inventory-catalog/#device-inventory","title":"Device Inventory","text":"

A device inventory is an instance of the AntaInventory class.

"},{"location":"usage-inventory-catalog/#device-inventory-file","title":"Device Inventory File","text":"

The ANTA device inventory can easily be defined as a YAML file. The file must comply with the following structure:

anta_inventory:\n  hosts:\n    - host: < ip address value >\n      port: < TCP port for eAPI. Default is 443 (Optional)>\n      name: < name to display in report. Default is host:port (Optional) >\n      tags: < list of tags to use to filter inventory during tests >\n      disable_cache: < Disable cache per hosts. Default is False. >\n  networks:\n    - network: < network using CIDR notation >\n      tags: < list of tags to use to filter inventory during tests >\n      disable_cache: < Disable cache per network. Default is False. >\n  ranges:\n    - start: < first ip address value of the range >\n      end: < last ip address value of the range >\n      tags: < list of tags to use to filter inventory during tests >\n      disable_cache: < Disable cache per range. Default is False. >\n

The inventory file must start with the anta_inventory key then define one or multiple methods:

  • hosts: define each device individually
  • networks: scan a network for devices accesible via eAPI
  • ranges: scan a range for devices accesible via eAPI

A full description of the inventory model is available in API documentation

Info

Caching can be disabled per device, network or range by setting the disable_cache key to True in the inventory file. For more details about how caching is implemented in ANTA, please refer to Caching in ANTA.

"},{"location":"usage-inventory-catalog/#example","title":"Example","text":"
---\nanta_inventory:\n  hosts:\n  - host: 192.168.0.10\n    name: spine01\n    tags: ['fabric', 'spine']\n  - host: 192.168.0.11\n    name: spine02\n    tags: ['fabric', 'spine']\n  networks:\n  - network: '192.168.110.0/24'\n    tags: ['fabric', 'leaf']\n  ranges:\n  - start: 10.0.0.9\n    end: 10.0.0.11\n    tags: ['fabric', 'l2leaf']\n
"},{"location":"usage-inventory-catalog/#test-catalog","title":"Test Catalog","text":"

A test catalog is an instance of the AntaCatalog class.

"},{"location":"usage-inventory-catalog/#test-catalog-file","title":"Test Catalog File","text":"

In addition to the inventory file, you also have to define a catalog of tests to execute against your devices. This catalog list all your tests, their inputs and their tags.

A valid test catalog file must have the following structure:

---\n<Python module>:\n    - <AntaTest subclass>:\n        <AntaTest.Input compliant dictionary>\n

"},{"location":"usage-inventory-catalog/#example_1","title":"Example","text":"
---\nanta.tests.connectivity:\n  - VerifyReachability:\n      hosts:\n        - source: Management0\n          destination: 1.1.1.1\n          vrf: MGMT\n        - source: Management0\n          destination: 8.8.8.8\n          vrf: MGMT\n      filters:\n        tags: ['leaf']\n      result_overwrite:\n        categories:\n          - \"Overwritten category 1\"\n        description: \"Test with overwritten description\"\n        custom_field: \"Test run by John Doe\"\n

It is also possible to nest Python module definition:

anta.tests:\n  connectivity:\n    - VerifyReachability:\n        hosts:\n          - source: Management0\n            destination: 1.1.1.1\n            vrf: MGMT\n          - source: Management0\n            destination: 8.8.8.8\n            vrf: MGMT\n        filters:\n          tags: ['leaf']\n        result_overwrite:\n          categories:\n            - \"Overwritten category 1\"\n          description: \"Test with overwritten description\"\n          custom_field: \"Test run by John Doe\"\n

This test catalog example is maintained with all the tests defined in the anta.tests Python module.

"},{"location":"usage-inventory-catalog/#test-tags","title":"Test tags","text":"

All tests can be defined with a list of user defined tags. These tags will be mapped with device tags: when at least one tag is defined for a test, this test will only be executed on devices with the same tag. If a test is defined in the catalog without any tags, the test will be executed on all devices.

anta.tests.system:\n  - VerifyUptime:\n      minimum: 10\n      filters:\n        tags: ['demo', 'leaf']\n  - VerifyReloadCause:\n  - VerifyCoredump:\n  - VerifyAgentLogs:\n  - VerifyCPUUtilization:\n      filters:\n        tags: ['leaf']\n

Info

When using the CLI, you can filter the NRFU execution using tags. Refer to this section of the CLI documentation.

"},{"location":"usage-inventory-catalog/#tests-available-in-anta","title":"Tests available in ANTA","text":"

All tests available as part of the ANTA framework are defined under the anta.tests Python module and are categorised per family (Python submodule). The complete list of the tests and their respective inputs is available at the tests section of this website.

To run test to verify the EOS software version, you can do:

anta.tests.software:\n  - VerifyEOSVersion:\n

It will load the test VerifyEOSVersion located in anta.tests.software. But since this test has mandatory inputs, we need to provide them as a dictionary in the YAML file:

anta.tests.software:\n  - VerifyEOSVersion:\n      # List of allowed EOS versions.\n      versions:\n        - 4.25.4M\n        - 4.26.1F\n

The following example is a very minimal test catalog:

---\n# Load anta.tests.software\nanta.tests.software:\n  # Verifies the device is running one of the allowed EOS version.\n  - VerifyEOSVersion:\n      # List of allowed EOS versions.\n      versions:\n        - 4.25.4M\n        - 4.26.1F\n\n# Load anta.tests.system\nanta.tests.system:\n  # Verifies the device uptime is higher than a value.\n  - VerifyUptime:\n      minimum: 1\n\n# Load anta.tests.configuration\nanta.tests.configuration:\n  # Verifies ZeroTouch is disabled.\n  - VerifyZeroTouch:\n  - VerifyRunningConfigDiffs:\n
"},{"location":"usage-inventory-catalog/#catalog-with-custom-tests","title":"Catalog with custom tests","text":"

In case you want to leverage your own tests collection, use your own Python package in the test catalog. So for instance, if my custom tests are defined in the titom73.tests.system Python module, the test catalog will be:

titom73.tests.system:\n  - VerifyPlatform:\n    type: ['cEOS-LAB']\n

How to create custom tests

To create your custom tests, you should refer to this documentation

"},{"location":"usage-inventory-catalog/#customize-test-description-and-categories","title":"Customize test description and categories","text":"

It might be interesting to use your own categories and customized test description to build a better report for your environment. ANTA comes with a handy feature to define your own categories and description in the report.

In your test catalog, use result_overwrite dictionary with categories and description to just overwrite this values in your report:

anta.tests.configuration:\n  - VerifyZeroTouch: # Verifies ZeroTouch is disabled.\n      result_overwrite:\n        categories: ['demo', 'pr296']\n        description: A custom test\n  - VerifyRunningConfigDiffs:\nanta.tests.interfaces:\n  - VerifyInterfaceUtilization:\n

Once you run anta nrfu table, you will see following output:

\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Device IP \u2503 Test Name                  \u2503 Test Status \u2503 Message(s) \u2503 Test description                              \u2503 Test category \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 spine01   \u2502 VerifyZeroTouch            \u2502 success     \u2502            \u2502 A custom test                                 \u2502 demo, pr296   \u2502\n\u2502 spine01   \u2502 VerifyRunningConfigDiffs   \u2502 success     \u2502            \u2502                                               \u2502 configuration \u2502\n\u2502 spine01   \u2502 VerifyInterfaceUtilization \u2502 success     \u2502            \u2502 Verifies interfaces utilization is below 75%. \u2502 interfaces    \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"advanced_usages/as-python-lib/","title":"ANTA as a Python Library","text":"

ANTA is a Python library that can be used in user applications. This section describes how you can leverage ANTA Python modules to help you create your own NRFU solution.

Tip

If you are unfamiliar with asyncio, refer to the Python documentation relevant to your Python version - https://docs.python.org/3/library/asyncio.html

"},{"location":"advanced_usages/as-python-lib/#antadevice-abstract-class","title":"AntaDevice Abstract Class","text":"

A device is represented in ANTA as a instance of a subclass of the AntaDevice abstract class. There are few abstract methods that needs to be implemented by child classes:

  • The collect() coroutine is in charge of collecting outputs of AntaCommand instances.
  • The refresh() coroutine is in charge of updating attributes of the AntaDevice instance. These attributes are used by AntaInventory to filter out unreachable devices or by AntaTest to skip devices based on their hardware models.

The copy() coroutine is used to copy files to and from the device. It does not need to be implemented if tests are not using it.

"},{"location":"advanced_usages/as-python-lib/#asynceosdevice-class","title":"AsyncEOSDevice Class","text":"

The AsyncEOSDevice class is an implementation of AntaDevice for Arista EOS. It uses the aio-eapi eAPI client and the AsyncSSH library.

  • The collect() coroutine collects AntaCommand outputs using eAPI.
  • The refresh() coroutine tries to open a TCP connection on the eAPI port and update the is_online attribute accordingly. If the TCP connection succeeds, it sends a show version command to gather the hardware model of the device and updates the established and hw_model attributes.
  • The copy() coroutine copies files to and from the device using the SCP protocol.
"},{"location":"advanced_usages/as-python-lib/#antainventory-class","title":"AntaInventory Class","text":"

The AntaInventory class is a subclass of the standard Python type dict. The keys of this dictionary are the device names, the values are AntaDevice instances.

AntaInventory provides methods to interact with the ANTA inventory:

  • The add_device() method adds an AntaDevice instance to the inventory. Adding an entry to AntaInventory with a key different from the device name is not allowed.
  • The get_inventory() returns a new AntaInventory instance with filtered out devices based on the method inputs.
  • The connect_inventory() coroutine will execute the refresh() coroutines of all the devices in the inventory.
  • The parse() static method creates an AntaInventory instance from a YAML file and returns it. The devices are AsyncEOSDevice instances.

To parse a YAML inventory file and print the devices connection status:

\"\"\"\nExample\n\"\"\"\nimport asyncio\n\nfrom anta.inventory import AntaInventory\n\n\nasync def main(inv: AntaInventory) -> None:\n    \"\"\"\n    Take an AntaInventory and:\n    1. try to connect to every device in the inventory\n    2. print a message for every device connection status\n    \"\"\"\n    await inv.connect_inventory()\n\n    for device in inv.values():\n        if device.established:\n            print(f\"Device {device.name} is online\")\n        else:\n            print(f\"Could not connect to device {device.name}\")\n\nif __name__ == \"__main__\":\n    # Create the AntaInventory instance\n    inventory = AntaInventory.parse(\n        filename=\"inv.yml\",\n        username=\"arista\",\n        password=\"@rista123\",\n        timeout=15,\n    )\n\n    # Run the main coroutine\n    res = asyncio.run(main(inventory))\n
How to create your inventory file

Please visit this dedicated section for how to use inventory and catalog files.

To run an EOS commands list on the reachable devices from the inventory:

\"\"\"\nExample\n\"\"\"\n# This is needed to run the script for python < 3.10 for typing annotations\nfrom __future__ import annotations\n\nimport asyncio\nfrom pprint import pprint\n\nfrom anta.inventory import AntaInventory\nfrom anta.models import AntaCommand\n\n\nasync def main(inv: AntaInventory, commands: list[str]) -> dict[str, list[AntaCommand]]:\n    \"\"\"\n    Take an AntaInventory and a list of commands as string and:\n    1. try to connect to every device in the inventory\n    2. collect the results of the commands from each device\n\n    Returns:\n      a dictionary where key is the device name and the value is the list of AntaCommand ran towards the device\n    \"\"\"\n    await inv.connect_inventory()\n\n    # Make a list of coroutine to run commands towards each connected device\n    coros = []\n    # dict to keep track of the commands per device\n    result_dict = {}\n    for name, device in inv.get_inventory(established_only=True).items():\n        anta_commands = [AntaCommand(command=command, ofmt=\"json\") for command in commands]\n        result_dict[name] = anta_commands\n        coros.append(device.collect_commands(anta_commands))\n\n    # Run the coroutines\n    await asyncio.gather(*coros)\n\n    return result_dict\n\n\nif __name__ == \"__main__\":\n    # Create the AntaInventory instance\n    inventory = AntaInventory.parse(\n        filename=\"inv.yml\",\n        username=\"arista\",\n        password=\"@rista123\",\n        timeout=15,\n    )\n\n    # Create a list of commands with json output\n    commands = [\"show version\", \"show ip bgp summary\"]\n\n    # Run the main asyncio  entry point\n    res = asyncio.run(main(inventory, commands))\n\n    pprint(res)\n

"},{"location":"advanced_usages/as-python-lib/#use-tests-from-anta","title":"Use tests from ANTA","text":"

All the test classes inherit from the same abstract Base Class AntaTest. The Class definition indicates which commands are required for the test and the user should focus only on writing the test function with optional keywords argument. The instance of the class upon creation instantiates a TestResult object that can be accessed later on to check the status of the test ([unset, skipped, success, failure, error]).

"},{"location":"advanced_usages/as-python-lib/#test-structure","title":"Test structure","text":"

All tests are built on a class named AntaTest which provides a complete toolset for a test:

  • Object creation
  • Test definition
  • TestResult definition
  • Abstracted method to collect data

This approach means each time you create a test it will be based on this AntaTest class. Besides that, you will have to provide some elements:

  • name: Name of the test
  • description: A human readable description of your test
  • categories: a list of categories to sort test.
  • commands: a list of command to run. This list must be a list of AntaCommand which is described in the next part of this document.

Here is an example of a hardware test related to device temperature:

from __future__ import annotations\n\nimport logging\nfrom typing import Any, Dict, List, Optional, cast\n\nfrom anta.models import AntaTest, AntaCommand\n\n\nclass VerifyTemperature(AntaTest):\n    \"\"\"\n    Verifies device temparture is currently OK.\n    \"\"\"\n\n    # The test name\n    name = \"VerifyTemperature\"\n    # A small description of the test, usually the first line of the class docstring\n    description = \"Verifies device temparture is currently OK\"\n    # The category of the test, usually the module name\n    categories = [\"hardware\"]\n    # The command(s) used for the test. Could be a template instead\n    commands = [AntaCommand(command=\"show system environment temperature\", ofmt=\"json\")]\n\n    # Decorator\n    @AntaTest.anta_test\n    # abstract method that must be defined by the child Test class\n    def test(self) -> None:\n        \"\"\"Run VerifyTemperature validation\"\"\"\n        command_output = cast(Dict[str, Dict[Any, Any]], self.instance_commands[0].output)\n        temperature_status = command_output[\"systemStatus\"] if \"systemStatus\" in command_output.keys() else \"\"\n        if temperature_status == \"temperatureOk\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device temperature is not OK, systemStatus: {temperature_status }\")\n

When you run the test, object will automatically call its anta.models.AntaTest.collect() method to get device output for each command if no pre-collected data was given to the test. This method does a loop to call anta.inventory.models.InventoryDevice.collect() methods which is in charge of managing device connection and how to get data.

run test offline

You can also pass eos data directly to your test if you want to validate data collected in a different workflow. An example is provided below just for information:

test = VerifyTemperature(device, eos_data=test_data[\"eos_data\"])\nasyncio.run(test.test())\n

The test function is always the same and must be defined with the @AntaTest.anta_test decorator. This function takes at least one argument which is a anta.inventory.models.InventoryDevice object. In some cases a test would rely on some additional inputs from the user, for instance the number of expected peers or some expected numbers. All parameters must come with a default value and the test function should validate the parameters values (at this stage this is the only place where validation can be done but there are future plans to make this better).

class VerifyTemperature(AntaTest):\n    ...\n    @AntaTest.anta_test\n    def test(self) -> None:\n        pass\n\nclass VerifyTransceiversManufacturers(AntaTest):\n    ...\n    @AntaTest.anta_test\n    def test(self, manufacturers: Optional[List[str]] = None) -> None:\n        # validate the manufactures parameter\n        pass\n

The test itself does not return any value, but the result is directly availble from your AntaTest object and exposes a anta.result_manager.models.TestResult object with result, name of the test and optional messages:

  • name (str): Device name where the test has run.
  • test (str): Test name runs on the device.
  • categories (List[str]): List of categories the TestResult belongs to, by default the AntaTest categories.
  • description (str): TestResult description, by default the AntaTest description.
  • results (str): Result of the test. Can be one of [\u201cunset\u201d, \u201csuccess\u201d, \u201cfailure\u201d, \u201cerror\u201d, \u201cskipped\u201d].
  • message (str, optional): Message to report after the test if any.
  • custom_field (str, optional): Custom field to store a string for flexibility in integrating with ANTA
from anta.tests.hardware import VerifyTemperature\n\ntest = VerifyTemperature(device, eos_data=test_data[\"eos_data\"])\nasyncio.run(test.test())\nassert test.result.result == \"success\"\n
"},{"location":"advanced_usages/as-python-lib/#classes-for-commands","title":"Classes for commands","text":"

To make it easier to get data, ANTA defines 2 different classes to manage commands to send to devices:

"},{"location":"advanced_usages/as-python-lib/#antacommand-class","title":"AntaCommand Class","text":"

Represent a command with following information:

  • Command to run
  • Ouput format expected
  • eAPI version
  • Output of the command

Usage example:

from anta.models import AntaCommand\n\ncmd1 = AntaCommand(command=\"show zerotouch\")\ncmd2 = AntaCommand(command=\"show running-config diffs\", ofmt=\"text\")\n

Command revision and version

  • Most of EOS commands return a JSON structure according to a model (some commands may not be modeled hence the necessity to use text outformat sometimes.
  • The model can change across time (adding feature, \u2026 ) and when the model is changed in a non backward-compatible way, the revision number is bumped. The initial model starts with revision 1.
  • A revision applies to a particular CLI command whereas a version is global to an eAPI call. The version is internally translated to a specific revision for each CLI command in the RPC call. The currently supported version vaues are 1 and latest.
  • A revision takes precedence over a version (e.g. if a command is run with version=\u201dlatest\u201d and revision=1, the first revision of the model is returned)
  • By default eAPI returns the first revision of each model to ensure that when upgrading, intergation with existing tools is not broken. This is done by using by default version=1 in eAPI calls.

ANTA uses by default version=\"latest\" in AntaCommand. For some commands, you may want to run them with a different revision or version.

For instance the VerifyRoutingTableSize test leverages the first revision of show bfd peers:

# revision 1 as later revision introduce additional nesting for type\ncommands = [AntaCommand(command=\"show bfd peers\", revision=1)]\n
"},{"location":"advanced_usages/as-python-lib/#antatemplate-class","title":"AntaTemplate Class","text":"

Because some command can require more dynamic than just a command with no parameter provided by user, ANTA supports command template: you define a template in your test class and user provide parameters when creating test object.

class RunArbitraryTemplateCommand(AntaTest):\n    \"\"\"\n    Run an EOS command and return result\n    Based on AntaTest to build relevant output for pytest\n    \"\"\"\n\n    name = \"Run aributrary EOS command\"\n    description = \"To be used only with anta debug commands\"\n    template = AntaTemplate(template=\"show interfaces {ifd}\")\n    categories = [\"debug\"]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        errdisabled_interfaces = [interface for interface, value in response[\"interfaceStatuses\"].items() if value[\"linkStatus\"] == \"errdisabled\"]\n        ...\n\n\nparams = [{\"ifd\": \"Ethernet2\"}, {\"ifd\": \"Ethernet49/1\"}]\nrun_command1 = RunArbitraryTemplateCommand(device_anta, params)\n

In this example, test waits for interfaces to check from user setup and will only check for interfaces in params

"},{"location":"advanced_usages/caching/","title":"Caching in ANTA","text":"

ANTA is a streamlined Python framework designed for efficient interaction with network devices. This section outlines how ANTA incorporates caching mechanisms to collect command outputs from network devices.

"},{"location":"advanced_usages/caching/#configuration","title":"Configuration","text":"

By default, ANTA utilizes aiocache\u2019s memory cache backend, also called SimpleMemoryCache. This library aims for simplicity and supports asynchronous operations to go along with Python asyncio used in ANTA.

The _init_cache() method of the AntaDevice abstract class initializes the cache. Child classes can override this method to tweak the cache configuration:

def _init_cache(self) -> None:\n    \"\"\"\n    Initialize cache for the device, can be overridden by subclasses to manipulate how it works\n    \"\"\"\n    self.cache = Cache(cache_class=Cache.MEMORY, ttl=60, namespace=self.name, plugins=[HitMissRatioPlugin()])\n    self.cache_locks = defaultdict(asyncio.Lock)\n

The cache is also configured with aiocache\u2019s HitMissRatioPlugin plugin to calculate the ratio of hits the cache has and give useful statistics for logging purposes in ANTA.

"},{"location":"advanced_usages/caching/#cache-key-design","title":"Cache key design","text":"

The cache is initialized per AntaDevice and uses the following cache key design:

<device_name>:<uid>

The uid is an attribute of AntaCommand, which is a unique identifier generated from the command, version, revision and output format.

Each UID has its own asyncio lock. This design allows coroutines that need to access the cache for different UIDs to do so concurrently. The locks are managed by the self.cache_locks dictionary.

"},{"location":"advanced_usages/caching/#mechanisms","title":"Mechanisms","text":"

By default, once the cache is initialized, it is used in the collect() method of AntaDevice. The collect() method prioritizes retrieving the output of the command from the cache. If the output is not in the cache, the private _collect() method will retrieve and then store it for future access.

"},{"location":"advanced_usages/caching/#how-to-disable-caching","title":"How to disable caching","text":"

Caching is enabled by default in ANTA following the previous configuration and mechanisms.

There might be scenarios where caching is not wanted. You can disable caching in multiple ways in ANTA:

  1. Caching can be disabled globally, for ALL commands on ALL devices, using the --disable-cache global flag when invoking anta at the CLI:
    anta --disable-cache --username arista --password arista nrfu table\n
  2. Caching can be disabled per device, network or range by setting the disable_cache key to True when definining the ANTA Inventory file:

    anta_inventory:\n  hosts:\n  - host: 172.20.20.101\n    name: DC1-SPINE1\n    tags: [\"SPINE\", \"DC1\"]\n    disable_cache: True  # Set this key to True\n  - host: 172.20.20.102\n    name: DC1-SPINE2\n    tags: [\"SPINE\", \"DC1\"]\n    disable_cache: False # Optional since it's the default\n\n  networks:\n  - network: \"172.21.21.0/24\"\n    disable_cache: True\n\n  ranges:\n  - start: 172.22.22.10\n    end: 172.22.22.19\n    disable_cache: True\n
    This approach effectively disables caching for ALL commands sent to devices targeted by the disable_cache key.

  3. For tests developpers, caching can be disabled for a specific AntaCommand or AntaTemplate by setting the use_cache attribute to False. That means the command output will always be collected on the device and therefore, never use caching.

"},{"location":"advanced_usages/caching/#disable-caching-in-a-child-class-of-antadevice","title":"Disable caching in a child class of AntaDevice","text":"

Since caching is implemented at the AntaDevice abstract class level, all subclasses will inherit that default behavior. As a result, if you need to disable caching in any custom implementation of AntaDevice outside of the ANTA framework, you must initialize AntaDevice with disable_cache set to True:

class AnsibleEOSDevice(AntaDevice):\n  \"\"\"\n  Implementation of an AntaDevice using Ansible HttpApi plugin for EOS.\n  \"\"\"\n  def __init__(self, name: str, connection: ConnectionBase, tags: list = None) -> None:\n      super().__init__(name, tags, disable_cache=True)\n
"},{"location":"advanced_usages/custom-tests/","title":"Developing ANTA tests","text":"

This documentation applies for both creating tests in ANTA or creating your own test package.

ANTA is not only a Python library with a CLI and a collection of built-in tests, it is also a framework you can extend by building your own tests.

"},{"location":"advanced_usages/custom-tests/#generic-approach","title":"Generic approach","text":"

A test is a Python class where a test function is defined and will be run by the framework.

ANTA provides an abstract class AntaTest. This class does the heavy lifting and provide the logic to define, collect and test data. The code below is an example of a simple test in ANTA, which is an AntaTest subclass:

from anta.models import AntaTest, AntaCommand\nfrom anta.decorators import skip_on_platforms\n\n\nclass VerifyTemperature(AntaTest):\n    \"\"\"Verifies if the device temperature is within acceptable limits.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device temperature is currently OK: 'temperatureOk'.\n    * Failure: The test will fail if the device temperature is NOT OK.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyTemperature:\n    ```\n    \"\"\"\n\n    name = \"VerifyTemperature\"\n    description = \"Verifies the device temperature.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTemperature.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        temperature_status = command_output.get(\"systemStatus\", \"\")\n        if temperature_status == \"temperatureOk\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'\")\n

AntaTest also provide more advanced capabilities like AntaCommand templating using the AntaTemplate class or test inputs definition and validation using AntaTest.Input pydantic model. This will be discussed in the sections below.

"},{"location":"advanced_usages/custom-tests/#antatest-structure","title":"AntaTest structure","text":""},{"location":"advanced_usages/custom-tests/#class-attributes","title":"Class Attributes","text":"
  • name (str): Name of the test. Used during reporting.
  • description (str): A human readable description of your test.
  • categories (list[str]): A list of categories in which the test belongs.
  • commands ([list[AntaCommand | AntaTemplate]]): A list of command to collect from devices. This list must be a list of AntaCommand or AntaTemplate instances. Rendering AntaTemplate instances will be discussed later.

Info

All these class attributes are mandatory. If any attribute is missing, a NotImplementedError exception will be raised during class instantiation.

"},{"location":"advanced_usages/custom-tests/#instance-attributes","title":"Instance Attributes","text":"

Info

You can access an instance attribute in your code using the self reference. E.g. you can access the test input values using self.inputs.

Logger object

ANTA already provides comprehensive logging at every steps of a test execution. The AntaTest class also provides a logger attribute that is a Python logger specific to the test instance. See Python documentation for more information.

AntaDevice object

Even if device is not a private attribute, you should not need to access this object in your code.

"},{"location":"advanced_usages/custom-tests/#test-inputs","title":"Test Inputs","text":"

AntaTest.Input is a pydantic model that allow test developers to define their test inputs. pydantic provides out of the box error handling for test input validation based on the type hints defined by the test developer.

The base definition of AntaTest.Input provides common test inputs for all AntaTest instances:

"},{"location":"advanced_usages/custom-tests/#input-model","title":"Input model","text":""},{"location":"advanced_usages/custom-tests/#resultoverwrite-model","title":"ResultOverwrite model","text":"

Attributes:

Name Type Description description overwrite TestResult.description

categories: overwrite TestResult.categories custom_field: a free string that will be included in the TestResult object

Note

The pydantic model is configured using the extra=forbid that will fail input validation if extra fields are provided.

"},{"location":"advanced_usages/custom-tests/#methods","title":"Methods","text":"
  • test(self) -> None: This is an abstract method that must be implemented. It contains the test logic that can access the collected command outputs using the instance_commands instance attribute, access the test inputs using the inputs instance attribute and must set the result instance attribute accordingly. It must be implemented using the AntaTest.anta_test decorator that provides logging and will collect commands before executing the test() method.
  • render(self, template: AntaTemplate) -> list[AntaCommand]: This method only needs to be implemented if AntaTemplate instances are present in the commands class attribute. It will be called for every AntaTemplate occurence and must return a list of AntaCommand using the AntaTemplate.render() method. It can access test inputs using the inputs instance attribute.
"},{"location":"advanced_usages/custom-tests/#test-execution","title":"Test execution","text":"

Below is a high level description of the test execution flow in ANTA:

  1. ANTA will parse the test catalog to get the list of AntaTest subclasses to instantiate and their associated input values. We consider a single AntaTest subclass in the following steps.

  2. ANTA will instantiate the AntaTest subclass and a single device will be provided to the test instance. The Input model defined in the class will also be instantiated at this moment. If any ValidationError is raised, the test execution will be stopped.

  3. If there is any AntaTemplate instance in the commands class attribute, render() will be called for every occurrence. At this moment, the instance_commands attribute has been initialized. If any rendering error occurs, the test execution will be stopped.

  4. The AntaTest.anta_test decorator will collect the commands from the device and update the instance_commands attribute with the outputs. If any collection error occurs, the test execution will be stopped.

  5. The test() method is executed.

"},{"location":"advanced_usages/custom-tests/#writing-an-antatest-subclass","title":"Writing an AntaTest subclass","text":"

In this section, we will go into all the details of writing an AntaTest subclass.

"},{"location":"advanced_usages/custom-tests/#class-definition","title":"Class definition","text":"

Import anta.models.AntaTest and define your own class. Define the mandatory class attributes using anta.models.AntaCommand, anta.models.AntaTemplate or both.

Info

Caching can be disabled per AntaCommand or AntaTemplate by setting the use_cache argument to False. For more details about how caching is implemented in ANTA, please refer to Caching in ANTA.

from anta.models import AntaTest, AntaCommand, AntaTemplate\n\n\nclass <YourTestName>(AntaTest):\n    \"\"\"\n    <a docstring description of your test>\n    \"\"\"\n\n    name = \"YourTestName\"                                           # should be your class name\n    description = \"<test description in human reading format>\"\n    categories = [\"<arbitrary category>\", \"<another arbitrary category>\"]\n    commands = [\n        AntaCommand(\n            command=\"<EOS command to run>\",\n            ofmt=\"<command format output>\",\n            version=\"<eAPI version to use>\",\n            revision=\"<revision to use for the command>\",           # revision has precedence over version\n            use_cache=\"<Use cache for the command>\",\n        ),\n        AntaTemplate(\n            template=\"<Python f-string to render an EOS command>\",\n            ofmt=\"<command format output>\",\n            version=\"<eAPI version to use>\",\n            revision=\"<revision to use for the command>\",           # revision has precedence over version\n            use_cache=\"<Use cache for the command>\",\n        )\n    ]\n
"},{"location":"advanced_usages/custom-tests/#inputs-definition","title":"Inputs definition","text":"

If the user needs to provide inputs for your test, you need to define a pydantic model that defines the schema of the test inputs:

class <YourTestName>(AntaTest):\n    \"\"\"Verifies ...\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if ...\n    * Failure: The test will fail if ...\n\n    Examples\n    --------\n    ```yaml\n    your.module.path:\n      - YourTestName:\n        field_name: example_field_value\n    ```\n    \"\"\"\n    ...\n    class Input(AntaTest.Input):\n        \"\"\"Inputs for my awesome test.\"\"\"\n        <input field name>: <input field type>\n        \"\"\"<input field docstring>\"\"\"\n

To define an input field type, refer to the pydantic documentation about types. You can also leverage anta.custom_types that provides reusable types defined in ANTA tests.

Regarding required, optional and nullable fields, refer to this documentation on how to define them.

Note

All the pydantic features are supported. For instance you can define validators for complex input validation.

"},{"location":"advanced_usages/custom-tests/#template-rendering","title":"Template rendering","text":"

Define the render() method if you have AntaTemplate instances in your commands class attribute:

class <YourTestName>(AntaTest):\n    ...\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        return [template.render(<template param>=input_value) for input_value in self.inputs.<input_field>]\n

You can access test inputs and render as many AntaCommand as desired.

"},{"location":"advanced_usages/custom-tests/#test-definition","title":"Test definition","text":"

Implement the test() method with your test logic:

class <YourTestName>(AntaTest):\n    ...\n    @AntaTest.anta_test\n    def test(self) -> None:\n        pass\n

The logic usually includes the following different stages: 1. Parse the command outputs using the self.instance_commands instance attribute. 2. If needed, access the test inputs using the self.inputs instance attribute and write your conditional logic. 3. Set the result instance attribute to reflect the test result by either calling self.result.is_success() or self.result.is_failure(\"<FAILURE REASON>\"). Sometimes, setting the test result to skipped using self.result.is_skipped(\"<SKIPPED REASON>\") can make sense (e.g. testing the OSPF neighbor states but no neighbor was found). However, you should not need to catch any exception and set the test result to error since the error handling is done by the framework, see below.

The example below is based on the VerifyTemperature test.

class VerifyTemperature(AntaTest):\n    ...\n    @AntaTest.anta_test\n    def test(self) -> None:\n        # Grab output of the collected command\n        command_output = self.instance_commands[0].json_output\n\n        # Do your test: In this example we check a specific field of the JSON output from EOS\n        temperature_status = command_output[\"systemStatus\"] if \"systemStatus\" in command_output.keys() else \"\"\n        if temperature_status == \"temperatureOk\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'\")\n

As you can see there is no error handling to do in your code. Everything is packaged in the AntaTest.anta_tests decorator and below is a simple example of error captured when trying to access a dictionary with an incorrect key:

class VerifyTemperature(AntaTest):\n    ...\n    @AntaTest.anta_test\n    def test(self) -> None:\n        # Grab output of the collected command\n        command_output = self.instance_commands[0].json_output\n\n        # Access the dictionary with an incorrect key\n        command_output['incorrectKey']\n
ERROR    Exception raised for test VerifyTemperature (on device 192.168.0.10) - KeyError ('incorrectKey')\n

Get stack trace for debugging

If you want to access to the full exception stack, you can run ANTA in debug mode by setting the ANTA_DEBUG environment variable to true. Example:

$ ANTA_DEBUG=true anta nrfu --catalog test_custom.yml text\n

"},{"location":"advanced_usages/custom-tests/#test-decorators","title":"Test decorators","text":"

In addition to the required AntaTest.anta_tests decorator, ANTA offers a set of optional decorators for further test customization:

  • anta.decorators.deprecated_test: Use this to log a message of WARNING severity when a test is deprecated.
  • anta.decorators.skip_on_platforms: Use this to skip tests for functionalities that are not supported on specific platforms.
from anta.decorators import skip_on_platforms\n\nclass VerifyTemperature(AntaTest):\n    ...\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        pass\n
"},{"location":"advanced_usages/custom-tests/#access-your-custom-tests-in-the-test-catalog","title":"Access your custom tests in the test catalog","text":"

This section is required only if you are not merging your development into ANTA. Otherwise, just follow contribution guide.

For that, you need to create your own Python package as described in this hitchhiker\u2019s guide to package Python code. We assume it is well known and we won\u2019t focus on this aspect. Thus, your package must be impartable by ANTA hence available in the module search path sys.path (you can use PYTHONPATH for example).

It is very similar to what is documented in catalog section but you have to use your own package name.2

Let say the custom Python package is anta_titom73 and the test is defined in anta_titom73.dc_project Python module, the test catalog would look like:

anta_titom73.dc_project:\n  - VerifyFeatureX:\n      minimum: 1\n
And now you can run your NRFU tests with the CLI:

anta nrfu text --catalog test_custom.yml\nspine01 :: verify_dynamic_vlan :: FAILURE (Device has 0 configured, we expect at least 1)\nspine02 :: verify_dynamic_vlan :: FAILURE (Device has 0 configured, we expect at least 1)\nleaf01 :: verify_dynamic_vlan :: SUCCESS\nleaf02 :: verify_dynamic_vlan :: SUCCESS\nleaf03 :: verify_dynamic_vlan :: SUCCESS\nleaf04 :: verify_dynamic_vlan :: SUCCESS\n
"},{"location":"api/catalog/","title":"Test Catalog","text":""},{"location":"api/catalog/#anta.catalog.AntaCatalog","title":"AntaCatalog","text":"
AntaCatalog(tests: list[AntaTestDefinition] | None = None, filename: str | Path | None = None)\n

Class representing an ANTA Catalog.

It can be instantiated using its contructor or one of the static methods: parse(), from_list() or from_dict()

Args:
tests: A list of AntaTestDefinition instances.\nfilename: The path from which the catalog is loaded.\n
Source code in anta/catalog.py
def __init__(\n    self,\n    tests: list[AntaTestDefinition] | None = None,\n    filename: str | Path | None = None,\n) -> None:\n    \"\"\"Instantiate an AntaCatalog instance.\n\n    Args:\n    ----\n        tests: A list of AntaTestDefinition instances.\n        filename: The path from which the catalog is loaded.\n\n    \"\"\"\n    self._tests: list[AntaTestDefinition] = []\n    if tests is not None:\n        self._tests = tests\n    self._filename: Path | None = None\n    if filename is not None:\n        if isinstance(filename, Path):\n            self._filename = filename\n        else:\n            self._filename = Path(filename)\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalog.filename","title":"filename property","text":"
filename: Path | None\n

Path of the file used to create this AntaCatalog instance.

"},{"location":"api/catalog/#anta.catalog.AntaCatalog.tests","title":"tests property writable","text":"
tests: list[AntaTestDefinition]\n

List of AntaTestDefinition in this catalog.

"},{"location":"api/catalog/#anta.catalog.AntaCatalog.from_dict","title":"from_dict staticmethod","text":"
from_dict(data: RawCatalogInput, filename: str | Path | None = None) -> AntaCatalog\n

Create an AntaCatalog instance from a dictionary data structure.

See RawCatalogInput type alias for details. It is the data structure returned by yaml.load() function of a valid YAML Test Catalog file.

Args:
data: Python dictionary used to instantiate the AntaCatalog instance\nfilename: value to be set as AntaCatalog instance attribute\n
Source code in anta/catalog.py
@staticmethod\ndef from_dict(data: RawCatalogInput, filename: str | Path | None = None) -> AntaCatalog:\n    \"\"\"Create an AntaCatalog instance from a dictionary data structure.\n\n    See RawCatalogInput type alias for details.\n    It is the data structure returned by `yaml.load()` function of a valid\n    YAML Test Catalog file.\n\n    Args:\n    ----\n        data: Python dictionary used to instantiate the AntaCatalog instance\n        filename: value to be set as AntaCatalog instance attribute\n\n    \"\"\"\n    tests: list[AntaTestDefinition] = []\n    if data is None:\n        logger.warning(\"Catalog input data is empty\")\n        return AntaCatalog(filename=filename)\n\n    if not isinstance(data, dict):\n        msg = f\"Wrong input type for catalog data{f' (from {filename})' if filename is not None else ''}, must be a dict, got {type(data).__name__}\"\n        raise TypeError(msg)\n\n    try:\n        catalog_data = AntaCatalogFile(**data)  # type: ignore[arg-type]\n    except ValidationError as e:\n        anta_log_exception(\n            e,\n            f\"Test catalog is invalid!{f' (from {filename})' if filename is not None else ''}\",\n            logger,\n        )\n        raise\n    for t in catalog_data.root.values():\n        tests.extend(t)\n    return AntaCatalog(tests, filename=filename)\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalog.from_list","title":"from_list staticmethod","text":"
from_list(data: ListAntaTestTuples) -> AntaCatalog\n

Create an AntaCatalog instance from a list data structure.

See ListAntaTestTuples type alias for details.

Args:
data: Python list used to instantiate the AntaCatalog instance\n
Source code in anta/catalog.py
@staticmethod\ndef from_list(data: ListAntaTestTuples) -> AntaCatalog:\n    \"\"\"Create an AntaCatalog instance from a list data structure.\n\n    See ListAntaTestTuples type alias for details.\n\n    Args:\n    ----\n        data: Python list used to instantiate the AntaCatalog instance\n\n    \"\"\"\n    tests: list[AntaTestDefinition] = []\n    try:\n        tests.extend(AntaTestDefinition(test=test, inputs=inputs) for test, inputs in data)\n    except ValidationError as e:\n        anta_log_exception(e, \"Test catalog is invalid!\", logger)\n        raise\n    return AntaCatalog(tests)\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalog.get_tests_by_tags","title":"get_tests_by_tags","text":"
get_tests_by_tags(tags: list[str], *, strict: bool = False) -> list[AntaTestDefinition]\n

Return all the tests that have matching tags in their input filters.

If strict=True, returns only tests that match all the tags provided as input. If strict=False, return all the tests that match at least one tag provided as input.

Source code in anta/catalog.py
def get_tests_by_tags(self, tags: list[str], *, strict: bool = False) -> list[AntaTestDefinition]:\n    \"\"\"Return all the tests that have matching tags in their input filters.\n\n    If strict=True, returns only tests that match all the tags provided as input.\n    If strict=False, return all the tests that match at least one tag provided as input.\n    \"\"\"\n    result: list[AntaTestDefinition] = []\n    for test in self.tests:\n        if test.inputs.filters and (f := test.inputs.filters.tags):\n            if strict:\n                if all(t in tags for t in f):\n                    result.append(test)\n            elif any(t in tags for t in f):\n                result.append(test)\n    return result\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalog.parse","title":"parse staticmethod","text":"
parse(filename: str | Path) -> AntaCatalog\n

Create an AntaCatalog instance from a test catalog file.

Args:
filename: Path to test catalog YAML file\n
Source code in anta/catalog.py
@staticmethod\ndef parse(filename: str | Path) -> AntaCatalog:\n    \"\"\"Create an AntaCatalog instance from a test catalog file.\n\n    Args:\n    ----\n        filename: Path to test catalog YAML file\n\n    \"\"\"\n    try:\n        file: Path = filename if isinstance(filename, Path) else Path(filename)\n        with file.open(encoding=\"UTF-8\") as f:\n            data = safe_load(f)\n    except (TypeError, YAMLError, OSError) as e:\n        message = f\"Unable to parse ANTA Test Catalog file '{filename}'\"\n        anta_log_exception(e, message, logger)\n        raise\n\n    return AntaCatalog.from_dict(data, filename=filename)\n
"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition","title":"AntaTestDefinition","text":"
AntaTestDefinition(**data: type[AntaTest] | AntaTest.Input | dict[str, Any] | None)\n

Bases: BaseModel

Define a test with its associated inputs.

test: An AntaTest concrete subclass inputs: The associated AntaTest.Input subclass instance

https://docs.pydantic.dev/2.0/usage/validators/#using-validation-context-with-basemodel-initialization.

Source code in anta/catalog.py
def __init__(self, **data: type[AntaTest] | AntaTest.Input | dict[str, Any] | None) -> None:\n    \"\"\"Inject test in the context to allow to instantiate Input in the BeforeValidator.\n\n    https://docs.pydantic.dev/2.0/usage/validators/#using-validation-context-with-basemodel-initialization.\n    \"\"\"\n    self.__pydantic_validator__.validate_python(\n        data,\n        self_instance=self,\n        context={\"test\": data[\"test\"]},\n    )\n    super(BaseModel, self).__init__()\n
"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.check_inputs","title":"check_inputs","text":"
check_inputs() -> AntaTestDefinition\n

Check the inputs field typing.

The inputs class attribute needs to be an instance of the AntaTest.Input subclass defined in the class test.

Source code in anta/catalog.py
@model_validator(mode=\"after\")\ndef check_inputs(self) -> AntaTestDefinition:\n    \"\"\"Check the `inputs` field typing.\n\n    The `inputs` class attribute needs to be an instance of the AntaTest.Input subclass defined in the class `test`.\n    \"\"\"\n    if not isinstance(self.inputs, self.test.Input):\n        msg = f\"Test input has type {self.inputs.__class__.__qualname__} but expected type {self.test.Input.__qualname__}\"\n        raise ValueError(msg)  # noqa: TRY004 pydantic catches ValueError or AssertionError, no TypeError\n    return self\n
"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.instantiate_inputs","title":"instantiate_inputs classmethod","text":"
instantiate_inputs(data: AntaTest.Input | dict[str, Any] | None, info: ValidationInfo) -> AntaTest.Input\n

Ensure the test inputs can be instantiated and thus are valid.

If the test has no inputs, allow the user to omit providing the inputs field. If the test has inputs, allow the user to provide a valid dictionary of the input fields. This model validator will instantiate an Input class from the test class field.

Source code in anta/catalog.py
@field_validator(\"inputs\", mode=\"before\")\n@classmethod\ndef instantiate_inputs(\n    cls: type[AntaTestDefinition],\n    data: AntaTest.Input | dict[str, Any] | None,\n    info: ValidationInfo,\n) -> AntaTest.Input:\n    \"\"\"Ensure the test inputs can be instantiated and thus are valid.\n\n    If the test has no inputs, allow the user to omit providing the `inputs` field.\n    If the test has inputs, allow the user to provide a valid dictionary of the input fields.\n    This model validator will instantiate an Input class from the `test` class field.\n    \"\"\"\n    if info.context is None:\n        msg = \"Could not validate inputs as no test class could be identified\"\n        raise ValueError(msg)\n    # Pydantic guarantees at this stage that test_class is a subclass of AntaTest because of the ordering\n    # of fields in the class definition - so no need to check for this\n    test_class = info.context[\"test\"]\n    if not (isclass(test_class) and issubclass(test_class, AntaTest)):\n        msg = f\"Could not validate inputs as no test class {test_class} is not a subclass of AntaTest\"\n        raise ValueError(msg)\n\n    if isinstance(data, AntaTest.Input):\n        return data\n    try:\n        if data is None:\n            return test_class.Input()\n        if isinstance(data, dict):\n            return test_class.Input(**data)\n    except ValidationError as e:\n        inputs_msg = str(e).replace(\"\\n\", \"\\n\\t\")\n        err_type = \"wrong_test_inputs\"\n        raise PydanticCustomError(\n            err_type,\n            f\"{test_class.name} test inputs are not valid: {inputs_msg}\\n\",\n            {\"errors\": e.errors()},\n        ) from e\n    msg = f\"Coud not instantiate inputs as type {type(data).__name__} is not valid\"\n    raise ValueError(msg)\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile","title":"AntaCatalogFile","text":"

Bases: RootModel[Dict[ImportString[Any], List[AntaTestDefinition]]]

Represents an ANTA Test Catalog File.

Example:
A valid test catalog file must have the following structure:\n```\n<Python module>:\n    - <AntaTest subclass>:\n        <AntaTest.Input compliant dictionary>\n```\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.check_tests","title":"check_tests classmethod","text":"
check_tests(data: Any) -> Any\n

Allow the user to provide a Python data structure that only has string values.

This validator will try to flatten and import Python modules, check if the tests classes are actually defined in their respective Python module and instantiate Input instances with provided value to validate test inputs.

Source code in anta/catalog.py
@model_validator(mode=\"before\")\n@classmethod\ndef check_tests(cls: type[AntaCatalogFile], data: Any) -> Any:  # noqa: ANN401\n    \"\"\"Allow the user to provide a Python data structure that only has string values.\n\n    This validator will try to flatten and import Python modules, check if the tests classes\n    are actually defined in their respective Python module and instantiate Input instances\n    with provided value to validate test inputs.\n    \"\"\"\n    if isinstance(data, dict):\n        typed_data: dict[ModuleType, list[Any]] = AntaCatalogFile.flatten_modules(data)\n        for module, tests in typed_data.items():\n            test_definitions: list[AntaTestDefinition] = []\n            for test_definition in tests:\n                if not isinstance(test_definition, dict):\n                    msg = f\"Syntax error when parsing: {test_definition}\\nIt must be a dictionary. Check the test catalog.\"\n                    raise ValueError(msg)  # noqa: TRY004 pydantic catches ValueError or AssertionError, no TypeError\n                if len(test_definition) != 1:\n                    msg = (\n                        f\"Syntax error when parsing: {test_definition}\\n\"\n                        \"It must be a dictionary with a single entry. Check the indentation in the test catalog.\"\n                    )\n                    raise ValueError(msg)\n                for test_name, test_inputs in test_definition.copy().items():\n                    test: type[AntaTest] | None = getattr(module, test_name, None)\n                    if test is None:\n                        msg = (\n                            f\"{test_name} is not defined in Python module {module.__name__}\"\n                            f\"{f' (from {module.__file__})' if module.__file__ is not None else ''}\"\n                        )\n                        raise ValueError(msg)\n                    test_definitions.append(AntaTestDefinition(test=test, inputs=test_inputs))\n            typed_data[module] = test_definitions\n    return typed_data\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.flatten_modules","title":"flatten_modules staticmethod","text":"
flatten_modules(data: dict[str, Any], package: str | None = None) -> dict[ModuleType, list[Any]]\n

Allow the user to provide a data structure with nested Python modules.

Example:
```\nanta.tests.routing:\n  generic:\n    - <AntaTestDefinition>\n  bgp:\n    - <AntaTestDefinition>\n```\n`anta.tests.routing.generic` and `anta.tests.routing.bgp` are importable Python modules.\n
Source code in anta/catalog.py
@staticmethod\ndef flatten_modules(data: dict[str, Any], package: str | None = None) -> dict[ModuleType, list[Any]]:\n    \"\"\"Allow the user to provide a data structure with nested Python modules.\n\n    Example:\n    -------\n        ```\n        anta.tests.routing:\n          generic:\n            - <AntaTestDefinition>\n          bgp:\n            - <AntaTestDefinition>\n        ```\n        `anta.tests.routing.generic` and `anta.tests.routing.bgp` are importable Python modules.\n\n    \"\"\"\n    modules: dict[ModuleType, list[Any]] = {}\n    for module_name, tests in data.items():\n        if package and not module_name.startswith(\".\"):\n            # PLW2901 - we redefine the loop variable on purpose here.\n            module_name = f\".{module_name}\"  # noqa: PLW2901\n        try:\n            module: ModuleType = importlib.import_module(name=module_name, package=package)\n        except Exception as e:  # pylint: disable=broad-exception-caught\n            # A test module is potentially user-defined code.\n            # We need to catch everything if we want to have meaningful logs\n            module_str = f\"{module_name[1:] if module_name.startswith('.') else module_name}{f' from package {package}' if package else ''}\"\n            message = f\"Module named {module_str} cannot be imported. Verify that the module exists and there is no Python syntax issues.\"\n            anta_log_exception(e, message, logger)\n            raise ValueError(message) from e\n        if isinstance(tests, dict):\n            # This is an inner Python module\n            modules.update(AntaCatalogFile.flatten_modules(data=tests, package=module.__name__))\n        else:\n            if not isinstance(tests, list):\n                msg = f\"Syntax error when parsing: {tests}\\nIt must be a list of ANTA tests. Check the test catalog.\"\n                raise ValueError(msg)  # noqa: TRY004 pydantic catches ValueError or AssertionError, no TypeError\n            # This is a list of AntaTestDefinition\n            modules[module] = tests\n    return modules\n
"},{"location":"api/device/","title":"Device","text":""},{"location":"api/device/#antadevice-base-class","title":"AntaDevice base class","text":""},{"location":"api/device/#uml-representation","title":"UML representation","text":""},{"location":"api/device/#anta.device.AntaDevice","title":"AntaDevice","text":"
AntaDevice(name: str, tags: list[str] | None = None, *, disable_cache: bool = False)\n

Bases: ABC

Abstract class representing a device in ANTA.

An implementation of this class must override the abstract coroutines _collect() and refresh().

Attributes:

Name Type Description name Device name

is_online: True if the device IP is reachable and a port can be open established: True if remote command execution succeeds hw_model: Hardware model of the device tags: List of tags for this device cache: In-memory cache from aiocache library for this device (None if cache is disabled) cache_locks: Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled

Args:
name: Device name\ntags: List of tags for this device\ndisable_cache: Disable caching for all commands for this device. Defaults to False.\n
Source code in anta/device.py
def __init__(self, name: str, tags: list[str] | None = None, *, disable_cache: bool = False) -> None:\n    \"\"\"Initialize an AntaDevice.\n\n    Args:\n    ----\n        name: Device name\n        tags: List of tags for this device\n        disable_cache: Disable caching for all commands for this device. Defaults to False.\n\n    \"\"\"\n    self.name: str = name\n    self.hw_model: str | None = None\n    self.tags: list[str] = tags if tags is not None else []\n    # A device always has its own name as tag\n    self.tags.append(self.name)\n    self.is_online: bool = False\n    self.established: bool = False\n    self.cache: Cache | None = None\n    self.cache_locks: defaultdict[str, asyncio.Lock] | None = None\n\n    # Initialize cache if not disabled\n    if not disable_cache:\n        self._init_cache()\n
"},{"location":"api/device/#anta.device.AntaDevice.cache_statistics","title":"cache_statistics property","text":"
cache_statistics: dict[str, Any] | None\n

Returns the device cache statistics for logging purposes.

"},{"location":"api/device/#anta.device.AntaDevice.__hash__","title":"__hash__","text":"
__hash__() -> int\n

Implement hashing for AntaDevice objects.

Source code in anta/device.py
def __hash__(self) -> int:\n    \"\"\"Implement hashing for AntaDevice objects.\"\"\"\n    return hash(self._keys)\n
"},{"location":"api/device/#anta.device.AntaDevice.collect","title":"collect async","text":"
collect(command: AntaCommand) -> None\n

Collect the output for a specified command.

When caching is activated on both the device and the command, this method prioritizes retrieving the output from the cache. In cases where the output isn\u2019t cached yet, it will be freshly collected and then stored in the cache for future access. The method employs asynchronous locks based on the command\u2019s UID to guarantee exclusive access to the cache.

When caching is NOT enabled, either at the device or command level, the method directly collects the output via the private _collect method without interacting with the cache.

Args:
command (AntaCommand): The command to process.\n
Source code in anta/device.py
async def collect(self, command: AntaCommand) -> None:\n    \"\"\"Collect the output for a specified command.\n\n    When caching is activated on both the device and the command,\n    this method prioritizes retrieving the output from the cache. In cases where the output isn't cached yet,\n    it will be freshly collected and then stored in the cache for future access.\n    The method employs asynchronous locks based on the command's UID to guarantee exclusive access to the cache.\n\n    When caching is NOT enabled, either at the device or command level, the method directly collects the output\n    via the private `_collect` method without interacting with the cache.\n\n    Args:\n    ----\n        command (AntaCommand): The command to process.\n\n    \"\"\"\n    # Need to ignore pylint no-member as Cache is a proxy class and pylint is not smart enough\n    # https://github.com/pylint-dev/pylint/issues/7258\n    if self.cache is not None and self.cache_locks is not None and command.use_cache:\n        async with self.cache_locks[command.uid]:\n            cached_output = await self.cache.get(command.uid)  # pylint: disable=no-member\n\n            if cached_output is not None:\n                logger.debug(\"Cache hit for %s on %s\", command.command, self.name)\n                command.output = cached_output\n            else:\n                await self._collect(command=command)\n                await self.cache.set(command.uid, command.output)  # pylint: disable=no-member\n    else:\n        await self._collect(command=command)\n
"},{"location":"api/device/#anta.device.AntaDevice.collect_commands","title":"collect_commands async","text":"
collect_commands(commands: list[AntaCommand]) -> None\n

Collect multiple commands.

Args:
commands: the commands to collect\n
Source code in anta/device.py
async def collect_commands(self, commands: list[AntaCommand]) -> None:\n    \"\"\"Collect multiple commands.\n\n    Args:\n    ----\n        commands: the commands to collect\n\n    \"\"\"\n    await asyncio.gather(*(self.collect(command=command) for command in commands))\n
"},{"location":"api/device/#anta.device.AntaDevice.copy","title":"copy async","text":"
copy(sources: list[Path], destination: Path, direction: Literal['to', 'from'] = 'from') -> None\n

Copy files to and from the device, usually through SCP.

It is not mandatory to implement this for a valid AntaDevice subclass.

Args:
sources: List of files to copy to or from the device.\ndestination: Local or remote destination when copying the files. Can be a folder.\ndirection: Defines if this coroutine copies files to or from the device.\n
Source code in anta/device.py
async def copy(self, sources: list[Path], destination: Path, direction: Literal[\"to\", \"from\"] = \"from\") -> None:\n    \"\"\"Copy files to and from the device, usually through SCP.\n\n    It is not mandatory to implement this for a valid AntaDevice subclass.\n\n    Args:\n    ----\n        sources: List of files to copy to or from the device.\n        destination: Local or remote destination when copying the files. Can be a folder.\n        direction: Defines if this coroutine copies files to or from the device.\n\n    \"\"\"\n    _ = (sources, destination, direction)\n    msg = f\"copy() method has not been implemented in {self.__class__.__name__} definition\"\n    raise NotImplementedError(msg)\n
"},{"location":"api/device/#anta.device.AntaDevice.refresh","title":"refresh abstractmethod async","text":"
refresh() -> None\n

Update attributes of an AntaDevice instance.

This coroutine must update the following attributes of AntaDevice: - is_online: When the device IP is reachable and a port can be open - established: When a command execution succeeds - hw_model: The hardware model of the device

Source code in anta/device.py
@abstractmethod\nasync def refresh(self) -> None:\n    \"\"\"Update attributes of an AntaDevice instance.\n\n    This coroutine must update the following attributes of AntaDevice:\n        - `is_online`: When the device IP is reachable and a port can be open\n        - `established`: When a command execution succeeds\n        - `hw_model`: The hardware model of the device\n    \"\"\"\n
"},{"location":"api/device/#anta.device.AntaDevice.supports","title":"supports","text":"
supports(command: AntaCommand) -> bool\n

Return True if the command is supported on the device hardware platform, False otherwise.

Source code in anta/device.py
def supports(self, command: AntaCommand) -> bool:\n    \"\"\"Return True if the command is supported on the device hardware platform, False otherwise.\"\"\"\n    unsupported = any(\"not supported on this hardware platform\" in e for e in command.errors)\n    logger.debug(command)\n    if unsupported:\n        logger.debug(\"%s is not supported on %s\", command.command, self.hw_model)\n    return not unsupported\n
"},{"location":"api/device/#async-eos-device-class","title":"Async EOS device class","text":""},{"location":"api/device/#uml-representation_1","title":"UML representation","text":""},{"location":"api/device/#anta.device.AsyncEOSDevice","title":"AsyncEOSDevice","text":"
AsyncEOSDevice(host: str, username: str, password: str, name: str | None = None, enable_password: str | None = None, port: int | None = None, ssh_port: int | None = 22, tags: list[str] | None = None, timeout: float | None = None, proto: Literal['http', 'https'] = 'https', *, enable: bool = False, insecure: bool = False, disable_cache: bool = False)\n

Bases: AntaDevice

Implementation of AntaDevice for EOS using aio-eapi.

Attributes:

Name Type Description name Device name

is_online: True if the device IP is reachable and a port can be open established: True if remote command execution succeeds hw_model: Hardware model of the device tags: List of tags for this device

Args:
host: Device FQDN or IP\nusername: Username to connect to eAPI and SSH\npassword: Password to connect to eAPI and SSH\nname: Device name\nenable: Device needs privileged access\nenable_password: Password used to gain privileged access on EOS\nport: eAPI port. Defaults to 80 is proto is 'http' or 443 if proto is 'https'.\nssh_port: SSH port\ntags: List of tags for this device\ntimeout: Timeout value in seconds for outgoing connections. Default to 10 secs.\ninsecure: Disable SSH Host Key validation\nproto: eAPI protocol. Value can be 'http' or 'https'\ndisable_cache: Disable caching for all commands for this device. Defaults to False.\n
Source code in anta/device.py
def __init__(\n    self,\n    host: str,\n    username: str,\n    password: str,\n    name: str | None = None,\n    enable_password: str | None = None,\n    port: int | None = None,\n    ssh_port: int | None = 22,\n    tags: list[str] | None = None,\n    timeout: float | None = None,\n    proto: Literal[\"http\", \"https\"] = \"https\",\n    *,\n    enable: bool = False,\n    insecure: bool = False,\n    disable_cache: bool = False,\n) -> None:\n    \"\"\"Instantiate an AsyncEOSDevice.\n\n    Args:\n    ----\n        host: Device FQDN or IP\n        username: Username to connect to eAPI and SSH\n        password: Password to connect to eAPI and SSH\n        name: Device name\n        enable: Device needs privileged access\n        enable_password: Password used to gain privileged access on EOS\n        port: eAPI port. Defaults to 80 is proto is 'http' or 443 if proto is 'https'.\n        ssh_port: SSH port\n        tags: List of tags for this device\n        timeout: Timeout value in seconds for outgoing connections. Default to 10 secs.\n        insecure: Disable SSH Host Key validation\n        proto: eAPI protocol. Value can be 'http' or 'https'\n        disable_cache: Disable caching for all commands for this device. Defaults to False.\n\n    \"\"\"\n    if host is None:\n        message = \"'host' is required to create an AsyncEOSDevice\"\n        logger.error(message)\n        raise ValueError(message)\n    if name is None:\n        name = f\"{host}{f':{port}' if port else ''}\"\n    super().__init__(name, tags, disable_cache=disable_cache)\n    if username is None:\n        message = f\"'username' is required to instantiate device '{self.name}'\"\n        logger.error(message)\n        raise ValueError(message)\n    if password is None:\n        message = f\"'password' is required to instantiate device '{self.name}'\"\n        logger.error(message)\n        raise ValueError(message)\n    self.enable = enable\n    self._enable_password = enable_password\n    self._session: aioeapi.Device = aioeapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)\n    ssh_params: dict[str, Any] = {}\n    if insecure:\n        ssh_params[\"known_hosts\"] = None\n    self._ssh_opts: SSHClientConnectionOptions = SSHClientConnectionOptions(host=host, port=ssh_port, username=username, password=password, **ssh_params)\n
"},{"location":"api/device/#anta.device.AsyncEOSDevice.copy","title":"copy async","text":"
copy(sources: list[Path], destination: Path, direction: Literal['to', 'from'] = 'from') -> None\n

Copy files to and from the device using asyncssh.scp().

Args:
sources: List of files to copy to or from the device.\ndestination: Local or remote destination when copying the files. Can be a folder.\ndirection: Defines if this coroutine copies files to or from the device.\n
Source code in anta/device.py
async def copy(self, sources: list[Path], destination: Path, direction: Literal[\"to\", \"from\"] = \"from\") -> None:\n    \"\"\"Copy files to and from the device using asyncssh.scp().\n\n    Args:\n    ----\n        sources: List of files to copy to or from the device.\n        destination: Local or remote destination when copying the files. Can be a folder.\n        direction: Defines if this coroutine copies files to or from the device.\n\n    \"\"\"\n    async with asyncssh.connect(\n        host=self._ssh_opts.host,\n        port=self._ssh_opts.port,\n        tunnel=self._ssh_opts.tunnel,\n        family=self._ssh_opts.family,\n        local_addr=self._ssh_opts.local_addr,\n        options=self._ssh_opts,\n    ) as conn:\n        src: list[tuple[SSHClientConnection, Path]] | list[Path]\n        dst: tuple[SSHClientConnection, Path] | Path\n        if direction == \"from\":\n            src = [(conn, file) for file in sources]\n            dst = destination\n            for file in sources:\n                message = f\"Copying '{file}' from device {self.name} to '{destination}' locally\"\n                logger.info(message)\n\n        elif direction == \"to\":\n            src = sources\n            dst = conn, destination\n            for file in src:\n                message = f\"Copying '{file}' to device {self.name} to '{destination}' remotely\"\n                logger.info(message)\n\n        else:\n            logger.critical(\"'direction' argument to copy() fonction is invalid: %s\", direction)\n\n            return\n        await asyncssh.scp(src, dst)\n
"},{"location":"api/device/#anta.device.AsyncEOSDevice.refresh","title":"refresh async","text":"
refresh() -> None\n

Update attributes of an AsyncEOSDevice instance.

This coroutine must update the following attributes of AsyncEOSDevice: - is_online: When a device IP is reachable and a port can be open - established: When a command execution succeeds - hw_model: The hardware model of the device

Source code in anta/device.py
async def refresh(self) -> None:\n    \"\"\"Update attributes of an AsyncEOSDevice instance.\n\n    This coroutine must update the following attributes of AsyncEOSDevice:\n    - is_online: When a device IP is reachable and a port can be open\n    - established: When a command execution succeeds\n    - hw_model: The hardware model of the device\n    \"\"\"\n    logger.debug(\"Refreshing device %s\", self.name)\n    self.is_online = await self._session.check_connection()\n    if self.is_online:\n        show_version = \"show version\"\n        hw_model_key = \"modelName\"\n        try:\n            response = await self._session.cli(command=show_version)\n        except aioeapi.EapiCommandError as e:\n            logger.warning(\"Cannot get hardware information from device %s: %s\", self.name, e.errmsg)\n\n        except (HTTPError, ConnectError) as e:\n            logger.warning(\"Cannot get hardware information from device %s: %s\", self.name, exc_to_str(e))\n\n        else:\n            if hw_model_key in response:\n                self.hw_model = response[hw_model_key]\n            else:\n                logger.warning(\"Cannot get hardware information from device %s: cannot parse '%s'\", self.name, show_version)\n\n    else:\n        logger.warning(\"Could not connect to device %s: cannot open eAPI port\", self.name)\n\n    self.established = bool(self.is_online and self.hw_model)\n
"},{"location":"api/inventory/","title":"Inventory module","text":""},{"location":"api/inventory/#anta.inventory.AntaInventory","title":"AntaInventory","text":"

Bases: Dict[str, AntaDevice]

Inventory abstraction for ANTA framework.

"},{"location":"api/inventory/#anta.inventory.AntaInventory.__setitem__","title":"__setitem__","text":"
__setitem__(key: str, value: AntaDevice) -> None\n

Set a device in the inventory.

Source code in anta/inventory/__init__.py
def __setitem__(self, key: str, value: AntaDevice) -> None:\n    \"\"\"Set a device in the inventory.\"\"\"\n    if key != value.name:\n        msg = f\"The key must be the device name for device '{value.name}'. Use AntaInventory.add_device().\"\n        raise RuntimeError(msg)\n    return super().__setitem__(key, value)\n
"},{"location":"api/inventory/#anta.inventory.AntaInventory.add_device","title":"add_device","text":"
add_device(device: AntaDevice) -> None\n

Add a device to final inventory.

Args:
device: Device object to be added\n
Source code in anta/inventory/__init__.py
def add_device(self, device: AntaDevice) -> None:\n    \"\"\"Add a device to final inventory.\n\n    Args:\n    ----\n        device: Device object to be added\n\n    \"\"\"\n    self[device.name] = device\n
"},{"location":"api/inventory/#anta.inventory.AntaInventory.connect_inventory","title":"connect_inventory async","text":"
connect_inventory() -> None\n

Run refresh() coroutines for all AntaDevice objects in this inventory.

Source code in anta/inventory/__init__.py
async def connect_inventory(self) -> None:\n    \"\"\"Run `refresh()` coroutines for all AntaDevice objects in this inventory.\"\"\"\n    logger.debug(\"Refreshing devices...\")\n    results = await asyncio.gather(\n        *(device.refresh() for device in self.values()),\n        return_exceptions=True,\n    )\n    for r in results:\n        if isinstance(r, Exception):\n            message = \"Error when refreshing inventory\"\n            anta_log_exception(r, message, logger)\n
"},{"location":"api/inventory/#anta.inventory.AntaInventory.get_inventory","title":"get_inventory","text":"
get_inventory(*, established_only: bool = False, tags: list[str] | None = None) -> AntaInventory\n

Return a filtered inventory.

Args:
established_only: Whether or not to include only established devices. Default False.\ntags: List of tags to filter devices.\n

Returns:

Type Description AntaInventory: An inventory with filtered AntaDevice objects. Source code in anta/inventory/__init__.py
def get_inventory(self, *, established_only: bool = False, tags: list[str] | None = None) -> AntaInventory:\n    \"\"\"Return a filtered inventory.\n\n    Args:\n    ----\n        established_only: Whether or not to include only established devices. Default False.\n        tags: List of tags to filter devices.\n\n    Returns\n    -------\n        AntaInventory: An inventory with filtered AntaDevice objects.\n\n    \"\"\"\n\n    def _filter_devices(device: AntaDevice) -> bool:\n        \"\"\"Select the devices based on the input tags and the requirement for an established connection.\"\"\"\n        if tags is not None and all(tag not in tags for tag in device.tags):\n            return False\n        return bool(not established_only or device.established)\n\n    devices: list[AntaDevice] = list(filter(_filter_devices, self.values()))\n    result = AntaInventory()\n    for device in devices:\n        result.add_device(device)\n    return result\n
"},{"location":"api/inventory/#anta.inventory.AntaInventory.parse","title":"parse staticmethod","text":"
parse(filename: str | Path, username: str, password: str, enable_password: str | None = None, timeout: float | None = None, *, enable: bool = False, insecure: bool = False, disable_cache: bool = False) -> AntaInventory\n

Create an AntaInventory instance from an inventory file.

The inventory devices are AsyncEOSDevice instances.

Args:
filename (str): Path to device inventory YAML file\nusername (str): Username to use to connect to devices\npassword (str): Password to use to connect to devices\nenable (bool): Whether or not the commands need to be run in enable mode towards the devices\nenable_password (str, optional): Enable password to use if required\ntimeout (float, optional): timeout in seconds for every API call.\ninsecure (bool): Disable SSH Host Key validation\ndisable_cache (bool): Disable cache globally\n

Raises:

Type Description InventoryRootKeyError: Root key of inventory is missing.

InventoryIncorrectSchemaError: Inventory file is not following AntaInventory Schema.

Source code in anta/inventory/__init__.py
@staticmethod\ndef parse(\n    filename: str | Path,\n    username: str,\n    password: str,\n    enable_password: str | None = None,\n    timeout: float | None = None,\n    *,\n    enable: bool = False,\n    insecure: bool = False,\n    disable_cache: bool = False,\n) -> AntaInventory:\n    \"\"\"Create an AntaInventory instance from an inventory file.\n\n    The inventory devices are AsyncEOSDevice instances.\n\n    Args:\n    ----\n        filename (str): Path to device inventory YAML file\n        username (str): Username to use to connect to devices\n        password (str): Password to use to connect to devices\n        enable (bool): Whether or not the commands need to be run in enable mode towards the devices\n        enable_password (str, optional): Enable password to use if required\n        timeout (float, optional): timeout in seconds for every API call.\n        insecure (bool): Disable SSH Host Key validation\n        disable_cache (bool): Disable cache globally\n\n    Raises\n    ------\n        InventoryRootKeyError: Root key of inventory is missing.\n        InventoryIncorrectSchemaError: Inventory file is not following AntaInventory Schema.\n\n    \"\"\"\n    inventory = AntaInventory()\n    kwargs: dict[str, Any] = {\n        \"username\": username,\n        \"password\": password,\n        \"enable\": enable,\n        \"enable_password\": enable_password,\n        \"timeout\": timeout,\n        \"insecure\": insecure,\n        \"disable_cache\": disable_cache,\n    }\n    if username is None:\n        message = \"'username' is required to create an AntaInventory\"\n        logger.error(message)\n        raise ValueError(message)\n    if password is None:\n        message = \"'password' is required to create an AntaInventory\"\n        logger.error(message)\n        raise ValueError(message)\n\n    try:\n        filename = Path(filename)\n        with filename.open(encoding=\"UTF-8\") as file:\n            data = safe_load(file)\n    except (TypeError, YAMLError, OSError) as e:\n        message = f\"Unable to parse ANTA Device Inventory file '{filename}'\"\n        anta_log_exception(e, message, logger)\n        raise\n\n    if AntaInventory.INVENTORY_ROOT_KEY not in data:\n        exc = InventoryRootKeyError(f\"Inventory root key ({AntaInventory.INVENTORY_ROOT_KEY}) is not defined in your inventory\")\n        anta_log_exception(exc, f\"Device inventory is invalid! (from {filename})\", logger)\n        raise exc\n\n    try:\n        inventory_input = AntaInventoryInput(**data[AntaInventory.INVENTORY_ROOT_KEY])\n    except ValidationError as e:\n        anta_log_exception(e, f\"Device inventory is invalid! (from {filename})\", logger)\n        raise\n\n    # Read data from input\n    AntaInventory._parse_hosts(inventory_input, inventory, **kwargs)\n    AntaInventory._parse_networks(inventory_input, inventory, **kwargs)\n    AntaInventory._parse_ranges(inventory_input, inventory, **kwargs)\n\n    return inventory\n
"},{"location":"api/inventory/#anta.inventory.exceptions","title":"exceptions","text":"

Manage Exception in Inventory module.

"},{"location":"api/inventory/#anta.inventory.exceptions.InventoryIncorrectSchemaError","title":"InventoryIncorrectSchemaError","text":"

Bases: Exception

Error when user data does not follow ANTA schema.

"},{"location":"api/inventory/#anta.inventory.exceptions.InventoryRootKeyError","title":"InventoryRootKeyError","text":"

Bases: Exception

Error raised when inventory root key is not found.

"},{"location":"api/inventory.models.input/","title":"Inventory models","text":""},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryInput","title":"AntaInventoryInput","text":"

Bases: BaseModel

User\u2019s inventory model.

Attributes:

Name Type Description networks (list[AntaInventoryNetwork],Optional) List of AntaInventoryNetwork objects for networks.

hosts (list[AntaInventoryHost],Optional): List of AntaInventoryHost objects for hosts. range (list[AntaInventoryRange],Optional): List of AntaInventoryRange objects for ranges.

"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryHost","title":"AntaInventoryHost","text":"

Bases: BaseModel

Host definition for user\u2019s inventory.

Attributes:

Name Type Description host (IPvAnyAddress) IPv4 or IPv6 address of the device

port (int): (Optional) eAPI port to use Default is 443. name (str): (Optional) Name to display during tests report. Default is hostname:port tags (list[str]): List of attached tags read from inventory file. disable_cache (bool): Disable cache per host. Defaults to False.

"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryNetwork","title":"AntaInventoryNetwork","text":"

Bases: BaseModel

Network definition for user\u2019s inventory.

Attributes:

Name Type Description network (IPvAnyNetwork) Subnet to use for testing.

tags (list[str]): List of attached tags read from inventory file. disable_cache (bool): Disable cache per network. Defaults to False.

"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryRange","title":"AntaInventoryRange","text":"

Bases: BaseModel

IP Range definition for user\u2019s inventory.

Attributes:

Name Type Description start (IPvAnyAddress) IPv4 or IPv6 address for the begining of the range.

stop (IPvAnyAddress): IPv4 or IPv6 address for the end of the range. tags (list[str]): List of attached tags read from inventory file. disable_cache (bool): Disable cache per range of hosts. Defaults to False.

"},{"location":"api/models/","title":"Test models","text":""},{"location":"api/models/#test-definition","title":"Test definition","text":""},{"location":"api/models/#uml-diagram","title":"UML Diagram","text":""},{"location":"api/models/#anta.models.AntaTest","title":"AntaTest","text":"
AntaTest(device: AntaDevice, inputs: dict[str, Any] | AntaTest.Input | None = None, eos_data: list[dict[Any, Any] | str] | None = None)\n

Bases: ABC

Abstract class defining a test in ANTA.

The goal of this class is to handle the heavy lifting and make writing a test as simple as possible.

Examples

The following is an example of an AntaTest subclass implementation:

    class VerifyReachability(AntaTest):\n        name = \"VerifyReachability\"\n        description = \"Test the network reachability to one or many destination IP(s).\"\n        categories = [\"connectivity\"]\n        commands = [AntaTemplate(template=\"ping vrf {vrf} {dst} source {src} repeat 2\")]\n\n        class Input(AntaTest.Input):\n            hosts: list[Host]\n            class Host(BaseModel):\n                dst: IPv4Address\n                src: IPv4Address\n                vrf: str = \"default\"\n\n        def render(self, template: AntaTemplate) -> list[AntaCommand]:\n            return [template.render({\"dst\": host.dst, \"src\": host.src, \"vrf\": host.vrf}) for host in self.inputs.hosts]\n\n        @AntaTest.anta_test\n        def test(self) -> None:\n            failures = []\n            for command in self.instance_commands:\n                if command.params and (\"src\" and \"dst\") in command.params:\n                    src, dst = command.params[\"src\"], command.params[\"dst\"]\n                if \"2 received\" not in command.json_output[\"messages\"][0]:\n                    failures.append((str(src), str(dst)))\n            if not failures:\n                self.result.is_success()\n            else:\n                self.result.is_failure(f\"Connectivity test failed for the following source-destination pairs: {failures}\")\n
Attributes: device: AntaDevice instance on which this test is run inputs: AntaTest.Input instance carrying the test inputs instance_commands: List of AntaCommand instances of this test result: TestResult instance representing the result of this test logger: Python logger for this test instance

Args:
device: AntaDevice instance on which the test will be run\ninputs: dictionary of attributes used to instantiate the AntaTest.Input instance\neos_data: Populate outputs of the test commands instead of collecting from devices.\n          This list must have the same length and order than the `instance_commands` instance attribute.\n
Source code in anta/models.py
def __init__(\n    self,\n    device: AntaDevice,\n    inputs: dict[str, Any] | AntaTest.Input | None = None,\n    eos_data: list[dict[Any, Any] | str] | None = None,\n) -> None:\n    \"\"\"AntaTest Constructor.\n\n    Args:\n    ----\n        device: AntaDevice instance on which the test will be run\n        inputs: dictionary of attributes used to instantiate the AntaTest.Input instance\n        eos_data: Populate outputs of the test commands instead of collecting from devices.\n                  This list must have the same length and order than the `instance_commands` instance attribute.\n\n    \"\"\"\n    self.logger: logging.Logger = logging.getLogger(f\"{self.__module__}.{self.__class__.__name__}\")\n    self.device: AntaDevice = device\n    self.inputs: AntaTest.Input\n    self.instance_commands: list[AntaCommand] = []\n    self.result: TestResult = TestResult(\n        name=device.name,\n        test=self.name,\n        categories=self.categories,\n        description=self.description,\n    )\n    self._init_inputs(inputs)\n    if self.result.result == \"unset\":\n        self._init_commands(eos_data)\n
"},{"location":"api/models/#anta.models.AntaTest.blocked","title":"blocked property","text":"
blocked: bool\n

Check if CLI commands contain a blocked keyword.

"},{"location":"api/models/#anta.models.AntaTest.collected","title":"collected property","text":"
collected: bool\n

Returns True if all commands for this test have been collected.

"},{"location":"api/models/#anta.models.AntaTest.failed_commands","title":"failed_commands property","text":"
failed_commands: list[AntaCommand]\n

Returns a list of all the commands that have failed.

"},{"location":"api/models/#anta.models.AntaTest.Input","title":"Input","text":"

Bases: BaseModel

Class defining inputs for a test in ANTA.

Examples

A valid test catalog will look like the following:

<Python module>:\n- <AntaTest subclass>:\n    result_overwrite:\n        categories:\n        - \"Overwritten category 1\"\n        description: \"Test with overwritten description\"\n        custom_field: \"Test run by John Doe\"\n
Attributes: result_overwrite: Define fields to overwrite in the TestResult object

"},{"location":"api/models/#anta.models.AntaTest.Input.Filters","title":"Filters","text":"

Bases: BaseModel

Runtime filters to map tests with list of tags or devices.

Attributes:

Name Type Description tags List of device's tags for the test."},{"location":"api/models/#anta.models.AntaTest.Input.ResultOverwrite","title":"ResultOverwrite","text":"

Bases: BaseModel

Test inputs model to overwrite result fields.

Attributes:

Name Type Description description overwrite TestResult.description

categories: overwrite TestResult.categories custom_field: a free string that will be included in the TestResult object

"},{"location":"api/models/#anta.models.AntaTest.Input.__hash__","title":"__hash__","text":"
__hash__() -> int\n

Implement generic hashing for AntaTest.Input.

This will work in most cases but this does not consider 2 lists with different ordering as equal.

Source code in anta/models.py
def __hash__(self) -> int:\n    \"\"\"Implement generic hashing for AntaTest.Input.\n\n    This will work in most cases but this does not consider 2 lists with different ordering as equal.\n    \"\"\"\n    return hash(self.model_dump_json())\n
"},{"location":"api/models/#anta.models.AntaTest.anta_test","title":"anta_test staticmethod","text":"
anta_test(function: F) -> Callable[..., Coroutine[Any, Any, TestResult]]\n

Decorate the test() method in child classes.

This decorator implements (in this order):

  1. Instantiate the command outputs if eos_data is provided to the test() method
  2. Collect the commands from the device
  3. Run the test() method
  4. Catches any exception in test() user code and set the result instance attribute
Source code in anta/models.py
@staticmethod\ndef anta_test(function: F) -> Callable[..., Coroutine[Any, Any, TestResult]]:\n    \"\"\"Decorate the `test()` method in child classes.\n\n    This decorator implements (in this order):\n\n    1. Instantiate the command outputs if `eos_data` is provided to the `test()` method\n    2. Collect the commands from the device\n    3. Run the `test()` method\n    4. Catches any exception in `test()` user code and set the `result` instance attribute\n    \"\"\"\n\n    @wraps(function)\n    async def wrapper(\n        self: AntaTest,\n        eos_data: list[dict[Any, Any] | str] | None = None,\n        **kwargs: dict[str, Any],\n    ) -> TestResult:\n        \"\"\"Inner function for the anta_test decorator.\n\n        Args:\n        ----\n            self: The test instance.\n            eos_data: Populate outputs of the test commands instead of collecting from devices.\n                      This list must have the same length and order than the `instance_commands` instance attribute.\n            kwargs: Any keyword argument to pass to the test.\n\n        Returns\n        -------\n            result: TestResult instance attribute populated with error status if any\n\n        \"\"\"\n\n        def format_td(seconds: float, digits: int = 3) -> str:\n            isec, fsec = divmod(round(seconds * 10**digits), 10**digits)\n            return f\"{timedelta(seconds=isec)}.{fsec:0{digits}.0f}\"\n\n        start_time = time.time()\n        if self.result.result != \"unset\":\n            return self.result\n\n        # Data\n        if eos_data is not None:\n            self.save_commands_data(eos_data)\n            self.logger.debug(\"Test %s initialized with input data %s\", self.name, eos_data)\n\n        # If some data is missing, try to collect\n        if not self.collected:\n            await self.collect()\n            if self.result.result != \"unset\":\n                return self.result\n\n            if cmds := self.failed_commands:\n                self.logger.debug(self.device.supports)\n                unsupported_commands = [f\"Skipped because {c.command} is not supported on {self.device.hw_model}\" for c in cmds if not self.device.supports(c)]\n                self.logger.debug(unsupported_commands)\n                if unsupported_commands:\n                    msg = f\"Test {self.name} has been skipped because it is not supported on {self.device.hw_model}: {GITHUB_SUGGESTION}\"\n                    self.logger.warning(msg)\n                    self.result.is_skipped(\"\\n\".join(unsupported_commands))\n                    return self.result\n                self.result.is_error(message=\"\\n\".join([f\"{c.command} has failed: {', '.join(c.errors)}\" for c in cmds]))\n                return self.result\n\n        try:\n            function(self, **kwargs)\n        except Exception as e:  # pylint: disable=broad-exception-caught\n            # test() is user-defined code.\n            # We need to catch everything if we want the AntaTest object\n            # to live until the reporting\n            message = f\"Exception raised for test {self.name} (on device {self.device.name})\"\n            anta_log_exception(e, message, self.logger)\n            self.result.is_error(message=exc_to_str(e))\n\n        test_duration = time.time() - start_time\n        msg = f\"Executing test {self.name} on device {self.device.name} took {format_td(test_duration)}\"\n        self.logger.debug(msg)\n\n        AntaTest.update_progress()\n        return self.result\n\n    return wrapper\n
"},{"location":"api/models/#anta.models.AntaTest.collect","title":"collect async","text":"
collect() -> None\n

Collect outputs of all commands of this test class from the device of this test instance.

Source code in anta/models.py
async def collect(self) -> None:\n    \"\"\"Collect outputs of all commands of this test class from the device of this test instance.\"\"\"\n    try:\n        if self.blocked is False:\n            await self.device.collect_commands(self.instance_commands)\n    except Exception as e:  # pylint: disable=broad-exception-caught\n        # device._collect() is user-defined code.\n        # We need to catch everything if we want the AntaTest object\n        # to live until the reporting\n        message = f\"Exception raised while collecting commands for test {self.name} (on device {self.device.name})\"\n        anta_log_exception(e, message, self.logger)\n        self.result.is_error(message=exc_to_str(e))\n
"},{"location":"api/models/#anta.models.AntaTest.render","title":"render","text":"
render(template: AntaTemplate) -> list[AntaCommand]\n

Render an AntaTemplate instance of this AntaTest using the provided AntaTest.Input instance at self.inputs.

This is not an abstract method because it does not need to be implemented if there is no AntaTemplate for this test.

Source code in anta/models.py
def render(self, template: AntaTemplate) -> list[AntaCommand]:  # pylint: disable=W0613  # noqa: ARG002\n    \"\"\"Render an AntaTemplate instance of this AntaTest using the provided AntaTest.Input instance at self.inputs.\n\n    This is not an abstract method because it does not need to be implemented if there is\n    no AntaTemplate for this test.\n    \"\"\"\n    msg = f\"AntaTemplate are provided but render() method has not been implemented for {self.__module__}.{self.name}\"\n    raise NotImplementedError(msg)\n
"},{"location":"api/models/#anta.models.AntaTest.save_commands_data","title":"save_commands_data","text":"
save_commands_data(eos_data: list[dict[str, Any] | str]) -> None\n

Populate output of all AntaCommand instances in instance_commands.

Source code in anta/models.py
def save_commands_data(self, eos_data: list[dict[str, Any] | str]) -> None:\n    \"\"\"Populate output of all AntaCommand instances in `instance_commands`.\"\"\"\n    if len(eos_data) > len(self.instance_commands):\n        self.result.is_error(message=\"Test initialization error: Trying to save more data than there are commands for the test\")\n        return\n    if len(eos_data) < len(self.instance_commands):\n        self.result.is_error(message=\"Test initialization error: Trying to save less data than there are commands for the test\")\n        return\n    for index, data in enumerate(eos_data or []):\n        self.instance_commands[index].output = data\n
"},{"location":"api/models/#anta.models.AntaTest.test","title":"test abstractmethod","text":"
test() -> Coroutine[Any, Any, TestResult]\n

Core of the test logic.

This is an abstractmethod that must be implemented by child classes. It must set the correct status of the result instance attribute with the appropriate outcome of the test.

Examples

It must be implemented using the AntaTest.anta_test decorator:

@AntaTest.anta_test\ndef test(self) -> None:\n    self.result.is_success()\n    for command in self.instance_commands:\n        if not self._test_command(command): # _test_command() is an arbitrary test logic\n            self.result.is_failure(\"Failure reson\")\n

Source code in anta/models.py
@abstractmethod\ndef test(self) -> Coroutine[Any, Any, TestResult]:\n    \"\"\"Core of the test logic.\n\n    This is an abstractmethod that must be implemented by child classes.\n    It must set the correct status of the `result` instance attribute with the appropriate outcome of the test.\n\n    Examples\n    --------\n    It must be implemented using the `AntaTest.anta_test` decorator:\n        ```python\n        @AntaTest.anta_test\n        def test(self) -> None:\n            self.result.is_success()\n            for command in self.instance_commands:\n                if not self._test_command(command): # _test_command() is an arbitrary test logic\n                    self.result.is_failure(\"Failure reson\")\n        ```\n\n    \"\"\"\n
"},{"location":"api/models/#command-definition","title":"Command definition","text":""},{"location":"api/models/#uml-diagram_1","title":"UML Diagram","text":"

Warning

CLI commands are protected to avoid execution of critical commands such as reload or write erase.

  • Reload command: ^reload\\s*\\w*
  • Configure mode: ^conf\\w*\\s*(terminal|session)*
  • Write: ^wr\\w*\\s*\\w+
"},{"location":"api/models/#anta.models.AntaCommand","title":"AntaCommand","text":"

Bases: BaseModel

Class to define a command.

Info

eAPI models are revisioned, this means that if a model is modified in a non-backwards compatible way, then its revision will be bumped up (revisions are numbers, default value is 1).

By default an eAPI request will return revision 1 of the model instance, this ensures that older management software will not suddenly stop working when a switch is upgraded. A revision applies to a particular CLI command whereas a version is global and is internally translated to a specific revision for each CLI command in the RPC.

Revision has precedence over version.

Attributes:

Name Type Description command Device command

version: eAPI version - valid values are 1 or \u201clatest\u201d - default is \u201clatest\u201d revision: eAPI revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt: eAPI output - json or text - default is json output: Output of the command populated by the collect() function template: AntaTemplate object used to render this command params: Dictionary of variables with string values to render the template errors: If the command execution fails, eAPI returns a list of strings detailing the error use_cache: Enable or disable caching for this AntaCommand if the AntaDevice supports it - default is True

"},{"location":"api/models/#anta.models.AntaCommand.collected","title":"collected property","text":"
collected: bool\n

Return True if the command has been collected.

"},{"location":"api/models/#anta.models.AntaCommand.json_output","title":"json_output property","text":"
json_output: dict[str, Any]\n

Get the command output as JSON.

"},{"location":"api/models/#anta.models.AntaCommand.text_output","title":"text_output property","text":"
text_output: str\n

Get the command output as a string.

"},{"location":"api/models/#anta.models.AntaCommand.uid","title":"uid property","text":"
uid: str\n

Generate a unique identifier for this command.

"},{"location":"api/models/#template-definition","title":"Template definition","text":""},{"location":"api/models/#uml-diagram_2","title":"UML Diagram","text":""},{"location":"api/models/#anta.models.AntaTemplate","title":"AntaTemplate","text":"

Bases: BaseModel

Class to define a command template as Python f-string.

Can render a command from parameters.

Attributes:

Name Type Description template Python f-string. Example: 'show vlan {vlan_id}'

version: eAPI version - valid values are 1 or \u201clatest\u201d - default is \u201clatest\u201d revision: Revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt: eAPI output - json or text - default is json use_cache: Enable or disable caching for this AntaTemplate if the AntaDevice supports it - default is True

"},{"location":"api/models/#anta.models.AntaTemplate.render","title":"render","text":"
render(**params: dict[str, Any]) -> AntaCommand\n

Render an AntaCommand from an AntaTemplate instance.

Keep the parameters used in the AntaTemplate instance.

Args:
params: dictionary of variables with string values to render the Python f-string\n

Returns:

Type Description command: The rendered AntaCommand.

This AntaCommand instance have a template attribute that references this AntaTemplate instance.

Source code in anta/models.py
def render(self, **params: dict[str, Any]) -> AntaCommand:\n    \"\"\"Render an AntaCommand from an AntaTemplate instance.\n\n    Keep the parameters used in the AntaTemplate instance.\n\n    Args:\n    ----\n        params: dictionary of variables with string values to render the Python f-string\n\n    Returns\n    -------\n        command: The rendered AntaCommand.\n                 This AntaCommand instance have a template attribute that references this\n                 AntaTemplate instance.\n\n    \"\"\"\n    try:\n        return AntaCommand(\n            command=self.template.format(**params),\n            ofmt=self.ofmt,\n            version=self.version,\n            revision=self.revision,\n            template=self,\n            params=params,\n            use_cache=self.use_cache,\n        )\n    except KeyError as e:\n        raise AntaTemplateRenderError(self, e.args[0]) from e\n
"},{"location":"api/report_manager/","title":"Report Manager","text":""},{"location":"api/report_manager/#anta.reporter.ReportTable","title":"ReportTable","text":"

TableReport Generate a Table based on TestResult.

"},{"location":"api/report_manager/#anta.reporter.ReportTable.report_all","title":"report_all","text":"
report_all(result_manager: ResultManager, host: str | None = None, testcase: str | None = None, title: str = 'All tests results') -> Table\n

Create a table report with all tests for one or all devices.

Create table with full output: Host / Test / Status / Message

Args:
result_manager (ResultManager): A manager with a list of tests.\nhost (str, optional): IP Address of a host to search for. Defaults to None.\ntestcase (str, optional): A test name to search for. Defaults to None.\ntitle (str, optional): Title for the report. Defaults to 'All tests results'.\n

Returns:

Type Description Table: A fully populated rich Table Source code in anta/reporter/__init__.py
def report_all(\n    self,\n    result_manager: ResultManager,\n    host: str | None = None,\n    testcase: str | None = None,\n    title: str = \"All tests results\",\n) -> Table:\n    \"\"\"Create a table report with all tests for one or all devices.\n\n    Create table with full output: Host / Test / Status / Message\n\n    Args:\n    ----\n        result_manager (ResultManager): A manager with a list of tests.\n        host (str, optional): IP Address of a host to search for. Defaults to None.\n        testcase (str, optional): A test name to search for. Defaults to None.\n        title (str, optional): Title for the report. Defaults to 'All tests results'.\n\n    Returns\n    -------\n        Table: A fully populated rich Table\n\n    \"\"\"\n    table = Table(title=title, show_lines=True)\n    headers = [\"Device\", \"Test Name\", \"Test Status\", \"Message(s)\", \"Test description\", \"Test category\"]\n    table = self._build_headers(headers=headers, table=table)\n\n    for result in result_manager.get_results():\n        # pylint: disable=R0916\n        if (host is None and testcase is None) or (host is not None and str(result.name) == host) or (testcase is not None and testcase == str(result.test)):\n            state = self._color_result(result.result)\n            message = self._split_list_to_txt_list(result.messages) if len(result.messages) > 0 else \"\"\n            categories = \", \".join(result.categories)\n            table.add_row(str(result.name), result.test, state, message, result.description, categories)\n    return table\n
"},{"location":"api/report_manager/#anta.reporter.ReportTable.report_summary_hosts","title":"report_summary_hosts","text":"
report_summary_hosts(result_manager: ResultManager, host: str | None = None, title: str = 'Summary per host') -> Table\n

Create a table report with result agregated per host.

Create table with full output: Host / Number of success / Number of failure / Number of error / List of nodes in error or failure

Args:
result_manager (ResultManager): A manager with a list of tests.\nhost (str, optional): IP Address of a host to search for. Defaults to None.\ntitle (str, optional): Title for the report. Defaults to 'All tests results'.\n

Returns:

Type Description Table: A fully populated rich Table Source code in anta/reporter/__init__.py
def report_summary_hosts(\n    self,\n    result_manager: ResultManager,\n    host: str | None = None,\n    title: str = \"Summary per host\",\n) -> Table:\n    \"\"\"Create a table report with result agregated per host.\n\n    Create table with full output: Host / Number of success / Number of failure / Number of error / List of nodes in error or failure\n\n    Args:\n    ----\n        result_manager (ResultManager): A manager with a list of tests.\n        host (str, optional): IP Address of a host to search for. Defaults to None.\n        title (str, optional): Title for the report. Defaults to 'All tests results'.\n\n    Returns\n    -------\n        Table: A fully populated rich Table\n\n    \"\"\"\n    table = Table(title=title, show_lines=True)\n    headers = [\n        \"Device\",\n        \"# of success\",\n        \"# of skipped\",\n        \"# of failure\",\n        \"# of errors\",\n        \"List of failed or error test cases\",\n    ]\n    table = self._build_headers(headers=headers, table=table)\n    for host_read in result_manager.get_hosts():\n        if host is None or str(host_read) == host:\n            results = result_manager.get_result_by_host(host_read)\n            logger.debug(\"data to use for computation\")\n            logger.debug(\"%s: %s\", host, results)\n            nb_failure = len([result for result in results if result.result == \"failure\"])\n            nb_error = len([result for result in results if result.result == \"error\"])\n            list_failure = [str(result.test) for result in results if result.result in [\"failure\", \"error\"]]\n            nb_success = len([result for result in results if result.result == \"success\"])\n            nb_skipped = len([result for result in results if result.result == \"skipped\"])\n            table.add_row(\n                str(host_read),\n                str(nb_success),\n                str(nb_skipped),\n                str(nb_failure),\n                str(nb_error),\n                str(list_failure),\n            )\n    return table\n
"},{"location":"api/report_manager/#anta.reporter.ReportTable.report_summary_tests","title":"report_summary_tests","text":"
report_summary_tests(result_manager: ResultManager, testcase: str | None = None, title: str = 'Summary per test case') -> Table\n

Create a table report with result agregated per test.

Create table with full output: Test / Number of success / Number of failure / Number of error / List of nodes in error or failure

Args:
result_manager (ResultManager): A manager with a list of tests.\ntestcase (str, optional): A test name to search for. Defaults to None.\ntitle (str, optional): Title for the report. Defaults to 'All tests results'.\n

Returns:

Type Description Table: A fully populated rich Table Source code in anta/reporter/__init__.py
def report_summary_tests(\n    self,\n    result_manager: ResultManager,\n    testcase: str | None = None,\n    title: str = \"Summary per test case\",\n) -> Table:\n    \"\"\"Create a table report with result agregated per test.\n\n    Create table with full output: Test / Number of success / Number of failure / Number of error / List of nodes in error or failure\n\n    Args:\n    ----\n        result_manager (ResultManager): A manager with a list of tests.\n        testcase (str, optional): A test name to search for. Defaults to None.\n        title (str, optional): Title for the report. Defaults to 'All tests results'.\n\n    Returns\n    -------\n        Table: A fully populated rich Table\n\n    \"\"\"\n    # sourcery skip: class-extract-method\n    table = Table(title=title, show_lines=True)\n    headers = [\n        \"Test Case\",\n        \"# of success\",\n        \"# of skipped\",\n        \"# of failure\",\n        \"# of errors\",\n        \"List of failed or error nodes\",\n    ]\n    table = self._build_headers(headers=headers, table=table)\n    for testcase_read in result_manager.get_testcases():\n        if testcase is None or str(testcase_read) == testcase:\n            results = result_manager.get_result_by_test(testcase_read)\n            nb_failure = len([result for result in results if result.result == \"failure\"])\n            nb_error = len([result for result in results if result.result == \"error\"])\n            list_failure = [str(result.name) for result in results if result.result in [\"failure\", \"error\"]]\n            nb_success = len([result for result in results if result.result == \"success\"])\n            nb_skipped = len([result for result in results if result.result == \"skipped\"])\n            table.add_row(\n                testcase_read,\n                str(nb_success),\n                str(nb_skipped),\n                str(nb_failure),\n                str(nb_error),\n                str(list_failure),\n            )\n    return table\n
"},{"location":"api/result_manager/","title":"Result Manager module","text":""},{"location":"api/result_manager/#result-manager-definition","title":"Result Manager definition","text":""},{"location":"api/result_manager/#uml-diagram","title":"UML Diagram","text":""},{"location":"api/result_manager/#anta.result_manager.ResultManager","title":"ResultManager","text":"
ResultManager()\n

Helper to manage Test Results and generate reports.

Examples
Create Inventory:\n\n    inventory_anta = AntaInventory.parse(\n        filename='examples/inventory.yml',\n        username='ansible',\n        password='ansible',\n        timeout=0.5\n    )\n\nCreate Result Manager:\n\n    manager = ResultManager()\n\nRun tests for all connected devices:\n\n    for device in inventory_anta.get_inventory():\n        manager.add_test_result(\n            VerifyNTP(device=device).test()\n        )\n        manager.add_test_result(\n            VerifyEOSVersion(device=device).test(version='4.28.3M')\n        )\n\nPrint result in native format:\n\n    manager.get_results()\n    [\n        TestResult(\n            host=IPv4Address('192.168.0.10'),\n            test='VerifyNTP',\n            result='failure',\n            message=\"device is not running NTP correctly\"\n        ),\n        TestResult(\n            host=IPv4Address('192.168.0.10'),\n            test='VerifyEOSVersion',\n            result='success',\n            message=None\n        ),\n    ]\n

The status of the class is initialized to \u201cunset\u201d

Then when adding a test with a status that is NOT \u2018error\u2019 the following table shows the updated status:

Current Status Added test Status Updated Status unset Any Any skipped unset, skipped skipped skipped success success skipped failure failure success unset, skipped, success success success failure failure failure unset, skipped success, failure failure

If the status of the added test is error, the status is untouched and the error_status is set to True.

Source code in anta/result_manager/__init__.py
def __init__(self) -> None:\n    \"\"\"Class constructor.\n\n    The status of the class is initialized to \"unset\"\n\n    Then when adding a test with a status that is NOT 'error' the following\n    table shows the updated status:\n\n    | Current Status |         Added test Status       | Updated Status |\n    | -------------- | ------------------------------- | -------------- |\n    |      unset     |              Any                |       Any      |\n    |     skipped    |         unset, skipped          |     skipped    |\n    |     skipped    |            success              |     success    |\n    |     skipped    |            failure              |     failure    |\n    |     success    |     unset, skipped, success     |     success    |\n    |     success    |            failure              |     failure    |\n    |     failure    | unset, skipped success, failure |     failure    |\n\n    If the status of the added test is error, the status is untouched and the\n    error_status is set to True.\n    \"\"\"\n    self._result_entries: list[TestResult] = []\n    # Initialize status\n    self.status: TestStatus = \"unset\"\n    self.error_status = False\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.add_test_result","title":"add_test_result","text":"
add_test_result(entry: TestResult) -> None\n

Add a result to the list.

Args:
entry (TestResult): TestResult data to add to the report\n
Source code in anta/result_manager/__init__.py
def add_test_result(self, entry: TestResult) -> None:\n    \"\"\"Add a result to the list.\n\n    Args:\n    ----\n        entry (TestResult): TestResult data to add to the report\n\n    \"\"\"\n    logger.debug(entry)\n    self._result_entries.append(entry)\n    self._update_status(entry.result)\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.add_test_results","title":"add_test_results","text":"
add_test_results(entries: list[TestResult]) -> None\n

Add a list of results to the list.

Args:
entries (list[TestResult]): List of TestResult data to add to the report\n
Source code in anta/result_manager/__init__.py
def add_test_results(self, entries: list[TestResult]) -> None:\n    \"\"\"Add a list of results to the list.\n\n    Args:\n    ----\n        entries (list[TestResult]): List of TestResult data to add to the report\n\n    \"\"\"\n    for e in entries:\n        self.add_test_result(e)\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_hosts","title":"get_hosts","text":"
get_hosts() -> list[str]\n

Get list of IP addresses in current manager.

Returns:

Type Description list[str]: List of IP addresses. Source code in anta/result_manager/__init__.py
def get_hosts(self) -> list[str]:\n    \"\"\"Get list of IP addresses in current manager.\n\n    Returns\n    -------\n        list[str]: List of IP addresses.\n\n    \"\"\"\n    result_list = []\n    for testcase in self._result_entries:\n        if str(testcase.name) not in result_list:\n            result_list.append(str(testcase.name))\n    return result_list\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_json_results","title":"get_json_results","text":"
get_json_results() -> str\n

Expose list of all test results in JSON.

Returns:

Type Description str: JSON dumps of the list of results Source code in anta/result_manager/__init__.py
def get_json_results(self) -> str:\n    \"\"\"Expose list of all test results in JSON.\n\n    Returns\n    -------\n        str: JSON dumps of the list of results\n\n    \"\"\"\n    result = [result.model_dump() for result in self._result_entries]\n    return json.dumps(result, indent=4)\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_result_by_host","title":"get_result_by_host","text":"
get_result_by_host(host_ip: str) -> list[TestResult]\n

Get list of test result for a given host.

Args:
host_ip (str): IP Address of the host to use to filter results.\n

Returns:

Type Description list[TestResult]: List of results related to the host. Source code in anta/result_manager/__init__.py
def get_result_by_host(self, host_ip: str) -> list[TestResult]:\n    \"\"\"Get list of test result for a given host.\n\n    Args:\n    ----\n        host_ip (str): IP Address of the host to use to filter results.\n\n    Returns\n    -------\n        list[TestResult]: List of results related to the host.\n\n    \"\"\"\n    return [result for result in self._result_entries if str(result.name) == host_ip]\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_result_by_test","title":"get_result_by_test","text":"
get_result_by_test(test_name: str) -> list[TestResult]\n

Get list of test result for a given test.

Args:
test_name (str): Test name to use to filter results\n

Returns:

Type Description list[TestResult]: List of results related to the test. Source code in anta/result_manager/__init__.py
def get_result_by_test(self, test_name: str) -> list[TestResult]:\n    \"\"\"Get list of test result for a given test.\n\n    Args:\n    ----\n        test_name (str): Test name to use to filter results\n\n    Returns\n    -------\n        list[TestResult]: List of results related to the test.\n\n    \"\"\"\n    return [result for result in self._result_entries if str(result.test) == test_name]\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_results","title":"get_results","text":"
get_results() -> list[TestResult]\n

Expose list of all test results in different format.

Returns:

Type Description any: List of results. Source code in anta/result_manager/__init__.py
def get_results(self) -> list[TestResult]:\n    \"\"\"Expose list of all test results in different format.\n\n    Returns\n    -------\n        any: List of results.\n\n    \"\"\"\n    return self._result_entries\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_status","title":"get_status","text":"
get_status(*, ignore_error: bool = False) -> str\n

Return the current status including error_status if ignore_error is False.

Source code in anta/result_manager/__init__.py
def get_status(self, *, ignore_error: bool = False) -> str:\n    \"\"\"Return the current status including error_status if ignore_error is False.\"\"\"\n    return \"error\" if self.error_status and not ignore_error else self.status\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_testcases","title":"get_testcases","text":"
get_testcases() -> list[str]\n

Get list of name of all test cases in current manager.

Returns:

Type Description list[str]: List of names for all tests. Source code in anta/result_manager/__init__.py
def get_testcases(self) -> list[str]:\n    \"\"\"Get list of name of all test cases in current manager.\n\n    Returns\n    -------\n        list[str]: List of names for all tests.\n\n    \"\"\"\n    result_list = []\n    for testcase in self._result_entries:\n        if str(testcase.test) not in result_list:\n            result_list.append(str(testcase.test))\n    return result_list\n
"},{"location":"api/result_manager_models/","title":"Result Manager models","text":""},{"location":"api/result_manager_models/#test-result-model","title":"Test Result model","text":""},{"location":"api/result_manager_models/#uml-diagram","title":"UML Diagram","text":""},{"location":"api/result_manager_models/#anta.result_manager.models.TestResult","title":"TestResult","text":"

Bases: BaseModel

Describe the result of a test from a single device.

Attributes:

Name Type Description name Device name where the test has run.

test: Test name runs on the device. categories: List of categories the TestResult belongs to, by default the AntaTest categories. description: TestResult description, by default the AntaTest description. result: Result of the test. Can be one of \u201cunset\u201d, \u201csuccess\u201d, \u201cfailure\u201d, \u201cerror\u201d or \u201cskipped\u201d. messages: Message to report after the test if any. custom_field: Custom field to store a string for flexibility in integrating with ANTA

"},{"location":"api/result_manager_models/#anta.result_manager.models.TestResult.is_error","title":"is_error","text":"
is_error(message: str | None = None) -> None\n

Set status to error.

Args:
message: Optional message related to the test\n
Source code in anta/result_manager/models.py
def is_error(self, message: str | None = None) -> None:\n    \"\"\"Set status to error.\n\n    Args:\n    ----\n        message: Optional message related to the test\n\n    \"\"\"\n    self._set_status(\"error\", message)\n
"},{"location":"api/result_manager_models/#anta.result_manager.models.TestResult.is_failure","title":"is_failure","text":"
is_failure(message: str | None = None) -> None\n

Set status to failure.

Args:
message: Optional message related to the test\n
Source code in anta/result_manager/models.py
def is_failure(self, message: str | None = None) -> None:\n    \"\"\"Set status to failure.\n\n    Args:\n    ----\n        message: Optional message related to the test\n\n    \"\"\"\n    self._set_status(\"failure\", message)\n
"},{"location":"api/result_manager_models/#anta.result_manager.models.TestResult.is_skipped","title":"is_skipped","text":"
is_skipped(message: str | None = None) -> None\n

Set status to skipped.

Args:
message: Optional message related to the test\n
Source code in anta/result_manager/models.py
def is_skipped(self, message: str | None = None) -> None:\n    \"\"\"Set status to skipped.\n\n    Args:\n    ----\n        message: Optional message related to the test\n\n    \"\"\"\n    self._set_status(\"skipped\", message)\n
"},{"location":"api/result_manager_models/#anta.result_manager.models.TestResult.is_success","title":"is_success","text":"
is_success(message: str | None = None) -> None\n

Set status to success.

Args:
message: Optional message related to the test\n
Source code in anta/result_manager/models.py
def is_success(self, message: str | None = None) -> None:\n    \"\"\"Set status to success.\n\n    Args:\n    ----\n        message: Optional message related to the test\n\n    \"\"\"\n    self._set_status(\"success\", message)\n
"},{"location":"api/tests.aaa/","title":"AAA","text":""},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAcctConsoleMethods","title":"VerifyAcctConsoleMethods","text":"

Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x).

Expected Results
  • Success: The test will pass if the provided AAA accounting console method list is matching in the configured accounting types.
  • Failure: The test will fail if the provided AAA accounting console method list is NOT matching in the configured accounting types.
Examples
anta.tests.aaa:\n  - VerifyAcctConsoleMethods:\n      methods:\n        - local\n        - none\n        - logging\n      types:\n        - system\n        - exec\n        - commands\n        - dot1x\n
Source code in anta/tests/aaa.py
class VerifyAcctConsoleMethods(AntaTest):\n    \"\"\"Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided AAA accounting console method list is matching in the configured accounting types.\n    * Failure: The test will fail if the provided AAA accounting console method list is NOT matching in the configured accounting types.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyAcctConsoleMethods:\n          methods:\n            - local\n            - none\n            - logging\n          types:\n            - system\n            - exec\n            - commands\n            - dot1x\n    ```\n    \"\"\"\n\n    name = \"VerifyAcctConsoleMethods\"\n    description = \"Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x).\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyAcctConsoleMethods test.\"\"\"\n\n        methods: list[AAAAuthMethod]\n        \"\"\"List of AAA accounting console methods. Methods should be in the right order.\"\"\"\n        types: set[Literal[\"commands\", \"exec\", \"system\", \"dot1x\"]]\n        \"\"\"List of accounting console types to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAcctConsoleMethods.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        not_matching = []\n        not_configured = []\n        for k, v in command_output.items():\n            acct_type = k.replace(\"AcctMethods\", \"\")\n            if acct_type not in self.inputs.types:\n                # We do not need to verify this accounting type\n                continue\n            for methods in v.values():\n                if \"consoleAction\" not in methods:\n                    not_configured.append(acct_type)\n                if methods[\"consoleMethods\"] != self.inputs.methods:\n                    not_matching.append(acct_type)\n        if not_configured:\n            self.result.is_failure(f\"AAA console accounting is not configured for {not_configured}\")\n            return\n        if not not_matching:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"AAA accounting console methods {self.inputs.methods} are not matching for {not_matching}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAcctConsoleMethods-attributes","title":"Inputs","text":"Name Type Description Default methods list[AAAAuthMethod] List of AAA accounting console methods. Methods should be in the right order. - types set[Literal['commands', 'exec', 'system', 'dot1x']] List of accounting console types to verify. -"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAcctDefaultMethods","title":"VerifyAcctDefaultMethods","text":"

Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x).

Expected Results
  • Success: The test will pass if the provided AAA accounting default method list is matching in the configured accounting types.
  • Failure: The test will fail if the provided AAA accounting default method list is NOT matching in the configured accounting types.
Examples
anta.tests.aaa:\n  - VerifyAcctDefaultMethods:\n      methods:\n        - local\n        - none\n        - logging\n      types:\n        - system\n        - exec\n        - commands\n        - dot1x\n
Source code in anta/tests/aaa.py
class VerifyAcctDefaultMethods(AntaTest):\n    \"\"\"Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided AAA accounting default method list is matching in the configured accounting types.\n    * Failure: The test will fail if the provided AAA accounting default method list is NOT matching in the configured accounting types.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyAcctDefaultMethods:\n          methods:\n            - local\n            - none\n            - logging\n          types:\n            - system\n            - exec\n            - commands\n            - dot1x\n    ```\n    \"\"\"\n\n    name = \"VerifyAcctDefaultMethods\"\n    description = \"Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x).\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyAcctDefaultMethods test.\"\"\"\n\n        methods: list[AAAAuthMethod]\n        \"\"\"List of AAA accounting methods. Methods should be in the right order.\"\"\"\n        types: set[Literal[\"commands\", \"exec\", \"system\", \"dot1x\"]]\n        \"\"\"List of accounting types to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAcctDefaultMethods.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        not_matching = []\n        not_configured = []\n        for k, v in command_output.items():\n            acct_type = k.replace(\"AcctMethods\", \"\")\n            if acct_type not in self.inputs.types:\n                # We do not need to verify this accounting type\n                continue\n            for methods in v.values():\n                if \"defaultAction\" not in methods:\n                    not_configured.append(acct_type)\n                if methods[\"defaultMethods\"] != self.inputs.methods:\n                    not_matching.append(acct_type)\n        if not_configured:\n            self.result.is_failure(f\"AAA default accounting is not configured for {not_configured}\")\n            return\n        if not not_matching:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"AAA accounting default methods {self.inputs.methods} are not matching for {not_matching}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAcctDefaultMethods-attributes","title":"Inputs","text":"Name Type Description Default methods list[AAAAuthMethod] List of AAA accounting methods. Methods should be in the right order. - types set[Literal['commands', 'exec', 'system', 'dot1x']] List of accounting types to verify. -"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAuthenMethods","title":"VerifyAuthenMethods","text":"

Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x).

Expected Results
  • Success: The test will pass if the provided AAA authentication method list is matching in the configured authentication types.
  • Failure: The test will fail if the provided AAA authentication method list is NOT matching in the configured authentication types.
Examples
anta.tests.aaa:\n  - VerifyAuthenMethods:\n    methods:\n      - local\n      - none\n      - logging\n    types:\n      - login\n      - enable\n      - dot1x\n
Source code in anta/tests/aaa.py
class VerifyAuthenMethods(AntaTest):\n    \"\"\"Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided AAA authentication method list is matching in the configured authentication types.\n    * Failure: The test will fail if the provided AAA authentication method list is NOT matching in the configured authentication types.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyAuthenMethods:\n        methods:\n          - local\n          - none\n          - logging\n        types:\n          - login\n          - enable\n          - dot1x\n    ```\n    \"\"\"\n\n    name = \"VerifyAuthenMethods\"\n    description = \"Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x).\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authentication\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyAuthenMethods test.\"\"\"\n\n        methods: list[AAAAuthMethod]\n        \"\"\"List of AAA authentication methods. Methods should be in the right order.\"\"\"\n        types: set[Literal[\"login\", \"enable\", \"dot1x\"]]\n        \"\"\"List of authentication types to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAuthenMethods.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        not_matching: list[str] = []\n        for k, v in command_output.items():\n            auth_type = k.replace(\"AuthenMethods\", \"\")\n            if auth_type not in self.inputs.types:\n                # We do not need to verify this accounting type\n                continue\n            if auth_type == \"login\":\n                if \"login\" not in v:\n                    self.result.is_failure(\"AAA authentication methods are not configured for login console\")\n                    return\n                if v[\"login\"][\"methods\"] != self.inputs.methods:\n                    self.result.is_failure(f\"AAA authentication methods {self.inputs.methods} are not matching for login console\")\n                    return\n            not_matching.extend(auth_type for methods in v.values() if methods[\"methods\"] != self.inputs.methods)\n\n        if not not_matching:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"AAA authentication methods {self.inputs.methods} are not matching for {not_matching}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAuthenMethods-attributes","title":"Inputs","text":"Name Type Description Default methods list[AAAAuthMethod] List of AAA authentication methods. Methods should be in the right order. - types set[Literal['login', 'enable', 'dot1x']] List of authentication types to verify. -"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAuthzMethods","title":"VerifyAuthzMethods","text":"

Verifies the AAA authorization method lists for different authorization types (commands, exec).

Expected Results
  • Success: The test will pass if the provided AAA authorization method list is matching in the configured authorization types.
  • Failure: The test will fail if the provided AAA authorization method list is NOT matching in the configured authorization types.
Examples
anta.tests.aaa:\n  - VerifyAuthzMethods:\n      methods:\n        - local\n        - none\n        - logging\n      types:\n        - commands\n        - exec\n
Source code in anta/tests/aaa.py
class VerifyAuthzMethods(AntaTest):\n    \"\"\"Verifies the AAA authorization method lists for different authorization types (commands, exec).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided AAA authorization method list is matching in the configured authorization types.\n    * Failure: The test will fail if the provided AAA authorization method list is NOT matching in the configured authorization types.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyAuthzMethods:\n          methods:\n            - local\n            - none\n            - logging\n          types:\n            - commands\n            - exec\n    ```\n    \"\"\"\n\n    name = \"VerifyAuthzMethods\"\n    description = \"Verifies the AAA authorization method lists for different authorization types (commands, exec).\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authorization\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyAuthzMethods test.\"\"\"\n\n        methods: list[AAAAuthMethod]\n        \"\"\"List of AAA authorization methods. Methods should be in the right order.\"\"\"\n        types: set[Literal[\"commands\", \"exec\"]]\n        \"\"\"List of authorization types to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAuthzMethods.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        not_matching: list[str] = []\n        for k, v in command_output.items():\n            authz_type = k.replace(\"AuthzMethods\", \"\")\n            if authz_type not in self.inputs.types:\n                # We do not need to verify this accounting type\n                continue\n            not_matching.extend(authz_type for methods in v.values() if methods[\"methods\"] != self.inputs.methods)\n\n        if not not_matching:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"AAA authorization methods {self.inputs.methods} are not matching for {not_matching}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAuthzMethods-attributes","title":"Inputs","text":"Name Type Description Default methods list[AAAAuthMethod] List of AAA authorization methods. Methods should be in the right order. - types set[Literal['commands', 'exec']] List of authorization types to verify. -"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsServerGroups","title":"VerifyTacacsServerGroups","text":"

Verifies if the provided TACACS server group(s) are configured.

Expected Results
  • Success: The test will pass if the provided TACACS server group(s) are configured.
  • Failure: The test will fail if one or all the provided TACACS server group(s) are NOT configured.
Examples
anta.tests.aaa:\n  - VerifyTacacsServerGroups:\n      groups:\n        - TACACS-GROUP1\n        - TACACS-GROUP2\n
Source code in anta/tests/aaa.py
class VerifyTacacsServerGroups(AntaTest):\n    \"\"\"Verifies if the provided TACACS server group(s) are configured.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided TACACS server group(s) are configured.\n    * Failure: The test will fail if one or all the provided TACACS server group(s) are NOT configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyTacacsServerGroups:\n          groups:\n            - TACACS-GROUP1\n            - TACACS-GROUP2\n    ```\n    \"\"\"\n\n    name = \"VerifyTacacsServerGroups\"\n    description = \"Verifies if the provided TACACS server group(s) are configured.\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTacacsServerGroups test.\"\"\"\n\n        groups: list[str]\n        \"\"\"List of TACACS server groups.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTacacsServerGroups.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        tacacs_groups = command_output[\"groups\"]\n        if not tacacs_groups:\n            self.result.is_failure(\"No TACACS server group(s) are configured\")\n            return\n        not_configured = [group for group in self.inputs.groups if group not in tacacs_groups]\n        if not not_configured:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"TACACS server group(s) {not_configured} are not configured\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsServerGroups-attributes","title":"Inputs","text":"Name Type Description Default groups list[str] List of TACACS server groups. -"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsServers","title":"VerifyTacacsServers","text":"

Verifies TACACS servers are configured for a specified VRF.

Expected Results
  • Success: The test will pass if the provided TACACS servers are configured in the specified VRF.
  • Failure: The test will fail if the provided TACACS servers are NOT configured in the specified VRF.
Examples
anta.tests.aaa:\n  - VerifyTacacsServers:\n      servers:\n        - 10.10.10.21\n        - 10.10.10.22\n      vrf: MGMT\n
Source code in anta/tests/aaa.py
class VerifyTacacsServers(AntaTest):\n    \"\"\"Verifies TACACS servers are configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided TACACS servers are configured in the specified VRF.\n    * Failure: The test will fail if the provided TACACS servers are NOT configured in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyTacacsServers:\n          servers:\n            - 10.10.10.21\n            - 10.10.10.22\n          vrf: MGMT\n    ```\n    \"\"\"\n\n    name = \"VerifyTacacsServers\"\n    description = \"Verifies TACACS servers are configured for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTacacsServers test.\"\"\"\n\n        servers: list[IPv4Address]\n        \"\"\"List of TACACS servers.\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF to transport TACACS messages. Defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTacacsServers.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        tacacs_servers = command_output[\"tacacsServers\"]\n        if not tacacs_servers:\n            self.result.is_failure(\"No TACACS servers are configured\")\n            return\n        not_configured = [\n            str(server)\n            for server in self.inputs.servers\n            if not any(\n                str(server) == tacacs_server[\"serverInfo\"][\"hostname\"] and self.inputs.vrf == tacacs_server[\"serverInfo\"][\"vrf\"] for tacacs_server in tacacs_servers\n            )\n        ]\n        if not not_configured:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"TACACS servers {not_configured} are not configured in VRF {self.inputs.vrf}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsServers-attributes","title":"Inputs","text":"Name Type Description Default servers list[IPv4Address] List of TACACS servers. - vrf str The name of the VRF to transport TACACS messages. Defaults to `default`. 'default'"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsSourceIntf","title":"VerifyTacacsSourceIntf","text":"

Verifies TACACS source-interface for a specified VRF.

Expected Results
  • Success: The test will pass if the provided TACACS source-interface is configured in the specified VRF.
  • Failure: The test will fail if the provided TACACS source-interface is NOT configured in the specified VRF.
Examples
anta.tests.aaa:\n  - VerifyTacacsSourceIntf:\n      intf: Management0\n      vrf: MGMT\n
Source code in anta/tests/aaa.py
class VerifyTacacsSourceIntf(AntaTest):\n    \"\"\"Verifies TACACS source-interface for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided TACACS source-interface is configured in the specified VRF.\n    * Failure: The test will fail if the provided TACACS source-interface is NOT configured in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyTacacsSourceIntf:\n          intf: Management0\n          vrf: MGMT\n    ```\n    \"\"\"\n\n    name = \"VerifyTacacsSourceIntf\"\n    description = \"Verifies TACACS source-interface for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTacacsSourceIntf test.\"\"\"\n\n        intf: str\n        \"\"\"Source-interface to use as source IP of TACACS messages.\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF to transport TACACS messages. Defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTacacsSourceIntf.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        try:\n            if command_output[\"srcIntf\"][self.inputs.vrf] == self.inputs.intf:\n                self.result.is_success()\n            else:\n                self.result.is_failure(f\"Wrong source-interface configured in VRF {self.inputs.vrf}\")\n        except KeyError:\n            self.result.is_failure(f\"Source-interface {self.inputs.intf} is not configured in VRF {self.inputs.vrf}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsSourceIntf-attributes","title":"Inputs","text":"Name Type Description Default intf str Source-interface to use as source IP of TACACS messages. - vrf str The name of the VRF to transport TACACS messages. Defaults to `default`. 'default'"},{"location":"api/tests.bfd/","title":"BFD","text":""},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersHealth","title":"VerifyBFDPeersHealth","text":"

Verifies the health of IPv4 BFD peers across all VRFs.

It checks that no BFD peer is in the down state and that the discriminator value of the remote system is not zero.

Optionally, it can also verify that BFD peers have not been down before a specified threshold of hours.

Expected Results
  • Success: The test will pass if all IPv4 BFD peers are up, the discriminator value of each remote system is non-zero, and the last downtime of each peer is above the defined threshold.
  • Failure: The test will fail if any IPv4 BFD peer is down, the discriminator value of any remote system is zero, or the last downtime of any peer is below the defined threshold.
Examples
anta.tests.bfd:\n  - VerifyBFDPeersHealth:\n      down_threshold: 2\n
Source code in anta/tests/bfd.py
class VerifyBFDPeersHealth(AntaTest):\n    \"\"\"Verifies the health of IPv4 BFD peers across all VRFs.\n\n    It checks that no BFD peer is in the down state and that the discriminator value of the remote system is not zero.\n\n    Optionally, it can also verify that BFD peers have not been down before a specified threshold of hours.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all IPv4 BFD peers are up, the discriminator value of each remote system is non-zero,\n               and the last downtime of each peer is above the defined threshold.\n    * Failure: The test will fail if any IPv4 BFD peer is down, the discriminator value of any remote system is zero,\n               or the last downtime of any peer is below the defined threshold.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.bfd:\n      - VerifyBFDPeersHealth:\n          down_threshold: 2\n    ```\n    \"\"\"\n\n    name = \"VerifyBFDPeersHealth\"\n    description = \"Verifies the health of all IPv4 BFD peers.\"\n    categories: ClassVar[list[str]] = [\"bfd\"]\n    # revision 1 as later revision introduces additional nesting for type\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaCommand(command=\"show bfd peers\", revision=1),\n        AntaCommand(command=\"show clock\"),\n    ]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBFDPeersHealth test.\"\"\"\n\n        down_threshold: int | None = Field(default=None, gt=0)\n        \"\"\"Optional down threshold in hours to check if a BFD peer was down before those hours or not.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBFDPeersHealth.\"\"\"\n        # Initialize failure strings\n        down_failures = []\n        up_failures = []\n\n        # Extract the current timestamp and command output\n        clock_output = self.instance_commands[1].json_output\n        current_timestamp = clock_output[\"utcTime\"]\n        bfd_output = self.instance_commands[0].json_output\n\n        # set the initial result\n        self.result.is_success()\n\n        # Check if any IPv4 BFD peer is configured\n        ipv4_neighbors_exist = any(vrf_data[\"ipv4Neighbors\"] for vrf_data in bfd_output[\"vrfs\"].values())\n        if not ipv4_neighbors_exist:\n            self.result.is_failure(\"No IPv4 BFD peers are configured for any VRF.\")\n            return\n\n        # Iterate over IPv4 BFD peers\n        for vrf, vrf_data in bfd_output[\"vrfs\"].items():\n            for peer, neighbor_data in vrf_data[\"ipv4Neighbors\"].items():\n                for peer_data in neighbor_data[\"peerStats\"].values():\n                    peer_status = peer_data[\"status\"]\n                    remote_disc = peer_data[\"remoteDisc\"]\n                    remote_disc_info = f\" with remote disc {remote_disc}\" if remote_disc == 0 else \"\"\n                    last_down = peer_data[\"lastDown\"]\n                    hours_difference = (\n                        datetime.fromtimestamp(current_timestamp, tz=timezone.utc) - datetime.fromtimestamp(last_down, tz=timezone.utc)\n                    ).total_seconds() / 3600\n\n                    # Check if peer status is not up\n                    if peer_status != \"up\":\n                        down_failures.append(f\"{peer} is {peer_status} in {vrf} VRF{remote_disc_info}.\")\n\n                    # Check if the last down is within the threshold\n                    elif self.inputs.down_threshold and hours_difference < self.inputs.down_threshold:\n                        up_failures.append(f\"{peer} in {vrf} VRF was down {round(hours_difference)} hours ago{remote_disc_info}.\")\n\n                    # Check if remote disc is 0\n                    elif remote_disc == 0:\n                        up_failures.append(f\"{peer} in {vrf} VRF has remote disc {remote_disc}.\")\n\n        # Check if there are any failures\n        if down_failures:\n            down_failures_str = \"\\n\".join(down_failures)\n            self.result.is_failure(f\"Following BFD peers are not up:\\n{down_failures_str}\")\n        if up_failures:\n            up_failures_str = \"\\n\".join(up_failures)\n            self.result.is_failure(f\"\\nFollowing BFD peers were down:\\n{up_failures_str}\")\n
"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersHealth-attributes","title":"Inputs","text":"Name Type Description Default down_threshold int | None Optional down threshold in hours to check if a BFD peer was down before those hours or not. Field(default=None, gt=0)"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersIntervals","title":"VerifyBFDPeersIntervals","text":"

Verifies the timers of the IPv4 BFD peers in the specified VRF.

Expected Results
  • Success: The test will pass if the timers of the IPv4 BFD peers are correct in the specified VRF.
  • Failure: The test will fail if the IPv4 BFD peers are not found or their timers are incorrect in the specified VRF.
Examples
anta.tests.bfd:\n  - VerifyBFDPeersIntervals:\n      bfd_peers:\n        - peer_address: 192.0.255.8\n          vrf: default\n          tx_interval: 1200\n          rx_interval: 1200\n          multiplier: 3\n        - peer_address: 192.0.255.7\n          vrf: default\n          tx_interval: 1200\n          rx_interval: 1200\n          multiplier: 3\n
Source code in anta/tests/bfd.py
class VerifyBFDPeersIntervals(AntaTest):\n    \"\"\"Verifies the timers of the IPv4 BFD peers in the specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the timers of the IPv4 BFD peers are correct in the specified VRF.\n    * Failure: The test will fail if the IPv4 BFD peers are not found or their timers are incorrect in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.bfd:\n      - VerifyBFDPeersIntervals:\n          bfd_peers:\n            - peer_address: 192.0.255.8\n              vrf: default\n              tx_interval: 1200\n              rx_interval: 1200\n              multiplier: 3\n            - peer_address: 192.0.255.7\n              vrf: default\n              tx_interval: 1200\n              rx_interval: 1200\n              multiplier: 3\n    ```\n    \"\"\"\n\n    name = \"VerifyBFDPeersIntervals\"\n    description = \"Verifies the timers of the IPv4 BFD peers in the specified VRF.\"\n    categories: ClassVar[list[str]] = [\"bfd\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBFDPeersIntervals test.\"\"\"\n\n        bfd_peers: list[BFDPeer]\n        \"\"\"List of BFD peers.\"\"\"\n\n        class BFDPeer(BaseModel):\n            \"\"\"Model for an IPv4 BFD peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BFD peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BFD peer. If not provided, it defaults to `default`.\"\"\"\n            tx_interval: BfdInterval\n            \"\"\"Tx interval of BFD peer in milliseconds.\"\"\"\n            rx_interval: BfdInterval\n            \"\"\"Rx interval of BFD peer in milliseconds.\"\"\"\n            multiplier: BfdMultiplier\n            \"\"\"Multiplier of BFD peer.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBFDPeersIntervals.\"\"\"\n        failures: dict[Any, Any] = {}\n\n        # Iterating over BFD peers\n        for bfd_peers in self.inputs.bfd_peers:\n            peer = str(bfd_peers.peer_address)\n            vrf = bfd_peers.vrf\n\n            # Converting milliseconds intervals into actual value\n            tx_interval = bfd_peers.tx_interval * 1000\n            rx_interval = bfd_peers.rx_interval * 1000\n            multiplier = bfd_peers.multiplier\n            bfd_output = get_value(\n                self.instance_commands[0].json_output,\n                f\"vrfs..{vrf}..ipv4Neighbors..{peer}..peerStats..\",\n                separator=\"..\",\n            )\n\n            # Check if BFD peer configured\n            if not bfd_output:\n                failures[peer] = {vrf: \"Not Configured\"}\n                continue\n\n            bfd_details = bfd_output.get(\"peerStatsDetail\", {})\n            intervals_ok = (\n                bfd_details.get(\"operTxInterval\") == tx_interval and bfd_details.get(\"operRxInterval\") == rx_interval and bfd_details.get(\"detectMult\") == multiplier\n            )\n\n            # Check timers of BFD peer\n            if not intervals_ok:\n                failures[peer] = {\n                    vrf: {\n                        \"tx_interval\": bfd_details.get(\"operTxInterval\"),\n                        \"rx_interval\": bfd_details.get(\"operRxInterval\"),\n                        \"multiplier\": bfd_details.get(\"detectMult\"),\n                    }\n                }\n\n        # Check if any failures\n        if not failures:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BFD peers are not configured or timers are not correct:\\n{failures}\")\n
"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersIntervals-attributes","title":"Inputs","text":"Name Type Description Default bfd_peers list[BFDPeer] List of BFD peers. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersIntervals-attributes","title":"BFDPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BFD peer. - vrf str Optional VRF for BFD peer. If not provided, it defaults to `default`. 'default' tx_interval BfdInterval Tx interval of BFD peer in milliseconds. - rx_interval BfdInterval Rx interval of BFD peer in milliseconds. - multiplier BfdMultiplier Multiplier of BFD peer. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDSpecificPeers","title":"VerifyBFDSpecificPeers","text":"

Verifies if the IPv4 BFD peer\u2019s sessions are UP and remote disc is non-zero in the specified VRF.

Expected Results
  • Success: The test will pass if IPv4 BFD peers are up and remote disc is non-zero in the specified VRF.
  • Failure: The test will fail if IPv4 BFD peers are not found, the status is not UP or remote disc is zero in the specified VRF.
Examples
anta.tests.bfd:\n  - VerifyBFDSpecificPeers:\n      bfd_peers:\n        - peer_address: 192.0.255.8\n          vrf: default\n        - peer_address: 192.0.255.7\n          vrf: default\n
Source code in anta/tests/bfd.py
class VerifyBFDSpecificPeers(AntaTest):\n    \"\"\"Verifies if the IPv4 BFD peer's sessions are UP and remote disc is non-zero in the specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if IPv4 BFD peers are up and remote disc is non-zero in the specified VRF.\n    * Failure: The test will fail if IPv4 BFD peers are not found, the status is not UP or remote disc is zero in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.bfd:\n      - VerifyBFDSpecificPeers:\n          bfd_peers:\n            - peer_address: 192.0.255.8\n              vrf: default\n            - peer_address: 192.0.255.7\n              vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyBFDSpecificPeers\"\n    description = \"Verifies the IPv4 BFD peer's sessions and remote disc in the specified VRF.\"\n    categories: ClassVar[list[str]] = [\"bfd\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBFDSpecificPeers test.\"\"\"\n\n        bfd_peers: list[BFDPeer]\n        \"\"\"List of IPv4 BFD peers.\"\"\"\n\n        class BFDPeer(BaseModel):\n            \"\"\"Model for an IPv4 BFD peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BFD peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BFD peer. If not provided, it defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBFDSpecificPeers.\"\"\"\n        failures: dict[Any, Any] = {}\n\n        # Iterating over BFD peers\n        for bfd_peer in self.inputs.bfd_peers:\n            peer = str(bfd_peer.peer_address)\n            vrf = bfd_peer.vrf\n            bfd_output = get_value(\n                self.instance_commands[0].json_output,\n                f\"vrfs..{vrf}..ipv4Neighbors..{peer}..peerStats..\",\n                separator=\"..\",\n            )\n\n            # Check if BFD peer configured\n            if not bfd_output:\n                failures[peer] = {vrf: \"Not Configured\"}\n                continue\n\n            # Check BFD peer status and remote disc\n            if not (bfd_output.get(\"status\") == \"up\" and bfd_output.get(\"remoteDisc\") != 0):\n                failures[peer] = {\n                    vrf: {\n                        \"status\": bfd_output.get(\"status\"),\n                        \"remote_disc\": bfd_output.get(\"remoteDisc\"),\n                    }\n                }\n\n        if not failures:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BFD peers are not configured, status is not up or remote disc is zero:\\n{failures}\")\n
"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDSpecificPeers-attributes","title":"Inputs","text":"Name Type Description Default bfd_peers list[BFDPeer] List of IPv4 BFD peers. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDSpecificPeers-attributes","title":"BFDPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BFD peer. - vrf str Optional VRF for BFD peer. If not provided, it defaults to `default`. 'default'"},{"location":"api/tests.configuration/","title":"Configuration","text":""},{"location":"api/tests.configuration/#anta.tests.configuration.VerifyRunningConfigDiffs","title":"VerifyRunningConfigDiffs","text":"

Verifies there is no difference between the running-config and the startup-config.

Expected Results
  • Success: The test will pass if there is no difference between the running-config and the startup-config.
  • Failure: The test will fail if there is a difference between the running-config and the startup-config.
Examples
anta.tests.configuration:\n  - VerifyRunningConfigDiffs:\n
Source code in anta/tests/configuration.py
class VerifyRunningConfigDiffs(AntaTest):\n    \"\"\"Verifies there is no difference between the running-config and the startup-config.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there is no difference between the running-config and the startup-config.\n    * Failure: The test will fail if there is a difference between the running-config and the startup-config.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.configuration:\n      - VerifyRunningConfigDiffs:\n    ```\n    \"\"\"\n\n    name = \"VerifyRunningConfigDiffs\"\n    description = \"Verifies there is no difference between the running-config and the startup-config\"\n    categories: ClassVar[list[str]] = [\"configuration\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show running-config diffs\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyRunningConfigDiffs.\"\"\"\n        command_output = self.instance_commands[0].text_output\n        if command_output == \"\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(command_output)\n
"},{"location":"api/tests.configuration/#anta.tests.configuration.VerifyZeroTouch","title":"VerifyZeroTouch","text":"

Verifies ZeroTouch is disabled.

Expected Results
  • Success: The test will pass if ZeroTouch is disabled.
  • Failure: The test will fail if ZeroTouch is enabled.
Examples
anta.tests.configuration:\n  - VerifyZeroTouch:\n
Source code in anta/tests/configuration.py
class VerifyZeroTouch(AntaTest):\n    \"\"\"Verifies ZeroTouch is disabled.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if ZeroTouch is disabled.\n    * Failure: The test will fail if ZeroTouch is enabled.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.configuration:\n      - VerifyZeroTouch:\n    ```\n    \"\"\"\n\n    name = \"VerifyZeroTouch\"\n    description = \"Verifies ZeroTouch is disabled\"\n    categories: ClassVar[list[str]] = [\"configuration\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show zerotouch\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyZeroTouch.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"mode\"] == \"disabled\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"ZTP is NOT disabled\")\n
"},{"location":"api/tests.connectivity/","title":"Connectivity","text":""},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors","title":"VerifyLLDPNeighbors","text":"

Verifies that the provided LLDP neighbors are present and connected with the correct configuration.

Expected Results
  • Success: The test will pass if each of the provided LLDP neighbors is present and connected to the specified port and device.
  • Failure: The test will fail if any of the following conditions are met:
    • The provided LLDP neighbor is not found.
    • The system name or port of the LLDP neighbor does not match the provided information.
Examples
anta.tests.connectivity:\n  - VerifyLLDPNeighbors:\n      neighbors:\n        - port: Ethernet1\n          neighbor_device: DC1-SPINE1\n          neighbor_port: Ethernet1\n        - port: Ethernet2\n          neighbor_device: DC1-SPINE2\n          neighbor_port: Ethernet1\n
Source code in anta/tests/connectivity.py
class VerifyLLDPNeighbors(AntaTest):\n    \"\"\"Verifies that the provided LLDP neighbors are present and connected with the correct configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if each of the provided LLDP neighbors is present and connected to the specified port and device.\n    * Failure: The test will fail if any of the following conditions are met:\n        - The provided LLDP neighbor is not found.\n        - The system name or port of the LLDP neighbor does not match the provided information.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.connectivity:\n      - VerifyLLDPNeighbors:\n          neighbors:\n            - port: Ethernet1\n              neighbor_device: DC1-SPINE1\n              neighbor_port: Ethernet1\n            - port: Ethernet2\n              neighbor_device: DC1-SPINE2\n              neighbor_port: Ethernet1\n    ```\n    \"\"\"\n\n    name = \"VerifyLLDPNeighbors\"\n    description = \"Verifies that the provided LLDP neighbors are connected properly.\"\n    categories: ClassVar[list[str]] = [\"connectivity\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show lldp neighbors detail\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyLLDPNeighbors test.\"\"\"\n\n        neighbors: list[Neighbor]\n        \"\"\"List of LLDP neighbors.\"\"\"\n\n        class Neighbor(BaseModel):\n            \"\"\"Model for an LLDP neighbor.\"\"\"\n\n            port: Interface\n            \"\"\"LLDP port.\"\"\"\n            neighbor_device: str\n            \"\"\"LLDP neighbor device.\"\"\"\n            neighbor_port: Interface\n            \"\"\"LLDP neighbor port.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLLDPNeighbors.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        failures: dict[str, list[str]] = {}\n\n        for neighbor in self.inputs.neighbors:\n            if neighbor.port not in command_output[\"lldpNeighbors\"]:\n                failures.setdefault(\"port_not_configured\", []).append(neighbor.port)\n            elif len(lldp_neighbor_info := command_output[\"lldpNeighbors\"][neighbor.port][\"lldpNeighborInfo\"]) == 0:\n                failures.setdefault(\"no_lldp_neighbor\", []).append(neighbor.port)\n            elif (\n                lldp_neighbor_info[0][\"systemName\"] != neighbor.neighbor_device\n                or lldp_neighbor_info[0][\"neighborInterfaceInfo\"][\"interfaceId_v2\"] != neighbor.neighbor_port\n            ):\n                failures.setdefault(\"wrong_lldp_neighbor\", []).append(neighbor.port)\n\n        if not failures:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following port(s) have issues: {failures}\")\n
"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors-attributes","title":"Inputs","text":"Name Type Description Default neighbors list[Neighbor] List of LLDP neighbors. -"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors-attributes","title":"Neighbor","text":"Name Type Description Default port Interface LLDP port. - neighbor_device str LLDP neighbor device. - neighbor_port Interface LLDP neighbor port. -"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyReachability","title":"VerifyReachability","text":"

Test network reachability to one or many destination IP(s).

Expected Results
  • Success: The test will pass if all destination IP(s) are reachable.
  • Failure: The test will fail if one or many destination IP(s) are unreachable.
Examples
anta.tests.connectivity:\n  - VerifyReachability:\n      hosts:\n        - source: Management0\n          destination: 1.1.1.1\n          vrf: MGMT\n        - source: Management0\n          destination: 8.8.8.8\n          vrf: MGMT\n
Source code in anta/tests/connectivity.py
class VerifyReachability(AntaTest):\n    \"\"\"Test network reachability to one or many destination IP(s).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all destination IP(s) are reachable.\n    * Failure: The test will fail if one or many destination IP(s) are unreachable.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.connectivity:\n      - VerifyReachability:\n          hosts:\n            - source: Management0\n              destination: 1.1.1.1\n              vrf: MGMT\n            - source: Management0\n              destination: 8.8.8.8\n              vrf: MGMT\n    ```\n    \"\"\"\n\n    name = \"VerifyReachability\"\n    description = \"Test the network reachability to one or many destination IP(s).\"\n    categories: ClassVar[list[str]] = [\"connectivity\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"ping vrf {vrf} {destination} source {source} repeat {repeat}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyReachability test.\"\"\"\n\n        hosts: list[Host]\n        \"\"\"List of host to ping.\"\"\"\n\n        class Host(BaseModel):\n            \"\"\"Model for a remote host to ping.\"\"\"\n\n            destination: IPv4Address\n            \"\"\"IPv4 address to ping.\"\"\"\n            source: IPv4Address | Interface\n            \"\"\"IPv4 address source IP or egress interface to use.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"VRF context. Defaults to `default`.\"\"\"\n            repeat: int = 2\n            \"\"\"Number of ping repetition. Defaults to 2.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each host in the input list.\"\"\"\n        return [template.render(destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat) for host in self.inputs.hosts]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyReachability.\"\"\"\n        failures = []\n        for command in self.instance_commands:\n            src = command.params.get(\"source\")\n            dst = command.params.get(\"destination\")\n            repeat = command.params.get(\"repeat\")\n\n            if any(elem is None for elem in (src, dst, repeat)):\n                msg = f\"A parameter is missing to execute the test for command {command}\"\n                raise AntaMissingParamError(msg)\n\n            if f\"{repeat} received\" not in command.json_output[\"messages\"][0]:\n                failures.append((str(src), str(dst)))\n\n        if not failures:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Connectivity test failed for the following source-destination pairs: {failures}\")\n
"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyReachability-attributes","title":"Inputs","text":"Name Type Description Default hosts list[Host] List of host to ping. -"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyReachability-attributes","title":"Host","text":"Name Type Description Default destination IPv4Address IPv4 address to ping. - source IPv4Address | Interface IPv4 address source IP or egress interface to use. - vrf str VRF context. Defaults to `default`. 'default' repeat int Number of ping repetition. Defaults to 2. 2"},{"location":"api/tests.field_notices/","title":"Field Notices","text":""},{"location":"api/tests.field_notices/#anta.tests.field_notices.VerifyFieldNotice44Resolution","title":"VerifyFieldNotice44Resolution","text":"

Verifies if the device is using an Aboot version that fixes the bug discussed in the Field Notice 44.

Aboot manages system settings prior to EOS initialization.

Reference: https://www.arista.com/en/support/advisories-notices/field-notice/8756-field-notice-44

Expected Results
  • Success: The test will pass if the device is using an Aboot version that fixes the bug discussed in the Field Notice 44.
  • Failure: The test will fail if the device is not using an Aboot version that fixes the bug discussed in the Field Notice 44.
Examples
anta.tests.field_notices:\n  - VerifyFieldNotice44Resolution:\n
Source code in anta/tests/field_notices.py
class VerifyFieldNotice44Resolution(AntaTest):\n    \"\"\"Verifies if the device is using an Aboot version that fixes the bug discussed in the Field Notice 44.\n\n    Aboot manages system settings prior to EOS initialization.\n\n    Reference: https://www.arista.com/en/support/advisories-notices/field-notice/8756-field-notice-44\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is using an Aboot version that fixes the bug discussed in the Field Notice 44.\n    * Failure: The test will fail if the device is not using an Aboot version that fixes the bug discussed in the Field Notice 44.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.field_notices:\n      - VerifyFieldNotice44Resolution:\n    ```\n    \"\"\"\n\n    name = \"VerifyFieldNotice44Resolution\"\n    description = \"Verifies that the device is using the correct Aboot version per FN0044.\"\n    categories: ClassVar[list[str]] = [\"field notices\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version detail\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyFieldNotice44Resolution.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        devices = [\n            \"DCS-7010T-48\",\n            \"DCS-7010T-48-DC\",\n            \"DCS-7050TX-48\",\n            \"DCS-7050TX-64\",\n            \"DCS-7050TX-72\",\n            \"DCS-7050TX-72Q\",\n            \"DCS-7050TX-96\",\n            \"DCS-7050TX2-128\",\n            \"DCS-7050SX-64\",\n            \"DCS-7050SX-72\",\n            \"DCS-7050SX-72Q\",\n            \"DCS-7050SX2-72Q\",\n            \"DCS-7050SX-96\",\n            \"DCS-7050SX2-128\",\n            \"DCS-7050QX-32S\",\n            \"DCS-7050QX2-32S\",\n            \"DCS-7050SX3-48YC12\",\n            \"DCS-7050CX3-32S\",\n            \"DCS-7060CX-32S\",\n            \"DCS-7060CX2-32S\",\n            \"DCS-7060SX2-48YC6\",\n            \"DCS-7160-48YC6\",\n            \"DCS-7160-48TC6\",\n            \"DCS-7160-32CQ\",\n            \"DCS-7280SE-64\",\n            \"DCS-7280SE-68\",\n            \"DCS-7280SE-72\",\n            \"DCS-7150SC-24-CLD\",\n            \"DCS-7150SC-64-CLD\",\n            \"DCS-7020TR-48\",\n            \"DCS-7020TRA-48\",\n            \"DCS-7020SR-24C2\",\n            \"DCS-7020SRG-24C2\",\n            \"DCS-7280TR-48C6\",\n            \"DCS-7280TRA-48C6\",\n            \"DCS-7280SR-48C6\",\n            \"DCS-7280SRA-48C6\",\n            \"DCS-7280SRAM-48C6\",\n            \"DCS-7280SR2K-48C6-M\",\n            \"DCS-7280SR2-48YC6\",\n            \"DCS-7280SR2A-48YC6\",\n            \"DCS-7280SRM-40CX2\",\n            \"DCS-7280QR-C36\",\n            \"DCS-7280QRA-C36S\",\n        ]\n        variants = [\"-SSD-F\", \"-SSD-R\", \"-M-F\", \"-M-R\", \"-F\", \"-R\"]\n\n        model = command_output[\"modelName\"]\n        for variant in variants:\n            model = model.replace(variant, \"\")\n        if model not in devices:\n            self.result.is_skipped(\"device is not impacted by FN044\")\n            return\n\n        for component in command_output[\"details\"][\"components\"]:\n            if component[\"name\"] == \"Aboot\":\n                aboot_version = component[\"version\"].split(\"-\")[2]\n        self.result.is_success()\n        incorrect_aboot_version = (\n            aboot_version.startswith(\"4.0.\")\n            and int(aboot_version.split(\".\")[2]) < 7\n            or aboot_version.startswith(\"4.1.\")\n            and int(aboot_version.split(\".\")[2]) < 1\n            or (\n                aboot_version.startswith(\"6.0.\")\n                and int(aboot_version.split(\".\")[2]) < 9\n                or aboot_version.startswith(\"6.1.\")\n                and int(aboot_version.split(\".\")[2]) < 7\n            )\n        )\n        if incorrect_aboot_version:\n            self.result.is_failure(f\"device is running incorrect version of aboot ({aboot_version})\")\n
"},{"location":"api/tests.field_notices/#anta.tests.field_notices.VerifyFieldNotice72Resolution","title":"VerifyFieldNotice72Resolution","text":"

Verifies if the device is potentially exposed to Field Notice 72, and if the issue has been mitigated.

Reference: https://www.arista.com/en/support/advisories-notices/field-notice/17410-field-notice-0072

Expected Results
  • Success: The test will pass if the device is not exposed to FN72 and the issue has been mitigated.
  • Failure: The test will fail if the device is exposed to FN72 and the issue has not been mitigated.
Examples
anta.tests.field_notices:\n  - VerifyFieldNotice72Resolution:\n
Source code in anta/tests/field_notices.py
class VerifyFieldNotice72Resolution(AntaTest):\n    \"\"\"Verifies if the device is potentially exposed to Field Notice 72, and if the issue has been mitigated.\n\n    Reference: https://www.arista.com/en/support/advisories-notices/field-notice/17410-field-notice-0072\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is not exposed to FN72 and the issue has been mitigated.\n    * Failure: The test will fail if the device is exposed to FN72 and the issue has not been mitigated.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.field_notices:\n      - VerifyFieldNotice72Resolution:\n    ```\n    \"\"\"\n\n    name = \"VerifyFieldNotice72Resolution\"\n    description = \"Verifies if the device is exposed to FN0072, and if the issue has been mitigated.\"\n    categories: ClassVar[list[str]] = [\"field notices\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version detail\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyFieldNotice72Resolution.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        devices = [\"DCS-7280SR3-48YC8\", \"DCS-7280SR3K-48YC8\"]\n        variants = [\"-SSD-F\", \"-SSD-R\", \"-M-F\", \"-M-R\", \"-F\", \"-R\"]\n        model = command_output[\"modelName\"]\n\n        for variant in variants:\n            model = model.replace(variant, \"\")\n        if model not in devices:\n            self.result.is_skipped(\"Platform is not impacted by FN072\")\n            return\n\n        serial = command_output[\"serialNumber\"]\n        number = int(serial[3:7])\n\n        if \"JPE\" not in serial and \"JAS\" not in serial:\n            self.result.is_skipped(\"Device not exposed\")\n            return\n\n        if model == \"DCS-7280SR3-48YC8\" and \"JPE\" in serial and number >= 2131:\n            self.result.is_skipped(\"Device not exposed\")\n            return\n\n        if model == \"DCS-7280SR3-48YC8\" and \"JAS\" in serial and number >= 2041:\n            self.result.is_skipped(\"Device not exposed\")\n            return\n\n        if model == \"DCS-7280SR3K-48YC8\" and \"JPE\" in serial and number >= 2134:\n            self.result.is_skipped(\"Device not exposed\")\n            return\n\n        if model == \"DCS-7280SR3K-48YC8\" and \"JAS\" in serial and number >= 2041:\n            self.result.is_skipped(\"Device not exposed\")\n            return\n\n        # Because each of the if checks above will return if taken, we only run the long check if we get this far\n        for entry in command_output[\"details\"][\"components\"]:\n            if entry[\"name\"] == \"FixedSystemvrm1\":\n                if int(entry[\"version\"]) < 7:\n                    self.result.is_failure(\"Device is exposed to FN72\")\n                else:\n                    self.result.is_success(\"FN72 is mitigated\")\n                return\n        # We should never hit this point\n        self.result.is_error(\"Error in running test - FixedSystemvrm1 not found\")\n        return\n
"},{"location":"api/tests.greent/","title":"GreenT","text":""},{"location":"api/tests.greent/#anta.tests.greent.VerifyGreenT","title":"VerifyGreenT","text":"

Verifies if a GreenT (GRE Encapsulated Telemetry) policy other than the default is created.

Expected Results
  • Success: The test will pass if a GreenT policy is created other than the default one.
  • Failure: The test will fail if no other GreenT policy is created.
Examples
anta.tests.greent:\n  - VerifyGreenTCounters:\n
Source code in anta/tests/greent.py
class VerifyGreenT(AntaTest):\n    \"\"\"Verifies if a GreenT (GRE Encapsulated Telemetry) policy other than the default is created.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if a GreenT policy is created other than the default one.\n    * Failure: The test will fail if no other GreenT policy is created.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.greent:\n      - VerifyGreenTCounters:\n    ```\n    \"\"\"\n\n    name = \"VerifyGreenT\"\n    description = \"Verifies if a GreenT policy is created.\"\n    categories: ClassVar[list[str]] = [\"greent\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show monitor telemetry postcard policy profile\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyGreenT.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        profiles = [profile for profile in command_output[\"profiles\"] if profile != \"default\"]\n\n        if profiles:\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"No GreenT policy is created\")\n
"},{"location":"api/tests.greent/#anta.tests.greent.VerifyGreenTCounters","title":"VerifyGreenTCounters","text":"

Verifies if the GreenT (GRE Encapsulated Telemetry) counters are incremented.

Expected Results
  • Success: The test will pass if the GreenT counters are incremented.
  • Failure: The test will fail if the GreenT counters are not incremented.
Examples
anta.tests.greent:\n  - VerifyGreenT:\n
Source code in anta/tests/greent.py
class VerifyGreenTCounters(AntaTest):\n    \"\"\"Verifies if the GreenT (GRE Encapsulated Telemetry) counters are incremented.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the GreenT counters are incremented.\n    * Failure: The test will fail if the GreenT counters are not incremented.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.greent:\n      - VerifyGreenT:\n    ```\n    \"\"\"\n\n    name = \"VerifyGreenTCounters\"\n    description = \"Verifies if the GreenT counters are incremented.\"\n    categories: ClassVar[list[str]] = [\"greent\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show monitor telemetry postcard counters\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyGreenTCounters.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        if command_output[\"grePktSent\"] > 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"GreenT counters are not incremented\")\n
"},{"location":"api/tests.hardware/","title":"Hardware","text":""},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyAdverseDrops","title":"VerifyAdverseDrops","text":"

Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches (Arad/Jericho chips).

Expected Results
  • Success: The test will pass if there are no adverse drops.
  • Failure: The test will fail if there are adverse drops.
Examples
anta.tests.hardware:\n  - VerifyAdverseDrops:\n
Source code in anta/tests/hardware.py
class VerifyAdverseDrops(AntaTest):\n    \"\"\"Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches (Arad/Jericho chips).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are no adverse drops.\n    * Failure: The test will fail if there are adverse drops.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyAdverseDrops:\n    ```\n    \"\"\"\n\n    name = \"VerifyAdverseDrops\"\n    description = \"Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hardware counter drop\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAdverseDrops.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        total_adverse_drop = command_output.get(\"totalAdverseDrops\", \"\")\n        if total_adverse_drop == 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device totalAdverseDrops counter is: '{total_adverse_drop}'\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyEnvironmentCooling","title":"VerifyEnvironmentCooling","text":"

Verifies the status of power supply fans and all fan trays.

Expected Results
  • Success: The test will pass if the fans status are within the accepted states list.
  • Failure: The test will fail if some fans status is not within the accepted states list.
Examples
anta.tests.hardware:\n  - VerifyEnvironmentCooling:\n      states:\n        - ok\n
Source code in anta/tests/hardware.py
class VerifyEnvironmentCooling(AntaTest):\n    \"\"\"Verifies the status of power supply fans and all fan trays.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the fans status are within the accepted states list.\n    * Failure: The test will fail if some fans status is not within the accepted states list.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyEnvironmentCooling:\n          states:\n            - ok\n    ```\n    \"\"\"\n\n    name = \"VerifyEnvironmentCooling\"\n    description = \"Verifies the status of power supply fans and all fan trays.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment cooling\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyEnvironmentCooling test.\"\"\"\n\n        states: list[str]\n        \"\"\"List of accepted states of fan status.\"\"\"\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEnvironmentCooling.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        self.result.is_success()\n        # First go through power supplies fans\n        for power_supply in command_output.get(\"powerSupplySlots\", []):\n            for fan in power_supply.get(\"fans\", []):\n                if (state := fan[\"status\"]) not in self.inputs.states:\n                    self.result.is_failure(f\"Fan {fan['label']} on PowerSupply {power_supply['label']} is: '{state}'\")\n        # Then go through fan trays\n        for fan_tray in command_output.get(\"fanTraySlots\", []):\n            for fan in fan_tray.get(\"fans\", []):\n                if (state := fan[\"status\"]) not in self.inputs.states:\n                    self.result.is_failure(f\"Fan {fan['label']} on Fan Tray {fan_tray['label']} is: '{state}'\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyEnvironmentCooling-attributes","title":"Inputs","text":"Name Type Description Default states list[str] List of accepted states of fan status. -"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyEnvironmentPower","title":"VerifyEnvironmentPower","text":"

Verifies the power supplies status.

Expected Results
  • Success: The test will pass if the power supplies status are within the accepted states list.
  • Failure: The test will fail if some power supplies status is not within the accepted states list.
Examples
anta.tests.hardware:\n  - VerifyEnvironmentPower:\n      states:\n        - ok\n
Source code in anta/tests/hardware.py
class VerifyEnvironmentPower(AntaTest):\n    \"\"\"Verifies the power supplies status.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the power supplies status are within the accepted states list.\n    * Failure: The test will fail if some power supplies status is not within the accepted states list.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyEnvironmentPower:\n          states:\n            - ok\n    ```\n    \"\"\"\n\n    name = \"VerifyEnvironmentPower\"\n    description = \"Verifies the power supplies status.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment power\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyEnvironmentPower test.\"\"\"\n\n        states: list[str]\n        \"\"\"List of accepted states list of power supplies status.\"\"\"\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEnvironmentPower.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        power_supplies = command_output.get(\"powerSupplies\", \"{}\")\n        wrong_power_supplies = {\n            powersupply: {\"state\": value[\"state\"]} for powersupply, value in dict(power_supplies).items() if value[\"state\"] not in self.inputs.states\n        }\n        if not wrong_power_supplies:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following power supplies status are not in the accepted states list: {wrong_power_supplies}\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyEnvironmentPower-attributes","title":"Inputs","text":"Name Type Description Default states list[str] List of accepted states list of power supplies status. -"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyEnvironmentSystemCooling","title":"VerifyEnvironmentSystemCooling","text":"

Verifies the device\u2019s system cooling status.

Expected Results
  • Success: The test will pass if the system cooling status is OK: \u2018coolingOk\u2019.
  • Failure: The test will fail if the system cooling status is NOT OK.
Examples
anta.tests.hardware:\n  - VerifyEnvironmentSystemCooling:\n
Source code in anta/tests/hardware.py
class VerifyEnvironmentSystemCooling(AntaTest):\n    \"\"\"Verifies the device's system cooling status.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the system cooling status is OK: 'coolingOk'.\n    * Failure: The test will fail if the system cooling status is NOT OK.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyEnvironmentSystemCooling:\n    ```\n    \"\"\"\n\n    name = \"VerifyEnvironmentSystemCooling\"\n    description = \"Verifies the system cooling status.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment cooling\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEnvironmentSystemCooling.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        sys_status = command_output.get(\"systemStatus\", \"\")\n        self.result.is_success()\n        if sys_status != \"coolingOk\":\n            self.result.is_failure(f\"Device system cooling is not OK: '{sys_status}'\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyTemperature","title":"VerifyTemperature","text":"

Verifies if the device temperature is within acceptable limits.

Expected Results
  • Success: The test will pass if the device temperature is currently OK: \u2018temperatureOk\u2019.
  • Failure: The test will fail if the device temperature is NOT OK.
Examples
anta.tests.hardware:\n  - VerifyTemperature:\n
Source code in anta/tests/hardware.py
class VerifyTemperature(AntaTest):\n    \"\"\"Verifies if the device temperature is within acceptable limits.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device temperature is currently OK: 'temperatureOk'.\n    * Failure: The test will fail if the device temperature is NOT OK.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyTemperature:\n    ```\n    \"\"\"\n\n    name = \"VerifyTemperature\"\n    description = \"Verifies the device temperature.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTemperature.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        temperature_status = command_output.get(\"systemStatus\", \"\")\n        if temperature_status == \"temperatureOk\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyTransceiversManufacturers","title":"VerifyTransceiversManufacturers","text":"

Verifies if all the transceivers come from approved manufacturers.

Expected Results
  • Success: The test will pass if all transceivers are from approved manufacturers.
  • Failure: The test will fail if some transceivers are from unapproved manufacturers.
Examples
anta.tests.hardware:\n  - VerifyTransceiversManufacturers:\n      manufacturers:\n        - Not Present\n        - Arista Networks\n        - Arastra, Inc.\n
Source code in anta/tests/hardware.py
class VerifyTransceiversManufacturers(AntaTest):\n    \"\"\"Verifies if all the transceivers come from approved manufacturers.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all transceivers are from approved manufacturers.\n    * Failure: The test will fail if some transceivers are from unapproved manufacturers.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyTransceiversManufacturers:\n          manufacturers:\n            - Not Present\n            - Arista Networks\n            - Arastra, Inc.\n    ```\n    \"\"\"\n\n    name = \"VerifyTransceiversManufacturers\"\n    description = \"Verifies if all transceivers come from approved manufacturers.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show inventory\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTransceiversManufacturers test.\"\"\"\n\n        manufacturers: list[str]\n        \"\"\"List of approved transceivers manufacturers.\"\"\"\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTransceiversManufacturers.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        wrong_manufacturers = {\n            interface: value[\"mfgName\"] for interface, value in command_output[\"xcvrSlots\"].items() if value[\"mfgName\"] not in self.inputs.manufacturers\n        }\n        if not wrong_manufacturers:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Some transceivers are from unapproved manufacturers: {wrong_manufacturers}\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyTransceiversManufacturers-attributes","title":"Inputs","text":"Name Type Description Default manufacturers list[str] List of approved transceivers manufacturers. -"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyTransceiversTemperature","title":"VerifyTransceiversTemperature","text":"

Verifies if all the transceivers are operating at an acceptable temperature.

Expected Results
  • Success: The test will pass if all transceivers status are OK: \u2018ok\u2019.
  • Failure: The test will fail if some transceivers are NOT OK.
Examples
anta.tests.hardware:\n  - VerifyTransceiversTemperature:\n
Source code in anta/tests/hardware.py
class VerifyTransceiversTemperature(AntaTest):\n    \"\"\"Verifies if all the transceivers are operating at an acceptable temperature.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all transceivers status are OK: 'ok'.\n    * Failure: The test will fail if some transceivers are NOT OK.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyTransceiversTemperature:\n    ```\n    \"\"\"\n\n    name = \"VerifyTransceiversTemperature\"\n    description = \"Verifies the transceivers temperature.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature transceiver\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTransceiversTemperature.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        sensors = command_output.get(\"tempSensors\", \"\")\n        wrong_sensors = {\n            sensor[\"name\"]: {\n                \"hwStatus\": sensor[\"hwStatus\"],\n                \"alertCount\": sensor[\"alertCount\"],\n            }\n            for sensor in sensors\n            if sensor[\"hwStatus\"] != \"ok\" or sensor[\"alertCount\"] != 0\n        }\n        if not wrong_sensors:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following sensors are operating outside the acceptable temperature range or have raised alerts: {wrong_sensors}\")\n
"},{"location":"api/tests.interfaces/","title":"Interfaces","text":""},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyIPProxyARP","title":"VerifyIPProxyARP","text":"

Verifies if Proxy-ARP is enabled for the provided list of interface(s).

Expected Results
  • Success: The test will pass if Proxy-ARP is enabled on the specified interface(s).
  • Failure: The test will fail if Proxy-ARP is disabled on the specified interface(s).
Examples
anta.tests.interfaces:\n  - VerifyIPProxyARP:\n      interfaces:\n        - Ethernet1\n        - Ethernet2\n
Source code in anta/tests/interfaces.py
class VerifyIPProxyARP(AntaTest):\n    \"\"\"Verifies if Proxy-ARP is enabled for the provided list of interface(s).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if Proxy-ARP is enabled on the specified interface(s).\n    * Failure: The test will fail if Proxy-ARP is disabled on the specified interface(s).\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyIPProxyARP:\n          interfaces:\n            - Ethernet1\n            - Ethernet2\n    ```\n    \"\"\"\n\n    name = \"VerifyIPProxyARP\"\n    description = \"Verifies if Proxy ARP is enabled.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip interface {intf}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyIPProxyARP test.\"\"\"\n\n        interfaces: list[str]\n        \"\"\"List of interfaces to be tested.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each interface in the input list.\"\"\"\n        return [template.render(intf=intf) for intf in self.inputs.interfaces]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIPProxyARP.\"\"\"\n        disabled_intf = []\n        for command in self.instance_commands:\n            if \"intf\" in command.params:\n                intf = command.params[\"intf\"]\n            if not command.json_output[\"interfaces\"][intf][\"proxyArp\"]:\n                disabled_intf.append(intf)\n        if disabled_intf:\n            self.result.is_failure(f\"The following interface(s) have Proxy-ARP disabled: {disabled_intf}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyIPProxyARP-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[str] List of interfaces to be tested. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyIllegalLACP","title":"VerifyIllegalLACP","text":"

Verifies there are no illegal LACP packets in all port channels.

Expected Results
  • Success: The test will pass if there are no illegal LACP packets received.
  • Failure: The test will fail if there is at least one illegal LACP packet received.
Examples
anta.tests.interfaces:\n  - VerifyIllegalLACP:\n
Source code in anta/tests/interfaces.py
class VerifyIllegalLACP(AntaTest):\n    \"\"\"Verifies there are no illegal LACP packets in all port channels.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are no illegal LACP packets received.\n    * Failure: The test will fail if there is at least one illegal LACP packet received.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyIllegalLACP:\n    ```\n    \"\"\"\n\n    name = \"VerifyIllegalLACP\"\n    description = \"Verifies there are no illegal LACP packets in all port channels.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show lacp counters all-ports\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIllegalLACP.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        po_with_illegal_lacp: list[dict[str, dict[str, int]]] = []\n        for portchannel, portchannel_dict in command_output[\"portChannels\"].items():\n            po_with_illegal_lacp.extend(\n                {portchannel: interface} for interface, interface_dict in portchannel_dict[\"interfaces\"].items() if interface_dict[\"illegalRxCount\"] != 0\n            )\n        if not po_with_illegal_lacp:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following port-channels have received illegal LACP packets on the following ports: {po_with_illegal_lacp}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceDiscards","title":"VerifyInterfaceDiscards","text":"

Verifies that the interfaces packet discard counters are equal to zero.

Expected Results
  • Success: The test will pass if all interfaces have discard counters equal to zero.
  • Failure: The test will fail if one or more interfaces have non-zero discard counters.
Examples
anta.tests.interfaces:\n  - VerifyInterfaceDiscards:\n
Source code in anta/tests/interfaces.py
class VerifyInterfaceDiscards(AntaTest):\n    \"\"\"Verifies that the interfaces packet discard counters are equal to zero.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all interfaces have discard counters equal to zero.\n    * Failure: The test will fail if one or more interfaces have non-zero discard counters.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfaceDiscards:\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfaceDiscards\"\n    description = \"Verifies there are no interface discard counters.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters discards\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfaceDiscards.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        wrong_interfaces: list[dict[str, dict[str, int]]] = []\n        for interface, outer_v in command_output[\"interfaces\"].items():\n            wrong_interfaces.extend({interface: outer_v} for value in outer_v.values() if value > 0)\n        if not wrong_interfaces:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following interfaces have non 0 discard counter(s): {wrong_interfaces}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceErrDisabled","title":"VerifyInterfaceErrDisabled","text":"

Verifies there are no interfaces in the errdisabled state.

Expected Results
  • Success: The test will pass if there are no interfaces in the errdisabled state.
  • Failure: The test will fail if there is at least one interface in the errdisabled state.
Examples
anta.tests.interfaces:\n  - VerifyInterfaceErrDisabled:\n
Source code in anta/tests/interfaces.py
class VerifyInterfaceErrDisabled(AntaTest):\n    \"\"\"Verifies there are no interfaces in the errdisabled state.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are no interfaces in the errdisabled state.\n    * Failure: The test will fail if there is at least one interface in the errdisabled state.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfaceErrDisabled:\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfaceErrDisabled\"\n    description = \"Verifies there are no interfaces in the errdisabled state.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces status\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfaceErrDisabled.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        errdisabled_interfaces = [interface for interface, value in command_output[\"interfaceStatuses\"].items() if value[\"linkStatus\"] == \"errdisabled\"]\n        if errdisabled_interfaces:\n            self.result.is_failure(f\"The following interfaces are in error disabled state: {errdisabled_interfaces}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceErrors","title":"VerifyInterfaceErrors","text":"

Verifies that the interfaces error counters are equal to zero.

Expected Results
  • Success: The test will pass if all interfaces have error counters equal to zero.
  • Failure: The test will fail if one or more interfaces have non-zero error counters.
Examples
anta.tests.interfaces:\n  - VerifyInterfaceErrors:\n
Source code in anta/tests/interfaces.py
class VerifyInterfaceErrors(AntaTest):\n    \"\"\"Verifies that the interfaces error counters are equal to zero.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all interfaces have error counters equal to zero.\n    * Failure: The test will fail if one or more interfaces have non-zero error counters.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfaceErrors:\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfaceErrors\"\n    description = \"Verifies there are no interface error counters.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters errors\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfaceErrors.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        wrong_interfaces: list[dict[str, dict[str, int]]] = []\n        for interface, counters in command_output[\"interfaceErrorCounters\"].items():\n            if any(value > 0 for value in counters.values()) and all(interface not in wrong_interface for wrong_interface in wrong_interfaces):\n                wrong_interfaces.append({interface: counters})\n        if not wrong_interfaces:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following interface(s) have non-zero error counters: {wrong_interfaces}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceIPv4","title":"VerifyInterfaceIPv4","text":"

Verifies if an interface is configured with a correct primary and list of optional secondary IPv4 addresses.

Expected Results
  • Success: The test will pass if an interface is configured with a correct primary and secondary IPv4 address.
  • Failure: The test will fail if an interface is not found or the primary and secondary IPv4 addresses do not match with the input.
Examples
anta.tests.interfaces:\n  - VerifyInterfaceIPv4:\n      interfaces:\n        - name: Ethernet2\n          primary_ip: 172.30.11.0/31\n          secondary_ips:\n            - 10.10.10.0/31\n            - 10.10.10.10/31\n
Source code in anta/tests/interfaces.py
class VerifyInterfaceIPv4(AntaTest):\n    \"\"\"Verifies if an interface is configured with a correct primary and list of optional secondary IPv4 addresses.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if an interface is configured with a correct primary and secondary IPv4 address.\n    * Failure: The test will fail if an interface is not found or the primary and secondary IPv4 addresses do not match with the input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfaceIPv4:\n          interfaces:\n            - name: Ethernet2\n              primary_ip: 172.30.11.0/31\n              secondary_ips:\n                - 10.10.10.0/31\n                - 10.10.10.10/31\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfaceIPv4\"\n    description = \"Verifies the interface IPv4 addresses.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip interface {interface}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyInterfaceIPv4 test.\"\"\"\n\n        interfaces: list[InterfaceDetail]\n        \"\"\"List of interfaces with their details.\"\"\"\n\n        class InterfaceDetail(BaseModel):\n            \"\"\"Model for an interface detail.\"\"\"\n\n            name: Interface\n            \"\"\"Name of the interface.\"\"\"\n            primary_ip: IPv4Network\n            \"\"\"Primary IPv4 address in CIDR notation.\"\"\"\n            secondary_ips: list[IPv4Network] | None = None\n            \"\"\"Optional list of secondary IPv4 addresses in CIDR notation.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each interface in the input list.\"\"\"\n        return [\n            template.render(interface=interface.name, primary_ip=interface.primary_ip, secondary_ips=interface.secondary_ips) for interface in self.inputs.interfaces\n        ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfaceIPv4.\"\"\"\n        self.result.is_success()\n        for command in self.instance_commands:\n            intf = command.params[\"interface\"]\n            input_primary_ip = str(command.params[\"primary_ip\"])\n            failed_messages = []\n\n            # Check if the interface has an IP address configured\n            if not (interface_output := get_value(command.json_output, f\"interfaces.{intf}.interfaceAddress\")):\n                self.result.is_failure(f\"For interface `{intf}`, IP address is not configured.\")\n                continue\n\n            primary_ip = get_value(interface_output, \"primaryIp\")\n\n            # Combine IP address and subnet for primary IP\n            actual_primary_ip = f\"{primary_ip['address']}/{primary_ip['maskLen']}\"\n\n            # Check if the primary IP address matches the input\n            if actual_primary_ip != input_primary_ip:\n                failed_messages.append(f\"The expected primary IP address is `{input_primary_ip}`, but the actual primary IP address is `{actual_primary_ip}`.\")\n\n            if command.params[\"secondary_ips\"] is not None:\n                input_secondary_ips = sorted([str(network) for network in command.params[\"secondary_ips\"]])\n                secondary_ips = get_value(interface_output, \"secondaryIpsOrderedList\")\n\n                # Combine IP address and subnet for secondary IPs\n                actual_secondary_ips = sorted([f\"{secondary_ip['address']}/{secondary_ip['maskLen']}\" for secondary_ip in secondary_ips])\n\n                # Check if the secondary IP address is configured\n                if not actual_secondary_ips:\n                    failed_messages.append(\n                        f\"The expected secondary IP addresses are `{input_secondary_ips}`, but the actual secondary IP address is not configured.\"\n                    )\n\n                # Check if the secondary IP addresses match the input\n                elif actual_secondary_ips != input_secondary_ips:\n                    failed_messages.append(\n                        f\"The expected secondary IP addresses are `{input_secondary_ips}`, but the actual secondary IP addresses are `{actual_secondary_ips}`.\"\n                    )\n\n            if failed_messages:\n                self.result.is_failure(f\"For interface `{intf}`, \" + \" \".join(failed_messages))\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceIPv4-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceDetail] List of interfaces with their details. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceIPv4-attributes","title":"InterfaceDetail","text":"Name Type Description Default name Interface Name of the interface. - primary_ip IPv4Network Primary IPv4 address in CIDR notation. - secondary_ips list[IPv4Network] | None Optional list of secondary IPv4 addresses in CIDR notation. None"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceUtilization","title":"VerifyInterfaceUtilization","text":"

Verifies that the utilization of interfaces is below a certain threshold.

Load interval (default to 5 minutes) is defined in device configuration.

Expected Results
  • Success: The test will pass if all interfaces have a usage below the threshold.
  • Failure: The test will fail if one or more interfaces have a usage above the threshold.
Examples
anta.tests.interfaces:\n  - VerifyInterfaceUtilization:\n      threshold: 70.0\n
Source code in anta/tests/interfaces.py
class VerifyInterfaceUtilization(AntaTest):\n    \"\"\"Verifies that the utilization of interfaces is below a certain threshold.\n\n    Load interval (default to 5 minutes) is defined in device configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all interfaces have a usage below the threshold.\n    * Failure: The test will fail if one or more interfaces have a usage above the threshold.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfaceUtilization:\n          threshold: 70.0\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfaceUtilization\"\n    description = \"Verifies that the utilization of interfaces is below a certain threshold.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters rates\"), AntaCommand(command=\"show interfaces\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyInterfaceUtilization test.\"\"\"\n\n        threshold: Percent = 75.0\n        \"\"\"Interface utilization threshold above which the test will fail. Defaults to 75%.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfaceUtilization.\"\"\"\n        duplex_full = \"duplexFull\"\n        failed_interfaces: dict[str, dict[str, float]] = {}\n        rates = self.instance_commands[0].json_output\n        interfaces = self.instance_commands[1].json_output\n\n        for intf, rate in rates[\"interfaces\"].items():\n            # Assuming the interface is full-duplex in the logic below\n            if \"duplex\" in interfaces[\"interfaces\"][intf]:\n                if interfaces[\"interfaces\"][intf][\"duplex\"] != duplex_full:\n                    self.result.is_error(f\"Interface {intf} is not Full-Duplex, VerifyInterfaceUtilization has not been implemented in ANTA\")\n                    return\n            elif \"memberInterfaces\" in interfaces[\"interfaces\"][intf]:\n                # This is a Port-Channel\n                for member, stats in interfaces[\"interfaces\"][intf][\"memberInterfaces\"].items():\n                    if stats[\"duplex\"] != duplex_full:\n                        self.result.is_error(f\"Member {member} of {intf} is not Full-Duplex, VerifyInterfaceUtilization has not been implemented in ANTA\")\n                        return\n\n            bandwidth = interfaces[\"interfaces\"][intf][\"bandwidth\"]\n\n            for bps_rate in (\"inBpsRate\", \"outBpsRate\"):\n                usage = rate[bps_rate] / bandwidth * 100\n                if usage > self.inputs.threshold:\n                    failed_interfaces.setdefault(intf, {})[bps_rate] = usage\n\n        if not failed_interfaces:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following interfaces have a usage > {self.inputs.threshold}%: {failed_interfaces}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceUtilization-attributes","title":"Inputs","text":"Name Type Description Default threshold Percent Interface utilization threshold above which the test will fail. Defaults to 75%. 75.0"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus","title":"VerifyInterfacesStatus","text":"

Verifies if the provided list of interfaces are all in the expected state.

  • If line protocol status is provided, prioritize checking against both status and line protocol status
  • If line protocol status is not provided and interface status is \u201cup\u201d, expect both status and line protocol to be \u201cup\u201d
  • If interface status is not \u201cup\u201d, check only the interface status without considering line protocol status
Expected Results
  • Success: The test will pass if the provided interfaces are all in the expected state.
  • Failure: The test will fail if any interface is not in the expected state.
Examples
anta.tests.interfaces:\n  - VerifyInterfacesStatus:\n      interfaces:\n        - name: Ethernet1\n          status: up\n        - name: Port-Channel100\n          status: down\n          line_protocol_status: lowerLayerDown\n        - name: Ethernet49/1\n          status: adminDown\n          line_protocol_status: notPresent\n
Source code in anta/tests/interfaces.py
class VerifyInterfacesStatus(AntaTest):\n    \"\"\"Verifies if the provided list of interfaces are all in the expected state.\n\n    - If line protocol status is provided, prioritize checking against both status and line protocol status\n    - If line protocol status is not provided and interface status is \"up\", expect both status and line protocol to be \"up\"\n    - If interface status is not \"up\", check only the interface status without considering line protocol status\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided interfaces are all in the expected state.\n    * Failure: The test will fail if any interface is not in the expected state.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfacesStatus:\n          interfaces:\n            - name: Ethernet1\n              status: up\n            - name: Port-Channel100\n              status: down\n              line_protocol_status: lowerLayerDown\n            - name: Ethernet49/1\n              status: adminDown\n              line_protocol_status: notPresent\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfacesStatus\"\n    description = \"Verifies the status of the provided interfaces.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyInterfacesStatus test.\"\"\"\n\n        interfaces: list[InterfaceState]\n        \"\"\"List of interfaces with their expected state.\"\"\"\n\n        class InterfaceState(BaseModel):\n            \"\"\"Model for an interface state.\"\"\"\n\n            name: Interface\n            \"\"\"Interface to validate.\"\"\"\n            status: Literal[\"up\", \"down\", \"adminDown\"]\n            \"\"\"Expected status of the interface.\"\"\"\n            line_protocol_status: Literal[\"up\", \"down\", \"testing\", \"unknown\", \"dormant\", \"notPresent\", \"lowerLayerDown\"] | None = None\n            \"\"\"Expected line protocol status of the interface.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfacesStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        self.result.is_success()\n\n        intf_not_configured = []\n        intf_wrong_state = []\n\n        for interface in self.inputs.interfaces:\n            if (intf_status := get_value(command_output[\"interfaceDescriptions\"], interface.name, separator=\"..\")) is None:\n                intf_not_configured.append(interface.name)\n                continue\n\n            status = \"up\" if intf_status[\"interfaceStatus\"] in {\"up\", \"connected\"} else intf_status[\"interfaceStatus\"]\n            proto = \"up\" if intf_status[\"lineProtocolStatus\"] in {\"up\", \"connected\"} else intf_status[\"lineProtocolStatus\"]\n\n            # If line protocol status is provided, prioritize checking against both status and line protocol status\n            if interface.line_protocol_status:\n                if interface.status != status or interface.line_protocol_status != proto:\n                    intf_wrong_state.append(f\"{interface.name} is {status}/{proto}\")\n\n            # If line protocol status is not provided and interface status is \"up\", expect both status and proto to be \"up\"\n            # If interface status is not \"up\", check only the interface status without considering line protocol status\n            elif (interface.status == \"up\" and (status != \"up\" or proto != \"up\")) or (interface.status != status):\n                intf_wrong_state.append(f\"{interface.name} is {status}/{proto}\")\n\n        if intf_not_configured:\n            self.result.is_failure(f\"The following interface(s) are not configured: {intf_not_configured}\")\n\n        if intf_wrong_state:\n            self.result.is_failure(f\"The following interface(s) are not in the expected state: {intf_wrong_state}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceState] List of interfaces with their expected state. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus-attributes","title":"InterfaceState","text":"Name Type Description Default name Interface Interface to validate. - status Literal['up', 'down', 'adminDown'] Expected status of the interface. - line_protocol_status Literal['up', 'down', 'testing', 'unknown', 'dormant', 'notPresent', 'lowerLayerDown'] | None Expected line protocol status of the interface. None"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyIpVirtualRouterMac","title":"VerifyIpVirtualRouterMac","text":"

Verifies the IP virtual router MAC address.

Expected Results
  • Success: The test will pass if the IP virtual router MAC address matches the input.
  • Failure: The test will fail if the IP virtual router MAC address does not match the input.
Examples
anta.tests.interfaces:\n  - VerifyIpVirtualRouterMac:\n      mac_address: 00:1c:73:00:dc:01\n
Source code in anta/tests/interfaces.py
class VerifyIpVirtualRouterMac(AntaTest):\n    \"\"\"Verifies the IP virtual router MAC address.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the IP virtual router MAC address matches the input.\n    * Failure: The test will fail if the IP virtual router MAC address does not match the input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyIpVirtualRouterMac:\n          mac_address: 00:1c:73:00:dc:01\n    ```\n    \"\"\"\n\n    name = \"VerifyIpVirtualRouterMac\"\n    description = \"Verifies the IP virtual router MAC address.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip virtual-router\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyIpVirtualRouterMac test.\"\"\"\n\n        mac_address: MacAddress\n        \"\"\"IP virtual router MAC address.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIpVirtualRouterMac.\"\"\"\n        command_output = self.instance_commands[0].json_output[\"virtualMacs\"]\n        mac_address_found = get_item(command_output, \"macAddress\", self.inputs.mac_address)\n\n        if mac_address_found is None:\n            self.result.is_failure(f\"IP virtual router MAC address `{self.inputs.mac_address}` is not configured.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyIpVirtualRouterMac-attributes","title":"Inputs","text":"Name Type Description Default mac_address MacAddress IP virtual router MAC address. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyL2MTU","title":"VerifyL2MTU","text":"

Verifies the global layer 2 Maximum Transfer Unit (MTU) for all L2 interfaces.

Test that L2 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces. You can define a global MTU to check and also an MTU per interface and also ignored some interfaces.

Expected Results
  • Success: The test will pass if all layer 2 interfaces have the proper MTU configured.
  • Failure: The test will fail if one or many layer 2 interfaces have the wrong MTU configured.
Examples
anta.tests.interfaces:\n  - VerifyL2MTU:\n      mtu: 1500\n      ignored_interfaces:\n        - Management1\n        - Vxlan1\n      specific_mtu:\n        - Ethernet1/1: 1500\n
Source code in anta/tests/interfaces.py
class VerifyL2MTU(AntaTest):\n    \"\"\"Verifies the global layer 2 Maximum Transfer Unit (MTU) for all L2 interfaces.\n\n    Test that L2 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces.\n    You can define a global MTU to check and also an MTU per interface and also ignored some interfaces.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all layer 2 interfaces have the proper MTU configured.\n    * Failure: The test will fail if one or many layer 2 interfaces have the wrong MTU configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyL2MTU:\n          mtu: 1500\n          ignored_interfaces:\n            - Management1\n            - Vxlan1\n          specific_mtu:\n            - Ethernet1/1: 1500\n    ```\n    \"\"\"\n\n    name = \"VerifyL2MTU\"\n    description = \"Verifies the global L2 MTU of all L2 interfaces.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyL2MTU test.\"\"\"\n\n        mtu: int = 9214\n        \"\"\"Default MTU we should have configured on all non-excluded interfaces. Defaults to 9214.\"\"\"\n        ignored_interfaces: list[str] = Field(default=[\"Management\", \"Loopback\", \"Vxlan\", \"Tunnel\"])\n        \"\"\"A list of L2 interfaces to ignore. Defaults to [\"Management\", \"Loopback\", \"Vxlan\", \"Tunnel\"]\"\"\"\n        specific_mtu: list[dict[str, int]] = Field(default=[])\n        \"\"\"A list of dictionary of L2 interfaces with their specific MTU configured\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyL2MTU.\"\"\"\n        # Parameter to save incorrect interface settings\n        wrong_l2mtu_intf: list[dict[str, int]] = []\n        command_output = self.instance_commands[0].json_output\n        # Set list of interfaces with specific settings\n        specific_interfaces: list[str] = []\n        if self.inputs.specific_mtu:\n            for d in self.inputs.specific_mtu:\n                specific_interfaces.extend(d)\n        for interface, values in command_output[\"interfaces\"].items():\n            catch_interface = re.findall(r\"^[e,p][a-zA-Z]+[-,a-zA-Z]*\\d+\\/*\\d*\", interface, re.IGNORECASE)\n            if len(catch_interface) and catch_interface[0] not in self.inputs.ignored_interfaces and values[\"forwardingModel\"] == \"bridged\":\n                if interface in specific_interfaces:\n                    wrong_l2mtu_intf.extend({interface: values[\"mtu\"]} for custom_data in self.inputs.specific_mtu if values[\"mtu\"] != custom_data[interface])\n                # Comparison with generic setting\n                elif values[\"mtu\"] != self.inputs.mtu:\n                    wrong_l2mtu_intf.append({interface: values[\"mtu\"]})\n        if wrong_l2mtu_intf:\n            self.result.is_failure(f\"Some L2 interfaces do not have correct MTU configured:\\n{wrong_l2mtu_intf}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyL2MTU-attributes","title":"Inputs","text":"Name Type Description Default mtu int Default MTU we should have configured on all non-excluded interfaces. Defaults to 9214. 9214 ignored_interfaces list[str] A list of L2 interfaces to ignore. Defaults to [\"Management\", \"Loopback\", \"Vxlan\", \"Tunnel\"] Field(default=['Management', 'Loopback', 'Vxlan', 'Tunnel']) specific_mtu list[dict[str, int]] A list of dictionary of L2 interfaces with their specific MTU configured Field(default=[])"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyL3MTU","title":"VerifyL3MTU","text":"

Verifies the global layer 3 Maximum Transfer Unit (MTU) for all L3 interfaces.

Test that L3 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces.

You can define a global MTU to check, or an MTU per interface and you can also ignored some interfaces.

Expected Results
  • Success: The test will pass if all layer 3 interfaces have the proper MTU configured.
  • Failure: The test will fail if one or many layer 3 interfaces have the wrong MTU configured.
Examples
anta.tests.interfaces:\n  - VerifyL3MTU:\n      mtu: 1500\n      ignored_interfaces:\n          - Vxlan1\n      specific_mtu:\n          - Ethernet1: 2500\n
Source code in anta/tests/interfaces.py
class VerifyL3MTU(AntaTest):\n    \"\"\"Verifies the global layer 3 Maximum Transfer Unit (MTU) for all L3 interfaces.\n\n    Test that L3 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces.\n\n    You can define a global MTU to check, or an MTU per interface and you can also ignored some interfaces.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all layer 3 interfaces have the proper MTU configured.\n    * Failure: The test will fail if one or many layer 3 interfaces have the wrong MTU configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyL3MTU:\n          mtu: 1500\n          ignored_interfaces:\n              - Vxlan1\n          specific_mtu:\n              - Ethernet1: 2500\n    ```\n    \"\"\"\n\n    name = \"VerifyL3MTU\"\n    description = \"Verifies the global L3 MTU of all L3 interfaces.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyL3MTU test.\"\"\"\n\n        mtu: int = 1500\n        \"\"\"Default MTU we should have configured on all non-excluded interfaces. Defaults to 1500.\"\"\"\n        ignored_interfaces: list[str] = Field(default=[\"Management\", \"Loopback\", \"Vxlan\", \"Tunnel\"])\n        \"\"\"A list of L3 interfaces to ignore\"\"\"\n        specific_mtu: list[dict[str, int]] = Field(default=[])\n        \"\"\"A list of dictionary of L3 interfaces with their specific MTU configured\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyL3MTU.\"\"\"\n        # Parameter to save incorrect interface settings\n        wrong_l3mtu_intf: list[dict[str, int]] = []\n        command_output = self.instance_commands[0].json_output\n        # Set list of interfaces with specific settings\n        specific_interfaces: list[str] = []\n        if self.inputs.specific_mtu:\n            for d in self.inputs.specific_mtu:\n                specific_interfaces.extend(d)\n        for interface, values in command_output[\"interfaces\"].items():\n            if re.findall(r\"[a-z]+\", interface, re.IGNORECASE)[0] not in self.inputs.ignored_interfaces and values[\"forwardingModel\"] == \"routed\":\n                if interface in specific_interfaces:\n                    wrong_l3mtu_intf.extend({interface: values[\"mtu\"]} for custom_data in self.inputs.specific_mtu if values[\"mtu\"] != custom_data[interface])\n                # Comparison with generic setting\n                elif values[\"mtu\"] != self.inputs.mtu:\n                    wrong_l3mtu_intf.append({interface: values[\"mtu\"]})\n        if wrong_l3mtu_intf:\n            self.result.is_failure(f\"Some interfaces do not have correct MTU configured:\\n{wrong_l3mtu_intf}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyL3MTU-attributes","title":"Inputs","text":"Name Type Description Default mtu int Default MTU we should have configured on all non-excluded interfaces. Defaults to 1500. 1500 ignored_interfaces list[str] A list of L3 interfaces to ignore Field(default=['Management', 'Loopback', 'Vxlan', 'Tunnel']) specific_mtu list[dict[str, int]] A list of dictionary of L3 interfaces with their specific MTU configured Field(default=[])"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLoopbackCount","title":"VerifyLoopbackCount","text":"

Verifies that the device has the expected number of loopback interfaces and all are operational.

Expected Results
  • Success: The test will pass if the device has the correct number of loopback interfaces and none are down.
  • Failure: The test will fail if the loopback interface count is incorrect or any are non-operational.
Examples
anta.tests.interfaces:\n  - VerifyLoopbackCount:\n      number: 3\n
Source code in anta/tests/interfaces.py
class VerifyLoopbackCount(AntaTest):\n    \"\"\"Verifies that the device has the expected number of loopback interfaces and all are operational.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device has the correct number of loopback interfaces and none are down.\n    * Failure: The test will fail if the loopback interface count is incorrect or any are non-operational.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyLoopbackCount:\n          number: 3\n    ```\n    \"\"\"\n\n    name = \"VerifyLoopbackCount\"\n    description = \"Verifies the number of loopback interfaces and their status.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip interface brief\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyLoopbackCount test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"Number of loopback interfaces expected to be present.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoopbackCount.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        loopback_count = 0\n        down_loopback_interfaces = []\n        for interface in command_output[\"interfaces\"]:\n            interface_dict = command_output[\"interfaces\"][interface]\n            if \"Loopback\" in interface:\n                loopback_count += 1\n                if not (interface_dict[\"lineProtocolStatus\"] == \"up\" and interface_dict[\"interfaceStatus\"] == \"connected\"):\n                    down_loopback_interfaces.append(interface)\n        if loopback_count == self.inputs.number and len(down_loopback_interfaces) == 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure()\n            if loopback_count != self.inputs.number:\n                self.result.is_failure(f\"Found {loopback_count} Loopbacks when expecting {self.inputs.number}\")\n            elif len(down_loopback_interfaces) != 0:\n                self.result.is_failure(f\"The following Loopbacks are not up: {down_loopback_interfaces}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLoopbackCount-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger Number of loopback interfaces expected to be present. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyPortChannels","title":"VerifyPortChannels","text":"

Verifies there are no inactive ports in all port channels.

Expected Results
  • Success: The test will pass if there are no inactive ports in all port channels.
  • Failure: The test will fail if there is at least one inactive port in a port channel.
Examples
anta.tests.interfaces:\n  - VerifyPortChannels:\n
Source code in anta/tests/interfaces.py
class VerifyPortChannels(AntaTest):\n    \"\"\"Verifies there are no inactive ports in all port channels.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are no inactive ports in all port channels.\n    * Failure: The test will fail if there is at least one inactive port in a port channel.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyPortChannels:\n    ```\n    \"\"\"\n\n    name = \"VerifyPortChannels\"\n    description = \"Verifies there are no inactive ports in all port channels.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show port-channel\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPortChannels.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        po_with_inactive_ports: list[dict[str, str]] = []\n        for portchannel, portchannel_dict in command_output[\"portChannels\"].items():\n            if len(portchannel_dict[\"inactivePorts\"]) != 0:\n                po_with_inactive_ports.extend({portchannel: portchannel_dict[\"inactivePorts\"]})\n        if not po_with_inactive_ports:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following port-channels have inactive port(s): {po_with_inactive_ports}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifySVI","title":"VerifySVI","text":"

Verifies the status of all SVIs.

Expected Results
  • Success: The test will pass if all SVIs are up.
  • Failure: The test will fail if one or many SVIs are not up.
Examples
anta.tests.interfaces:\n  - VerifySVI:\n
Source code in anta/tests/interfaces.py
class VerifySVI(AntaTest):\n    \"\"\"Verifies the status of all SVIs.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all SVIs are up.\n    * Failure: The test will fail if one or many SVIs are not up.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifySVI:\n    ```\n    \"\"\"\n\n    name = \"VerifySVI\"\n    description = \"Verifies the status of all SVIs.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip interface brief\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySVI.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        down_svis = []\n        for interface in command_output[\"interfaces\"]:\n            interface_dict = command_output[\"interfaces\"][interface]\n            if \"Vlan\" in interface and not (interface_dict[\"lineProtocolStatus\"] == \"up\" and interface_dict[\"interfaceStatus\"] == \"connected\"):\n                down_svis.append(interface)\n        if len(down_svis) == 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following SVIs are not up: {down_svis}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyStormControlDrops","title":"VerifyStormControlDrops","text":"

Verifies there are no interface storm-control drop counters.

Expected Results
  • Success: The test will pass if there are no storm-control drop counters.
  • Failure: The test will fail if there is at least one storm-control drop counter.
Examples
anta.tests.interfaces:\n  - VerifyStormControlDrops:\n
Source code in anta/tests/interfaces.py
class VerifyStormControlDrops(AntaTest):\n    \"\"\"Verifies there are no interface storm-control drop counters.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are no storm-control drop counters.\n    * Failure: The test will fail if there is at least one storm-control drop counter.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyStormControlDrops:\n    ```\n    \"\"\"\n\n    name = \"VerifyStormControlDrops\"\n    description = \"Verifies there are no interface storm-control drop counters.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show storm-control\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyStormControlDrops.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        storm_controlled_interfaces: dict[str, dict[str, Any]] = {}\n        for interface, interface_dict in command_output[\"interfaces\"].items():\n            for traffic_type, traffic_type_dict in interface_dict[\"trafficTypes\"].items():\n                if \"drop\" in traffic_type_dict and traffic_type_dict[\"drop\"] != 0:\n                    storm_controlled_interface_dict = storm_controlled_interfaces.setdefault(interface, {})\n                    storm_controlled_interface_dict.update({traffic_type: traffic_type_dict[\"drop\"]})\n        if not storm_controlled_interfaces:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following interfaces have none 0 storm-control drop counters {storm_controlled_interfaces}\")\n
"},{"location":"api/tests.lanz/","title":"LANZ","text":""},{"location":"api/tests.lanz/#anta.tests.lanz.VerifyLANZ","title":"VerifyLANZ","text":"

Verifies if LANZ (Latency Analyzer) is enabled.

Expected Results
  • Success: The test will pass if LANZ is enabled.
  • Failure: The test will fail if LANZ is disabled.
Examples
anta.tests.lanz:\n  - VerifyLANZ:\n
Source code in anta/tests/lanz.py
class VerifyLANZ(AntaTest):\n    \"\"\"Verifies if LANZ (Latency Analyzer) is enabled.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if LANZ is enabled.\n    * Failure: The test will fail if LANZ is disabled.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.lanz:\n      - VerifyLANZ:\n    ```\n    \"\"\"\n\n    name = \"VerifyLANZ\"\n    description = \"Verifies if LANZ is enabled.\"\n    categories: ClassVar[list[str]] = [\"lanz\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show queue-monitor length status\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLANZ.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        if command_output[\"lanzEnabled\"] is not True:\n            self.result.is_failure(\"LANZ is not enabled\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.logging/","title":"Logging","text":""},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingAccounting","title":"VerifyLoggingAccounting","text":"

Verifies if AAA accounting logs are generated.

Expected Results
  • Success: The test will pass if AAA accounting logs are generated.
  • Failure: The test will fail if AAA accounting logs are NOT generated.
Examples
anta.tests.logging:\n  - VerifyLoggingAccounting:\n
Source code in anta/tests/logging.py
class VerifyLoggingAccounting(AntaTest):\n    \"\"\"Verifies if AAA accounting logs are generated.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if AAA accounting logs are generated.\n    * Failure: The test will fail if AAA accounting logs are NOT generated.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingAccounting:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingAccounting\"\n    description = \"Verifies if AAA accounting logs are generated.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa accounting logs | tail\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingAccounting.\"\"\"\n        pattern = r\"cmd=show aaa accounting logs\"\n        output = self.instance_commands[0].text_output\n        if re.search(pattern, output):\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"AAA accounting logs are not generated\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingErrors","title":"VerifyLoggingErrors","text":"

Verifies there are no syslog messages with a severity of ERRORS or higher.

Expected Results
  • Success: The test will pass if there are NO syslog messages with a severity of ERRORS or higher.
  • Failure: The test will fail if ERRORS or higher syslog messages are present.
Examples
anta.tests.logging:\n  - VerifyLoggingErrors:\n
Source code in anta/tests/logging.py
class VerifyLoggingErrors(AntaTest):\n    \"\"\"Verifies there are no syslog messages with a severity of ERRORS or higher.\n\n    Expected Results\n    ----------------\n      * Success: The test will pass if there are NO syslog messages with a severity of ERRORS or higher.\n      * Failure: The test will fail if ERRORS or higher syslog messages are present.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingErrors:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingErrors\"\n    description = \"Verifies there are no syslog messages with a severity of ERRORS or higher.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show logging threshold errors\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingErrors.\"\"\"\n        command_output = self.instance_commands[0].text_output\n\n        if len(command_output) == 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"Device has reported syslog messages with a severity of ERRORS or higher\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingHostname","title":"VerifyLoggingHostname","text":"

Verifies if logs are generated with the device FQDN.

Expected Results
  • Success: The test will pass if logs are generated with the device FQDN.
  • Failure: The test will fail if logs are NOT generated with the device FQDN.
Examples
anta.tests.logging:\n  - VerifyLoggingHostname:\n
Source code in anta/tests/logging.py
class VerifyLoggingHostname(AntaTest):\n    \"\"\"Verifies if logs are generated with the device FQDN.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if logs are generated with the device FQDN.\n    * Failure: The test will fail if logs are NOT generated with the device FQDN.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingHostname:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingHostname\"\n    description = \"Verifies if logs are generated with the device FQDN.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaCommand(command=\"show hostname\"),\n        AntaCommand(command=\"send log level informational message ANTA VerifyLoggingHostname validation\"),\n        AntaCommand(command=\"show logging informational last 30 seconds | grep ANTA\", ofmt=\"text\", use_cache=False),\n    ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingHostname.\"\"\"\n        output_hostname = self.instance_commands[0].json_output\n        output_logging = self.instance_commands[2].text_output\n        fqdn = output_hostname[\"fqdn\"]\n        lines = output_logging.strip().split(\"\\n\")[::-1]\n        log_pattern = r\"ANTA VerifyLoggingHostname validation\"\n        last_line_with_pattern = \"\"\n        for line in lines:\n            if re.search(log_pattern, line):\n                last_line_with_pattern = line\n                break\n        if fqdn in last_line_with_pattern:\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"Logs are not generated with the device FQDN\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingHosts","title":"VerifyLoggingHosts","text":"

Verifies logging hosts (syslog servers) for a specified VRF.

Expected Results
  • Success: The test will pass if the provided syslog servers are configured in the specified VRF.
  • Failure: The test will fail if the provided syslog servers are NOT configured in the specified VRF.
Examples
anta.tests.logging:\n  - VerifyLoggingHosts:\n      hosts:\n        - 1.1.1.1\n        - 2.2.2.2\n      vrf: default\n
Source code in anta/tests/logging.py
class VerifyLoggingHosts(AntaTest):\n    \"\"\"Verifies logging hosts (syslog servers) for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided syslog servers are configured in the specified VRF.\n    * Failure: The test will fail if the provided syslog servers are NOT configured in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingHosts:\n          hosts:\n            - 1.1.1.1\n            - 2.2.2.2\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingHosts\"\n    description = \"Verifies logging hosts (syslog servers) for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show logging\", ofmt=\"text\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyLoggingHosts test.\"\"\"\n\n        hosts: list[IPv4Address]\n        \"\"\"List of hosts (syslog servers) IP addresses.\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF to transport log messages. Defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingHosts.\"\"\"\n        output = self.instance_commands[0].text_output\n        not_configured = []\n        for host in self.inputs.hosts:\n            pattern = rf\"Logging to '{host!s}'.*VRF {self.inputs.vrf}\"\n            if not re.search(pattern, _get_logging_states(self.logger, output)):\n                not_configured.append(str(host))\n\n        if not not_configured:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Syslog servers {not_configured} are not configured in VRF {self.inputs.vrf}\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingHosts-attributes","title":"Inputs","text":"Name Type Description Default hosts list[IPv4Address] List of hosts (syslog servers) IP addresses. - vrf str The name of the VRF to transport log messages. Defaults to `default`. 'default'"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingLogsGeneration","title":"VerifyLoggingLogsGeneration","text":"

Verifies if logs are generated.

Expected Results
  • Success: The test will pass if logs are generated.
  • Failure: The test will fail if logs are NOT generated.
Examples
anta.tests.logging:\n  - VerifyLoggingLogsGeneration:\n
Source code in anta/tests/logging.py
class VerifyLoggingLogsGeneration(AntaTest):\n    \"\"\"Verifies if logs are generated.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if logs are generated.\n    * Failure: The test will fail if logs are NOT generated.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingLogsGeneration:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingLogsGeneration\"\n    description = \"Verifies if logs are generated.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaCommand(command=\"send log level informational message ANTA VerifyLoggingLogsGeneration validation\"),\n        AntaCommand(command=\"show logging informational last 30 seconds | grep ANTA\", ofmt=\"text\", use_cache=False),\n    ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingLogsGeneration.\"\"\"\n        log_pattern = r\"ANTA VerifyLoggingLogsGeneration validation\"\n        output = self.instance_commands[1].text_output\n        lines = output.strip().split(\"\\n\")[::-1]\n        for line in lines:\n            if re.search(log_pattern, line):\n                self.result.is_success()\n                return\n        self.result.is_failure(\"Logs are not generated\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingPersistent","title":"VerifyLoggingPersistent","text":"

Verifies if logging persistent is enabled and logs are saved in flash.

Expected Results
  • Success: The test will pass if logging persistent is enabled and logs are in flash.
  • Failure: The test will fail if logging persistent is disabled or no logs are saved in flash.
Examples
anta.tests.logging:\n  - VerifyLoggingPersistent:\n
Source code in anta/tests/logging.py
class VerifyLoggingPersistent(AntaTest):\n    \"\"\"Verifies if logging persistent is enabled and logs are saved in flash.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if logging persistent is enabled and logs are in flash.\n    * Failure: The test will fail if logging persistent is disabled or no logs are saved in flash.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingPersistent:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingPersistent\"\n    description = \"Verifies if logging persistent is enabled and logs are saved in flash.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaCommand(command=\"show logging\", ofmt=\"text\"),\n        AntaCommand(command=\"dir flash:/persist/messages\", ofmt=\"text\"),\n    ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingPersistent.\"\"\"\n        self.result.is_success()\n        log_output = self.instance_commands[0].text_output\n        dir_flash_output = self.instance_commands[1].text_output\n        if \"Persistent logging: disabled\" in _get_logging_states(self.logger, log_output):\n            self.result.is_failure(\"Persistent logging is disabled\")\n            return\n        pattern = r\"-rw-\\s+(\\d+)\"\n        persist_logs = re.search(pattern, dir_flash_output)\n        if not persist_logs or int(persist_logs.group(1)) == 0:\n            self.result.is_failure(\"No persistent logs are saved in flash\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingSourceIntf","title":"VerifyLoggingSourceIntf","text":"

Verifies logging source-interface for a specified VRF.

Expected Results
  • Success: The test will pass if the provided logging source-interface is configured in the specified VRF.
  • Failure: The test will fail if the provided logging source-interface is NOT configured in the specified VRF.
Examples
anta.tests.logging:\n  - VerifyLoggingSourceIntf:\n      interface: Management0\n      vrf: default\n
Source code in anta/tests/logging.py
class VerifyLoggingSourceIntf(AntaTest):\n    \"\"\"Verifies logging source-interface for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided logging source-interface is configured in the specified VRF.\n    * Failure: The test will fail if the provided logging source-interface is NOT configured in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingSourceIntf:\n          interface: Management0\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingSourceInt\"\n    description = \"Verifies logging source-interface for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show logging\", ofmt=\"text\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyLoggingSourceInt test.\"\"\"\n\n        interface: str\n        \"\"\"Source-interface to use as source IP of log messages.\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF to transport log messages. Defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingSourceInt.\"\"\"\n        output = self.instance_commands[0].text_output\n        pattern = rf\"Logging source-interface '{self.inputs.interface}'.*VRF {self.inputs.vrf}\"\n        if re.search(pattern, _get_logging_states(self.logger, output)):\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Source-interface '{self.inputs.interface}' is not configured in VRF {self.inputs.vrf}\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingSourceIntf-attributes","title":"Inputs","text":"Name Type Description Default interface str Source-interface to use as source IP of log messages. - vrf str The name of the VRF to transport log messages. Defaults to `default`. 'default'"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingTimestamp","title":"VerifyLoggingTimestamp","text":"

Verifies if logs are generated with the approprate timestamp.

Expected Results
  • Success: The test will pass if logs are generated with the appropriated timestamp.
  • Failure: The test will fail if logs are NOT generated with the appropriated timestamp.
Examples
anta.tests.logging:\n  - VerifyLoggingTimestamp:\n
Source code in anta/tests/logging.py
class VerifyLoggingTimestamp(AntaTest):\n    \"\"\"Verifies if logs are generated with the approprate timestamp.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if logs are generated with the appropriated timestamp.\n    * Failure: The test will fail if logs are NOT generated with the appropriated timestamp.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingTimestamp:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingTimestamp\"\n    description = \"Verifies if logs are generated with the appropriate timestamp.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaCommand(command=\"send log level informational message ANTA VerifyLoggingTimestamp validation\"),\n        AntaCommand(command=\"show logging informational last 30 seconds | grep ANTA\", ofmt=\"text\", use_cache=False),\n    ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingTimestamp.\"\"\"\n        log_pattern = r\"ANTA VerifyLoggingTimestamp validation\"\n        timestamp_pattern = r\"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{6}-\\d{2}:\\d{2}\"\n        output = self.instance_commands[1].text_output\n        lines = output.strip().split(\"\\n\")[::-1]\n        last_line_with_pattern = \"\"\n        for line in lines:\n            if re.search(log_pattern, line):\n                last_line_with_pattern = line\n                break\n        if re.search(timestamp_pattern, last_line_with_pattern):\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"Logs are not generated with the appropriate timestamp format\")\n
"},{"location":"api/tests.logging/#anta.tests.logging._get_logging_states","title":"_get_logging_states","text":"
_get_logging_states(logger: logging.Logger, command_output: str) -> str\n

Parse show logging output and gets operational logging states used in the tests in this module.

Args:
logger: The logger object.\ncommand_output: The `show logging` output.\n

Returns:

Type Description str: The operational logging states. Source code in anta/tests/logging.py
def _get_logging_states(logger: logging.Logger, command_output: str) -> str:\n    \"\"\"Parse `show logging` output and gets operational logging states used in the tests in this module.\n\n    Args:\n    ----\n        logger: The logger object.\n        command_output: The `show logging` output.\n\n    Returns\n    -------\n        str: The operational logging states.\n\n    \"\"\"\n    log_states = command_output.partition(\"\\n\\nExternal configuration:\")[0]\n    logger.debug(\"Device logging states:\\n%s\", log_states)\n    return log_states\n
"},{"location":"api/tests/","title":"Overview","text":""},{"location":"api/tests/#anta-tests-landing-page","title":"ANTA Tests Landing Page","text":"

This section describes all the available tests provided by the ANTA package.

"},{"location":"api/tests/#available-tests","title":"Available Tests","text":"

Here are the tests that we currently provide:

  • AAA
  • BFD
  • Configuration
  • Connectivity
  • Field Notice
  • GreenT
  • Hardware
  • Interfaces
  • LANZ
  • Logging
  • MLAG
  • Multicast
  • Profiles
  • PTP
  • Routing Generic
  • Routing BGP
  • Routing OSPF
  • Security
  • Services
  • SNMP
  • Software
  • STP
  • System
  • VLAN
  • VXLAN
"},{"location":"api/tests/#using-the-tests","title":"Using the Tests","text":"

All these tests can be imported in a catalog to be used by the ANTA CLI or in your own framework.

"},{"location":"api/tests.mlag/","title":"MLAG","text":""},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagConfigSanity","title":"VerifyMlagConfigSanity","text":"

Verifies there are no MLAG config-sanity inconsistencies.

Expected Results
  • Success: The test will pass if there are NO MLAG config-sanity inconsistencies.
  • Failure: The test will fail if there are MLAG config-sanity inconsistencies.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
  • Error: The test will give an error if \u2018mlagActive\u2019 is not found in the JSON response.
Examples
anta.tests.mlag:\n  - VerifyMlagConfigSanity:\n
Source code in anta/tests/mlag.py
class VerifyMlagConfigSanity(AntaTest):\n    \"\"\"Verifies there are no MLAG config-sanity inconsistencies.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO MLAG config-sanity inconsistencies.\n    * Failure: The test will fail if there are MLAG config-sanity inconsistencies.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n    * Error: The test will give an error if 'mlagActive' is not found in the JSON response.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagConfigSanity:\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagConfigSanity\"\n    description = \"Verifies there are no MLAG config-sanity inconsistencies.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag config-sanity\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagConfigSanity.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if (mlag_status := get_value(command_output, \"mlagActive\")) is None:\n            self.result.is_error(message=\"Incorrect JSON response - 'mlagActive' state was not found\")\n            return\n        if mlag_status is False:\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n        keys_to_verify = [\"globalConfiguration\", \"interfaceConfiguration\"]\n        verified_output = {key: get_value(command_output, key) for key in keys_to_verify}\n        if not any(verified_output.values()):\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"MLAG config-sanity returned inconsistencies: {verified_output}\")\n
"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagDualPrimary","title":"VerifyMlagDualPrimary","text":"

Verifies the dual-primary detection and its parameters of the MLAG configuration.

Expected Results
  • Success: The test will pass if the dual-primary detection is enabled and its parameters are configured properly.
  • Failure: The test will fail if the dual-primary detection is NOT enabled or its parameters are NOT configured properly.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
Examples
anta.tests.mlag:\n  - VerifyMlagDualPrimary:\n      detection_delay: 200\n      errdisabled: True\n      recovery_delay: 60\n      recovery_delay_non_mlag: 0\n
Source code in anta/tests/mlag.py
class VerifyMlagDualPrimary(AntaTest):\n    \"\"\"Verifies the dual-primary detection and its parameters of the MLAG configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the dual-primary detection is enabled and its parameters are configured properly.\n    * Failure: The test will fail if the dual-primary detection is NOT enabled or its parameters are NOT configured properly.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagDualPrimary:\n          detection_delay: 200\n          errdisabled: True\n          recovery_delay: 60\n          recovery_delay_non_mlag: 0\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagDualPrimary\"\n    description = \"Verifies the MLAG dual-primary detection parameters.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag detail\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyMlagDualPrimary test.\"\"\"\n\n        detection_delay: PositiveInteger\n        \"\"\"Delay detection (seconds).\"\"\"\n        errdisabled: bool = False\n        \"\"\"Errdisabled all interfaces when dual-primary is detected.\"\"\"\n        recovery_delay: PositiveInteger\n        \"\"\"Delay (seconds) after dual-primary detection resolves until non peer-link ports that are part of an MLAG are enabled.\"\"\"\n        recovery_delay_non_mlag: PositiveInteger\n        \"\"\"Delay (seconds) after dual-primary detection resolves until ports that are not part of an MLAG are enabled.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagDualPrimary.\"\"\"\n        errdisabled_action = \"errdisableAllInterfaces\" if self.inputs.errdisabled else \"none\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"state\"] == \"disabled\":\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n        if command_output[\"dualPrimaryDetectionState\"] == \"disabled\":\n            self.result.is_failure(\"Dual-primary detection is disabled\")\n            return\n        keys_to_verify = [\"detail.dualPrimaryDetectionDelay\", \"detail.dualPrimaryAction\", \"dualPrimaryMlagRecoveryDelay\", \"dualPrimaryNonMlagRecoveryDelay\"]\n        verified_output = {key: get_value(command_output, key) for key in keys_to_verify}\n        if (\n            verified_output[\"detail.dualPrimaryDetectionDelay\"] == self.inputs.detection_delay\n            and verified_output[\"detail.dualPrimaryAction\"] == errdisabled_action\n            and verified_output[\"dualPrimaryMlagRecoveryDelay\"] == self.inputs.recovery_delay\n            and verified_output[\"dualPrimaryNonMlagRecoveryDelay\"] == self.inputs.recovery_delay_non_mlag\n        ):\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The dual-primary parameters are not configured properly: {verified_output}\")\n
"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagDualPrimary-attributes","title":"Inputs","text":"Name Type Description Default detection_delay PositiveInteger Delay detection (seconds). - errdisabled bool Errdisabled all interfaces when dual-primary is detected. False recovery_delay PositiveInteger Delay (seconds) after dual-primary detection resolves until non peer-link ports that are part of an MLAG are enabled. - recovery_delay_non_mlag PositiveInteger Delay (seconds) after dual-primary detection resolves until ports that are not part of an MLAG are enabled. -"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagInterfaces","title":"VerifyMlagInterfaces","text":"

Verifies there are no inactive or active-partial MLAG ports.

Expected Results
  • Success: The test will pass if there are NO inactive or active-partial MLAG ports.
  • Failure: The test will fail if there are inactive or active-partial MLAG ports.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
Examples
anta.tests.mlag:\n  - VerifyMlagInterfaces:\n
Source code in anta/tests/mlag.py
class VerifyMlagInterfaces(AntaTest):\n    \"\"\"Verifies there are no inactive or active-partial MLAG ports.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO inactive or active-partial MLAG ports.\n    * Failure: The test will fail if there are inactive or active-partial MLAG ports.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagInterfaces:\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagInterfaces\"\n    description = \"Verifies there are no inactive or active-partial MLAG ports.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagInterfaces.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"state\"] == \"disabled\":\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n        if command_output[\"mlagPorts\"][\"Inactive\"] == 0 and command_output[\"mlagPorts\"][\"Active-partial\"] == 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"MLAG status is not OK: {command_output['mlagPorts']}\")\n
"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagPrimaryPriority","title":"VerifyMlagPrimaryPriority","text":"

Verify the MLAG (Multi-Chassis Link Aggregation) primary priority.

Expected Results
  • Success: The test will pass if the MLAG state is set as \u2018primary\u2019 and the priority matches the input.
  • Failure: The test will fail if the MLAG state is not \u2018primary\u2019 or the priority doesn\u2019t match the input.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
Examples
anta.tests.mlag:\n  - VerifyMlagPrimaryPriority:\n      primary_priority: 3276\n
Source code in anta/tests/mlag.py
class VerifyMlagPrimaryPriority(AntaTest):\n    \"\"\"Verify the MLAG (Multi-Chassis Link Aggregation) primary priority.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the MLAG state is set as 'primary' and the priority matches the input.\n    * Failure: The test will fail if the MLAG state is not 'primary' or the priority doesn't match the input.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagPrimaryPriority:\n          primary_priority: 3276\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagPrimaryPriority\"\n    description = \"Verifies the configuration of the MLAG primary priority.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag detail\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyMlagPrimaryPriority test.\"\"\"\n\n        primary_priority: MlagPriority\n        \"\"\"The expected MLAG primary priority.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagPrimaryPriority.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        self.result.is_success()\n        # Skip the test if MLAG is disabled\n        if command_output[\"state\"] == \"disabled\":\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n\n        mlag_state = get_value(command_output, \"detail.mlagState\")\n        primary_priority = get_value(command_output, \"detail.primaryPriority\")\n\n        # Check MLAG state\n        if mlag_state != \"primary\":\n            self.result.is_failure(\"The device is not set as MLAG primary.\")\n\n        # Check primary priority\n        if primary_priority != self.inputs.primary_priority:\n            self.result.is_failure(\n                f\"The primary priority does not match expected. Expected `{self.inputs.primary_priority}`, but found `{primary_priority}` instead.\",\n            )\n
"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagPrimaryPriority-attributes","title":"Inputs","text":"Name Type Description Default primary_priority MlagPriority The expected MLAG primary priority. -"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagReloadDelay","title":"VerifyMlagReloadDelay","text":"

Verifies the reload-delay parameters of the MLAG configuration.

Expected Results
  • Success: The test will pass if the reload-delay parameters are configured properly.
  • Failure: The test will fail if the reload-delay parameters are NOT configured properly.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
Examples
anta.tests.mlag:\n  - VerifyMlagReloadDelay:\n      reload_delay: 300\n      reload_delay_non_mlag: 330\n
Source code in anta/tests/mlag.py
class VerifyMlagReloadDelay(AntaTest):\n    \"\"\"Verifies the reload-delay parameters of the MLAG configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the reload-delay parameters are configured properly.\n    * Failure: The test will fail if the reload-delay parameters are NOT configured properly.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagReloadDelay:\n          reload_delay: 300\n          reload_delay_non_mlag: 330\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagReloadDelay\"\n    description = \"Verifies the MLAG reload-delay parameters.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyMlagReloadDelay test.\"\"\"\n\n        reload_delay: PositiveInteger\n        \"\"\"Delay (seconds) after reboot until non peer-link ports that are part of an MLAG are enabled.\"\"\"\n        reload_delay_non_mlag: PositiveInteger\n        \"\"\"Delay (seconds) after reboot until ports that are not part of an MLAG are enabled.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagReloadDelay.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"state\"] == \"disabled\":\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n        keys_to_verify = [\"reloadDelay\", \"reloadDelayNonMlag\"]\n        verified_output = {key: get_value(command_output, key) for key in keys_to_verify}\n        if verified_output[\"reloadDelay\"] == self.inputs.reload_delay and verified_output[\"reloadDelayNonMlag\"] == self.inputs.reload_delay_non_mlag:\n            self.result.is_success()\n\n        else:\n            self.result.is_failure(f\"The reload-delay parameters are not configured properly: {verified_output}\")\n
"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagReloadDelay-attributes","title":"Inputs","text":"Name Type Description Default reload_delay PositiveInteger Delay (seconds) after reboot until non peer-link ports that are part of an MLAG are enabled. - reload_delay_non_mlag PositiveInteger Delay (seconds) after reboot until ports that are not part of an MLAG are enabled. -"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagStatus","title":"VerifyMlagStatus","text":"

Verifies the health status of the MLAG configuration.

Expected Results
  • Success: The test will pass if the MLAG state is \u2018active\u2019, negotiation status is \u2018connected\u2019, peer-link status and local interface status are \u2018up\u2019.
  • Failure: The test will fail if the MLAG state is not \u2018active\u2019, negotiation status is not \u2018connected\u2019, peer-link status or local interface status are not \u2018up\u2019.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
Examples
anta.tests.mlag:\n  - VerifyMlagStatus:\n
Source code in anta/tests/mlag.py
class VerifyMlagStatus(AntaTest):\n    \"\"\"Verifies the health status of the MLAG configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the MLAG state is 'active', negotiation status is 'connected',\n                   peer-link status and local interface status are 'up'.\n    * Failure: The test will fail if the MLAG state is not 'active', negotiation status is not 'connected',\n                   peer-link status or local interface status are not 'up'.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagStatus\"\n    description = \"Verifies the health status of the MLAG configuration.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"state\"] == \"disabled\":\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n        keys_to_verify = [\"state\", \"negStatus\", \"localIntfStatus\", \"peerLinkStatus\"]\n        verified_output = {key: get_value(command_output, key) for key in keys_to_verify}\n        if (\n            verified_output[\"state\"] == \"active\"\n            and verified_output[\"negStatus\"] == \"connected\"\n            and verified_output[\"localIntfStatus\"] == \"up\"\n            and verified_output[\"peerLinkStatus\"] == \"up\"\n        ):\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"MLAG status is not OK: {verified_output}\")\n
"},{"location":"api/tests.multicast/","title":"Multicast","text":""},{"location":"api/tests.multicast/#anta.tests.multicast.VerifyIGMPSnoopingGlobal","title":"VerifyIGMPSnoopingGlobal","text":"

Verifies the IGMP snooping global status.

Expected Results
  • Success: The test will pass if the IGMP snooping global status matches the expected status.
  • Failure: The test will fail if the IGMP snooping global status does not match the expected status.
Examples
anta.tests.multicast:\n  - VerifyIGMPSnoopingGlobal:\n      enabled: True\n
Source code in anta/tests/multicast.py
class VerifyIGMPSnoopingGlobal(AntaTest):\n    \"\"\"Verifies the IGMP snooping global status.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the IGMP snooping global status matches the expected status.\n    * Failure: The test will fail if the IGMP snooping global status does not match the expected status.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.multicast:\n      - VerifyIGMPSnoopingGlobal:\n          enabled: True\n    ```\n    \"\"\"\n\n    name = \"VerifyIGMPSnoopingGlobal\"\n    description = \"Verifies the IGMP snooping global configuration.\"\n    categories: ClassVar[list[str]] = [\"multicast\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyIGMPSnoopingGlobal test.\"\"\"\n\n        enabled: bool\n        \"\"\"Whether global IGMP snopping must be enabled (True) or disabled (False).\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIGMPSnoopingGlobal.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        self.result.is_success()\n        igmp_state = command_output[\"igmpSnoopingState\"]\n        if igmp_state != \"enabled\" if self.inputs.enabled else igmp_state != \"disabled\":\n            self.result.is_failure(f\"IGMP state is not valid: {igmp_state}\")\n
"},{"location":"api/tests.multicast/#anta.tests.multicast.VerifyIGMPSnoopingGlobal-attributes","title":"Inputs","text":"Name Type Description Default enabled bool Whether global IGMP snopping must be enabled (True) or disabled (False). -"},{"location":"api/tests.multicast/#anta.tests.multicast.VerifyIGMPSnoopingVlans","title":"VerifyIGMPSnoopingVlans","text":"

Verifies the IGMP snooping status for the provided VLANs.

Expected Results
  • Success: The test will pass if the IGMP snooping status matches the expected status for the provided VLANs.
  • Failure: The test will fail if the IGMP snooping status does not match the expected status for the provided VLANs.
Examples
anta.tests.multicast:\n  - VerifyIGMPSnoopingVlans:\n      vlans:\n        10: False\n        12: False\n
Source code in anta/tests/multicast.py
class VerifyIGMPSnoopingVlans(AntaTest):\n    \"\"\"Verifies the IGMP snooping status for the provided VLANs.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the IGMP snooping status matches the expected status for the provided VLANs.\n    * Failure: The test will fail if the IGMP snooping status does not match the expected status for the provided VLANs.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.multicast:\n      - VerifyIGMPSnoopingVlans:\n          vlans:\n            10: False\n            12: False\n    ```\n    \"\"\"\n\n    name = \"VerifyIGMPSnoopingVlans\"\n    description = \"Verifies the IGMP snooping status for the provided VLANs.\"\n    categories: ClassVar[list[str]] = [\"multicast\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyIGMPSnoopingVlans test.\"\"\"\n\n        vlans: dict[Vlan, bool]\n        \"\"\"Dictionary with VLAN ID and whether IGMP snooping must be enabled (True) or disabled (False).\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIGMPSnoopingVlans.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        self.result.is_success()\n        for vlan, enabled in self.inputs.vlans.items():\n            if str(vlan) not in command_output[\"vlans\"]:\n                self.result.is_failure(f\"Supplied vlan {vlan} is not present on the device.\")\n                continue\n\n            igmp_state = command_output[\"vlans\"][str(vlan)][\"igmpSnoopingState\"]\n            if igmp_state != \"enabled\" if enabled else igmp_state != \"disabled\":\n                self.result.is_failure(f\"IGMP state for vlan {vlan} is {igmp_state}\")\n
"},{"location":"api/tests.multicast/#anta.tests.multicast.VerifyIGMPSnoopingVlans-attributes","title":"Inputs","text":"Name Type Description Default vlans dict[Vlan, bool] Dictionary with VLAN ID and whether IGMP snooping must be enabled (True) or disabled (False). -"},{"location":"api/tests.profiles/","title":"Profiles","text":""},{"location":"api/tests.profiles/#anta.tests.profiles.VerifyTcamProfile","title":"VerifyTcamProfile","text":"

Verifies that the device is using the provided Ternary Content-Addressable Memory (TCAM) profile.

Expected Results
  • Success: The test will pass if the provided TCAM profile is actually running on the device.
  • Failure: The test will fail if the provided TCAM profile is not running on the device.
Examples
anta.tests.profiles:\n  - VerifyTcamProfile:\n      profile: vxlan-routing\n
Source code in anta/tests/profiles.py
class VerifyTcamProfile(AntaTest):\n    \"\"\"Verifies that the device is using the provided Ternary Content-Addressable Memory (TCAM) profile.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided TCAM profile is actually running on the device.\n    * Failure: The test will fail if the provided TCAM profile is not running on the device.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.profiles:\n      - VerifyTcamProfile:\n          profile: vxlan-routing\n    ```\n    \"\"\"\n\n    name = \"VerifyTcamProfile\"\n    description = \"Verifies the device TCAM profile.\"\n    categories: ClassVar[list[str]] = [\"profiles\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hardware tcam profile\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTcamProfile test.\"\"\"\n\n        profile: str\n        \"\"\"Expected TCAM profile.\"\"\"\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTcamProfile.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"pmfProfiles\"][\"FixedSystem\"][\"status\"] == command_output[\"pmfProfiles\"][\"FixedSystem\"][\"config\"] == self.inputs.profile:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Incorrect profile running on device: {command_output['pmfProfiles']['FixedSystem']['status']}\")\n
"},{"location":"api/tests.profiles/#anta.tests.profiles.VerifyTcamProfile-attributes","title":"Inputs","text":"Name Type Description Default profile str Expected TCAM profile. -"},{"location":"api/tests.profiles/#anta.tests.profiles.VerifyUnifiedForwardingTableMode","title":"VerifyUnifiedForwardingTableMode","text":"

Verifies the device is using the expected UFT (Unified Forwarding Table) mode.

Expected Results
  • Success: The test will pass if the device is using the expected UFT mode.
  • Failure: The test will fail if the device is not using the expected UFT mode.
Examples
anta.tests.profiles:\n  - VerifyUnifiedForwardingTableMode:\n      mode: 3\n
Source code in anta/tests/profiles.py
class VerifyUnifiedForwardingTableMode(AntaTest):\n    \"\"\"Verifies the device is using the expected UFT (Unified Forwarding Table) mode.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is using the expected UFT mode.\n    * Failure: The test will fail if the device is not using the expected UFT mode.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.profiles:\n      - VerifyUnifiedForwardingTableMode:\n          mode: 3\n    ```\n    \"\"\"\n\n    name = \"VerifyUnifiedForwardingTableMode\"\n    description = \"Verifies the device is using the expected UFT mode.\"\n    categories: ClassVar[list[str]] = [\"profiles\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show platform trident forwarding-table partition\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyUnifiedForwardingTableMode test.\"\"\"\n\n        mode: Literal[0, 1, 2, 3, 4, \"flexible\"]\n        \"\"\"Expected UFT mode. Valid values are 0, 1, 2, 3, 4, or \"flexible\".\"\"\"\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyUnifiedForwardingTableMode.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"uftMode\"] == str(self.inputs.mode):\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device is not running correct UFT mode (expected: {self.inputs.mode} / running: {command_output['uftMode']})\")\n
"},{"location":"api/tests.profiles/#anta.tests.profiles.VerifyUnifiedForwardingTableMode-attributes","title":"Inputs","text":"Name Type Description Default mode Literal[0, 1, 2, 3, 4, 'flexible'] Expected UFT mode. Valid values are 0, 1, 2, 3, 4, or \"flexible\". -"},{"location":"api/tests.ptp/","title":"PTP","text":""},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpGMStatus","title":"VerifyPtpGMStatus","text":"

Verifies that the device is locked to a valid Precision Time Protocol (PTP) Grandmaster (GM).

To test PTP failover, re-run the test with a secondary GMID configured.

Expected Results
  • Success: The test will pass if the device is locked to the provided Grandmaster.
  • Failure: The test will fail if the device is not locked to the provided Grandmaster.
  • Error: The test will error if the \u2018gmClockIdentity\u2019 variable is not present in the command output.
Examples
anta.tests.ptp:\n  - VerifyPtpGMStatus:\n      gmid: 0xec:46:70:ff:fe:00:ff:a9\n
Source code in anta/tests/ptp.py
class VerifyPtpGMStatus(AntaTest):\n    \"\"\"Verifies that the device is locked to a valid Precision Time Protocol (PTP) Grandmaster (GM).\n\n    To test PTP failover, re-run the test with a secondary GMID configured.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is locked to the provided Grandmaster.\n    * Failure: The test will fail if the device is not locked to the provided Grandmaster.\n    * Error: The test will error if the 'gmClockIdentity' variable is not present in the command output.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.ptp:\n      - VerifyPtpGMStatus:\n          gmid: 0xec:46:70:ff:fe:00:ff:a9\n    ```\n    \"\"\"\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyPtpGMStatus test.\"\"\"\n\n        gmid: str\n        \"\"\"Identifier of the Grandmaster to which the device should be locked.\"\"\"\n\n    name = \"VerifyPtpGMStatus\"\n    description = \"Verifies that the device is locked to a valid PTP Grandmaster.\"\n    categories: ClassVar[list[str]] = [\"ptp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPtpGMStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        if (ptp_clock_summary := command_output.get(\"ptpClockSummary\")) is None:\n            self.result.is_error(\"'ptpClockSummary' variable is not present in the command output\")\n            return\n\n        if ptp_clock_summary[\"gmClockIdentity\"] != self.inputs.gmid:\n            self.result.is_failure(\n                f\"The device is locked to the following Grandmaster: '{ptp_clock_summary['gmClockIdentity']}', which differ from the expected one.\",\n            )\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpGMStatus-attributes","title":"Inputs","text":"Name Type Description Default gmid str Identifier of the Grandmaster to which the device should be locked. -"},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpLockStatus","title":"VerifyPtpLockStatus","text":"

Verifies that the device was locked to the upstream Precision Time Protocol (PTP) Grandmaster (GM) in the last minute.

Expected Results
  • Success: The test will pass if the device was locked to the upstream GM in the last minute.
  • Failure: The test will fail if the device was not locked to the upstream GM in the last minute.
  • Error: The test will error if the \u2018lastSyncTime\u2019 variable is not present in the command output.
Examples
anta.tests.ptp:\n  - VerifyPtpLockStatus:\n
Source code in anta/tests/ptp.py
class VerifyPtpLockStatus(AntaTest):\n    \"\"\"Verifies that the device was locked to the upstream Precision Time Protocol (PTP) Grandmaster (GM) in the last minute.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device was locked to the upstream GM in the last minute.\n    * Failure: The test will fail if the device was not locked to the upstream GM in the last minute.\n    * Error: The test will error if the 'lastSyncTime' variable is not present in the command output.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.ptp:\n      - VerifyPtpLockStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyPtpLockStatus\"\n    description = \"Verifies that the device was locked to the upstream PTP GM in the last minute.\"\n    categories: ClassVar[list[str]] = [\"ptp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPtpLockStatus.\"\"\"\n        threshold = 60\n        command_output = self.instance_commands[0].json_output\n\n        if (ptp_clock_summary := command_output.get(\"ptpClockSummary\")) is None:\n            self.result.is_error(\"'ptpClockSummary' variable is not present in the command output\")\n            return\n\n        time_difference = ptp_clock_summary[\"currentPtpSystemTime\"] - ptp_clock_summary[\"lastSyncTime\"]\n\n        if time_difference >= threshold:\n            self.result.is_failure(f\"The device lock is more than {threshold}s old: {time_difference}s\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpModeStatus","title":"VerifyPtpModeStatus","text":"

Verifies that the device is configured as a Precision Time Protocol (PTP) Boundary Clock (BC).

Expected Results
  • Success: The test will pass if the device is a BC.
  • Failure: The test will fail if the device is not a BC.
  • Error: The test will error if the \u2018ptpMode\u2019 variable is not present in the command output.
Examples
anta.tests.ptp:\n  - VerifyPtpModeStatus:\n
Source code in anta/tests/ptp.py
class VerifyPtpModeStatus(AntaTest):\n    \"\"\"Verifies that the device is configured as a Precision Time Protocol (PTP) Boundary Clock (BC).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is a BC.\n    * Failure: The test will fail if the device is not a BC.\n    * Error: The test will error if the 'ptpMode' variable is not present in the command output.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.ptp:\n      - VerifyPtpModeStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyPtpModeStatus\"\n    description = \"Verifies that the device is configured as a PTP Boundary Clock.\"\n    categories: ClassVar[list[str]] = [\"ptp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPtpModeStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        if (ptp_mode := command_output.get(\"ptpMode\")) is None:\n            self.result.is_error(\"'ptpMode' variable is not present in the command output\")\n            return\n\n        if ptp_mode != \"ptpBoundaryClock\":\n            self.result.is_failure(f\"The device is not configured as a PTP Boundary Clock: '{ptp_mode}'\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpOffset","title":"VerifyPtpOffset","text":"

Verifies that the Precision Time Protocol (PTP) timing offset is within \u00b1 1000ns from the master clock.

Expected Results
  • Success: The test will pass if the PTP timing offset is within \u00b1 1000ns from the master clock.
  • Failure: The test will fail if the PTP timing offset is greater than \u00b1 1000ns from the master clock.
  • Skipped: The test will be skipped if PTP is not configured.
Examples
anta.tests.ptp:\n  - VerifyPtpOffset:\n
Source code in anta/tests/ptp.py
class VerifyPtpOffset(AntaTest):\n    \"\"\"Verifies that the Precision Time Protocol (PTP) timing offset is within +/- 1000ns from the master clock.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the PTP timing offset is within +/- 1000ns from the master clock.\n    * Failure: The test will fail if the PTP timing offset is greater than +/- 1000ns from the master clock.\n    * Skipped: The test will be skipped if PTP is not configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.ptp:\n      - VerifyPtpOffset:\n    ```\n    \"\"\"\n\n    name = \"VerifyPtpOffset\"\n    description = \"Verifies that the PTP timing offset is within +/- 1000ns from the master clock.\"\n    categories: ClassVar[list[str]] = [\"ptp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp monitor\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPtpOffset.\"\"\"\n        threshold = 1000\n        offset_interfaces: dict[str, list[int]] = {}\n        command_output = self.instance_commands[0].json_output\n\n        if not command_output[\"ptpMonitorData\"]:\n            self.result.is_skipped(\"PTP is not configured\")\n            return\n\n        for interface in command_output[\"ptpMonitorData\"]:\n            if abs(interface[\"offsetFromMaster\"]) > threshold:\n                offset_interfaces.setdefault(interface[\"intf\"], []).append(interface[\"offsetFromMaster\"])\n\n        if offset_interfaces:\n            self.result.is_failure(f\"The device timing offset from master is greater than +/- {threshold}ns: {offset_interfaces}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpPortModeStatus","title":"VerifyPtpPortModeStatus","text":"

Verifies that all interfaces are in a valid Precision Time Protocol (PTP) state.

The interfaces can be in one of the following state: Master, Slave, Passive, or Disabled.

Expected Results
  • Success: The test will pass if all PTP enabled interfaces are in a valid state.
  • Failure: The test will fail if there are no PTP enabled interfaces or if some interfaces are not in a valid state.
Examples
anta.tests.ptp:\n  - VerifyPtpPortModeStatus:\n
Source code in anta/tests/ptp.py
class VerifyPtpPortModeStatus(AntaTest):\n    \"\"\"Verifies that all interfaces are in a valid Precision Time Protocol (PTP) state.\n\n    The interfaces can be in one of the following state: Master, Slave, Passive, or Disabled.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all PTP enabled interfaces are in a valid state.\n    * Failure: The test will fail if there are no PTP enabled interfaces or if some interfaces are not in a valid state.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.ptp:\n      - VerifyPtpPortModeStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyPtpPortModeStatus\"\n    description = \"Verifies the PTP interfaces state.\"\n    categories: ClassVar[list[str]] = [\"ptp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPtpPortModeStatus.\"\"\"\n        valid_state = (\"psMaster\", \"psSlave\", \"psPassive\", \"psDisabled\")\n        command_output = self.instance_commands[0].json_output\n\n        if not command_output[\"ptpIntfSummaries\"]:\n            self.result.is_failure(\"No interfaces are PTP enabled\")\n            return\n\n        invalid_interfaces = [\n            interface\n            for interface in command_output[\"ptpIntfSummaries\"]\n            for vlan in command_output[\"ptpIntfSummaries\"][interface][\"ptpIntfVlanSummaries\"]\n            if vlan[\"portState\"] not in valid_state\n        ]\n\n        if not invalid_interfaces:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following interface(s) are not in a valid PTP state: '{invalid_interfaces}'\")\n
"},{"location":"api/tests.routing.bgp/","title":"BGP","text":""},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPAdvCommunities","title":"VerifyBGPAdvCommunities","text":"

Verifies if the advertised communities of BGP peers are standard, extended, and large in the specified VRF.

Expected Results
  • Success: The test will pass if the advertised communities of BGP peers are standard, extended, and large in the specified VRF.
  • Failure: The test will fail if the advertised communities of BGP peers are not standard, extended, and large in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPAdvCommunities:\n        bgp_peers:\n          - peer_address: 172.30.11.17\n            vrf: default\n          - peer_address: 172.30.11.21\n            vrf: default\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPAdvCommunities(AntaTest):\n    \"\"\"Verifies if the advertised communities of BGP peers are standard, extended, and large in the specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the advertised communities of BGP peers are standard, extended, and large in the specified VRF.\n    * Failure: The test will fail if the advertised communities of BGP peers are not standard, extended, and large in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPAdvCommunities:\n            bgp_peers:\n              - peer_address: 172.30.11.17\n                vrf: default\n              - peer_address: 172.30.11.21\n                vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPAdvCommunities\"\n    description = \"Verifies the advertised communities of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPAdvCommunities test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of BGP peers.\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPAdvCommunities.\"\"\"\n        failures: dict[str, Any] = {\"bgp_peers\": {}}\n\n        # Iterate over each bgp peer\n        for bgp_peer in self.inputs.bgp_peers:\n            peer = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            failure: dict[str, dict[str, dict[str, Any]]] = {\"bgp_peers\": {peer: {vrf: {}}}}\n\n            # Verify BGP peer\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer)) is None\n            ):\n                failure[\"bgp_peers\"][peer][vrf] = {\"status\": \"Not configured\"}\n                failures = deep_update(failures, failure)\n                continue\n\n            # Verify BGP peer's advertised communities\n            bgp_output = bgp_output.get(\"advertisedCommunities\")\n            if not bgp_output[\"standard\"] or not bgp_output[\"extended\"] or not bgp_output[\"large\"]:\n                failure[\"bgp_peers\"][peer][vrf] = {\"advertised_communities\": bgp_output}\n                failures = deep_update(failures, failure)\n\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peers are not configured or advertised communities are not standard, extended, and large:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPAdvCommunities-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of BGP peers. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPAdvCommunities-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default'"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPExchangedRoutes","title":"VerifyBGPExchangedRoutes","text":"

Verifies if the BGP peers have correctly advertised and received routes.

The route type should be \u2018valid\u2019 and \u2018active\u2019 for a specified VRF.

Expected Results
  • Success: If the BGP peers have correctly advertised and received routes of type \u2018valid\u2019 and \u2018active\u2019 for a specified VRF.
  • Failure: If a BGP peer is not found, the expected advertised/received routes are not found, or the routes are not \u2018valid\u2019 or \u2018active\u2019.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPExchangedRoutes:\n        bgp_peers:\n          - peer_address: 172.30.255.5\n            vrf: default\n            advertised_routes:\n              - 192.0.254.5/32\n            received_routes:\n              - 192.0.255.4/32\n          - peer_address: 172.30.255.1\n            vrf: default\n            advertised_routes:\n              - 192.0.255.1/32\n              - 192.0.254.5/32\n            received_routes:\n              - 192.0.254.3/32\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPExchangedRoutes(AntaTest):\n    \"\"\"Verifies if the BGP peers have correctly advertised and received routes.\n\n    The route type should be 'valid' and 'active' for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: If the BGP peers have correctly advertised and received routes of type 'valid' and 'active' for a specified VRF.\n    * Failure: If a BGP peer is not found, the expected advertised/received routes are not found, or the routes are not 'valid' or 'active'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPExchangedRoutes:\n            bgp_peers:\n              - peer_address: 172.30.255.5\n                vrf: default\n                advertised_routes:\n                  - 192.0.254.5/32\n                received_routes:\n                  - 192.0.255.4/32\n              - peer_address: 172.30.255.1\n                vrf: default\n                advertised_routes:\n                  - 192.0.255.1/32\n                  - 192.0.254.5/32\n                received_routes:\n                  - 192.0.254.3/32\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPExchangedRoutes\"\n    description = \"Verifies the advertised and received routes of BGP peers.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaTemplate(template=\"show bgp neighbors {peer} advertised-routes vrf {vrf}\"),\n        AntaTemplate(template=\"show bgp neighbors {peer} routes vrf {vrf}\"),\n    ]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPExchangedRoutes test.\"\"\"\n\n        bgp_peers: list[BgpNeighbor]\n        \"\"\"List of BGP neighbors.\"\"\"\n\n        class BgpNeighbor(BaseModel):\n            \"\"\"Model for a BGP neighbor.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n            advertised_routes: list[IPv4Network]\n            \"\"\"List of advertised routes in CIDR format.\"\"\"\n            received_routes: list[IPv4Network]\n            \"\"\"List of received routes in CIDR format.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each BGP neighbor in the input list.\"\"\"\n        return [\n            template.render(peer=bgp_peer.peer_address, vrf=bgp_peer.vrf, advertised_routes=bgp_peer.advertised_routes, received_routes=bgp_peer.received_routes)\n            for bgp_peer in self.inputs.bgp_peers\n        ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPExchangedRoutes.\"\"\"\n        failures: dict[str, dict[str, Any]] = {\"bgp_peers\": {}}\n\n        # Iterating over command output for different peers\n        for command in self.instance_commands:\n            peer = str(command.params[\"peer\"])\n            vrf = command.params[\"vrf\"]\n            advertised_routes = command.params[\"advertised_routes\"]\n            received_routes = command.params[\"received_routes\"]\n            failure = {vrf: \"\"}\n\n            # Verify if a BGP peer is configured with the provided vrf\n            if not (bgp_routes := get_value(command.json_output, f\"vrfs.{vrf}.bgpRouteEntries\")):\n                failure[vrf] = \"Not configured\"\n                failures[\"bgp_peers\"][peer] = failure\n                continue\n\n            # Validate advertised routes\n            if \"advertised-routes\" in command.command:\n                failure_routes = _add_bgp_routes_failure(advertised_routes, bgp_routes, peer, vrf)\n\n            # Validate received routes\n            else:\n                failure_routes = _add_bgp_routes_failure(received_routes, bgp_routes, peer, vrf, route_type=\"received_routes\")\n            failures = deep_update(failures, failure_routes)\n\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peers are not found or routes are not exchanged properly:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPExchangedRoutes-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpNeighbor] List of BGP neighbors. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPExchangedRoutes-attributes","title":"BgpNeighbor","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default' advertised_routes list[IPv4Network] List of advertised routes in CIDR format. - received_routes list[IPv4Network] List of received routes in CIDR format. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerASNCap","title":"VerifyBGPPeerASNCap","text":"

Verifies the four octet asn capabilities of a BGP peer in a specified VRF.

Expected Results
  • Success: The test will pass if BGP peer\u2019s four octet asn capabilities are advertised, received, and enabled in the specified VRF.
  • Failure: The test will fail if BGP peers are not found or four octet asn capabilities are not advertised, received, and enabled in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeerASNCap:\n        bgp_peers:\n          - peer_address: 172.30.11.1\n            vrf: default\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeerASNCap(AntaTest):\n    \"\"\"Verifies the four octet asn capabilities of a BGP peer in a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if BGP peer's four octet asn capabilities are advertised, received, and enabled in the specified VRF.\n    * Failure: The test will fail if BGP peers are not found or four octet asn capabilities are not advertised, received, and enabled in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeerASNCap:\n            bgp_peers:\n              - peer_address: 172.30.11.1\n                vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeerASNCap\"\n    description = \"Verifies the four octet asn capabilities of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeerASNCap test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of BGP peers.\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeerASNCap.\"\"\"\n        failures: dict[str, Any] = {\"bgp_peers\": {}}\n\n        # Iterate over each bgp peer\n        for bgp_peer in self.inputs.bgp_peers:\n            peer = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            failure: dict[str, dict[str, dict[str, Any]]] = {\"bgp_peers\": {peer: {vrf: {}}}}\n\n            # Check if BGP output exists\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer)) is None\n            ):\n                failure[\"bgp_peers\"][peer][vrf] = {\"status\": \"Not configured\"}\n                failures = deep_update(failures, failure)\n                continue\n\n            bgp_output = get_value(bgp_output, \"neighborCapabilities.fourOctetAsnCap\")\n\n            # Check if  four octet asn capabilities are found\n            if not bgp_output:\n                failure[\"bgp_peers\"][peer][vrf] = {\"fourOctetAsnCap\": \"not found\"}\n                failures = deep_update(failures, failure)\n\n            # Check if capabilities are not advertised, received, or enabled\n            elif not all(bgp_output.get(prop, False) for prop in [\"advertised\", \"received\", \"enabled\"]):\n                failure[\"bgp_peers\"][peer][vrf] = {\"fourOctetAsnCap\": bgp_output}\n                failures = deep_update(failures, failure)\n\n        # Check if there are any failures\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peer four octet asn capabilities are not found or not ok:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerASNCap-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of BGP peers. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerASNCap-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default'"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount","title":"VerifyBGPPeerCount","text":"

Verifies the count of BGP peers for a given address family.

It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).

For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.

Please refer to the Input class attributes below for details.

Expected Results
  • Success: If the count of BGP peers matches the expected count for each address family and VRF.
  • Failure: If the count of BGP peers does not match the expected count, or if BGP is not configured for an expected VRF or address family.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeerCount:\n        address_families:\n          - afi: \"evpn\"\n            num_peers: 2\n          - afi: \"ipv4\"\n            safi: \"unicast\"\n            vrf: \"PROD\"\n            num_peers: 2\n          - afi: \"ipv4\"\n            safi: \"unicast\"\n            vrf: \"default\"\n            num_peers: 3\n          - afi: \"ipv4\"\n            safi: \"multicast\"\n            vrf: \"DEV\"\n            num_peers: 3\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeerCount(AntaTest):\n    \"\"\"Verifies the count of BGP peers for a given address family.\n\n    It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).\n\n    For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.\n\n    Please refer to the Input class attributes below for details.\n\n    Expected Results\n    ----------------\n    * Success: If the count of BGP peers matches the expected count for each address family and VRF.\n    * Failure: If the count of BGP peers does not match the expected count, or if BGP is not configured for an expected VRF or address family.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeerCount:\n            address_families:\n              - afi: \"evpn\"\n                num_peers: 2\n              - afi: \"ipv4\"\n                safi: \"unicast\"\n                vrf: \"PROD\"\n                num_peers: 2\n              - afi: \"ipv4\"\n                safi: \"unicast\"\n                vrf: \"default\"\n                num_peers: 3\n              - afi: \"ipv4\"\n                safi: \"multicast\"\n                vrf: \"DEV\"\n                num_peers: 3\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeerCount\"\n    description = \"Verifies the count of BGP peers.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaTemplate(template=\"show bgp {afi} {safi} summary vrf {vrf}\"),\n        AntaTemplate(template=\"show bgp {afi} summary\"),\n    ]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeerCount test.\"\"\"\n\n        address_families: list[BgpAfi]\n        \"\"\"List of BGP address families (BgpAfi).\"\"\"\n\n        class BgpAfi(BaseModel):\n            \"\"\"Model for a BGP address family (AFI) and subsequent address family (SAFI).\"\"\"\n\n            afi: Afi\n            \"\"\"BGP address family (AFI).\"\"\"\n            safi: Safi | None = None\n            \"\"\"Optional BGP subsequent service family (SAFI).\n\n            If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided.\n            \"\"\"\n            vrf: str = \"default\"\n            \"\"\"\n            Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`.\n\n            If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`.\n            \"\"\"\n            num_peers: PositiveInt\n            \"\"\"Number of expected BGP peer(s).\"\"\"\n\n            @model_validator(mode=\"after\")\n            def validate_inputs(self: BaseModel) -> BaseModel:\n                \"\"\"Validate the inputs provided to the BgpAfi class.\n\n                If afi is either ipv4 or ipv6, safi must be provided.\n\n                If afi is not ipv4 or ipv6, safi must not be provided and vrf must be default.\n                \"\"\"\n                if self.afi in [\"ipv4\", \"ipv6\"]:\n                    if self.safi is None:\n                        msg = \"'safi' must be provided when afi is ipv4 or ipv6\"\n                        raise ValueError(msg)\n                elif self.safi is not None:\n                    msg = \"'safi' must not be provided when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                elif self.vrf != \"default\":\n                    msg = \"'vrf' must be default when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                return self\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each BGP address family in the input list.\"\"\"\n        commands = []\n        for afi in self.inputs.address_families:\n            if template == VerifyBGPPeerCount.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi != \"sr-te\":\n                commands.append(template.render(afi=afi.afi, safi=afi.safi, vrf=afi.vrf, num_peers=afi.num_peers))\n\n            # For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6\n            elif template == VerifyBGPPeerCount.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi == \"sr-te\":\n                commands.append(template.render(afi=afi.safi, safi=afi.afi, vrf=afi.vrf, num_peers=afi.num_peers))\n            elif template == VerifyBGPPeerCount.commands[1] and afi.afi not in [\"ipv4\", \"ipv6\"]:\n                commands.append(template.render(afi=afi.afi, vrf=afi.vrf, num_peers=afi.num_peers))\n        return commands\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeerCount.\"\"\"\n        self.result.is_success()\n\n        failures: dict[tuple[str, Any], dict[str, Any]] = {}\n\n        for command in self.instance_commands:\n            peer_count = 0\n            command_output = command.json_output\n\n            afi = cast(Afi, command.params.get(\"afi\"))\n            safi = cast(Optional[Safi], command.params.get(\"safi\"))\n            afi_vrf = cast(str, command.params.get(\"vrf\"))\n            num_peers = cast(PositiveInt, command.params.get(\"num_peers\"))\n\n            # Swaping AFI and SAFI in case of SR-TE\n            if afi == \"sr-te\":\n                afi, safi = safi, afi\n\n            if not (vrfs := command_output.get(\"vrfs\")):\n                _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=\"Not Configured\")\n                continue\n\n            if afi_vrf == \"all\":\n                for vrf_data in vrfs.values():\n                    peer_count += len(vrf_data[\"peers\"])\n            else:\n                peer_count += len(command_output[\"vrfs\"][afi_vrf][\"peers\"])\n\n            if peer_count != num_peers:\n                _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=f\"Expected: {num_peers}, Actual: {peer_count}\")\n\n        if failures:\n            self.result.is_failure(f\"Failures: {list(failures.values())}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAfi] List of BGP address families (BgpAfi). -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount-attributes","title":"BgpAfi","text":"Name Type Description Default afi Afi BGP address family (AFI). - safi Safi | None Optional BGP subsequent service family (SAFI). If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided. None vrf str Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`. If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`. 'default' num_peers PositiveInt Number of expected BGP peer(s). -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMD5Auth","title":"VerifyBGPPeerMD5Auth","text":"

Verifies the MD5 authentication and state of IPv4 BGP peers in a specified VRF.

Expected Results
  • Success: The test will pass if IPv4 BGP peers are configured with MD5 authentication and state as established in the specified VRF.
  • Failure: The test will fail if IPv4 BGP peers are not found, state is not as established or MD5 authentication is not enabled in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeerMD5Auth:\n        bgp_peers:\n          - peer_address: 172.30.11.1\n            vrf: default\n          - peer_address: 172.30.11.5\n            vrf: default\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeerMD5Auth(AntaTest):\n    \"\"\"Verifies the MD5 authentication and state of IPv4 BGP peers in a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if IPv4 BGP peers are configured with MD5 authentication and state as established in the specified VRF.\n    * Failure: The test will fail if IPv4 BGP peers are not found, state is not as established or MD5 authentication is not enabled in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeerMD5Auth:\n            bgp_peers:\n              - peer_address: 172.30.11.1\n                vrf: default\n              - peer_address: 172.30.11.5\n                vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeerMD5Auth\"\n    description = \"Verifies the MD5 authentication and state of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeerMD5Auth test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of IPv4 BGP peers.\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeerMD5Auth.\"\"\"\n        failures: dict[str, Any] = {\"bgp_peers\": {}}\n\n        # Iterate over each command\n        for bgp_peer in self.inputs.bgp_peers:\n            peer = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            failure: dict[str, dict[str, dict[str, Any]]] = {\"bgp_peers\": {peer: {vrf: {}}}}\n\n            # Check if BGP output exists\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer)) is None\n            ):\n                failure[\"bgp_peers\"][peer][vrf] = {\"status\": \"Not configured\"}\n                failures = deep_update(failures, failure)\n                continue\n\n            # Check if BGP peer state and authentication\n            state = bgp_output.get(\"state\")\n            md5_auth_enabled = bgp_output.get(\"md5AuthEnabled\")\n            if state != \"Established\" or not md5_auth_enabled:\n                failure[\"bgp_peers\"][peer][vrf] = {\"state\": state, \"md5_auth_enabled\": md5_auth_enabled}\n                failures = deep_update(failures, failure)\n\n        # Check if there are any failures\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peers are not configured, not established or MD5 authentication is not enabled:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMD5Auth-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of IPv4 BGP peers. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMD5Auth-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default'"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMPCaps","title":"VerifyBGPPeerMPCaps","text":"

Verifies the multiprotocol capabilities of a BGP peer in a specified VRF.

Expected Results
  • Success: The test will pass if the BGP peer\u2019s multiprotocol capabilities are advertised, received, and enabled in the specified VRF.
  • Failure: The test will fail if BGP peers are not found or multiprotocol capabilities are not advertised, received, and enabled in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeerMPCaps:\n        bgp_peers:\n          - peer_address: 172.30.11.1\n            vrf: default\n            capabilities:\n              - ipv4Unicast\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeerMPCaps(AntaTest):\n    \"\"\"Verifies the multiprotocol capabilities of a BGP peer in a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the BGP peer's multiprotocol capabilities are advertised, received, and enabled in the specified VRF.\n    * Failure: The test will fail if BGP peers are not found or multiprotocol capabilities are not advertised, received, and enabled in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeerMPCaps:\n            bgp_peers:\n              - peer_address: 172.30.11.1\n                vrf: default\n                capabilities:\n                  - ipv4Unicast\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeerMPCaps\"\n    description = \"Verifies the multiprotocol capabilities of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeerMPCaps test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of BGP peers\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n            capabilities: list[MultiProtocolCaps]\n            \"\"\"List of multiprotocol capabilities to be verified.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeerMPCaps.\"\"\"\n        failures: dict[str, Any] = {\"bgp_peers\": {}}\n\n        # Iterate over each bgp peer\n        for bgp_peer in self.inputs.bgp_peers:\n            peer = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            capabilities = bgp_peer.capabilities\n            failure: dict[str, dict[str, dict[str, Any]]] = {\"bgp_peers\": {peer: {vrf: {}}}}\n\n            # Check if BGP output exists\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer)) is None\n            ):\n                failure[\"bgp_peers\"][peer][vrf] = {\"status\": \"Not configured\"}\n                failures = deep_update(failures, failure)\n                continue\n\n            # Check each capability\n            bgp_output = get_value(bgp_output, \"neighborCapabilities.multiprotocolCaps\")\n            for capability in capabilities:\n                capability_output = bgp_output.get(capability)\n\n                # Check if capabilities are missing\n                if not capability_output:\n                    failure[\"bgp_peers\"][peer][vrf][capability] = \"not found\"\n                    failures = deep_update(failures, failure)\n\n                # Check if capabilities are not advertised, received, or enabled\n                elif not all(capability_output.get(prop, False) for prop in [\"advertised\", \"received\", \"enabled\"]):\n                    failure[\"bgp_peers\"][peer][vrf][capability] = capability_output\n                    failures = deep_update(failures, failure)\n\n        # Check if there are any failures\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peer multiprotocol capabilities are not found or not ok:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMPCaps-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of BGP peers -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMPCaps-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default' capabilities list[MultiProtocolCaps] List of multiprotocol capabilities to be verified. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteRefreshCap","title":"VerifyBGPPeerRouteRefreshCap","text":"

Verifies the route refresh capabilities of a BGP peer in a specified VRF.

Expected Results
  • Success: The test will pass if the BGP peer\u2019s route refresh capabilities are advertised, received, and enabled in the specified VRF.
  • Failure: The test will fail if BGP peers are not found or route refresh capabilities are not advertised, received, and enabled in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeerRouteRefreshCap:\n        bgp_peers:\n          - peer_address: 172.30.11.1\n            vrf: default\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeerRouteRefreshCap(AntaTest):\n    \"\"\"Verifies the route refresh capabilities of a BGP peer in a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the BGP peer's route refresh capabilities are advertised, received, and enabled in the specified VRF.\n    * Failure: The test will fail if BGP peers are not found or route refresh capabilities are not advertised, received, and enabled in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeerRouteRefreshCap:\n            bgp_peers:\n              - peer_address: 172.30.11.1\n                vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeerRouteRefreshCap\"\n    description = \"Verifies the route refresh capabilities of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeerRouteRefreshCap test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of BGP peers\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeerRouteRefreshCap.\"\"\"\n        failures: dict[str, Any] = {\"bgp_peers\": {}}\n\n        # Iterate over each bgp peer\n        for bgp_peer in self.inputs.bgp_peers:\n            peer = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            failure: dict[str, dict[str, dict[str, Any]]] = {\"bgp_peers\": {peer: {vrf: {}}}}\n\n            # Check if BGP output exists\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer)) is None\n            ):\n                failure[\"bgp_peers\"][peer][vrf] = {\"status\": \"Not configured\"}\n                failures = deep_update(failures, failure)\n                continue\n\n            bgp_output = get_value(bgp_output, \"neighborCapabilities.routeRefreshCap\")\n\n            # Check if route refresh capabilities are found\n            if not bgp_output:\n                failure[\"bgp_peers\"][peer][vrf] = {\"routeRefreshCap\": \"not found\"}\n                failures = deep_update(failures, failure)\n\n            # Check if capabilities are not advertised, received, or enabled\n            elif not all(bgp_output.get(prop, False) for prop in [\"advertised\", \"received\", \"enabled\"]):\n                failure[\"bgp_peers\"][peer][vrf] = {\"routeRefreshCap\": bgp_output}\n                failures = deep_update(failures, failure)\n\n        # Check if there are any failures\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peer route refresh capabilities are not found or not ok:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteRefreshCap-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of BGP peers -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteRefreshCap-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default'"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth","title":"VerifyBGPPeersHealth","text":"

Verifies the health of BGP peers.

It will validate that all BGP sessions are established and all message queues for these BGP sessions are empty for a given address family.

It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).

For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.

Please refer to the Input class attributes below for details.

Expected Results
  • Success: If all BGP sessions are established and all messages queues are empty for each address family and VRF.
  • Failure: If there are issues with any of the BGP sessions, or if BGP is not configured for an expected VRF or address family.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeersHealth:\n        address_families:\n          - afi: \"evpn\"\n          - afi: \"ipv4\"\n            safi: \"unicast\"\n            vrf: \"default\"\n          - afi: \"ipv6\"\n            safi: \"unicast\"\n            vrf: \"DEV\"\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeersHealth(AntaTest):\n    \"\"\"Verifies the health of BGP peers.\n\n    It will validate that all BGP sessions are established and all message queues for these BGP sessions are empty for a given address family.\n\n    It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).\n\n    For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.\n\n    Please refer to the Input class attributes below for details.\n\n    Expected Results\n    ----------------\n    * Success: If all BGP sessions are established and all messages queues are empty for each address family and VRF.\n    * Failure: If there are issues with any of the BGP sessions, or if BGP is not configured for an expected VRF or address family.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeersHealth:\n            address_families:\n              - afi: \"evpn\"\n              - afi: \"ipv4\"\n                safi: \"unicast\"\n                vrf: \"default\"\n              - afi: \"ipv6\"\n                safi: \"unicast\"\n                vrf: \"DEV\"\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeersHealth\"\n    description = \"Verifies the health of BGP peers\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaTemplate(template=\"show bgp {afi} {safi} summary vrf {vrf}\"),\n        AntaTemplate(template=\"show bgp {afi} summary\"),\n    ]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeersHealth test.\"\"\"\n\n        address_families: list[BgpAfi]\n        \"\"\"List of BGP address families (BgpAfi).\"\"\"\n\n        class BgpAfi(BaseModel):\n            \"\"\"Model for a BGP address family (AFI) and subsequent address family (SAFI).\"\"\"\n\n            afi: Afi\n            \"\"\"BGP address family (AFI).\"\"\"\n            safi: Safi | None = None\n            \"\"\"Optional BGP subsequent service family (SAFI).\n\n            If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided.\n            \"\"\"\n            vrf: str = \"default\"\n            \"\"\"\n            Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`.\n\n            If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`.\n            \"\"\"\n\n            @model_validator(mode=\"after\")\n            def validate_inputs(self: BaseModel) -> BaseModel:\n                \"\"\"Validate the inputs provided to the BgpAfi class.\n\n                If afi is either ipv4 or ipv6, safi must be provided.\n\n                If afi is not ipv4 or ipv6, safi must not be provided and vrf must be default.\n                \"\"\"\n                if self.afi in [\"ipv4\", \"ipv6\"]:\n                    if self.safi is None:\n                        msg = \"'safi' must be provided when afi is ipv4 or ipv6\"\n                        raise ValueError(msg)\n                elif self.safi is not None:\n                    msg = \"'safi' must not be provided when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                elif self.vrf != \"default\":\n                    msg = \"'vrf' must be default when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                return self\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each BGP address family in the input list.\"\"\"\n        commands = []\n        for afi in self.inputs.address_families:\n            if template == VerifyBGPPeersHealth.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi != \"sr-te\":\n                commands.append(template.render(afi=afi.afi, safi=afi.safi, vrf=afi.vrf))\n\n            # For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6\n            elif template == VerifyBGPPeersHealth.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi == \"sr-te\":\n                commands.append(template.render(afi=afi.safi, safi=afi.afi, vrf=afi.vrf))\n            elif template == VerifyBGPPeersHealth.commands[1] and afi.afi not in [\"ipv4\", \"ipv6\"]:\n                commands.append(template.render(afi=afi.afi, vrf=afi.vrf))\n        return commands\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeersHealth.\"\"\"\n        self.result.is_success()\n\n        failures: dict[tuple[str, Any], dict[str, Any]] = {}\n\n        for command in self.instance_commands:\n            command_output = command.json_output\n\n            afi = cast(Afi, command.params.get(\"afi\"))\n            safi = cast(Optional[Safi], command.params.get(\"safi\"))\n\n            # Swaping AFI and SAFI in case of SR-TE\n            if afi == \"sr-te\":\n                afi, safi = safi, afi\n            afi_vrf = cast(str, command.params.get(\"vrf\"))\n\n            if not (vrfs := command_output.get(\"vrfs\")):\n                _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=\"Not Configured\")\n                continue\n\n            for vrf, vrf_data in vrfs.items():\n                if not (peers := vrf_data.get(\"peers\")):\n                    _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=\"No Peers\")\n                    continue\n\n                peer_issues = {}\n                for peer, peer_data in peers.items():\n                    issues = _check_peer_issues(peer_data)\n\n                    if issues:\n                        peer_issues[peer] = issues\n\n                if peer_issues:\n                    _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=vrf, issue=peer_issues)\n\n        if failures:\n            self.result.is_failure(f\"Failures: {list(failures.values())}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAfi] List of BGP address families (BgpAfi). -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth-attributes","title":"BgpAfi","text":"Name Type Description Default afi Afi BGP address family (AFI). - safi Safi | None Optional BGP subsequent service family (SAFI). If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided. None vrf str Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`. If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`. 'default'"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers","title":"VerifyBGPSpecificPeers","text":"

Verifies the health of specific BGP peer(s).

It will validate that the BGP session is established and all message queues for this BGP session are empty for the given peer(s).

It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).

For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.

Please refer to the Input class attributes below for details.

Expected Results
  • Success: If the BGP session is established and all messages queues are empty for each given peer.
  • Failure: If the BGP session has issues or is not configured, or if BGP is not configured for an expected VRF or address family.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPSpecificPeers:\n        address_families:\n          - afi: \"evpn\"\n            peers:\n              - 10.1.0.1\n              - 10.1.0.2\n          - afi: \"ipv4\"\n            safi: \"unicast\"\n            peers:\n              - 10.1.254.1\n              - 10.1.255.0\n              - 10.1.255.2\n              - 10.1.255.4\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPSpecificPeers(AntaTest):\n    \"\"\"Verifies the health of specific BGP peer(s).\n\n    It will validate that the BGP session is established and all message queues for this BGP session are empty for the given peer(s).\n\n    It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).\n\n    For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.\n\n    Please refer to the Input class attributes below for details.\n\n    Expected Results\n    ----------------\n    * Success: If the BGP session is established and all messages queues are empty for each given peer.\n    * Failure: If the BGP session has issues or is not configured, or if BGP is not configured for an expected VRF or address family.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPSpecificPeers:\n            address_families:\n              - afi: \"evpn\"\n                peers:\n                  - 10.1.0.1\n                  - 10.1.0.2\n              - afi: \"ipv4\"\n                safi: \"unicast\"\n                peers:\n                  - 10.1.254.1\n                  - 10.1.255.0\n                  - 10.1.255.2\n                  - 10.1.255.4\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPSpecificPeers\"\n    description = \"Verifies the health of specific BGP peer(s).\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaTemplate(template=\"show bgp {afi} {safi} summary vrf {vrf}\"),\n        AntaTemplate(template=\"show bgp {afi} summary\"),\n    ]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPSpecificPeers test.\"\"\"\n\n        address_families: list[BgpAfi]\n        \"\"\"List of BGP address families (BgpAfi).\"\"\"\n\n        class BgpAfi(BaseModel):\n            \"\"\"Model for a BGP address family (AFI) and subsequent address family (SAFI).\"\"\"\n\n            afi: Afi\n            \"\"\"BGP address family (AFI).\"\"\"\n            safi: Safi | None = None\n            \"\"\"Optional BGP subsequent service family (SAFI).\n\n            If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided.\n            \"\"\"\n            vrf: str = \"default\"\n            \"\"\"\n            Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`.\n\n            `all` is NOT supported.\n\n            If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`.\n            \"\"\"\n            peers: list[IPv4Address | IPv6Address]\n            \"\"\"List of BGP IPv4 or IPv6 peer.\"\"\"\n\n            @model_validator(mode=\"after\")\n            def validate_inputs(self: BaseModel) -> BaseModel:\n                \"\"\"Validate the inputs provided to the BgpAfi class.\n\n                If afi is either ipv4 or ipv6, safi must be provided and vrf must NOT be all.\n\n                If afi is not ipv4 or ipv6, safi must not be provided and vrf must be default.\n                \"\"\"\n                if self.afi in [\"ipv4\", \"ipv6\"]:\n                    if self.safi is None:\n                        msg = \"'safi' must be provided when afi is ipv4 or ipv6\"\n                        raise ValueError(msg)\n                    if self.vrf == \"all\":\n                        msg = \"'all' is not supported in this test. Use VerifyBGPPeersHealth test instead.\"\n                        raise ValueError(msg)\n                elif self.safi is not None:\n                    msg = \"'safi' must not be provided when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                elif self.vrf != \"default\":\n                    msg = \"'vrf' must be default when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                return self\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each BGP address family in the input list.\"\"\"\n        commands = []\n\n        for afi in self.inputs.address_families:\n            if template == VerifyBGPSpecificPeers.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi != \"sr-te\":\n                commands.append(template.render(afi=afi.afi, safi=afi.safi, vrf=afi.vrf, peers=afi.peers))\n\n            # For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6\n            elif template == VerifyBGPSpecificPeers.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi == \"sr-te\":\n                commands.append(template.render(afi=afi.safi, safi=afi.afi, vrf=afi.vrf, peers=afi.peers))\n            elif template == VerifyBGPSpecificPeers.commands[1] and afi.afi not in [\"ipv4\", \"ipv6\"]:\n                commands.append(template.render(afi=afi.afi, vrf=afi.vrf, peers=afi.peers))\n        return commands\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPSpecificPeers.\"\"\"\n        self.result.is_success()\n\n        failures: dict[tuple[str, Any], dict[str, Any]] = {}\n\n        for command in self.instance_commands:\n            command_output = command.json_output\n\n            afi = cast(Afi, command.params.get(\"afi\"))\n            safi = cast(Optional[Safi], command.params.get(\"safi\"))\n\n            # Swaping AFI and SAFI in case of SR-TE\n            if afi == \"sr-te\":\n                afi, safi = safi, afi\n            afi_vrf = cast(str, command.params.get(\"vrf\"))\n            afi_peers = cast(List[Union[IPv4Address, IPv6Address]], command.params.get(\"peers\", []))\n\n            if not (vrfs := command_output.get(\"vrfs\")):\n                _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=\"Not Configured\")\n                continue\n\n            peer_issues = {}\n            for peer in afi_peers:\n                peer_ip = str(peer)\n                peer_data = get_value(dictionary=vrfs, key=f\"{afi_vrf}_peers_{peer_ip}\", separator=\"_\")\n                issues = _check_peer_issues(peer_data)\n                if issues:\n                    peer_issues[peer_ip] = issues\n\n            if peer_issues:\n                _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=peer_issues)\n\n        if failures:\n            self.result.is_failure(f\"Failures: {list(failures.values())}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAfi] List of BGP address families (BgpAfi). -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers-attributes","title":"BgpAfi","text":"Name Type Description Default afi Afi BGP address family (AFI). - safi Safi | None Optional BGP subsequent service family (SAFI). If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided. None vrf str Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`. `all` is NOT supported. If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`. 'default' peers list[IPv4Address | IPv6Address] List of BGP IPv4 or IPv6 peer. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPTimers","title":"VerifyBGPTimers","text":"

Verifies if the BGP peers are configured with the correct hold and keep-alive timers in the specified VRF.

Expected Results
  • Success: The test will pass if the hold and keep-alive timers are correct for BGP peers in the specified VRF.
  • Failure: The test will fail if BGP peers are not found or hold and keep-alive timers are not correct in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPTimers:\n        bgp_peers:\n          - peer_address: 172.30.11.1\n            vrf: default\n            hold_time: 180\n            keep_alive_time: 60\n          - peer_address: 172.30.11.5\n            vrf: default\n            hold_time: 180\n            keep_alive_time: 60\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPTimers(AntaTest):\n    \"\"\"Verifies if the BGP peers are configured with the correct hold and keep-alive timers in the specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the hold and keep-alive timers are correct for BGP peers in the specified VRF.\n    * Failure: The test will fail if BGP peers are not found or hold and keep-alive timers are not correct in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPTimers:\n            bgp_peers:\n              - peer_address: 172.30.11.1\n                vrf: default\n                hold_time: 180\n                keep_alive_time: 60\n              - peer_address: 172.30.11.5\n                vrf: default\n                hold_time: 180\n                keep_alive_time: 60\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPTimers\"\n    description = \"Verifies the timers of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPTimers test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of BGP peers\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n            hold_time: int = Field(ge=3, le=7200)\n            \"\"\"BGP hold time in seconds.\"\"\"\n            keep_alive_time: int = Field(ge=0, le=3600)\n            \"\"\"BGP keep-alive time in seconds.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPTimers.\"\"\"\n        failures: dict[str, Any] = {}\n\n        # Iterate over each bgp peer\n        for bgp_peer in self.inputs.bgp_peers:\n            peer_address = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            hold_time = bgp_peer.hold_time\n            keep_alive_time = bgp_peer.keep_alive_time\n\n            # Verify BGP peer\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer_address)) is None\n            ):\n                failures[peer_address] = {vrf: \"Not configured\"}\n                continue\n\n            # Verify BGP peer's hold and keep alive timers\n            if bgp_output.get(\"holdTime\") != hold_time or bgp_output.get(\"keepaliveTime\") != keep_alive_time:\n                failures[peer_address] = {vrf: {\"hold_time\": bgp_output.get(\"holdTime\"), \"keep_alive_time\": bgp_output.get(\"keepaliveTime\")}}\n\n        if not failures:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peers are not configured or hold and keep-alive timers are not correct:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPTimers-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of BGP peers -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPTimers-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default' hold_time int BGP hold time in seconds. Field(ge=3, le=7200) keep_alive_time int BGP keep-alive time in seconds. Field(ge=0, le=3600)"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyEVPNType2Route","title":"VerifyEVPNType2Route","text":"

Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI.

Expected Results
  • Success: If all provided VXLAN endpoints have at least one valid and active path to their EVPN Type-2 routes.
  • Failure: If any of the provided VXLAN endpoints do not have at least one valid and active path to their EVPN Type-2 routes.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyEVPNType2Route:\n        vxlan_endpoints:\n          - address: 192.168.20.102\n            vni: 10020\n          - address: aac1.ab5d.b41e\n            vni: 10010\n
Source code in anta/tests/routing/bgp.py
class VerifyEVPNType2Route(AntaTest):\n    \"\"\"Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI.\n\n    Expected Results\n    ----------------\n    * Success: If all provided VXLAN endpoints have at least one valid and active path to their EVPN Type-2 routes.\n    * Failure: If any of the provided VXLAN endpoints do not have at least one valid and active path to their EVPN Type-2 routes.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyEVPNType2Route:\n            vxlan_endpoints:\n              - address: 192.168.20.102\n                vni: 10020\n              - address: aac1.ab5d.b41e\n                vni: 10010\n    ```\n    \"\"\"\n\n    name = \"VerifyEVPNType2Route\"\n    description = \"Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp evpn route-type mac-ip {address} vni {vni}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyEVPNType2Route test.\"\"\"\n\n        vxlan_endpoints: list[VxlanEndpoint]\n        \"\"\"List of VXLAN endpoints to verify.\"\"\"\n\n        class VxlanEndpoint(BaseModel):\n            \"\"\"Model for a VXLAN endpoint.\"\"\"\n\n            address: IPv4Address | MacAddress\n            \"\"\"IPv4 or MAC address of the VXLAN endpoint.\"\"\"\n            vni: Vni\n            \"\"\"VNI of the VXLAN endpoint.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each VXLAN endpoint in the input list.\"\"\"\n        return [template.render(address=endpoint.address, vni=endpoint.vni) for endpoint in self.inputs.vxlan_endpoints]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEVPNType2Route.\"\"\"\n        self.result.is_success()\n        no_evpn_routes = []\n        bad_evpn_routes = []\n\n        for command in self.instance_commands:\n            address = str(command.params[\"address\"])\n            vni = command.params[\"vni\"]\n            # Verify that the VXLAN endpoint is in the BGP EVPN table\n            evpn_routes = command.json_output[\"evpnRoutes\"]\n            if not evpn_routes:\n                no_evpn_routes.append((address, vni))\n                continue\n            # Verify that each EVPN route has at least one valid and active path\n            for route, route_data in evpn_routes.items():\n                has_active_path = False\n                for path in route_data[\"evpnRoutePaths\"]:\n                    if path[\"routeType\"][\"valid\"] is True and path[\"routeType\"][\"active\"] is True:\n                        # At least one path is valid and active, no need to check the other paths\n                        has_active_path = True\n                        break\n                if not has_active_path:\n                    bad_evpn_routes.append(route)\n\n        if no_evpn_routes:\n            self.result.is_failure(f\"The following VXLAN endpoint do not have any EVPN Type-2 route: {no_evpn_routes}\")\n        if bad_evpn_routes:\n            self.result.is_failure(f\"The following EVPN Type-2 routes do not have at least one valid and active path: {bad_evpn_routes}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyEVPNType2Route-attributes","title":"Inputs","text":"Name Type Description Default vxlan_endpoints list[VxlanEndpoint] List of VXLAN endpoints to verify. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyEVPNType2Route-attributes","title":"VxlanEndpoint","text":"Name Type Description Default address IPv4Address | MacAddress IPv4 or MAC address of the VXLAN endpoint. - vni Vni VNI of the VXLAN endpoint. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp._add_bgp_failures","title":"_add_bgp_failures","text":"
_add_bgp_failures(failures: dict[tuple[str, str | None], dict[str, Any]], afi: Afi, safi: Safi | None, vrf: str, issue: str | dict[str, Any]) -> None\n

Add a BGP failure entry to the given failures dictionary.

Note: This function modifies failures in-place.

Args:
failures (dict): The dictionary to which the failure will be added.\nafi (Afi): The address family identifier.\nvrf (str): The VRF name.\nsafi (Safi, optional): The subsequent address family identifier.\nissue (Any): A description of the issue. Can be of any type.\n
Example:

The failures dictionnary will have the following structure: { (\u2018afi1\u2019, \u2018safi1\u2019): { \u2018afi\u2019: \u2018afi1\u2019, \u2018safi\u2019: \u2018safi1\u2019, \u2018vrfs\u2019: { \u2018vrf1\u2019: issue1, \u2018vrf2\u2019: issue2 } }, (\u2018afi2\u2019, None): { \u2018afi\u2019: \u2018afi2\u2019, \u2018vrfs\u2019: { \u2018vrf1\u2019: issue3 } } }

Source code in anta/tests/routing/bgp.py
def _add_bgp_failures(failures: dict[tuple[str, str | None], dict[str, Any]], afi: Afi, safi: Safi | None, vrf: str, issue: str | dict[str, Any]) -> None:\n    \"\"\"Add a BGP failure entry to the given `failures` dictionary.\n\n    Note: This function modifies `failures` in-place.\n\n    Args:\n    ----\n        failures (dict): The dictionary to which the failure will be added.\n        afi (Afi): The address family identifier.\n        vrf (str): The VRF name.\n        safi (Safi, optional): The subsequent address family identifier.\n        issue (Any): A description of the issue. Can be of any type.\n\n    Example:\n    -------\n    The `failures` dictionnary will have the following structure:\n        {\n            ('afi1', 'safi1'): {\n                'afi': 'afi1',\n                'safi': 'safi1',\n                'vrfs': {\n                    'vrf1': issue1,\n                    'vrf2': issue2\n                }\n            },\n            ('afi2', None): {\n                'afi': 'afi2',\n                'vrfs': {\n                    'vrf1': issue3\n                }\n            }\n        }\n\n    \"\"\"\n    key = (afi, safi)\n\n    failure_entry = failures.setdefault(key, {\"afi\": afi, \"safi\": safi, \"vrfs\": {}}) if safi else failures.setdefault(key, {\"afi\": afi, \"vrfs\": {}})\n\n    failure_entry[\"vrfs\"][vrf] = issue\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp._add_bgp_routes_failure","title":"_add_bgp_routes_failure","text":"
_add_bgp_routes_failure(bgp_routes: list[str], bgp_output: dict[str, Any], peer: str, vrf: str, route_type: str = 'advertised_routes') -> dict[str, dict[str, dict[str, dict[str, list[str]]]]]\n

Identify missing BGP routes and invalid or inactive route entries.

This function checks the BGP output from the device against the expected routes.

It identifies any missing routes as well as any routes that are invalid or inactive. The results are returned in a dictionary.

Args:
bgp_routes (list[str]): The list of expected routes.\nbgp_output (dict[str, Any]): The BGP output from the device.\npeer (str): The IP address of the BGP peer.\nvrf (str): The name of the VRF for which the routes need to be verified.\nroute_type (str, optional): The type of BGP routes. Defaults to 'advertised_routes'.\n

Returns:

Type Description dict[str, dict[str, dict[str, dict[str, list[str]]]]]: A dictionary containing the missing routes and invalid or inactive routes. Source code in anta/tests/routing/bgp.py
def _add_bgp_routes_failure(\n    bgp_routes: list[str],\n    bgp_output: dict[str, Any],\n    peer: str,\n    vrf: str,\n    route_type: str = \"advertised_routes\",\n) -> dict[str, dict[str, dict[str, dict[str, list[str]]]]]:\n    \"\"\"Identify missing BGP routes and invalid or inactive route entries.\n\n    This function checks the BGP output from the device against the expected routes.\n\n    It identifies any missing routes as well as any routes that are invalid or inactive. The results are returned in a dictionary.\n\n    Args:\n    ----\n        bgp_routes (list[str]): The list of expected routes.\n        bgp_output (dict[str, Any]): The BGP output from the device.\n        peer (str): The IP address of the BGP peer.\n        vrf (str): The name of the VRF for which the routes need to be verified.\n        route_type (str, optional): The type of BGP routes. Defaults to 'advertised_routes'.\n\n    Returns\n    -------\n        dict[str, dict[str, dict[str, dict[str, list[str]]]]]: A dictionary containing the missing routes and invalid or inactive routes.\n\n    \"\"\"\n    # Prepare the failure routes dictionary\n    failure_routes: dict[str, dict[str, Any]] = {}\n\n    # Iterate over the expected BGP routes\n    for route in bgp_routes:\n        str_route = str(route)\n        failure = {\"bgp_peers\": {peer: {vrf: {route_type: {str_route: Any}}}}}\n\n        # Check if the route is missing in the BGP output\n        if str_route not in bgp_output:\n            # If missing, add it to the failure routes dictionary\n            failure[\"bgp_peers\"][peer][vrf][route_type][str_route] = \"Not found\"\n            failure_routes = deep_update(failure_routes, failure)\n            continue\n\n        # Check if the route is active and valid\n        is_active = bgp_output[str_route][\"bgpRoutePaths\"][0][\"routeType\"][\"valid\"]\n        is_valid = bgp_output[str_route][\"bgpRoutePaths\"][0][\"routeType\"][\"active\"]\n\n        # If the route is either inactive or invalid, add it to the failure routes dictionary\n        if not is_active or not is_valid:\n            failure[\"bgp_peers\"][peer][vrf][route_type][str_route] = {\"valid\": is_valid, \"active\": is_active}\n            failure_routes = deep_update(failure_routes, failure)\n\n    return failure_routes\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp._check_peer_issues","title":"_check_peer_issues","text":"
_check_peer_issues(peer_data: dict[str, Any] | None) -> dict[str, Any]\n

Check for issues in BGP peer data.

Args:
peer_data (dict, optional): The BGP peer data dictionary nested in the `show bgp <afi> <safi> summary` command.\n

Returns:

Type Description dict: Dictionary with keys indicating issues or an empty dictionary if no issues.

Raises:

Type Description ValueError: If any of the required keys (\"peerState\", \"inMsgQueue\", \"outMsgQueue\") are missing in `peer_data`, i.e. invalid BGP peer data. Example:
{\"peerNotFound\": True}\n{\"peerState\": \"Idle\", \"inMsgQueue\": 2, \"outMsgQueue\": 0}\n{}\n
Source code in anta/tests/routing/bgp.py
def _check_peer_issues(peer_data: dict[str, Any] | None) -> dict[str, Any]:\n    \"\"\"Check for issues in BGP peer data.\n\n    Args:\n    ----\n        peer_data (dict, optional): The BGP peer data dictionary nested in the `show bgp <afi> <safi> summary` command.\n\n    Returns\n    -------\n        dict: Dictionary with keys indicating issues or an empty dictionary if no issues.\n\n    Raises\n    ------\n        ValueError: If any of the required keys (\"peerState\", \"inMsgQueue\", \"outMsgQueue\") are missing in `peer_data`, i.e. invalid BGP peer data.\n\n    Example:\n    -------\n        {\"peerNotFound\": True}\n        {\"peerState\": \"Idle\", \"inMsgQueue\": 2, \"outMsgQueue\": 0}\n        {}\n\n    \"\"\"\n    if peer_data is None:\n        return {\"peerNotFound\": True}\n\n    if any(key not in peer_data for key in [\"peerState\", \"inMsgQueue\", \"outMsgQueue\"]):\n        msg = \"Provided BGP peer data is invalid.\"\n        raise ValueError(msg)\n\n    if peer_data[\"peerState\"] != \"Established\" or peer_data[\"inMsgQueue\"] != 0 or peer_data[\"outMsgQueue\"] != 0:\n        return {\"peerState\": peer_data[\"peerState\"], \"inMsgQueue\": peer_data[\"inMsgQueue\"], \"outMsgQueue\": peer_data[\"outMsgQueue\"]}\n\n    return {}\n
"},{"location":"api/tests.routing.generic/","title":"Generic","text":""},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingProtocolModel","title":"VerifyRoutingProtocolModel","text":"

Verifies the configured routing protocol model is the one we expect.

Expected Results
  • Success: The test will pass if the configured routing protocol model is the one we expect.
  • Failure: The test will fail if the configured routing protocol model is not the one we expect.
Examples
anta.tests.routing:\n  generic:\n    - VerifyRoutingProtocolModel:\n        model: multi-agent\n
Source code in anta/tests/routing/generic.py
class VerifyRoutingProtocolModel(AntaTest):\n    \"\"\"Verifies the configured routing protocol model is the one we expect.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the configured routing protocol model is the one we expect.\n    * Failure: The test will fail if the configured routing protocol model is not the one we expect.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      generic:\n        - VerifyRoutingProtocolModel:\n            model: multi-agent\n    ```\n    \"\"\"\n\n    name = \"VerifyRoutingProtocolModel\"\n    description = \"Verifies the configured routing protocol model.\"\n    categories: ClassVar[list[str]] = [\"routing\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip route summary\", revision=3)]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyRoutingProtocolModel test.\"\"\"\n\n        model: Literal[\"multi-agent\", \"ribd\"] = \"multi-agent\"\n        \"\"\"Expected routing protocol model. Defaults to `multi-agent`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyRoutingProtocolModel.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        configured_model = command_output[\"protoModelStatus\"][\"configuredProtoModel\"]\n        operating_model = command_output[\"protoModelStatus\"][\"operatingProtoModel\"]\n        if configured_model == operating_model == self.inputs.model:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"routing model is misconfigured: configured: {configured_model} - operating: {operating_model} - expected: {self.inputs.model}\")\n
"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingProtocolModel-attributes","title":"Inputs","text":"Name Type Description Default model Literal['multi-agent', 'ribd'] Expected routing protocol model. Defaults to `multi-agent`. 'multi-agent'"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableEntry","title":"VerifyRoutingTableEntry","text":"

Verifies that the provided routes are present in the routing table of a specified VRF.

Expected Results
  • Success: The test will pass if the provided routes are present in the routing table.
  • Failure: The test will fail if one or many provided routes are missing from the routing table.
Examples
anta.tests.routing:\n  generic:\n    - VerifyRoutingTableEntry:\n        vrf: default\n        routes:\n          - 10.1.0.1\n          - 10.1.0.2\n
Source code in anta/tests/routing/generic.py
class VerifyRoutingTableEntry(AntaTest):\n    \"\"\"Verifies that the provided routes are present in the routing table of a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided routes are present in the routing table.\n    * Failure: The test will fail if one or many provided routes are missing from the routing table.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      generic:\n        - VerifyRoutingTableEntry:\n            vrf: default\n            routes:\n              - 10.1.0.1\n              - 10.1.0.2\n    ```\n    \"\"\"\n\n    name = \"VerifyRoutingTableEntry\"\n    description = \"Verifies that the provided routes are present in the routing table of a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"routing\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip route vrf {vrf} {route}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyRoutingTableEntry test.\"\"\"\n\n        vrf: str = \"default\"\n        \"\"\"VRF context. Defaults to `default` VRF.\"\"\"\n        routes: list[IPv4Address]\n        \"\"\"List of routes to verify.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each route in the input list.\"\"\"\n        return [template.render(vrf=self.inputs.vrf, route=route) for route in self.inputs.routes]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyRoutingTableEntry.\"\"\"\n        missing_routes = []\n\n        for command in self.instance_commands:\n            if \"vrf\" in command.params and \"route\" in command.params:\n                vrf, route = command.params[\"vrf\"], command.params[\"route\"]\n                if len(routes := command.json_output[\"vrfs\"][vrf][\"routes\"]) == 0 or route != ip_interface(next(iter(routes))).ip:\n                    missing_routes.append(str(route))\n\n        if not missing_routes:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following route(s) are missing from the routing table of VRF {self.inputs.vrf}: {missing_routes}\")\n
"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableEntry-attributes","title":"Inputs","text":"Name Type Description Default vrf str VRF context. Defaults to `default` VRF. 'default' routes list[IPv4Address] List of routes to verify. -"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableSize","title":"VerifyRoutingTableSize","text":"

Verifies the size of the IP routing table of the default VRF.

Expected Results
  • Success: The test will pass if the routing table size is between the provided minimum and maximum values.
  • Failure: The test will fail if the routing table size is not between the provided minimum and maximum values.
Examples
anta.tests.routing:\n  generic:\n    - VerifyRoutingTableSize:\n        minimum: 2\n        maximum: 20\n
Source code in anta/tests/routing/generic.py
class VerifyRoutingTableSize(AntaTest):\n    \"\"\"Verifies the size of the IP routing table of the default VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the routing table size is between the provided minimum and maximum values.\n    * Failure: The test will fail if the routing table size is not between the provided minimum and maximum values.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      generic:\n        - VerifyRoutingTableSize:\n            minimum: 2\n            maximum: 20\n    ```\n    \"\"\"\n\n    name = \"VerifyRoutingTableSize\"\n    description = \"Verifies the size of the IP routing table of the default VRF.\"\n    categories: ClassVar[list[str]] = [\"routing\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip route summary\", revision=3)]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyRoutingTableSize test.\"\"\"\n\n        minimum: int\n        \"\"\"Expected minimum routing table size.\"\"\"\n        maximum: int\n        \"\"\"Expected maximum routing table size.\"\"\"\n\n        @model_validator(mode=\"after\")  # type: ignore[misc]\n        def check_min_max(self) -> AntaTest.Input:\n            \"\"\"Validate that maximum is greater than minimum.\"\"\"\n            if self.minimum > self.maximum:\n                msg = f\"Minimum {self.minimum} is greater than maximum {self.maximum}\"\n                raise ValueError(msg)\n            return self\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyRoutingTableSize.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        total_routes = int(command_output[\"vrfs\"][\"default\"][\"totalRoutes\"])\n        if self.inputs.minimum <= total_routes <= self.inputs.maximum:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"routing-table has {total_routes} routes and not between min ({self.inputs.minimum}) and maximum ({self.inputs.maximum})\")\n
"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableSize-attributes","title":"Inputs","text":"Name Type Description Default minimum int Expected minimum routing table size. - maximum int Expected maximum routing table size. -"},{"location":"api/tests.routing.ospf/","title":"OSPF","text":""},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFNeighborCount","title":"VerifyOSPFNeighborCount","text":"

Verifies the number of OSPF neighbors in FULL state is the one we expect.

Expected Results
  • Success: The test will pass if the number of OSPF neighbors in FULL state is the one we expect.
  • Failure: The test will fail if the number of OSPF neighbors in FULL state is not the one we expect.
  • Skipped: The test will be skipped if no OSPF neighbor is found.
Examples
anta.tests.routing:\n  ospf:\n    - VerifyOSPFNeighborCount:\n        number: 3\n
Source code in anta/tests/routing/ospf.py
class VerifyOSPFNeighborCount(AntaTest):\n    \"\"\"Verifies the number of OSPF neighbors in FULL state is the one we expect.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the number of OSPF neighbors in FULL state is the one we expect.\n    * Failure: The test will fail if the number of OSPF neighbors in FULL state is not the one we expect.\n    * Skipped: The test will be skipped if no OSPF neighbor is found.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      ospf:\n        - VerifyOSPFNeighborCount:\n            number: 3\n    ```\n    \"\"\"\n\n    name = \"VerifyOSPFNeighborCount\"\n    description = \"Verifies the number of OSPF neighbors in FULL state is the one we expect.\"\n    categories: ClassVar[list[str]] = [\"ospf\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyOSPFNeighborCount test.\"\"\"\n\n        number: int\n        \"\"\"The expected number of OSPF neighbors in FULL state.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyOSPFNeighborCount.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if (neighbor_count := _count_ospf_neighbor(command_output)) == 0:\n            self.result.is_skipped(\"no OSPF neighbor found\")\n            return\n        self.result.is_success()\n        if neighbor_count != self.inputs.number:\n            self.result.is_failure(f\"device has {neighbor_count} neighbors (expected {self.inputs.number})\")\n        not_full_neighbors = _get_not_full_ospf_neighbors(command_output)\n        if not_full_neighbors:\n            self.result.is_failure(f\"Some neighbors are not correctly configured: {not_full_neighbors}.\")\n
"},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFNeighborCount-attributes","title":"Inputs","text":"Name Type Description Default number int The expected number of OSPF neighbors in FULL state. -"},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFNeighborState","title":"VerifyOSPFNeighborState","text":"

Verifies all OSPF neighbors are in FULL state.

Expected Results
  • Success: The test will pass if all OSPF neighbors are in FULL state.
  • Failure: The test will fail if some OSPF neighbors are not in FULL state.
  • Skipped: The test will be skipped if no OSPF neighbor is found.
Examples
anta.tests.routing:\n  ospf:\n    - VerifyOSPFNeighborState:\n
Source code in anta/tests/routing/ospf.py
class VerifyOSPFNeighborState(AntaTest):\n    \"\"\"Verifies all OSPF neighbors are in FULL state.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all OSPF neighbors are in FULL state.\n    * Failure: The test will fail if some OSPF neighbors are not in FULL state.\n    * Skipped: The test will be skipped if no OSPF neighbor is found.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      ospf:\n        - VerifyOSPFNeighborState:\n    ```\n    \"\"\"\n\n    name = \"VerifyOSPFNeighborState\"\n    description = \"Verifies all OSPF neighbors are in FULL state.\"\n    categories: ClassVar[list[str]] = [\"ospf\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyOSPFNeighborState.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if _count_ospf_neighbor(command_output) == 0:\n            self.result.is_skipped(\"no OSPF neighbor found\")\n            return\n        self.result.is_success()\n        not_full_neighbors = _get_not_full_ospf_neighbors(command_output)\n        if not_full_neighbors:\n            self.result.is_failure(f\"Some neighbors are not correctly configured: {not_full_neighbors}.\")\n
"},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf._count_ospf_neighbor","title":"_count_ospf_neighbor","text":"
_count_ospf_neighbor(ospf_neighbor_json: dict[str, Any]) -> int\n

Count the number of OSPF neighbors.

Args:

ospf_neighbor_json (dict[str, Any]): The JSON output of the show ip ospf neighbor command.

Returns:

Type Description int: The number of OSPF neighbors. Source code in anta/tests/routing/ospf.py
def _count_ospf_neighbor(ospf_neighbor_json: dict[str, Any]) -> int:\n    \"\"\"Count the number of OSPF neighbors.\n\n    Args:\n    ----\n      ospf_neighbor_json (dict[str, Any]): The JSON output of the `show ip ospf neighbor` command.\n\n    Returns\n    -------\n      int: The number of OSPF neighbors.\n\n    \"\"\"\n    count = 0\n    for vrf_data in ospf_neighbor_json[\"vrfs\"].values():\n        for instance_data in vrf_data[\"instList\"].values():\n            count += len(instance_data.get(\"ospfNeighborEntries\", []))\n    return count\n
"},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf._get_not_full_ospf_neighbors","title":"_get_not_full_ospf_neighbors","text":"
_get_not_full_ospf_neighbors(ospf_neighbor_json: dict[str, Any]) -> list[dict[str, Any]]\n

Return the OSPF neighbors whose adjacency state is not full.

Args:

ospf_neighbor_json (dict[str, Any]): The JSON output of the show ip ospf neighbor command.

Returns:

Type Description list[dict[str, Any]]: A list of OSPF neighbors whose adjacency state is not `full`. Source code in anta/tests/routing/ospf.py
def _get_not_full_ospf_neighbors(ospf_neighbor_json: dict[str, Any]) -> list[dict[str, Any]]:\n    \"\"\"Return the OSPF neighbors whose adjacency state is not `full`.\n\n    Args:\n    ----\n      ospf_neighbor_json (dict[str, Any]): The JSON output of the `show ip ospf neighbor` command.\n\n    Returns\n    -------\n      list[dict[str, Any]]: A list of OSPF neighbors whose adjacency state is not `full`.\n\n    \"\"\"\n    return [\n        {\n            \"vrf\": vrf,\n            \"instance\": instance,\n            \"neighbor\": neighbor_data[\"routerId\"],\n            \"state\": state,\n        }\n        for vrf, vrf_data in ospf_neighbor_json[\"vrfs\"].items()\n        for instance, instance_data in vrf_data[\"instList\"].items()\n        for neighbor_data in instance_data.get(\"ospfNeighborEntries\", [])\n        if (state := neighbor_data[\"adjacencyState\"]) != \"full\"\n    ]\n
"},{"location":"api/tests.security/","title":"Security","text":""},{"location":"api/tests.security/#anta.tests.security.VerifyAPIHttpStatus","title":"VerifyAPIHttpStatus","text":"

Verifies if eAPI HTTP server is disabled globally.

Expected Results
  • Success: The test will pass if eAPI HTTP server is disabled globally.
  • Failure: The test will fail if eAPI HTTP server is NOT disabled globally.
Examples
anta.tests.security:\n  - VerifyAPIHttpStatus:\n
Source code in anta/tests/security.py
class VerifyAPIHttpStatus(AntaTest):\n    \"\"\"Verifies if eAPI HTTP server is disabled globally.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if eAPI HTTP server is disabled globally.\n    * Failure: The test will fail if eAPI HTTP server is NOT disabled globally.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyAPIHttpStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyAPIHttpStatus\"\n    description = \"Verifies if eAPI HTTP server is disabled globally.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAPIHttpStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"enabled\"] and not command_output[\"httpServer\"][\"running\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"eAPI HTTP server is enabled globally\")\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIHttpsSSL","title":"VerifyAPIHttpsSSL","text":"

Verifies if eAPI HTTPS server SSL profile is configured and valid.

Expected Results
  • Success: The test will pass if the eAPI HTTPS server SSL profile is configured and valid.
  • Failure: The test will fail if the eAPI HTTPS server SSL profile is NOT configured, misconfigured or invalid.
Examples
anta.tests.security:\n  - VerifyAPIHttpsSSL:\n      profile: default\n
Source code in anta/tests/security.py
class VerifyAPIHttpsSSL(AntaTest):\n    \"\"\"Verifies if eAPI HTTPS server SSL profile is configured and valid.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the eAPI HTTPS server SSL profile is configured and valid.\n    * Failure: The test will fail if the eAPI HTTPS server SSL profile is NOT configured, misconfigured or invalid.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyAPIHttpsSSL:\n          profile: default\n    ```\n    \"\"\"\n\n    name = \"VerifyAPIHttpsSSL\"\n    description = \"Verifies if the eAPI has a valid SSL profile.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyAPIHttpsSSL test.\"\"\"\n\n        profile: str\n        \"\"\"SSL profile to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAPIHttpsSSL.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        try:\n            if command_output[\"sslProfile\"][\"name\"] == self.inputs.profile and command_output[\"sslProfile\"][\"state\"] == \"valid\":\n                self.result.is_success()\n            else:\n                self.result.is_failure(f\"eAPI HTTPS server SSL profile ({self.inputs.profile}) is misconfigured or invalid\")\n\n        except KeyError:\n            self.result.is_failure(f\"eAPI HTTPS server SSL profile ({self.inputs.profile}) is not configured\")\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIHttpsSSL-attributes","title":"Inputs","text":"Name Type Description Default profile str SSL profile to verify. -"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIIPv4Acl","title":"VerifyAPIIPv4Acl","text":"

Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if eAPI has the provided number of IPv4 ACL(s) in the specified VRF.
  • Failure: The test will fail if eAPI has not the right number of IPv4 ACL(s) in the specified VRF.
Examples
anta.tests.security:\n  - VerifyAPIIPv4Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/security.py
class VerifyAPIIPv4Acl(AntaTest):\n    \"\"\"Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if eAPI has the provided number of IPv4 ACL(s) in the specified VRF.\n    * Failure: The test will fail if eAPI has not the right number of IPv4 ACL(s) in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyAPIIPv4Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyAPIIPv4Acl\"\n    description = \"Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ip access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input parameters for the VerifyAPIIPv4Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv4 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for eAPI. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAPIIPv4Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv4_acl_list = command_output[\"ipAclList\"][\"aclList\"]\n        ipv4_acl_number = len(ipv4_acl_list)\n        if ipv4_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} eAPI IPv4 ACL(s) in vrf {self.inputs.vrf} but got {ipv4_acl_number}\")\n            return\n\n        not_configured_acl = [acl[\"name\"] for acl in ipv4_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if not_configured_acl:\n            self.result.is_failure(f\"eAPI IPv4 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIIPv4Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv4 ACL(s). - vrf str The name of the VRF in which to check for eAPI. Defaults to `default` VRF. 'default'"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIIPv6Acl","title":"VerifyAPIIPv6Acl","text":"

Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if eAPI has the provided number of IPv6 ACL(s) in the specified VRF.
  • Failure: The test will fail if eAPI has not the right number of IPv6 ACL(s) in the specified VRF.
  • Skipped: The test will be skipped if the number of IPv6 ACL(s) or VRF parameter is not provided.
Examples
anta.tests.security:\n  - VerifyAPIIPv6Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/security.py
class VerifyAPIIPv6Acl(AntaTest):\n    \"\"\"Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if eAPI has the provided number of IPv6 ACL(s) in the specified VRF.\n    * Failure: The test will fail if eAPI has not the right number of IPv6 ACL(s) in the specified VRF.\n    * Skipped: The test will be skipped if the number of IPv6 ACL(s) or VRF parameter is not provided.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyAPIIPv6Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyAPIIPv6Acl\"\n    description = \"Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ipv6 access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input parameters for the VerifyAPIIPv6Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv6 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for eAPI. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAPIIPv6Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv6_acl_list = command_output[\"ipv6AclList\"][\"aclList\"]\n        ipv6_acl_number = len(ipv6_acl_list)\n        if ipv6_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} eAPI IPv6 ACL(s) in vrf {self.inputs.vrf} but got {ipv6_acl_number}\")\n            return\n\n        not_configured_acl = [acl[\"name\"] for acl in ipv6_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if not_configured_acl:\n            self.result.is_failure(f\"eAPI IPv6 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIIPv6Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv6 ACL(s). - vrf str The name of the VRF in which to check for eAPI. Defaults to `default` VRF. 'default'"},{"location":"api/tests.security/#anta.tests.security.VerifyAPISSLCertificate","title":"VerifyAPISSLCertificate","text":"

Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size.

Expected Results
  • Success: The test will pass if the certificate\u2019s expiry date is greater than the threshold, and the certificate has the correct name, encryption algorithm, and key size.
  • Failure: The test will fail if the certificate is expired or is going to expire, or if the certificate has an incorrect name, encryption algorithm, or key size.
Examples
anta.tests.security:\n  - VerifyAPISSLCertificate:\n      certificates:\n        - certificate_name: ARISTA_SIGNING_CA.crt\n          expiry_threshold: 30\n          common_name: AristaIT-ICA ECDSA Issuing Cert Authority\n          encryption_algorithm: ECDSA\n          key_size: 256\n        - certificate_name: ARISTA_ROOT_CA.crt\n          expiry_threshold: 30\n          common_name: Arista Networks Internal IT Root Cert Authority\n          encryption_algorithm: RSA\n          key_size: 4096\n
Source code in anta/tests/security.py
class VerifyAPISSLCertificate(AntaTest):\n    \"\"\"Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the certificate's expiry date is greater than the threshold,\n                   and the certificate has the correct name, encryption algorithm, and key size.\n    * Failure: The test will fail if the certificate is expired or is going to expire,\n                   or if the certificate has an incorrect name, encryption algorithm, or key size.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyAPISSLCertificate:\n          certificates:\n            - certificate_name: ARISTA_SIGNING_CA.crt\n              expiry_threshold: 30\n              common_name: AristaIT-ICA ECDSA Issuing Cert Authority\n              encryption_algorithm: ECDSA\n              key_size: 256\n            - certificate_name: ARISTA_ROOT_CA.crt\n              expiry_threshold: 30\n              common_name: Arista Networks Internal IT Root Cert Authority\n              encryption_algorithm: RSA\n              key_size: 4096\n    ```\n    \"\"\"\n\n    name = \"VerifyAPISSLCertificate\"\n    description = \"Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management security ssl certificate\"), AntaCommand(command=\"show clock\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input parameters for the VerifyAPISSLCertificate test.\"\"\"\n\n        certificates: list[APISSLCertificate]\n        \"\"\"List of API SSL certificates.\"\"\"\n\n        class APISSLCertificate(BaseModel):\n            \"\"\"Model for an API SSL certificate.\"\"\"\n\n            certificate_name: str\n            \"\"\"The name of the certificate to be verified.\"\"\"\n            expiry_threshold: int\n            \"\"\"The expiry threshold of the certificate in days.\"\"\"\n            common_name: str\n            \"\"\"The common subject name of the certificate.\"\"\"\n            encryption_algorithm: EncryptionAlgorithm\n            \"\"\"The encryption algorithm of the certificate.\"\"\"\n            key_size: RsaKeySize | EcdsaKeySize\n            \"\"\"The encryption algorithm key size of the certificate.\"\"\"\n\n            @model_validator(mode=\"after\")\n            def validate_inputs(self: BaseModel) -> BaseModel:\n                \"\"\"Validate the key size provided to the APISSLCertificates class.\n\n                If encryption_algorithm is RSA then key_size should be in {2048, 3072, 4096}.\n\n                If encryption_algorithm is ECDSA then key_size should be in {256, 384, 521}.\n                \"\"\"\n                if self.encryption_algorithm == \"RSA\" and self.key_size not in RsaKeySize.__args__:\n                    msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for RSA encryption. Allowed sizes are {RsaKeySize.__args__}.\"\n                    raise ValueError(msg)\n\n                if self.encryption_algorithm == \"ECDSA\" and self.key_size not in EcdsaKeySize.__args__:\n                    msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for ECDSA encryption. Allowed sizes are {EcdsaKeySize.__args__}.\"\n                    raise ValueError(msg)\n\n                return self\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAPISSLCertificate.\"\"\"\n        # Mark the result as success by default\n        self.result.is_success()\n\n        # Extract certificate and clock output\n        certificate_output = self.instance_commands[0].json_output\n        clock_output = self.instance_commands[1].json_output\n        current_timestamp = clock_output[\"utcTime\"]\n\n        # Iterate over each API SSL certificate\n        for certificate in self.inputs.certificates:\n            # Collecting certificate expiry time and current EOS time.\n            # These times are used to calculate the number of days until the certificate expires.\n            if not (certificate_data := get_value(certificate_output, f\"certificates..{certificate.certificate_name}\", separator=\"..\")):\n                self.result.is_failure(f\"SSL certificate '{certificate.certificate_name}', is not configured.\\n\")\n                continue\n\n            expiry_time = certificate_data[\"notAfter\"]\n            day_difference = (datetime.fromtimestamp(expiry_time, tz=timezone.utc) - datetime.fromtimestamp(current_timestamp, tz=timezone.utc)).days\n\n            # Verify certificate expiry\n            if 0 < day_difference < certificate.expiry_threshold:\n                self.result.is_failure(f\"SSL certificate `{certificate.certificate_name}` is about to expire in {day_difference} days.\\n\")\n            elif day_difference < 0:\n                self.result.is_failure(f\"SSL certificate `{certificate.certificate_name}` is expired.\\n\")\n\n            # Verify certificate common subject name, encryption algorithm and key size\n            keys_to_verify = [\"subject.commonName\", \"publicKey.encryptionAlgorithm\", \"publicKey.size\"]\n            actual_certificate_details = {key: get_value(certificate_data, key) for key in keys_to_verify}\n\n            expected_certificate_details = {\n                \"subject.commonName\": certificate.common_name,\n                \"publicKey.encryptionAlgorithm\": certificate.encryption_algorithm,\n                \"publicKey.size\": certificate.key_size,\n            }\n\n            if actual_certificate_details != expected_certificate_details:\n                failed_log = f\"SSL certificate `{certificate.certificate_name}` is not configured properly:\"\n                failed_log += get_failed_logs(expected_certificate_details, actual_certificate_details)\n                self.result.is_failure(f\"{failed_log}\\n\")\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyAPISSLCertificate-attributes","title":"Inputs","text":"Name Type Description Default certificates list[APISSLCertificate] List of API SSL certificates. -"},{"location":"api/tests.security/#anta.tests.security.VerifyAPISSLCertificate-attributes","title":"APISSLCertificate","text":"Name Type Description Default certificate_name str The name of the certificate to be verified. - expiry_threshold int The expiry threshold of the certificate in days. - common_name str The common subject name of the certificate. - encryption_algorithm EncryptionAlgorithm The encryption algorithm of the certificate. - key_size RsaKeySize | EcdsaKeySize The encryption algorithm key size of the certificate. -"},{"location":"api/tests.security/#anta.tests.security.VerifyBannerLogin","title":"VerifyBannerLogin","text":"

Verifies the login banner of a device.

Expected Results
  • Success: The test will pass if the login banner matches the provided input.
  • Failure: The test will fail if the login banner does not match the provided input.
Examples
anta.tests.security:\n  - VerifyBannerLogin:\n        login_banner: |\n            # Copyright (c) 2023-2024 Arista Networks, Inc.\n            # Use of this source code is governed by the Apache License 2.0\n            # that can be found in the LICENSE file.\n
Source code in anta/tests/security.py
class VerifyBannerLogin(AntaTest):\n    \"\"\"Verifies the login banner of a device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the login banner matches the provided input.\n    * Failure: The test will fail if the login banner does not match the provided input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyBannerLogin:\n            login_banner: |\n                # Copyright (c) 2023-2024 Arista Networks, Inc.\n                # Use of this source code is governed by the Apache License 2.0\n                # that can be found in the LICENSE file.\n    ```\n    \"\"\"\n\n    name = \"VerifyBannerLogin\"\n    description = \"Verifies the login banner of a device.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner login\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBannerLogin test.\"\"\"\n\n        login_banner: str\n        \"\"\"Expected login banner of the device.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBannerLogin.\"\"\"\n        login_banner = self.instance_commands[0].json_output[\"loginBanner\"]\n\n        # Remove leading and trailing whitespaces from each line\n        cleaned_banner = \"\\n\".join(line.strip() for line in self.inputs.login_banner.split(\"\\n\"))\n        if login_banner != cleaned_banner:\n            self.result.is_failure(f\"Expected `{cleaned_banner}` as the login banner, but found `{login_banner}` instead.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyBannerLogin-attributes","title":"Inputs","text":"Name Type Description Default login_banner str Expected login banner of the device. -"},{"location":"api/tests.security/#anta.tests.security.VerifyBannerMotd","title":"VerifyBannerMotd","text":"

Verifies the motd banner of a device.

Expected Results
  • Success: The test will pass if the motd banner matches the provided input.
  • Failure: The test will fail if the motd banner does not match the provided input.
Examples
anta.tests.security:\n  - VerifyBannerMotd:\n        motd_banner: |\n            # Copyright (c) 2023-2024 Arista Networks, Inc.\n            # Use of this source code is governed by the Apache License 2.0\n            # that can be found in the LICENSE file.\n
Source code in anta/tests/security.py
class VerifyBannerMotd(AntaTest):\n    \"\"\"Verifies the motd banner of a device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the motd banner matches the provided input.\n    * Failure: The test will fail if the motd banner does not match the provided input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyBannerMotd:\n            motd_banner: |\n                # Copyright (c) 2023-2024 Arista Networks, Inc.\n                # Use of this source code is governed by the Apache License 2.0\n                # that can be found in the LICENSE file.\n    ```\n    \"\"\"\n\n    name = \"VerifyBannerMotd\"\n    description = \"Verifies the motd banner of a device.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner motd\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBannerMotd test.\"\"\"\n\n        motd_banner: str\n        \"\"\"Expected motd banner of the device.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBannerMotd.\"\"\"\n        motd_banner = self.instance_commands[0].json_output[\"motd\"]\n\n        # Remove leading and trailing whitespaces from each line\n        cleaned_banner = \"\\n\".join(line.strip() for line in self.inputs.motd_banner.split(\"\\n\"))\n        if motd_banner != cleaned_banner:\n            self.result.is_failure(f\"Expected `{cleaned_banner}` as the motd banner, but found `{motd_banner}` instead.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyBannerMotd-attributes","title":"Inputs","text":"Name Type Description Default motd_banner str Expected motd banner of the device. -"},{"location":"api/tests.security/#anta.tests.security.VerifyIPv4ACL","title":"VerifyIPv4ACL","text":"

Verifies the configuration of IPv4 ACLs.

Expected Results
  • Success: The test will pass if an IPv4 ACL is configured with the correct sequence entries.
  • Failure: The test will fail if an IPv4 ACL is not configured or entries are not in sequence.
Examples
anta.tests.security:\n  - VerifyIPv4ACL:\n      ipv4_access_lists:\n        - name: default-control-plane-acl\n          entries:\n            - sequence: 10\n              action: permit icmp any any\n            - sequence: 20\n              action: permit ip any any tracked\n            - sequence: 30\n              action: permit udp any any eq bfd ttl eq 255\n        - name: LabTest\n          entries:\n            - sequence: 10\n              action: permit icmp any any\n            - sequence: 20\n              action: permit tcp any any range 5900 5910\n
Source code in anta/tests/security.py
class VerifyIPv4ACL(AntaTest):\n    \"\"\"Verifies the configuration of IPv4 ACLs.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if an IPv4 ACL is configured with the correct sequence entries.\n    * Failure: The test will fail if an IPv4 ACL is not configured or entries are not in sequence.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyIPv4ACL:\n          ipv4_access_lists:\n            - name: default-control-plane-acl\n              entries:\n                - sequence: 10\n                  action: permit icmp any any\n                - sequence: 20\n                  action: permit ip any any tracked\n                - sequence: 30\n                  action: permit udp any any eq bfd ttl eq 255\n            - name: LabTest\n              entries:\n                - sequence: 10\n                  action: permit icmp any any\n                - sequence: 20\n                  action: permit tcp any any range 5900 5910\n    ```\n    \"\"\"\n\n    name = \"VerifyIPv4ACL\"\n    description = \"Verifies the configuration of IPv4 ACLs.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip access-lists {acl}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyIPv4ACL test.\"\"\"\n\n        ipv4_access_lists: list[IPv4ACL]\n        \"\"\"List of IPv4 ACLs to verify.\"\"\"\n\n        class IPv4ACL(BaseModel):\n            \"\"\"Model for an IPv4 ACL.\"\"\"\n\n            name: str\n            \"\"\"Name of IPv4 ACL.\"\"\"\n\n            entries: list[IPv4ACLEntry]\n            \"\"\"List of IPv4 ACL entries.\"\"\"\n\n            class IPv4ACLEntry(BaseModel):\n                \"\"\"Model for an IPv4 ACL entry.\"\"\"\n\n                sequence: int = Field(ge=1, le=4294967295)\n                \"\"\"Sequence number of an ACL entry.\"\"\"\n                action: str\n                \"\"\"Action of an ACL entry.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each input ACL.\"\"\"\n        return [template.render(acl=acl.name, entries=acl.entries) for acl in self.inputs.ipv4_access_lists]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIPv4ACL.\"\"\"\n        self.result.is_success()\n        for command_output in self.instance_commands:\n            # Collecting input ACL details\n            acl_name = command_output.params[\"acl\"]\n            acl_entries = command_output.params[\"entries\"]\n\n            # Check if ACL is configured\n            ipv4_acl_list = command_output.json_output[\"aclList\"]\n            if not ipv4_acl_list:\n                self.result.is_failure(f\"{acl_name}: Not found\")\n                continue\n\n            # Check if the sequence number is configured and has the correct action applied\n            failed_log = f\"{acl_name}:\\n\"\n            for acl_entry in acl_entries:\n                acl_seq = acl_entry.sequence\n                acl_action = acl_entry.action\n                if (actual_entry := get_item(ipv4_acl_list[0][\"sequence\"], \"sequenceNumber\", acl_seq)) is None:\n                    failed_log += f\"Sequence number `{acl_seq}` is not found.\\n\"\n                    continue\n\n                if actual_entry[\"text\"] != acl_action:\n                    failed_log += f\"Expected `{acl_action}` as sequence number {acl_seq} action but found `{actual_entry['text']}` instead.\\n\"\n\n            if failed_log != f\"{acl_name}:\\n\":\n                self.result.is_failure(f\"{failed_log}\")\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyIPv4ACL-attributes","title":"Inputs","text":"Name Type Description Default ipv4_access_lists list[IPv4ACL] List of IPv4 ACLs to verify. -"},{"location":"api/tests.security/#anta.tests.security.VerifyIPv4ACL-attributes","title":"IPv4ACL","text":"Name Type Description Default name str Name of IPv4 ACL. - entries list[IPv4ACLEntry] List of IPv4 ACL entries. -"},{"location":"api/tests.security/#anta.tests.security.VerifyIPv4ACL-attributes","title":"IPv4ACLEntry","text":"Name Type Description Default sequence int Sequence number of an ACL entry. Field(ge=1, le=4294967295) action str Action of an ACL entry. -"},{"location":"api/tests.security/#anta.tests.security.VerifySSHIPv4Acl","title":"VerifySSHIPv4Acl","text":"

Verifies if the SSHD agent has the right number IPv4 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if the SSHD agent has the provided number of IPv4 ACL(s) in the specified VRF.
  • Failure: The test will fail if the SSHD agent has not the right number of IPv4 ACL(s) in the specified VRF.
Examples
anta.tests.security:\n  - VerifySSHIPv4Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/security.py
class VerifySSHIPv4Acl(AntaTest):\n    \"\"\"Verifies if the SSHD agent has the right number IPv4 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SSHD agent has the provided number of IPv4 ACL(s) in the specified VRF.\n    * Failure: The test will fail if the SSHD agent has not the right number of IPv4 ACL(s) in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifySSHIPv4Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifySSHIPv4Acl\"\n    description = \"Verifies if the SSHD agent has IPv4 ACL(s) configured.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management ssh ip access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySSHIPv4Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv4 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for the SSHD agent. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySSHIPv4Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv4_acl_list = command_output[\"ipAclList\"][\"aclList\"]\n        ipv4_acl_number = len(ipv4_acl_list)\n        if ipv4_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} SSH IPv4 ACL(s) in vrf {self.inputs.vrf} but got {ipv4_acl_number}\")\n            return\n\n        not_configured_acl = [acl[\"name\"] for acl in ipv4_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if not_configured_acl:\n            self.result.is_failure(f\"SSH IPv4 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifySSHIPv4Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv4 ACL(s). - vrf str The name of the VRF in which to check for the SSHD agent. Defaults to `default` VRF. 'default'"},{"location":"api/tests.security/#anta.tests.security.VerifySSHIPv6Acl","title":"VerifySSHIPv6Acl","text":"

Verifies if the SSHD agent has the right number IPv6 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if the SSHD agent has the provided number of IPv6 ACL(s) in the specified VRF.
  • Failure: The test will fail if the SSHD agent has not the right number of IPv6 ACL(s) in the specified VRF.
Examples
anta.tests.security:\n  - VerifySSHIPv6Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/security.py
class VerifySSHIPv6Acl(AntaTest):\n    \"\"\"Verifies if the SSHD agent has the right number IPv6 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SSHD agent has the provided number of IPv6 ACL(s) in the specified VRF.\n    * Failure: The test will fail if the SSHD agent has not the right number of IPv6 ACL(s) in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifySSHIPv6Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifySSHIPv6Acl\"\n    description = \"Verifies if the SSHD agent has IPv6 ACL(s) configured.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management ssh ipv6 access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySSHIPv6Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv6 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for the SSHD agent. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySSHIPv6Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv6_acl_list = command_output[\"ipv6AclList\"][\"aclList\"]\n        ipv6_acl_number = len(ipv6_acl_list)\n        if ipv6_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} SSH IPv6 ACL(s) in vrf {self.inputs.vrf} but got {ipv6_acl_number}\")\n            return\n\n        not_configured_acl = [acl[\"name\"] for acl in ipv6_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if not_configured_acl:\n            self.result.is_failure(f\"SSH IPv6 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifySSHIPv6Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv6 ACL(s). - vrf str The name of the VRF in which to check for the SSHD agent. Defaults to `default` VRF. 'default'"},{"location":"api/tests.security/#anta.tests.security.VerifySSHStatus","title":"VerifySSHStatus","text":"

Verifies if the SSHD agent is disabled in the default VRF.

Expected Results
  • Success: The test will pass if the SSHD agent is disabled in the default VRF.
  • Failure: The test will fail if the SSHD agent is NOT disabled in the default VRF.
Examples
anta.tests.security:\n  - VerifySSHStatus:\n
Source code in anta/tests/security.py
class VerifySSHStatus(AntaTest):\n    \"\"\"Verifies if the SSHD agent is disabled in the default VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SSHD agent is disabled in the default VRF.\n    * Failure: The test will fail if the SSHD agent is NOT disabled in the default VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifySSHStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifySSHStatus\"\n    description = \"Verifies if the SSHD agent is disabled in the default VRF.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management ssh\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySSHStatus.\"\"\"\n        command_output = self.instance_commands[0].text_output\n\n        line = next(line for line in command_output.split(\"\\n\") if line.startswith(\"SSHD status\"))\n        status = line.split(\"is \")[1]\n\n        if status == \"disabled\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(line)\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyTelnetStatus","title":"VerifyTelnetStatus","text":"

Verifies if Telnet is disabled in the default VRF.

Expected Results
  • Success: The test will pass if Telnet is disabled in the default VRF.
  • Failure: The test will fail if Telnet is NOT disabled in the default VRF.
Examples
anta.tests.security:\n  - VerifyTelnetStatus:\n
Source code in anta/tests/security.py
class VerifyTelnetStatus(AntaTest):\n    \"\"\"Verifies if Telnet is disabled in the default VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if Telnet is disabled in the default VRF.\n    * Failure: The test will fail if Telnet is NOT disabled in the default VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyTelnetStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyTelnetStatus\"\n    description = \"Verifies if Telnet is disabled in the default VRF.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management telnet\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTelnetStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"serverState\"] == \"disabled\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"Telnet status for Default VRF is enabled\")\n
"},{"location":"api/tests.services/","title":"Services","text":""},{"location":"api/tests.services/#anta.tests.services.VerifyDNSLookup","title":"VerifyDNSLookup","text":"

Verifies the DNS (Domain Name Service) name to IP address resolution.

Expected Results
  • Success: The test will pass if a domain name is resolved to an IP address.
  • Failure: The test will fail if a domain name does not resolve to an IP address.
  • Error: This test will error out if a domain name is invalid.
Examples
anta.tests.services:\n  - VerifyDNSLookup:\n      domain_names:\n        - arista.com\n        - www.google.com\n        - arista.ca\n
Source code in anta/tests/services.py
class VerifyDNSLookup(AntaTest):\n    \"\"\"Verifies the DNS (Domain Name Service) name to IP address resolution.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if a domain name is resolved to an IP address.\n    * Failure: The test will fail if a domain name does not resolve to an IP address.\n    * Error: This test will error out if a domain name is invalid.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.services:\n      - VerifyDNSLookup:\n          domain_names:\n            - arista.com\n            - www.google.com\n            - arista.ca\n    ```\n    \"\"\"\n\n    name = \"VerifyDNSLookup\"\n    description = \"Verifies the DNS name to IP address resolution.\"\n    categories: ClassVar[list[str]] = [\"services\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"bash timeout 10 nslookup {domain}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyDNSLookup test.\"\"\"\n\n        domain_names: list[str]\n        \"\"\"List of domain names.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each domain name in the input list.\"\"\"\n        return [template.render(domain=domain_name) for domain_name in self.inputs.domain_names]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyDNSLookup.\"\"\"\n        self.result.is_success()\n        failed_domains = []\n        for command in self.instance_commands:\n            domain = command.params[\"domain\"]\n            output = command.json_output[\"messages\"][0]\n            if f\"Can't find {domain}: No answer\" in output:\n                failed_domains.append(domain)\n        if failed_domains:\n            self.result.is_failure(f\"The following domain(s) are not resolved to an IP address: {', '.join(failed_domains)}\")\n
"},{"location":"api/tests.services/#anta.tests.services.VerifyDNSLookup-attributes","title":"Inputs","text":"Name Type Description Default domain_names list[str] List of domain names. -"},{"location":"api/tests.services/#anta.tests.services.VerifyDNSServers","title":"VerifyDNSServers","text":"

Verifies if the DNS (Domain Name Service) servers are correctly configured.

Expected Results
  • Success: The test will pass if the DNS server specified in the input is configured with the correct VRF and priority.
  • Failure: The test will fail if the DNS server is not configured or if the VRF and priority of the DNS server do not match the input.
Examples
anta.tests.services:\n  - VerifyDNSServers:\n      dns_servers:\n        - server_address: 10.14.0.1\n          vrf: default\n          priority: 1\n        - server_address: 10.14.0.11\n          vrf: MGMT\n          priority: 0\n
Source code in anta/tests/services.py
class VerifyDNSServers(AntaTest):\n    \"\"\"Verifies if the DNS (Domain Name Service) servers are correctly configured.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the DNS server specified in the input is configured with the correct VRF and priority.\n    * Failure: The test will fail if the DNS server is not configured or if the VRF and priority of the DNS server do not match the input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.services:\n      - VerifyDNSServers:\n          dns_servers:\n            - server_address: 10.14.0.1\n              vrf: default\n              priority: 1\n            - server_address: 10.14.0.11\n              vrf: MGMT\n              priority: 0\n    ```\n    \"\"\"\n\n    name = \"VerifyDNSServers\"\n    description = \"Verifies if the DNS servers are correctly configured.\"\n    categories: ClassVar[list[str]] = [\"services\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip name-server\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyDNSServers test.\"\"\"\n\n        dns_servers: list[DnsServer]\n        \"\"\"List of DNS servers to verify.\"\"\"\n\n        class DnsServer(BaseModel):\n            \"\"\"Model for a DNS server.\"\"\"\n\n            server_address: IPv4Address | IPv6Address\n            \"\"\"The IPv4/IPv6 address of the DNS server.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"The VRF for the DNS server. Defaults to 'default' if not provided.\"\"\"\n            priority: int = Field(ge=0, le=4)\n            \"\"\"The priority of the DNS server from 0 to 4, lower is first.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyDNSServers.\"\"\"\n        command_output = self.instance_commands[0].json_output[\"nameServerConfigs\"]\n        self.result.is_success()\n        for server in self.inputs.dns_servers:\n            address = str(server.server_address)\n            vrf = server.vrf\n            priority = server.priority\n            input_dict = {\"ipAddr\": address, \"vrf\": vrf}\n\n            if get_item(command_output, \"ipAddr\", address) is None:\n                self.result.is_failure(f\"DNS server `{address}` is not configured with any VRF.\")\n                continue\n\n            if (output := get_dict_superset(command_output, input_dict)) is None:\n                self.result.is_failure(f\"DNS server `{address}` is not configured with VRF `{vrf}`.\")\n                continue\n\n            if output[\"priority\"] != priority:\n                self.result.is_failure(f\"For DNS server `{address}`, the expected priority is `{priority}`, but `{output['priority']}` was found instead.\")\n
"},{"location":"api/tests.services/#anta.tests.services.VerifyDNSServers-attributes","title":"Inputs","text":"Name Type Description Default dns_servers list[DnsServer] List of DNS servers to verify. -"},{"location":"api/tests.services/#anta.tests.services.VerifyDNSServers-attributes","title":"DnsServer","text":"Name Type Description Default server_address IPv4Address | IPv6Address The IPv4/IPv6 address of the DNS server. - vrf str The VRF for the DNS server. Defaults to 'default' if not provided. 'default' priority int The priority of the DNS server from 0 to 4, lower is first. Field(ge=0, le=4)"},{"location":"api/tests.services/#anta.tests.services.VerifyErrdisableRecovery","title":"VerifyErrdisableRecovery","text":"

Verifies the errdisable recovery reason, status, and interval.

Expected Results
  • Success: The test will pass if the errdisable recovery reason status is enabled and the interval matches the input.
  • Failure: The test will fail if the errdisable recovery reason is not found, the status is not enabled, or the interval does not match the input.
Examples
anta.tests.services:\n  - VerifyErrdisableRecovery:\n      reasons:\n        - reason: acl\n          interval: 30\n        - reason: bpduguard\n          interval: 30\n
Source code in anta/tests/services.py
class VerifyErrdisableRecovery(AntaTest):\n    \"\"\"Verifies the errdisable recovery reason, status, and interval.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the errdisable recovery reason status is enabled and the interval matches the input.\n    * Failure: The test will fail if the errdisable recovery reason is not found, the status is not enabled, or the interval does not match the input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.services:\n      - VerifyErrdisableRecovery:\n          reasons:\n            - reason: acl\n              interval: 30\n            - reason: bpduguard\n              interval: 30\n    ```\n    \"\"\"\n\n    name = \"VerifyErrdisableRecovery\"\n    description = \"Verifies the errdisable recovery reason, status, and interval.\"\n    categories: ClassVar[list[str]] = [\"services\"]\n    # NOTE: Only `text` output format is supported for this command\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show errdisable recovery\", ofmt=\"text\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyErrdisableRecovery test.\"\"\"\n\n        reasons: list[ErrDisableReason]\n        \"\"\"List of errdisable reasons.\"\"\"\n\n        class ErrDisableReason(BaseModel):\n            \"\"\"Model for an errdisable reason.\"\"\"\n\n            reason: ErrDisableReasons\n            \"\"\"Type or name of the errdisable reason.\"\"\"\n            interval: ErrDisableInterval\n            \"\"\"Interval of the reason in seconds.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyErrdisableRecovery.\"\"\"\n        command_output = self.instance_commands[0].text_output\n        self.result.is_success()\n        for error_reason in self.inputs.reasons:\n            input_reason = error_reason.reason\n            input_interval = error_reason.interval\n            reason_found = False\n\n            # Skip header and last empty line\n            lines = command_output.split(\"\\n\")[2:-1]\n            for line in lines:\n                # Skip empty lines\n                if not line.strip():\n                    continue\n                # Split by first two whitespaces\n                reason, status, interval = line.split(None, 2)\n                if reason != input_reason:\n                    continue\n                reason_found = True\n                actual_reason_data = {\"interval\": interval, \"status\": status}\n                expected_reason_data = {\"interval\": str(input_interval), \"status\": \"Enabled\"}\n                if actual_reason_data != expected_reason_data:\n                    failed_log = get_failed_logs(expected_reason_data, actual_reason_data)\n                    self.result.is_failure(f\"`{input_reason}`:{failed_log}\\n\")\n                break\n\n            if not reason_found:\n                self.result.is_failure(f\"`{input_reason}`: Not found.\\n\")\n
"},{"location":"api/tests.services/#anta.tests.services.VerifyErrdisableRecovery-attributes","title":"Inputs","text":"Name Type Description Default reasons list[ErrDisableReason] List of errdisable reasons. -"},{"location":"api/tests.services/#anta.tests.services.VerifyErrdisableRecovery-attributes","title":"ErrDisableReason","text":"Name Type Description Default reason ErrDisableReasons Type or name of the errdisable reason. - interval ErrDisableInterval Interval of the reason in seconds. -"},{"location":"api/tests.services/#anta.tests.services.VerifyHostname","title":"VerifyHostname","text":"

Verifies the hostname of a device.

Expected Results
  • Success: The test will pass if the hostname matches the provided input.
  • Failure: The test will fail if the hostname does not match the provided input.
Examples
anta.tests.services:\n  - VerifyHostname:\n      hostname: s1-spine1\n
Source code in anta/tests/services.py
class VerifyHostname(AntaTest):\n    \"\"\"Verifies the hostname of a device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the hostname matches the provided input.\n    * Failure: The test will fail if the hostname does not match the provided input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.services:\n      - VerifyHostname:\n          hostname: s1-spine1\n    ```\n    \"\"\"\n\n    name = \"VerifyHostname\"\n    description = \"Verifies the hostname of a device.\"\n    categories: ClassVar[list[str]] = [\"services\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hostname\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyHostname test.\"\"\"\n\n        hostname: str\n        \"\"\"Expected hostname of the device.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyHostname.\"\"\"\n        hostname = self.instance_commands[0].json_output[\"hostname\"]\n\n        if hostname != self.inputs.hostname:\n            self.result.is_failure(f\"Expected `{self.inputs.hostname}` as the hostname, but found `{hostname}` instead.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.services/#anta.tests.services.VerifyHostname-attributes","title":"Inputs","text":"Name Type Description Default hostname str Expected hostname of the device. -"},{"location":"api/tests.snmp/","title":"SNMP","text":""},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpContact","title":"VerifySnmpContact","text":"

Verifies the SNMP contact of a device.

Expected Results
  • Success: The test will pass if the SNMP contact matches the provided input.
  • Failure: The test will fail if the SNMP contact does not match the provided input.
Examples
anta.tests.snmp:\n  - VerifySnmpContact:\n      contact: Jon@example.com\n
Source code in anta/tests/snmp.py
class VerifySnmpContact(AntaTest):\n    \"\"\"Verifies the SNMP contact of a device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SNMP contact matches the provided input.\n    * Failure: The test will fail if the SNMP contact does not match the provided input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.snmp:\n      - VerifySnmpContact:\n          contact: Jon@example.com\n    ```\n    \"\"\"\n\n    name = \"VerifySnmpContact\"\n    description = \"Verifies the SNMP contact of a device.\"\n    categories: ClassVar[list[str]] = [\"snmp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySnmpContact test.\"\"\"\n\n        contact: str\n        \"\"\"Expected SNMP contact details of the device.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySnmpContact.\"\"\"\n        contact = self.instance_commands[0].json_output[\"contact\"][\"contact\"]\n\n        if contact != self.inputs.contact:\n            self.result.is_failure(f\"Expected `{self.inputs.contact}` as the contact, but found `{contact}` instead.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpContact-attributes","title":"Inputs","text":"Name Type Description Default contact str Expected SNMP contact details of the device. -"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpIPv4Acl","title":"VerifySnmpIPv4Acl","text":"

Verifies if the SNMP agent has the right number IPv4 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if the SNMP agent has the provided number of IPv4 ACL(s) in the specified VRF.
  • Failure: The test will fail if the SNMP agent has not the right number of IPv4 ACL(s) in the specified VRF.
Examples
anta.tests.snmp:\n  - VerifySnmpIPv4Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/snmp.py
class VerifySnmpIPv4Acl(AntaTest):\n    \"\"\"Verifies if the SNMP agent has the right number IPv4 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SNMP agent has the provided number of IPv4 ACL(s) in the specified VRF.\n    * Failure: The test will fail if the SNMP agent has not the right number of IPv4 ACL(s) in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.snmp:\n      - VerifySnmpIPv4Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifySnmpIPv4Acl\"\n    description = \"Verifies if the SNMP agent has IPv4 ACL(s) configured.\"\n    categories: ClassVar[list[str]] = [\"snmp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp ipv4 access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySnmpIPv4Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv4 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySnmpIPv4Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv4_acl_list = command_output[\"ipAclList\"][\"aclList\"]\n        ipv4_acl_number = len(ipv4_acl_list)\n        if ipv4_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} SNMP IPv4 ACL(s) in vrf {self.inputs.vrf} but got {ipv4_acl_number}\")\n            return\n\n        not_configured_acl = [acl[\"name\"] for acl in ipv4_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if not_configured_acl:\n            self.result.is_failure(f\"SNMP IPv4 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpIPv4Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv4 ACL(s). - vrf str The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF. 'default'"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpIPv6Acl","title":"VerifySnmpIPv6Acl","text":"

Verifies if the SNMP agent has the right number IPv6 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if the SNMP agent has the provided number of IPv6 ACL(s) in the specified VRF.
  • Failure: The test will fail if the SNMP agent has not the right number of IPv6 ACL(s) in the specified VRF.
Examples
anta.tests.snmp:\n  - VerifySnmpIPv6Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/snmp.py
class VerifySnmpIPv6Acl(AntaTest):\n    \"\"\"Verifies if the SNMP agent has the right number IPv6 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SNMP agent has the provided number of IPv6 ACL(s) in the specified VRF.\n    * Failure: The test will fail if the SNMP agent has not the right number of IPv6 ACL(s) in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.snmp:\n      - VerifySnmpIPv6Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifySnmpIPv6Acl\"\n    description = \"Verifies if the SNMP agent has IPv6 ACL(s) configured.\"\n    categories: ClassVar[list[str]] = [\"snmp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp ipv6 access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySnmpIPv6Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv6 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySnmpIPv6Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv6_acl_list = command_output[\"ipv6AclList\"][\"aclList\"]\n        ipv6_acl_number = len(ipv6_acl_list)\n        if ipv6_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} SNMP IPv6 ACL(s) in vrf {self.inputs.vrf} but got {ipv6_acl_number}\")\n            return\n\n        acl_not_configured = [acl[\"name\"] for acl in ipv6_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if acl_not_configured:\n            self.result.is_failure(f\"SNMP IPv6 ACL(s) not configured or active in vrf {self.inputs.vrf}: {acl_not_configured}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpIPv6Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv6 ACL(s). - vrf str The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF. 'default'"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpLocation","title":"VerifySnmpLocation","text":"

Verifies the SNMP location of a device.

Expected Results
  • Success: The test will pass if the SNMP location matches the provided input.
  • Failure: The test will fail if the SNMP location does not match the provided input.
Examples
anta.tests.snmp:\n  - VerifySnmpLocation:\n      location: New York\n
Source code in anta/tests/snmp.py
class VerifySnmpLocation(AntaTest):\n    \"\"\"Verifies the SNMP location of a device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SNMP location matches the provided input.\n    * Failure: The test will fail if the SNMP location does not match the provided input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.snmp:\n      - VerifySnmpLocation:\n          location: New York\n    ```\n    \"\"\"\n\n    name = \"VerifySnmpLocation\"\n    description = \"Verifies the SNMP location of a device.\"\n    categories: ClassVar[list[str]] = [\"snmp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySnmpLocation test.\"\"\"\n\n        location: str\n        \"\"\"Expected SNMP location of the device.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySnmpLocation.\"\"\"\n        location = self.instance_commands[0].json_output[\"location\"][\"location\"]\n\n        if location != self.inputs.location:\n            self.result.is_failure(f\"Expected `{self.inputs.location}` as the location, but found `{location}` instead.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpLocation-attributes","title":"Inputs","text":"Name Type Description Default location str Expected SNMP location of the device. -"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpStatus","title":"VerifySnmpStatus","text":"

Verifies whether the SNMP agent is enabled in a specified VRF.

Expected Results
  • Success: The test will pass if the SNMP agent is enabled in the specified VRF.
  • Failure: The test will fail if the SNMP agent is disabled in the specified VRF.
Examples
anta.tests.snmp:\n  - VerifySnmpStatus:\n      vrf: default\n
Source code in anta/tests/snmp.py
class VerifySnmpStatus(AntaTest):\n    \"\"\"Verifies whether the SNMP agent is enabled in a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SNMP agent is enabled in the specified VRF.\n    * Failure: The test will fail if the SNMP agent is disabled in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.snmp:\n      - VerifySnmpStatus:\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifySnmpStatus\"\n    description = \"Verifies if the SNMP agent is enabled.\"\n    categories: ClassVar[list[str]] = [\"snmp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySnmpStatus test.\"\"\"\n\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySnmpStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"enabled\"] and self.inputs.vrf in command_output[\"vrfs\"][\"snmpVrfs\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"SNMP agent disabled in vrf {self.inputs.vrf}\")\n
"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpStatus-attributes","title":"Inputs","text":"Name Type Description Default vrf str The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF. 'default'"},{"location":"api/tests.software/","title":"Software","text":""},{"location":"api/tests.software/#anta.tests.software.VerifyEOSExtensions","title":"VerifyEOSExtensions","text":"

Verifies that all EOS extensions installed on the device are enabled for boot persistence.

Expected Results
  • Success: The test will pass if all EOS extensions installed on the device are enabled for boot persistence.
  • Failure: The test will fail if some EOS extensions installed on the device are not enabled for boot persistence.
Examples
anta.tests.software:\n  - VerifyEOSExtensions:\n
Source code in anta/tests/software.py
class VerifyEOSExtensions(AntaTest):\n    \"\"\"Verifies that all EOS extensions installed on the device are enabled for boot persistence.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all EOS extensions installed on the device are enabled for boot persistence.\n    * Failure: The test will fail if some EOS extensions installed on the device are not enabled for boot persistence.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.software:\n      - VerifyEOSExtensions:\n    ```\n    \"\"\"\n\n    name = \"VerifyEOSExtensions\"\n    description = \"Verifies that all EOS extensions installed on the device are enabled for boot persistence.\"\n    categories: ClassVar[list[str]] = [\"software\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show extensions\"), AntaCommand(command=\"show boot-extensions\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEOSExtensions.\"\"\"\n        boot_extensions = []\n        show_extensions_command_output = self.instance_commands[0].json_output\n        show_boot_extensions_command_output = self.instance_commands[1].json_output\n        installed_extensions = [\n            extension for extension, extension_data in show_extensions_command_output[\"extensions\"].items() if extension_data[\"status\"] == \"installed\"\n        ]\n        for extension in show_boot_extensions_command_output[\"extensions\"]:\n            formatted_extension = extension.strip(\"\\n\")\n            if formatted_extension != \"\":\n                boot_extensions.append(formatted_extension)\n        installed_extensions.sort()\n        boot_extensions.sort()\n        if installed_extensions == boot_extensions:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Missing EOS extensions: installed {installed_extensions} / configured: {boot_extensions}\")\n
"},{"location":"api/tests.software/#anta.tests.software.VerifyEOSVersion","title":"VerifyEOSVersion","text":"

Verifies that the device is running one of the allowed EOS version.

Expected Results
  • Success: The test will pass if the device is running one of the allowed EOS version.
  • Failure: The test will fail if the device is not running one of the allowed EOS version.
Examples
anta.tests.software:\n  - VerifyEOSVersion:\n      versions:\n        - 4.25.4M\n        - 4.26.1F\n
Source code in anta/tests/software.py
class VerifyEOSVersion(AntaTest):\n    \"\"\"Verifies that the device is running one of the allowed EOS version.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is running one of the allowed EOS version.\n    * Failure: The test will fail if the device is not running one of the allowed EOS version.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.software:\n      - VerifyEOSVersion:\n          versions:\n            - 4.25.4M\n            - 4.26.1F\n    ```\n    \"\"\"\n\n    name = \"VerifyEOSVersion\"\n    description = \"Verifies the EOS version of the device.\"\n    categories: ClassVar[list[str]] = [\"software\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyEOSVersion test.\"\"\"\n\n        versions: list[str]\n        \"\"\"List of allowed EOS versions.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEOSVersion.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"version\"] in self.inputs.versions:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f'device is running version \"{command_output[\"version\"]}\" not in expected versions: {self.inputs.versions}')\n
"},{"location":"api/tests.software/#anta.tests.software.VerifyEOSVersion-attributes","title":"Inputs","text":"Name Type Description Default versions list[str] List of allowed EOS versions. -"},{"location":"api/tests.software/#anta.tests.software.VerifyTerminAttrVersion","title":"VerifyTerminAttrVersion","text":"

Verifies that he device is running one of the allowed TerminAttr version.

Expected Results
  • Success: The test will pass if the device is running one of the allowed TerminAttr version.
  • Failure: The test will fail if the device is not running one of the allowed TerminAttr version.
Examples
anta.tests.software:\n  - VerifyTerminAttrVersion:\n      versions:\n        - v1.13.6\n        - v1.8.0\n
Source code in anta/tests/software.py
class VerifyTerminAttrVersion(AntaTest):\n    \"\"\"Verifies that he device is running one of the allowed TerminAttr version.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is running one of the allowed TerminAttr version.\n    * Failure: The test will fail if the device is not running one of the allowed TerminAttr version.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.software:\n      - VerifyTerminAttrVersion:\n          versions:\n            - v1.13.6\n            - v1.8.0\n    ```\n    \"\"\"\n\n    name = \"VerifyTerminAttrVersion\"\n    description = \"Verifies the TerminAttr version of the device.\"\n    categories: ClassVar[list[str]] = [\"software\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version detail\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTerminAttrVersion test.\"\"\"\n\n        versions: list[str]\n        \"\"\"List of allowed TerminAttr versions.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTerminAttrVersion.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        command_output_data = command_output[\"details\"][\"packages\"][\"TerminAttr-core\"][\"version\"]\n        if command_output_data in self.inputs.versions:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"device is running TerminAttr version {command_output_data} and is not in the allowed list: {self.inputs.versions}\")\n
"},{"location":"api/tests.software/#anta.tests.software.VerifyTerminAttrVersion-attributes","title":"Inputs","text":"Name Type Description Default versions list[str] List of allowed TerminAttr versions. -"},{"location":"api/tests.stp/","title":"STP","text":""},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPBlockedPorts","title":"VerifySTPBlockedPorts","text":"

Verifies there is no STP blocked ports.

Expected Results
  • Success: The test will pass if there are NO ports blocked by STP.
  • Failure: The test will fail if there are ports blocked by STP.
Examples
anta.tests.stp:\n  - VerifySTPBlockedPorts:\n
Source code in anta/tests/stp.py
class VerifySTPBlockedPorts(AntaTest):\n    \"\"\"Verifies there is no STP blocked ports.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO ports blocked by STP.\n    * Failure: The test will fail if there are ports blocked by STP.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.stp:\n      - VerifySTPBlockedPorts:\n    ```\n    \"\"\"\n\n    name = \"VerifySTPBlockedPorts\"\n    description = \"Verifies there is no STP blocked ports.\"\n    categories: ClassVar[list[str]] = [\"stp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree blockedports\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySTPBlockedPorts.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if not (stp_instances := command_output[\"spanningTreeInstances\"]):\n            self.result.is_success()\n        else:\n            for key, value in stp_instances.items():\n                stp_instances[key] = value.pop(\"spanningTreeBlockedPorts\")\n            self.result.is_failure(f\"The following ports are blocked by STP: {stp_instances}\")\n
"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPCounters","title":"VerifySTPCounters","text":"

Verifies there is no errors in STP BPDU packets.

Expected Results
  • Success: The test will pass if there are NO STP BPDU packet errors under all interfaces participating in STP.
  • Failure: The test will fail if there are STP BPDU packet errors on one or many interface(s).
Examples
anta.tests.stp:\n  - VerifySTPCounters:\n
Source code in anta/tests/stp.py
class VerifySTPCounters(AntaTest):\n    \"\"\"Verifies there is no errors in STP BPDU packets.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO STP BPDU packet errors under all interfaces participating in STP.\n    * Failure: The test will fail if there are STP BPDU packet errors on one or many interface(s).\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.stp:\n      - VerifySTPCounters:\n    ```\n    \"\"\"\n\n    name = \"VerifySTPCounters\"\n    description = \"Verifies there is no errors in STP BPDU packets.\"\n    categories: ClassVar[list[str]] = [\"stp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree counters\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySTPCounters.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        interfaces_with_errors = [\n            interface for interface, counters in command_output[\"interfaces\"].items() if counters[\"bpduTaggedError\"] or counters[\"bpduOtherError\"] != 0\n        ]\n        if interfaces_with_errors:\n            self.result.is_failure(f\"The following interfaces have STP BPDU packet errors: {interfaces_with_errors}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPForwardingPorts","title":"VerifySTPForwardingPorts","text":"

Verifies that all interfaces are in a forwarding state for a provided list of VLAN(s).

Expected Results
  • Success: The test will pass if all interfaces are in a forwarding state for the specified VLAN(s).
  • Failure: The test will fail if one or many interfaces are NOT in a forwarding state in the specified VLAN(s).
Examples
anta.tests.stp:\n  - VerifySTPForwardingPorts:\n      vlans:\n        - 10\n        - 20\n
Source code in anta/tests/stp.py
class VerifySTPForwardingPorts(AntaTest):\n    \"\"\"Verifies that all interfaces are in a forwarding state for a provided list of VLAN(s).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all interfaces are in a forwarding state for the specified VLAN(s).\n    * Failure: The test will fail if one or many interfaces are NOT in a forwarding state in the specified VLAN(s).\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.stp:\n      - VerifySTPForwardingPorts:\n          vlans:\n            - 10\n            - 20\n    ```\n    \"\"\"\n\n    name = \"VerifySTPForwardingPorts\"\n    description = \"Verifies that all interfaces are forwarding for a provided list of VLAN(s).\"\n    categories: ClassVar[list[str]] = [\"stp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show spanning-tree topology vlan {vlan} status\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySTPForwardingPorts test.\"\"\"\n\n        vlans: list[Vlan]\n        \"\"\"List of VLAN on which to verify forwarding states.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each VLAN in the input list.\"\"\"\n        return [template.render(vlan=vlan) for vlan in self.inputs.vlans]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySTPForwardingPorts.\"\"\"\n        not_configured = []\n        not_forwarding = []\n        for command in self.instance_commands:\n            if \"vlan\" in command.params:\n                vlan_id = command.params[\"vlan\"]\n            if not (topologies := get_value(command.json_output, \"topologies\")):\n                not_configured.append(vlan_id)\n            else:\n                for value in topologies.values():\n                    if int(vlan_id) in value[\"vlans\"]:\n                        interfaces_not_forwarding = [interface for interface, state in value[\"interfaces\"].items() if state[\"state\"] != \"forwarding\"]\n                if interfaces_not_forwarding:\n                    not_forwarding.append({f\"VLAN {vlan_id}\": interfaces_not_forwarding})\n        if not_configured:\n            self.result.is_failure(f\"STP instance is not configured for the following VLAN(s): {not_configured}\")\n        if not_forwarding:\n            self.result.is_failure(f\"The following VLAN(s) have interface(s) that are not in a fowarding state: {not_forwarding}\")\n        if not not_configured and not interfaces_not_forwarding:\n            self.result.is_success()\n
"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPForwardingPorts-attributes","title":"Inputs","text":"Name Type Description Default vlans list[Vlan] List of VLAN on which to verify forwarding states. -"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPMode","title":"VerifySTPMode","text":"

Verifies the configured STP mode for a provided list of VLAN(s).

Expected Results
  • Success: The test will pass if the STP mode is configured properly in the specified VLAN(s).
  • Failure: The test will fail if the STP mode is NOT configured properly for one or more specified VLAN(s).
Examples
anta.tests.stp:\n  - VerifySTPMode:\n      mode: rapidPvst\n      vlans:\n        - 10\n        - 20\n
Source code in anta/tests/stp.py
class VerifySTPMode(AntaTest):\n    \"\"\"Verifies the configured STP mode for a provided list of VLAN(s).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the STP mode is configured properly in the specified VLAN(s).\n    * Failure: The test will fail if the STP mode is NOT configured properly for one or more specified VLAN(s).\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.stp:\n      - VerifySTPMode:\n          mode: rapidPvst\n          vlans:\n            - 10\n            - 20\n    ```\n    \"\"\"\n\n    name = \"VerifySTPMode\"\n    description = \"Verifies the configured STP mode for a provided list of VLAN(s).\"\n    categories: ClassVar[list[str]] = [\"stp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show spanning-tree vlan {vlan}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySTPMode test.\"\"\"\n\n        mode: Literal[\"mstp\", \"rstp\", \"rapidPvst\"] = \"mstp\"\n        \"\"\"STP mode to verify. Supported values: mstp, rstp, rapidPvst. Defaults to mstp.\"\"\"\n        vlans: list[Vlan]\n        \"\"\"List of VLAN on which to verify STP mode.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each VLAN in the input list.\"\"\"\n        return [template.render(vlan=vlan) for vlan in self.inputs.vlans]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySTPMode.\"\"\"\n        not_configured = []\n        wrong_stp_mode = []\n        for command in self.instance_commands:\n            if \"vlan\" in command.params:\n                vlan_id = command.params[\"vlan\"]\n            if not (\n                stp_mode := get_value(\n                    command.json_output,\n                    f\"spanningTreeVlanInstances.{vlan_id}.spanningTreeVlanInstance.protocol\",\n                )\n            ):\n                not_configured.append(vlan_id)\n            elif stp_mode != self.inputs.mode:\n                wrong_stp_mode.append(vlan_id)\n        if not_configured:\n            self.result.is_failure(f\"STP mode '{self.inputs.mode}' not configured for the following VLAN(s): {not_configured}\")\n        if wrong_stp_mode:\n            self.result.is_failure(f\"Wrong STP mode configured for the following VLAN(s): {wrong_stp_mode}\")\n        if not not_configured and not wrong_stp_mode:\n            self.result.is_success()\n
"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPMode-attributes","title":"Inputs","text":"Name Type Description Default mode Literal['mstp', 'rstp', 'rapidPvst'] STP mode to verify. Supported values: mstp, rstp, rapidPvst. Defaults to mstp. 'mstp' vlans list[Vlan] List of VLAN on which to verify STP mode. -"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPRootPriority","title":"VerifySTPRootPriority","text":"

Verifies the STP root priority for a provided list of VLAN or MST instance ID(s).

Expected Results
  • Success: The test will pass if the STP root priority is configured properly for the specified VLAN or MST instance ID(s).
  • Failure: The test will fail if the STP root priority is NOT configured properly for the specified VLAN or MST instance ID(s).
Examples
anta.tests.stp:\n  - VerifySTPRootPriority:\n      priority: 32768\n      instances:\n        - 10\n        - 20\n
Source code in anta/tests/stp.py
class VerifySTPRootPriority(AntaTest):\n    \"\"\"Verifies the STP root priority for a provided list of VLAN or MST instance ID(s).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the STP root priority is configured properly for the specified VLAN or MST instance ID(s).\n    * Failure: The test will fail if the STP root priority is NOT configured properly for the specified VLAN or MST instance ID(s).\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.stp:\n      - VerifySTPRootPriority:\n          priority: 32768\n          instances:\n            - 10\n            - 20\n    ```\n    \"\"\"\n\n    name = \"VerifySTPRootPriority\"\n    description = \"Verifies the STP root priority for a provided list of VLAN or MST instance ID(s).\"\n    categories: ClassVar[list[str]] = [\"stp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree root detail\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySTPRootPriority test.\"\"\"\n\n        priority: int\n        \"\"\"STP root priority to verify.\"\"\"\n        instances: list[Vlan] = Field(default=[])\n        \"\"\"List of VLAN or MST instance ID(s). If empty, ALL VLAN or MST instance ID(s) will be verified.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySTPRootPriority.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if not (stp_instances := command_output[\"instances\"]):\n            self.result.is_failure(\"No STP instances configured\")\n            return\n        # Checking the type of instances based on first instance\n        first_name = next(iter(stp_instances))\n        if first_name.startswith(\"MST\"):\n            prefix = \"MST\"\n        elif first_name.startswith(\"VL\"):\n            prefix = \"VL\"\n        else:\n            self.result.is_failure(f\"Unsupported STP instance type: {first_name}\")\n            return\n        check_instances = [f\"{prefix}{instance_id}\" for instance_id in self.inputs.instances] if self.inputs.instances else command_output[\"instances\"].keys()\n        wrong_priority_instances = [\n            instance for instance in check_instances if get_value(command_output, f\"instances.{instance}.rootBridge.priority\") != self.inputs.priority\n        ]\n        if wrong_priority_instances:\n            self.result.is_failure(f\"The following instance(s) have the wrong STP root priority configured: {wrong_priority_instances}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPRootPriority-attributes","title":"Inputs","text":"Name Type Description Default priority int STP root priority to verify. - instances list[Vlan] List of VLAN or MST instance ID(s). If empty, ALL VLAN or MST instance ID(s) will be verified. Field(default=[])"},{"location":"api/tests.system/","title":"System","text":""},{"location":"api/tests.system/#anta.tests.system.VerifyAgentLogs","title":"VerifyAgentLogs","text":"

Verifies that no agent crash reports are present on the device.

Expected Results
  • Success: The test will pass if there is NO agent crash reported.
  • Failure: The test will fail if any agent crashes are reported.
Examples
anta.tests.system:\n  - VerifyAgentLogs:\n
Source code in anta/tests/system.py
class VerifyAgentLogs(AntaTest):\n    \"\"\"Verifies that no agent crash reports are present on the device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there is NO agent crash reported.\n    * Failure: The test will fail if any agent crashes are reported.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyAgentLogs:\n    ```\n    \"\"\"\n\n    name = \"VerifyAgentLogs\"\n    description = \"Verifies there are no agent crash reports.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show agent logs crash\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAgentLogs.\"\"\"\n        command_output = self.instance_commands[0].text_output\n        if len(command_output) == 0:\n            self.result.is_success()\n        else:\n            pattern = re.compile(r\"^===> (.*?) <===$\", re.MULTILINE)\n            agents = \"\\n * \".join(pattern.findall(command_output))\n            self.result.is_failure(f\"Device has reported agent crashes:\\n * {agents}\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyCPUUtilization","title":"VerifyCPUUtilization","text":"

Verifies whether the CPU utilization is below 75%.

Expected Results
  • Success: The test will pass if the CPU utilization is below 75%.
  • Failure: The test will fail if the CPU utilization is over 75%.
Examples
anta.tests.system:\n  - VerifyCPUUtilization:\n
Source code in anta/tests/system.py
class VerifyCPUUtilization(AntaTest):\n    \"\"\"Verifies whether the CPU utilization is below 75%.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the CPU utilization is below 75%.\n    * Failure: The test will fail if the CPU utilization is over 75%.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyCPUUtilization:\n    ```\n    \"\"\"\n\n    name = \"VerifyCPUUtilization\"\n    description = \"Verifies whether the CPU utilization is below 75%.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show processes top once\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyCPUUtilization.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        command_output_data = command_output[\"cpuInfo\"][\"%Cpu(s)\"][\"idle\"]\n        if command_output_data > CPU_IDLE_THRESHOLD:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device has reported a high CPU utilization: {100 - command_output_data}%\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyCoredump","title":"VerifyCoredump","text":"

Verifies if there are core dump files in the /var/core directory.

Expected Results
  • Success: The test will pass if there are NO core dump(s) in /var/core.
  • Failure: The test will fail if there are core dump(s) in /var/core.
Info
  • This test will NOT check for minidump(s) generated by certain agents in /var/core/minidump.
Examples
anta.tests.system:\n  - VerifyCoreDump:\n
Source code in anta/tests/system.py
class VerifyCoredump(AntaTest):\n    \"\"\"Verifies if there are core dump files in the /var/core directory.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO core dump(s) in /var/core.\n    * Failure: The test will fail if there are core dump(s) in /var/core.\n\n    Info\n    ----\n    * This test will NOT check for minidump(s) generated by certain agents in /var/core/minidump.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyCoreDump:\n    ```\n    \"\"\"\n\n    name = \"VerifyCoredump\"\n    description = \"Verifies there are no core dump files.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system coredump\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyCoredump.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        core_files = command_output[\"coreFiles\"]\n        if \"minidump\" in core_files:\n            core_files.remove(\"minidump\")\n        if not core_files:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Core dump(s) have been found: {core_files}\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyFileSystemUtilization","title":"VerifyFileSystemUtilization","text":"

Verifies that no partition is utilizing more than 75% of its disk space.

Expected Results
  • Success: The test will pass if all partitions are using less than 75% of its disk space.
  • Failure: The test will fail if any partitions are using more than 75% of its disk space.
Examples
anta.tests.system:\n  - VerifyFileSystemUtilization:\n
Source code in anta/tests/system.py
class VerifyFileSystemUtilization(AntaTest):\n    \"\"\"Verifies that no partition is utilizing more than 75% of its disk space.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all partitions are using less than 75% of its disk space.\n    * Failure: The test will fail if any partitions are using more than 75% of its disk space.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyFileSystemUtilization:\n    ```\n    \"\"\"\n\n    name = \"VerifyFileSystemUtilization\"\n    description = \"Verifies that no partition is utilizing more than 75% of its disk space.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"bash timeout 10 df -h\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyFileSystemUtilization.\"\"\"\n        command_output = self.instance_commands[0].text_output\n        self.result.is_success()\n        for line in command_output.split(\"\\n\")[1:]:\n            if \"loop\" not in line and len(line) > 0 and (percentage := int(line.split()[4].replace(\"%\", \"\"))) > DISK_SPACE_THRESHOLD:\n                self.result.is_failure(f\"Mount point {line} is higher than 75%: reported {percentage}%\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyMemoryUtilization","title":"VerifyMemoryUtilization","text":"

Verifies whether the memory utilization is below 75%.

Expected Results
  • Success: The test will pass if the memory utilization is below 75%.
  • Failure: The test will fail if the memory utilization is over 75%.
Examples
anta.tests.system:\n  - VerifyMemoryUtilization:\n
Source code in anta/tests/system.py
class VerifyMemoryUtilization(AntaTest):\n    \"\"\"Verifies whether the memory utilization is below 75%.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the memory utilization is below 75%.\n    * Failure: The test will fail if the memory utilization is over 75%.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyMemoryUtilization:\n    ```\n    \"\"\"\n\n    name = \"VerifyMemoryUtilization\"\n    description = \"Verifies whether the memory utilization is below 75%.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMemoryUtilization.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        memory_usage = command_output[\"memFree\"] / command_output[\"memTotal\"]\n        if memory_usage > MEMORY_THRESHOLD:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device has reported a high memory usage: {(1 - memory_usage)*100:.2f}%\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyNTP","title":"VerifyNTP","text":"

Verifies that the Network Time Protocol (NTP) is synchronized.

Expected Results
  • Success: The test will pass if the NTP is synchronised.
  • Failure: The test will fail if the NTP is NOT synchronised.
Examples
anta.tests.system:\n  - VerifyNTP:\n
Source code in anta/tests/system.py
class VerifyNTP(AntaTest):\n    \"\"\"Verifies that the Network Time Protocol (NTP) is synchronized.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the NTP is synchronised.\n    * Failure: The test will fail if the NTP is NOT synchronised.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyNTP:\n    ```\n    \"\"\"\n\n    name = \"VerifyNTP\"\n    description = \"Verifies if NTP is synchronised.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ntp status\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyNTP.\"\"\"\n        command_output = self.instance_commands[0].text_output\n        if command_output.split(\"\\n\")[0].split(\" \")[0] == \"synchronised\":\n            self.result.is_success()\n        else:\n            data = command_output.split(\"\\n\")[0]\n            self.result.is_failure(f\"The device is not synchronized with the configured NTP server(s): '{data}'\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyReloadCause","title":"VerifyReloadCause","text":"

Verifies the last reload cause of the device.

Expected Results
  • Success: The test will pass if there are NO reload causes or if the last reload was caused by the user or after an FPGA upgrade.
  • Failure: The test will fail if the last reload was NOT caused by the user or after an FPGA upgrade.
  • Error: The test will report an error if the reload cause is NOT available.
Examples
anta.tests.system:\n  - VerifyReloadCause:\n
Source code in anta/tests/system.py
class VerifyReloadCause(AntaTest):\n    \"\"\"Verifies the last reload cause of the device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO reload causes or if the last reload was caused by the user or after an FPGA upgrade.\n    * Failure: The test will fail if the last reload was NOT caused by the user or after an FPGA upgrade.\n    * Error: The test will report an error if the reload cause is NOT available.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyReloadCause:\n    ```\n    \"\"\"\n\n    name = \"VerifyReloadCause\"\n    description = \"Verifies the last reload cause of the device.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show reload cause\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyReloadCause.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if \"resetCauses\" not in command_output:\n            self.result.is_error(message=\"No reload causes available\")\n            return\n        if len(command_output[\"resetCauses\"]) == 0:\n            # No reload causes\n            self.result.is_success()\n            return\n        reset_causes = command_output[\"resetCauses\"]\n        command_output_data = reset_causes[0].get(\"description\")\n        if command_output_data in [\n            \"Reload requested by the user.\",\n            \"Reload requested after FPGA upgrade\",\n        ]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Reload cause is: '{command_output_data}'\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyUptime","title":"VerifyUptime","text":"

Verifies if the device uptime is higher than the provided minimum uptime value.

Expected Results
  • Success: The test will pass if the device uptime is higher than the provided value.
  • Failure: The test will fail if the device uptime is lower than the provided value.
Examples
anta.tests.system:\n  - VerifyUptime:\n      minimum: 86400\n
Source code in anta/tests/system.py
class VerifyUptime(AntaTest):\n    \"\"\"Verifies if the device uptime is higher than the provided minimum uptime value.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device uptime is higher than the provided value.\n    * Failure: The test will fail if the device uptime is lower than the provided value.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyUptime:\n          minimum: 86400\n    ```\n    \"\"\"\n\n    name = \"VerifyUptime\"\n    description = \"Verifies the device uptime.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show uptime\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyUptime test.\"\"\"\n\n        minimum: PositiveInteger\n        \"\"\"Minimum uptime in seconds.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyUptime.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"upTime\"] > self.inputs.minimum:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device uptime is {command_output['upTime']} seconds\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyUptime-attributes","title":"Inputs","text":"Name Type Description Default minimum PositiveInteger Minimum uptime in seconds. -"},{"location":"api/tests.vlan/","title":"VLAN","text":""},{"location":"api/tests.vlan/#anta.tests.vlan.VerifyVlanInternalPolicy","title":"VerifyVlanInternalPolicy","text":"

Verifies if the VLAN internal allocation policy is ascending or descending and if the VLANs are within the specified range.

Expected Results
  • Success: The test will pass if the VLAN internal allocation policy is either ascending or descending and the VLANs are within the specified range.
  • Failure: The test will fail if the VLAN internal allocation policy is neither ascending nor descending or the VLANs are outside the specified range.
Examples
anta.tests.vlan:\n  - VerifyVlanInternalPolicy:\n      policy: ascending\n      start_vlan_id: 1006\n      end_vlan_id: 4094\n
Source code in anta/tests/vlan.py
class VerifyVlanInternalPolicy(AntaTest):\n    \"\"\"Verifies if the VLAN internal allocation policy is ascending or descending and if the VLANs are within the specified range.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the VLAN internal allocation policy is either ascending or descending\n                 and the VLANs are within the specified range.\n    * Failure: The test will fail if the VLAN internal allocation policy is neither ascending nor descending\n                 or the VLANs are outside the specified range.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vlan:\n      - VerifyVlanInternalPolicy:\n          policy: ascending\n          start_vlan_id: 1006\n          end_vlan_id: 4094\n    ```\n    \"\"\"\n\n    name = \"VerifyVlanInternalPolicy\"\n    description = \"Verifies the VLAN internal allocation policy and the range of VLANs.\"\n    categories: ClassVar[list[str]] = [\"vlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vlan internal allocation policy\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyVlanInternalPolicy test.\"\"\"\n\n        policy: Literal[\"ascending\", \"descending\"]\n        \"\"\"The VLAN internal allocation policy. Supported values: ascending, descending.\"\"\"\n        start_vlan_id: Vlan\n        \"\"\"The starting VLAN ID in the range.\"\"\"\n        end_vlan_id: Vlan\n        \"\"\"The ending VLAN ID in the range.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVlanInternalPolicy.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        keys_to_verify = [\"policy\", \"startVlanId\", \"endVlanId\"]\n        actual_policy_output = {key: get_value(command_output, key) for key in keys_to_verify}\n        expected_policy_output = {\"policy\": self.inputs.policy, \"startVlanId\": self.inputs.start_vlan_id, \"endVlanId\": self.inputs.end_vlan_id}\n\n        # Check if the actual output matches the expected output\n        if actual_policy_output != expected_policy_output:\n            failed_log = \"The VLAN internal allocation policy is not configured properly:\"\n            failed_log += get_failed_logs(expected_policy_output, actual_policy_output)\n            self.result.is_failure(failed_log)\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.vlan/#anta.tests.vlan.VerifyVlanInternalPolicy-attributes","title":"Inputs","text":"Name Type Description Default policy Literal['ascending', 'descending'] The VLAN internal allocation policy. Supported values: ascending, descending. - start_vlan_id Vlan The starting VLAN ID in the range. - end_vlan_id Vlan The ending VLAN ID in the range. -"},{"location":"api/tests.vxlan/","title":"VXLAN","text":""},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlan1ConnSettings","title":"VerifyVxlan1ConnSettings","text":"

Verifies the interface vxlan1 source interface and UDP port.

Expected Results
  • Success: Passes if the interface vxlan1 source interface and UDP port are correct.
  • Failure: Fails if the interface vxlan1 source interface or UDP port are incorrect.
  • Skipped: Skips if the Vxlan1 interface is not configured.
Examples
anta.tests.vxlan:\n  - VerifyVxlan1ConnSettings:\n      source_interface: Loopback1\n      udp_port: 4789\n
Source code in anta/tests/vxlan.py
class VerifyVxlan1ConnSettings(AntaTest):\n    \"\"\"Verifies the interface vxlan1 source interface and UDP port.\n\n    Expected Results\n    ----------------\n    * Success: Passes if the interface vxlan1 source interface and UDP port are correct.\n    * Failure: Fails if the interface vxlan1 source interface or UDP port are incorrect.\n    * Skipped: Skips if the Vxlan1 interface is not configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vxlan:\n      - VerifyVxlan1ConnSettings:\n          source_interface: Loopback1\n          udp_port: 4789\n    ```\n    \"\"\"\n\n    name = \"VerifyVxlan1ConnSettings\"\n    description = \"Verifies the interface vxlan1 source interface and UDP port.\"\n    categories: ClassVar[list[str]] = [\"vxlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyVxlan1ConnSettings test.\"\"\"\n\n        source_interface: VxlanSrcIntf\n        \"\"\"Source loopback interface of vxlan1 interface.\"\"\"\n        udp_port: int = Field(ge=1024, le=65335)\n        \"\"\"UDP port used for vxlan1 interface.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVxlan1ConnSettings.\"\"\"\n        self.result.is_success()\n        command_output = self.instance_commands[0].json_output\n\n        # Skip the test case if vxlan1 interface is not configured\n        vxlan_output = get_value(command_output, \"interfaces.Vxlan1\")\n        if not vxlan_output:\n            self.result.is_skipped(\"Vxlan1 interface is not configured.\")\n            return\n\n        src_intf = vxlan_output.get(\"srcIpIntf\")\n        port = vxlan_output.get(\"udpPort\")\n\n        # Check vxlan1 source interface and udp port\n        if src_intf != self.inputs.source_interface:\n            self.result.is_failure(f\"Source interface is not correct. Expected `{self.inputs.source_interface}` as source interface but found `{src_intf}` instead.\")\n        if port != self.inputs.udp_port:\n            self.result.is_failure(f\"UDP port is not correct. Expected `{self.inputs.udp_port}` as UDP port but found `{port}` instead.\")\n
"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlan1ConnSettings-attributes","title":"Inputs","text":"Name Type Description Default source_interface VxlanSrcIntf Source loopback interface of vxlan1 interface. - udp_port int UDP port used for vxlan1 interface. Field(ge=1024, le=65335)"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlan1Interface","title":"VerifyVxlan1Interface","text":"

Verifies if the Vxlan1 interface is configured and \u2018up/up\u2019.

Warning

The name of this test has been updated from \u2018VerifyVxlan\u2019 for better representation.

Expected Results
  • Success: The test will pass if the Vxlan1 interface is configured with line protocol status and interface status \u2018up\u2019.
  • Failure: The test will fail if the Vxlan1 interface line protocol status or interface status are not \u2018up\u2019.
  • Skipped: The test will be skipped if the Vxlan1 interface is not configured.
Examples
anta.tests.vxlan:\n  - VerifyVxlan1Interface:\n
Source code in anta/tests/vxlan.py
class VerifyVxlan1Interface(AntaTest):\n    \"\"\"Verifies if the Vxlan1 interface is configured and 'up/up'.\n\n    Warning\n    -------\n    The name of this test has been updated from 'VerifyVxlan' for better representation.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the Vxlan1 interface is configured with line protocol status and interface status 'up'.\n    * Failure: The test will fail if the Vxlan1 interface line protocol status or interface status are not 'up'.\n    * Skipped: The test will be skipped if the Vxlan1 interface is not configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vxlan:\n      - VerifyVxlan1Interface:\n    ```\n    \"\"\"\n\n    name = \"VerifyVxlan1Interface\"\n    description = \"Verifies the Vxlan1 interface status.\"\n    categories: ClassVar[list[str]] = [\"vxlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVxlan1Interface.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if \"Vxlan1\" not in command_output[\"interfaceDescriptions\"]:\n            self.result.is_skipped(\"Vxlan1 interface is not configured\")\n        elif (\n            command_output[\"interfaceDescriptions\"][\"Vxlan1\"][\"lineProtocolStatus\"] == \"up\"\n            and command_output[\"interfaceDescriptions\"][\"Vxlan1\"][\"interfaceStatus\"] == \"up\"\n        ):\n            self.result.is_success()\n        else:\n            self.result.is_failure(\n                f\"Vxlan1 interface is {command_output['interfaceDescriptions']['Vxlan1']['lineProtocolStatus']}\"\n                f\"/{command_output['interfaceDescriptions']['Vxlan1']['interfaceStatus']}\",\n            )\n
"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlanConfigSanity","title":"VerifyVxlanConfigSanity","text":"

Verifies that no issues are detected with the VXLAN configuration.

Expected Results
  • Success: The test will pass if no issues are detected with the VXLAN configuration.
  • Failure: The test will fail if issues are detected with the VXLAN configuration.
  • Skipped: The test will be skipped if VXLAN is not configured on the device.
Examples
anta.tests.vxlan:\n  - VerifyVxlanConfigSanity:\n
Source code in anta/tests/vxlan.py
class VerifyVxlanConfigSanity(AntaTest):\n    \"\"\"Verifies that no issues are detected with the VXLAN configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if no issues are detected with the VXLAN configuration.\n    * Failure: The test will fail if issues are detected with the VXLAN configuration.\n    * Skipped: The test will be skipped if VXLAN is not configured on the device.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vxlan:\n      - VerifyVxlanConfigSanity:\n    ```\n    \"\"\"\n\n    name = \"VerifyVxlanConfigSanity\"\n    description = \"Verifies there are no VXLAN config-sanity inconsistencies.\"\n    categories: ClassVar[list[str]] = [\"vxlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan config-sanity\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVxlanConfigSanity.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if \"categories\" not in command_output or len(command_output[\"categories\"]) == 0:\n            self.result.is_skipped(\"VXLAN is not configured\")\n            return\n        failed_categories = {\n            category: content\n            for category, content in command_output[\"categories\"].items()\n            if category in [\"localVtep\", \"mlag\", \"pd\"] and content[\"allCheckPass\"] is not True\n        }\n        if len(failed_categories) > 0:\n            self.result.is_failure(f\"VXLAN config sanity check is not passing: {failed_categories}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlanVniBinding","title":"VerifyVxlanVniBinding","text":"

Verifies the VNI-VLAN bindings of the Vxlan1 interface.

Expected Results
  • Success: The test will pass if the VNI-VLAN bindings provided are properly configured.
  • Failure: The test will fail if any VNI lacks bindings or if any bindings are incorrect.
  • Skipped: The test will be skipped if the Vxlan1 interface is not configured.
Examples
anta.tests.vxlan:\n  - VerifyVxlanVniBinding:\n      bindings:\n        10010: 10\n        10020: 20\n
Source code in anta/tests/vxlan.py
class VerifyVxlanVniBinding(AntaTest):\n    \"\"\"Verifies the VNI-VLAN bindings of the Vxlan1 interface.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the VNI-VLAN bindings provided are properly configured.\n    * Failure: The test will fail if any VNI lacks bindings or if any bindings are incorrect.\n    * Skipped: The test will be skipped if the Vxlan1 interface is not configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vxlan:\n      - VerifyVxlanVniBinding:\n          bindings:\n            10010: 10\n            10020: 20\n    ```\n    \"\"\"\n\n    name = \"VerifyVxlanVniBinding\"\n    description = \"Verifies the VNI-VLAN bindings of the Vxlan1 interface.\"\n    categories: ClassVar[list[str]] = [\"vxlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vni\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyVxlanVniBinding test.\"\"\"\n\n        bindings: dict[Vni, Vlan]\n        \"\"\"VNI to VLAN bindings to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVxlanVniBinding.\"\"\"\n        self.result.is_success()\n\n        no_binding = []\n        wrong_binding = []\n\n        if (vxlan1 := get_value(self.instance_commands[0].json_output, \"vxlanIntfs.Vxlan1\")) is None:\n            self.result.is_skipped(\"Vxlan1 interface is not configured\")\n            return\n\n        for vni, vlan in self.inputs.bindings.items():\n            str_vni = str(vni)\n            if str_vni in vxlan1[\"vniBindings\"]:\n                retrieved_vlan = vxlan1[\"vniBindings\"][str_vni][\"vlan\"]\n            elif str_vni in vxlan1[\"vniBindingsToVrf\"]:\n                retrieved_vlan = vxlan1[\"vniBindingsToVrf\"][str_vni][\"vlan\"]\n            else:\n                no_binding.append(str_vni)\n                retrieved_vlan = None\n\n            if retrieved_vlan and vlan != retrieved_vlan:\n                wrong_binding.append({str_vni: retrieved_vlan})\n\n        if no_binding:\n            self.result.is_failure(f\"The following VNI(s) have no binding: {no_binding}\")\n\n        if wrong_binding:\n            self.result.is_failure(f\"The following VNI(s) have the wrong VLAN binding: {wrong_binding}\")\n
"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlanVniBinding-attributes","title":"Inputs","text":"Name Type Description Default bindings dict[Vni, Vlan] VNI to VLAN bindings to verify. -"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlanVtep","title":"VerifyVxlanVtep","text":"

Verifies the VTEP peers of the Vxlan1 interface.

Expected Results
  • Success: The test will pass if all provided VTEP peers are identified and matching.
  • Failure: The test will fail if any VTEP peer is missing or there are unexpected VTEP peers.
  • Skipped: The test will be skipped if the Vxlan1 interface is not configured.
Examples
anta.tests.vxlan:\n  - VerifyVxlanVtep:\n      vteps:\n        - 10.1.1.5\n        - 10.1.1.6\n
Source code in anta/tests/vxlan.py
class VerifyVxlanVtep(AntaTest):\n    \"\"\"Verifies the VTEP peers of the Vxlan1 interface.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all provided VTEP peers are identified and matching.\n    * Failure: The test will fail if any VTEP peer is missing or there are unexpected VTEP peers.\n    * Skipped: The test will be skipped if the Vxlan1 interface is not configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vxlan:\n      - VerifyVxlanVtep:\n          vteps:\n            - 10.1.1.5\n            - 10.1.1.6\n    ```\n    \"\"\"\n\n    name = \"VerifyVxlanVtep\"\n    description = \"Verifies the VTEP peers of the Vxlan1 interface\"\n    categories: ClassVar[list[str]] = [\"vxlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vtep\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyVxlanVtep test.\"\"\"\n\n        vteps: list[IPv4Address]\n        \"\"\"List of VTEP peers to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVxlanVtep.\"\"\"\n        self.result.is_success()\n\n        inputs_vteps = [str(input_vtep) for input_vtep in self.inputs.vteps]\n\n        if (vxlan1 := get_value(self.instance_commands[0].json_output, \"interfaces.Vxlan1\")) is None:\n            self.result.is_skipped(\"Vxlan1 interface is not configured\")\n            return\n\n        difference1 = set(inputs_vteps).difference(set(vxlan1[\"vteps\"]))\n        difference2 = set(vxlan1[\"vteps\"]).difference(set(inputs_vteps))\n\n        if difference1:\n            self.result.is_failure(f\"The following VTEP peer(s) are missing from the Vxlan1 interface: {sorted(difference1)}\")\n\n        if difference2:\n            self.result.is_failure(f\"Unexpected VTEP peer(s) on Vxlan1 interface: {sorted(difference2)}\")\n
"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlanVtep-attributes","title":"Inputs","text":"Name Type Description Default vteps list[IPv4Address] List of VTEP peers to verify. -"},{"location":"api/types/","title":"Input Types","text":""},{"location":"api/types/#anta.custom_types","title":"anta.custom_types","text":"

Module that provides predefined types for AntaTest.Input instances.

"},{"location":"api/types/#anta.custom_types.AAAAuthMethod","title":"AAAAuthMethod module-attribute","text":"
AAAAuthMethod = Annotated[str, AfterValidator(aaa_group_prefix)]\n
"},{"location":"api/types/#anta.custom_types.Afi","title":"Afi module-attribute","text":"
Afi = Literal['ipv4', 'ipv6', 'vpn-ipv4', 'vpn-ipv6', 'evpn', 'rt-membership', 'path-selection', 'link-state']\n
"},{"location":"api/types/#anta.custom_types.BfdInterval","title":"BfdInterval module-attribute","text":"
BfdInterval = Annotated[int, Field(ge=50, le=60000)]\n
"},{"location":"api/types/#anta.custom_types.BfdMultiplier","title":"BfdMultiplier module-attribute","text":"
BfdMultiplier = Annotated[int, Field(ge=3, le=50)]\n
"},{"location":"api/types/#anta.custom_types.EcdsaKeySize","title":"EcdsaKeySize module-attribute","text":"
EcdsaKeySize = Literal[256, 384, 521]\n
"},{"location":"api/types/#anta.custom_types.EncryptionAlgorithm","title":"EncryptionAlgorithm module-attribute","text":"
EncryptionAlgorithm = Literal['RSA', 'ECDSA']\n
"},{"location":"api/types/#anta.custom_types.ErrDisableInterval","title":"ErrDisableInterval module-attribute","text":"
ErrDisableInterval = Annotated[int, Field(ge=30, le=86400)]\n
"},{"location":"api/types/#anta.custom_types.ErrDisableReasons","title":"ErrDisableReasons module-attribute","text":"
ErrDisableReasons = Literal['acl', 'arp-inspection', 'bpduguard', 'dot1x-session-replace', 'hitless-reload-down', 'lacp-rate-limit', 'link-flap', 'no-internal-vlan', 'portchannelguard', 'portsec', 'tapagg', 'uplink-failure-detection']\n
"},{"location":"api/types/#anta.custom_types.Hostname","title":"Hostname module-attribute","text":"
Hostname = Annotated[str, Field(pattern='^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\\\-]*[a-zA-Z0-9])\\\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\\\-]*[A-Za-z0-9])$')]\n
"},{"location":"api/types/#anta.custom_types.Interface","title":"Interface module-attribute","text":"
Interface = Annotated[str, Field(pattern='^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\\\\/[0-9]+)*(\\\\.[0-9]+)?$'), BeforeValidator(interface_autocomplete), BeforeValidator(interface_case_sensitivity)]\n
"},{"location":"api/types/#anta.custom_types.MlagPriority","title":"MlagPriority module-attribute","text":"
MlagPriority = Annotated[int, Field(ge=1, le=32767)]\n
"},{"location":"api/types/#anta.custom_types.MultiProtocolCaps","title":"MultiProtocolCaps module-attribute","text":"
MultiProtocolCaps = Annotated[str, BeforeValidator(bgp_multiprotocol_capabilities_abbreviations)]\n
"},{"location":"api/types/#anta.custom_types.Percent","title":"Percent module-attribute","text":"
Percent = Annotated[float, Field(ge=0.0, le=100.0)]\n
"},{"location":"api/types/#anta.custom_types.Port","title":"Port module-attribute","text":"
Port = Annotated[int, Field(ge=1, le=65535)]\n
"},{"location":"api/types/#anta.custom_types.PositiveInteger","title":"PositiveInteger module-attribute","text":"
PositiveInteger = Annotated[int, Field(ge=0)]\n
"},{"location":"api/types/#anta.custom_types.Revision","title":"Revision module-attribute","text":"
Revision = Annotated[int, Field(ge=1, le=99)]\n
"},{"location":"api/types/#anta.custom_types.RsaKeySize","title":"RsaKeySize module-attribute","text":"
RsaKeySize = Literal[2048, 3072, 4096]\n
"},{"location":"api/types/#anta.custom_types.Safi","title":"Safi module-attribute","text":"
Safi = Literal['unicast', 'multicast', 'labeled-unicast', 'sr-te']\n
"},{"location":"api/types/#anta.custom_types.TestStatus","title":"TestStatus module-attribute","text":"
TestStatus = Literal['unset', 'success', 'failure', 'error', 'skipped']\n
"},{"location":"api/types/#anta.custom_types.Vlan","title":"Vlan module-attribute","text":"
Vlan = Annotated[int, Field(ge=0, le=4094)]\n
"},{"location":"api/types/#anta.custom_types.Vni","title":"Vni module-attribute","text":"
Vni = Annotated[int, Field(ge=1, le=16777215)]\n
"},{"location":"api/types/#anta.custom_types.VxlanSrcIntf","title":"VxlanSrcIntf module-attribute","text":"
VxlanSrcIntf = Annotated[str, Field(pattern='^(Loopback)([0-9]|[1-9][0-9]{1,2}|[1-7][0-9]{3}|8[01][0-9]{2}|819[01])$'), BeforeValidator(interface_autocomplete), BeforeValidator(interface_case_sensitivity)]\n
"},{"location":"api/types/#anta.custom_types.aaa_group_prefix","title":"aaa_group_prefix","text":"
aaa_group_prefix(v: str) -> str\n

Prefix the AAA method with \u2018group\u2019 if it is known.

Source code in anta/custom_types.py
def aaa_group_prefix(v: str) -> str:\n    \"\"\"Prefix the AAA method with 'group' if it is known.\"\"\"\n    built_in_methods = [\"local\", \"none\", \"logging\"]\n    return f\"group {v}\" if v not in built_in_methods and not v.startswith(\"group \") else v\n
"},{"location":"api/types/#anta.custom_types.bgp_multiprotocol_capabilities_abbreviations","title":"bgp_multiprotocol_capabilities_abbreviations","text":"
bgp_multiprotocol_capabilities_abbreviations(value: str) -> str\n

Abbreviations for different BGP multiprotocol capabilities.

Examples
- IPv4 Unicast\n- L2vpnEVPN\n- ipv4 MPLS Labels\n- ipv4Mplsvpn\n
Source code in anta/custom_types.py
def bgp_multiprotocol_capabilities_abbreviations(value: str) -> str:\n    \"\"\"Abbreviations for different BGP multiprotocol capabilities.\n\n    Examples\n    --------\n        - IPv4 Unicast\n        - L2vpnEVPN\n        - ipv4 MPLS Labels\n        - ipv4Mplsvpn\n\n    \"\"\"\n    patterns = {\n        r\"\\b(l2[\\s\\-]?vpn[\\s\\-]?evpn)\\b\": \"l2VpnEvpn\",\n        r\"\\bipv4[\\s_-]?mpls[\\s_-]?label(s)?\\b\": \"ipv4MplsLabels\",\n        r\"\\bipv4[\\s_-]?mpls[\\s_-]?vpn\\b\": \"ipv4MplsVpn\",\n        r\"\\bipv4[\\s_-]?uni[\\s_-]?cast\\b\": \"ipv4Unicast\",\n    }\n\n    for pattern, replacement in patterns.items():\n        match = re.search(pattern, value, re.IGNORECASE)\n        if match:\n            return replacement\n\n    return value\n
"},{"location":"api/types/#anta.custom_types.interface_autocomplete","title":"interface_autocomplete","text":"
interface_autocomplete(v: str) -> str\n

Allow the user to only provide the beginning of an interface name.

Supported alias: - et, eth will be changed to Ethernet - po will be changed to Port-Channel - lo will be changed to Loopback

Source code in anta/custom_types.py
def interface_autocomplete(v: str) -> str:\n    \"\"\"Allow the user to only provide the beginning of an interface name.\n\n    Supported alias:\n         - `et`, `eth` will be changed to `Ethernet`\n         - `po` will be changed to `Port-Channel`\n    - `lo` will be changed to `Loopback`\n    \"\"\"\n    intf_id_re = re.compile(r\"[0-9]+(\\/[0-9]+)*(\\.[0-9]+)?\")\n    m = intf_id_re.search(v)\n    if m is None:\n        msg = f\"Could not parse interface ID in interface '{v}'\"\n        raise ValueError(msg)\n    intf_id = m[0]\n\n    alias_map = {\"et\": \"Ethernet\", \"eth\": \"Ethernet\", \"po\": \"Port-Channel\", \"lo\": \"Loopback\"}\n\n    for alias, full_name in alias_map.items():\n        if v.lower().startswith(alias):\n            return f\"{full_name}{intf_id}\"\n\n    return v\n
"},{"location":"api/types/#anta.custom_types.interface_case_sensitivity","title":"interface_case_sensitivity","text":"
interface_case_sensitivity(v: str) -> str\n

Reformat interface name to match expected case sensitivity.

Examples
 - ethernet -> Ethernet\n - vlan -> Vlan\n - loopback -> Loopback\n
Source code in anta/custom_types.py
def interface_case_sensitivity(v: str) -> str:\n    \"\"\"Reformat interface name to match expected case sensitivity.\n\n    Examples\n    --------\n         - ethernet -> Ethernet\n         - vlan -> Vlan\n         - loopback -> Loopback\n\n    \"\"\"\n    if isinstance(v, str) and len(v) > 0 and not v[0].isupper():\n        return f\"{v[0].upper()}{v[1:]}\"\n    return v\n
"},{"location":"cli/check/","title":"Check","text":""},{"location":"cli/check/#anta-check-commands","title":"ANTA check commands","text":"

The ANTA check command allow to execute some checks on the ANTA input files. Only checking the catalog is currently supported.

anta check --help\nUsage: anta check [OPTIONS] COMMAND [ARGS]...\n\n  Check commands for building ANTA\n\nOptions:\n  --help  Show this message and exit.\n\nCommands:\n  catalog  Check that the catalog is valid\n
"},{"location":"cli/check/#checking-the-catalog","title":"Checking the catalog","text":"
Usage: anta check catalog [OPTIONS]\n\n  Check that the catalog is valid\n\nOptions:\n  -c, --catalog FILE  Path to the test catalog YAML file  [env var:\n                      ANTA_CATALOG; required]\n  --help              Show this message and exit.\n
"},{"location":"cli/debug/","title":"Helpers","text":""},{"location":"cli/debug/#anta-debug-commands","title":"ANTA debug commands","text":"

The ANTA CLI includes a set of debugging tools, making it easier to build and test ANTA content. This functionality is accessed via the debug subcommand and offers the following options:

  • Executing a command on a device from your inventory and retrieving the result.
  • Running a templated command on a device from your inventory and retrieving the result.

These tools are especially helpful in building the tests, as they give a visual access to the output received from the eAPI. They also facilitate the extraction of output content for use in unit tests, as described in our contribution guide.

Warning

The debug tools require a device from your inventory. Thus, you MUST use a valid ANTA Inventory.

"},{"location":"cli/debug/#executing-an-eos-command","title":"Executing an EOS command","text":"

You can use the run-cmd entrypoint to run a command, which includes the following options:

"},{"location":"cli/debug/#command-overview","title":"Command overview","text":"
$ anta debug run-cmd --help\nUsage: anta debug run-cmd [OPTIONS]\n\n  Run arbitrary command to an ANTA device\n\nOptions:\n  -u, --username TEXT       Username to connect to EOS  [env var:\n                            ANTA_USERNAME; required]\n  -p, --password TEXT       Password to connect to EOS that must be provided.\n                            It can be prompted using '--prompt' option.  [env\n                            var: ANTA_PASSWORD]\n  --enable-password TEXT    Password to access EOS Privileged EXEC mode. It\n                            can be prompted using '--prompt' option. Requires\n                            '--enable' option.  [env var:\n                            ANTA_ENABLE_PASSWORD]\n  --enable                  Some commands may require EOS Privileged EXEC\n                            mode. This option tries to access this mode before\n                            sending a command to the device.  [env var:\n                            ANTA_ENABLE]\n  -P, --prompt              Prompt for passwords if they are not provided.\n                            [env var: ANTA_PROMPT]\n  --timeout INTEGER         Global connection timeout  [env var: ANTA_TIMEOUT;\n                            default: 30]\n  --insecure                Disable SSH Host Key validation  [env var:\n                            ANTA_INSECURE]\n  --disable-cache           Disable cache globally  [env var:\n                            ANTA_DISABLE_CACHE]\n  -i, --inventory FILE      Path to the inventory YAML file  [env var:\n                            ANTA_INVENTORY; required]\n  -t, --tags TEXT           List of tags using comma as separator:\n                            tag1,tag2,tag3  [env var: ANTA_TAGS]\n  --ofmt [json|text]        EOS eAPI format to use. can be text or json\n  -v, --version [1|latest]  EOS eAPI version\n  -r, --revision INTEGER    eAPI command revision\n  -d, --device TEXT         Device from inventory to use  [required]\n  -c, --command TEXT        Command to run  [required]\n  --help                    Show this message and exit.\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

"},{"location":"cli/debug/#example","title":"Example","text":"

This example illustrates how to run the show interfaces description command with a JSON format (default):

anta debug run-cmd --command \"show interfaces description\" --device DC1-SPINE1\nRun command show interfaces description on DC1-SPINE1\n{\n    'interfaceDescriptions': {\n        'Ethernet1': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-LEAF1A_Ethernet1', 'interfaceStatus': 'up'},\n        'Ethernet2': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-LEAF1B_Ethernet1', 'interfaceStatus': 'up'},\n        'Ethernet3': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-BL1_Ethernet1', 'interfaceStatus': 'up'},\n        'Ethernet4': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-BL2_Ethernet1', 'interfaceStatus': 'up'},\n        'Loopback0': {'lineProtocolStatus': 'up', 'description': 'EVPN_Overlay_Peering', 'interfaceStatus': 'up'},\n        'Management0': {'lineProtocolStatus': 'up', 'description': 'oob_management', 'interfaceStatus': 'up'}\n    }\n}\n
"},{"location":"cli/debug/#executing-an-eos-command-using-templates","title":"Executing an EOS command using templates","text":"

The run-template entrypoint allows the user to provide an f-string templated command. It is followed by a list of arguments (key-value pairs) that build a dictionary used as template parameters.

"},{"location":"cli/debug/#command-overview_1","title":"Command overview","text":"
$ anta debug run-template --help\nUsage: anta debug run-template [OPTIONS] PARAMS...\n\n  Run arbitrary templated command to an ANTA device.\n\n  Takes a list of arguments (keys followed by a value) to build a dictionary\n  used as template parameters. Example:\n\n  anta debug run-template -d leaf1a -t 'show vlan {vlan_id}' vlan_id 1\n\nOptions:\n  -u, --username TEXT       Username to connect to EOS  [env var:\n                            ANTA_USERNAME; required]\n  -p, --password TEXT       Password to connect to EOS that must be provided.\n                            It can be prompted using '--prompt' option.  [env\n                            var: ANTA_PASSWORD]\n  --enable-password TEXT    Password to access EOS Privileged EXEC mode. It\n                            can be prompted using '--prompt' option. Requires\n                            '--enable' option.  [env var:\n                            ANTA_ENABLE_PASSWORD]\n  --enable                  Some commands may require EOS Privileged EXEC\n                            mode. This option tries to access this mode before\n                            sending a command to the device.  [env var:\n                            ANTA_ENABLE]\n  -P, --prompt              Prompt for passwords if they are not provided.\n                            [env var: ANTA_PROMPT]\n  --timeout INTEGER         Global connection timeout  [env var: ANTA_TIMEOUT;\n                            default: 30]\n  --insecure                Disable SSH Host Key validation  [env var:\n                            ANTA_INSECURE]\n  --disable-cache           Disable cache globally  [env var:\n                            ANTA_DISABLE_CACHE]\n  -i, --inventory FILE      Path to the inventory YAML file  [env var:\n                            ANTA_INVENTORY; required]\n  -t, --tags TEXT           List of tags using comma as separator:\n                            tag1,tag2,tag3  [env var: ANTA_TAGS]\n  --ofmt [json|text]        EOS eAPI format to use. can be text or json\n  -v, --version [1|latest]  EOS eAPI version\n  -r, --revision INTEGER    eAPI command revision\n  -d, --device TEXT         Device from inventory to use  [required]\n  -t, --template TEXT       Command template to run. E.g. 'show vlan\n                            {vlan_id}'  [required]\n  --help                    Show this message and exit.\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

"},{"location":"cli/debug/#example_1","title":"Example","text":"

This example uses the show vlan {vlan_id} command in a JSON format:

anta debug run-template --template \"show vlan {vlan_id}\" vlan_id 10 --device DC1-LEAF1A\nRun templated command 'show vlan {vlan_id}' with {'vlan_id': '10'} on DC1-LEAF1A\n{\n    'vlans': {\n        '10': {\n            'name': 'VRFPROD_VLAN10',\n            'dynamic': False,\n            'status': 'active',\n            'interfaces': {\n                'Cpu': {'privatePromoted': False, 'blocked': None},\n                'Port-Channel11': {'privatePromoted': False, 'blocked': None},\n                'Vxlan1': {'privatePromoted': False, 'blocked': None}\n            }\n        }\n    },\n    'sourceDetail': ''\n}\n

Warning

If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters.

"},{"location":"cli/debug/#example-of-multiple-arguments","title":"Example of multiple arguments","text":"
anta -log DEBUG debug run-template --template \"ping {dst} source {src}\" dst \"8.8.8.8\" src Loopback0 --device DC1-SPINE1 \u00a0 \u00a0\n> {'dst': '8.8.8.8', 'src': 'Loopback0'}\n\nanta -log DEBUG debug run-template --template \"ping {dst} source {src}\" dst \"8.8.8.8\" src Loopback0 dst \"1.1.1.1\" src Loopback1 --device DC1-SPINE1 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0\n> {'dst': '1.1.1.1', 'src': 'Loopback1'}\n# Notice how `src` and `dst` keep only the latest value\n
"},{"location":"cli/exec/","title":"Execute commands","text":""},{"location":"cli/exec/#executing-commands-on-devices","title":"Executing Commands on Devices","text":"

ANTA CLI provides a set of entrypoints to facilitate remote command execution on EOS devices.

"},{"location":"cli/exec/#exec-command-overview","title":"EXEC Command overview","text":"
anta exec --help\nUsage: anta exec [OPTIONS] COMMAND [ARGS]...\n\n  Execute commands to inventory devices\n\nOptions:\n  --help  Show this message and exit.\n\nCommands:\n  clear-counters        Clear counter statistics on EOS devices\n  collect-tech-support  Collect scheduled tech-support from EOS devices\n  snapshot              Collect commands output from devices in inventory\n
"},{"location":"cli/exec/#clear-interfaces-counters","title":"Clear interfaces counters","text":"

This command clears interface counters on EOS devices specified in your inventory.

"},{"location":"cli/exec/#command-overview","title":"Command overview","text":"
anta exec clear-counters --help\nUsage: anta exec clear-counters [OPTIONS]\n\n  Clear counter statistics on EOS devices\n\nOptions:\n  -u, --username TEXT     Username to connect to EOS  [env var: ANTA_USERNAME;\n                          required]\n  -p, --password TEXT     Password to connect to EOS that must be provided. It\n                          can be prompted using '--prompt' option.  [env var:\n                          ANTA_PASSWORD]\n  --enable-password TEXT  Password to access EOS Privileged EXEC mode. It can\n                          be prompted using '--prompt' option. Requires '--\n                          enable' option.  [env var: ANTA_ENABLE_PASSWORD]\n  --enable                Some commands may require EOS Privileged EXEC mode.\n                          This option tries to access this mode before sending\n                          a command to the device.  [env var: ANTA_ENABLE]\n  -P, --prompt            Prompt for passwords if they are not provided.  [env\n                          var: ANTA_PROMPT]\n  --timeout INTEGER       Global connection timeout  [env var: ANTA_TIMEOUT;\n                          default: 30]\n  --insecure              Disable SSH Host Key validation  [env var:\n                          ANTA_INSECURE]\n  --disable-cache         Disable cache globally  [env var:\n                          ANTA_DISABLE_CACHE]\n  -i, --inventory FILE    Path to the inventory YAML file  [env var:\n                          ANTA_INVENTORY; required]\n  -t, --tags TEXT         List of tags using comma as separator:\n                          tag1,tag2,tag3  [env var: ANTA_TAGS]\n  --help                  Show this message and exit.\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

"},{"location":"cli/exec/#example","title":"Example","text":"
anta exec clear-counters --tags SPINE\n[20:19:13] INFO     Connecting to devices...                                                                                                                         utils.py:43\n           INFO     Clearing counters on remote devices...                                                                                                           utils.py:46\n           INFO     Cleared counters on DC1-SPINE2 (cEOSLab)                                                                                                         utils.py:41\n           INFO     Cleared counters on DC2-SPINE1 (cEOSLab)                                                                                                         utils.py:41\n           INFO     Cleared counters on DC1-SPINE1 (cEOSLab)                                                                                                         utils.py:41\n           INFO     Cleared counters on DC2-SPINE2 (cEOSLab)\n
"},{"location":"cli/exec/#collect-a-set-of-commands","title":"Collect a set of commands","text":"

This command collects all the commands specified in a commands-list file, which can be in either json or text format.

"},{"location":"cli/exec/#command-overview_1","title":"Command overview","text":"
anta exec snapshot --help\nUsage: anta exec snapshot [OPTIONS]\n\n  Collect commands output from devices in inventory\n\nOptions:\n  -u, --username TEXT       Username to connect to EOS  [env var:\n                            ANTA_USERNAME; required]\n  -p, --password TEXT       Password to connect to EOS that must be provided.\n                            It can be prompted using '--prompt' option.  [env\n                            var: ANTA_PASSWORD]\n  --enable-password TEXT    Password to access EOS Privileged EXEC mode. It\n                            can be prompted using '--prompt' option. Requires\n                            '--enable' option.  [env var:\n                            ANTA_ENABLE_PASSWORD]\n  --enable                  Some commands may require EOS Privileged EXEC\n                            mode. This option tries to access this mode before\n                            sending a command to the device.  [env var:\n                            ANTA_ENABLE]\n  -P, --prompt              Prompt for passwords if they are not provided.\n                            [env var: ANTA_PROMPT]\n  --timeout INTEGER         Global connection timeout  [env var: ANTA_TIMEOUT;\n                            default: 30]\n  --insecure                Disable SSH Host Key validation  [env var:\n                            ANTA_INSECURE]\n  --disable-cache           Disable cache globally  [env var:\n                            ANTA_DISABLE_CACHE]\n  -i, --inventory FILE      Path to the inventory YAML file  [env var:\n                            ANTA_INVENTORY; required]\n  -t, --tags TEXT           List of tags using comma as separator:\n                            tag1,tag2,tag3  [env var: ANTA_TAGS]\n  -c, --commands-list FILE  File with list of commands to collect  [env var:\n                            ANTA_EXEC_SNAPSHOT_COMMANDS_LIST; required]\n  -o, --output DIRECTORY    Directory to save commands output.  [env var:\n                            ANTA_EXEC_SNAPSHOT_OUTPUT; default:\n                            anta_snapshot_2023-12-06_09_22_11]\n  --help                    Show this message and exit.\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

The commands-list file should follow this structure:

---\njson_format:\n  - show version\ntext_format:\n  - show bfd peers\n
"},{"location":"cli/exec/#example_1","title":"Example","text":"
anta exec snapshot --tags SPINE --commands-list ./commands.yaml --output ./\n[20:25:15] INFO     Connecting to devices...                                                                                                                         utils.py:78\n           INFO     Collecting commands from remote devices                                                                                                          utils.py:81\n           INFO     Collected command 'show version' from device DC2-SPINE1 (cEOSLab)                                                                                utils.py:76\n           INFO     Collected command 'show version' from device DC2-SPINE2 (cEOSLab)                                                                                utils.py:76\n           INFO     Collected command 'show version' from device DC1-SPINE1 (cEOSLab)                                                                                utils.py:76\n           INFO     Collected command 'show version' from device DC1-SPINE2 (cEOSLab)                                                                                utils.py:76\n[20:25:16] INFO     Collected command 'show bfd peers' from device DC2-SPINE2 (cEOSLab)                                                                              utils.py:76\n           INFO     Collected command 'show bfd peers' from device DC2-SPINE1 (cEOSLab)                                                                              utils.py:76\n           INFO     Collected command 'show bfd peers' from device DC1-SPINE1 (cEOSLab)                                                                              utils.py:76\n           INFO     Collected command 'show bfd peers' from device DC1-SPINE2 (cEOSLab)\n

The results of the executed commands will be stored in the output directory specified during command execution:

tree _2023-07-14_20_25_15\n_2023-07-14_20_25_15\n\u251c\u2500\u2500 DC1-SPINE1\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 json\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 show version.json\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 text\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 show bfd peers.log\n\u251c\u2500\u2500 DC1-SPINE2\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 json\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 show version.json\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 text\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 show bfd peers.log\n\u251c\u2500\u2500 DC2-SPINE1\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 json\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 show version.json\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 text\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 show bfd peers.log\n\u2514\u2500\u2500 DC2-SPINE2\n    \u251c\u2500\u2500 json\n    \u2502\u00a0\u00a0 \u2514\u2500\u2500 show version.json\n    \u2514\u2500\u2500 text\n        \u2514\u2500\u2500 show bfd peers.log\n\n12 directories, 8 files\n
"},{"location":"cli/exec/#get-scheduled-tech-support","title":"Get Scheduled tech-support","text":"

EOS offers a feature that automatically creates a tech-support archive every hour by default. These archives are stored under /mnt/flash/schedule/tech-support.

leaf1#show schedule summary\nMaximum concurrent jobs  1\nPrepend host name to logfile: Yes\nName                 At Time       Last        Interval       Timeout        Max        Max     Logfile Location                  Status\n                                   Time         (mins)        (mins)         Log        Logs\n                                                                            Files       Size\n----------------- ------------- ----------- -------------- ------------- ----------- ---------- --------------------------------- ------\ntech-support           now         08:37          60            30           100         -      flash:schedule/tech-support/      Success\n\n\nleaf1#bash ls /mnt/flash/schedule/tech-support\nleaf1_tech-support_2023-03-09.1337.log.gz  leaf1_tech-support_2023-03-10.0837.log.gz  leaf1_tech-support_2023-03-11.0337.log.gz\n

For Network Readiness for Use (NRFU) tests and to keep a comprehensive report of the system state before going live, ANTA provides a command-line interface that efficiently retrieves these files.

"},{"location":"cli/exec/#command-overview_2","title":"Command overview","text":"
anta exec collect-tech-support --help\nUsage: anta exec collect-tech-support [OPTIONS]\n\n  Collect scheduled tech-support from EOS devices\n\nOptions:\n  -u, --username TEXT     Username to connect to EOS  [env var: ANTA_USERNAME;\n                          required]\n  -p, --password TEXT     Password to connect to EOS that must be provided. It\n                          can be prompted using '--prompt' option.  [env var:\n                          ANTA_PASSWORD]\n  --enable-password TEXT  Password to access EOS Privileged EXEC mode. It can\n                          be prompted using '--prompt' option. Requires '--\n                          enable' option.  [env var: ANTA_ENABLE_PASSWORD]\n  --enable                Some commands may require EOS Privileged EXEC mode.\n                          This option tries to access this mode before sending\n                          a command to the device.  [env var: ANTA_ENABLE]\n  -P, --prompt            Prompt for passwords if they are not provided.  [env\n                          var: ANTA_PROMPT]\n  --timeout INTEGER       Global connection timeout  [env var: ANTA_TIMEOUT;\n                          default: 30]\n  --insecure              Disable SSH Host Key validation  [env var:\n                          ANTA_INSECURE]\n  --disable-cache         Disable cache globally  [env var:\n                          ANTA_DISABLE_CACHE]\n  -i, --inventory FILE    Path to the inventory YAML file  [env var:\n                          ANTA_INVENTORY; required]\n  -t, --tags TEXT         List of tags using comma as separator:\n                          tag1,tag2,tag3  [env var: ANTA_TAGS]\n  -o, --output PATH       Path for test catalog  [default: ./tech-support]\n  --latest INTEGER        Number of scheduled show-tech to retrieve\n  --configure             Ensure devices have 'aaa authorization exec default\n                          local' configured (required for SCP on EOS). THIS\n                          WILL CHANGE THE CONFIGURATION OF YOUR NETWORK.\n  --help                  Show this message and exit.\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

When executed, this command fetches tech-support files and downloads them locally into a device-specific subfolder within the designated folder. You can specify the output folder with the --output option.

ANTA uses SCP to download files from devices and will not trust unknown SSH hosts by default. Add the SSH public keys of your devices to your known_hosts file or use the anta --insecure option to ignore SSH host keys validation.

The configuration aaa authorization exec default must be present on devices to be able to use SCP. ANTA can automatically configure aaa authorization exec default local using the anta exec collect-tech-support --configure option. If you require specific AAA configuration for aaa authorization exec default, like aaa authorization exec default none or aaa authorization exec default group tacacs+, you will need to configure it manually.

The --latest option allows retrieval of a specific number of the most recent tech-support files.

Warning

By default all the tech-support files present on the devices are retrieved.

"},{"location":"cli/exec/#example_2","title":"Example","text":"
anta --insecure exec collect-tech-support\n[15:27:19] INFO     Connecting to devices...\nINFO     Copying '/mnt/flash/schedule/tech-support/spine1_tech-support_2023-06-09.1315.log.gz' from device spine1 to 'tech-support/spine1' locally\nINFO     Copying '/mnt/flash/schedule/tech-support/leaf3_tech-support_2023-06-09.1315.log.gz' from device leaf3 to 'tech-support/leaf3' locally\nINFO     Copying '/mnt/flash/schedule/tech-support/leaf1_tech-support_2023-06-09.1315.log.gz' from device leaf1 to 'tech-support/leaf1' locally\nINFO     Copying '/mnt/flash/schedule/tech-support/leaf2_tech-support_2023-06-09.1315.log.gz' from device leaf2 to 'tech-support/leaf2' locally\nINFO     Copying '/mnt/flash/schedule/tech-support/spine2_tech-support_2023-06-09.1315.log.gz' from device spine2 to 'tech-support/spine2' locally\nINFO     Copying '/mnt/flash/schedule/tech-support/leaf4_tech-support_2023-06-09.1315.log.gz' from device leaf4 to 'tech-support/leaf4' locally\nINFO     Collected 1 scheduled tech-support from leaf2\nINFO     Collected 1 scheduled tech-support from spine2\nINFO     Collected 1 scheduled tech-support from leaf3\nINFO     Collected 1 scheduled tech-support from spine1\nINFO     Collected 1 scheduled tech-support from leaf1\nINFO     Collected 1 scheduled tech-support from leaf4\n

The output folder structure is as follows:

tree tech-support/\ntech-support/\n\u251c\u2500\u2500 leaf1\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 leaf1_tech-support_2023-06-09.1315.log.gz\n\u251c\u2500\u2500 leaf2\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 leaf2_tech-support_2023-06-09.1315.log.gz\n\u251c\u2500\u2500 leaf3\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 leaf3_tech-support_2023-06-09.1315.log.gz\n\u251c\u2500\u2500 leaf4\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 leaf4_tech-support_2023-06-09.1315.log.gz\n\u251c\u2500\u2500 spine1\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 spine1_tech-support_2023-06-09.1315.log.gz\n\u2514\u2500\u2500 spine2\n    \u2514\u2500\u2500 spine2_tech-support_2023-06-09.1315.log.gz\n\n6 directories, 6 files\n

Each device has its own subdirectory containing the collected tech-support files.

"},{"location":"cli/get-inventory-information/","title":"Get Inventory Information","text":""},{"location":"cli/get-inventory-information/#retrieving-inventory-information","title":"Retrieving Inventory Information","text":"

The ANTA CLI offers multiple entrypoints to access data from your local inventory.

"},{"location":"cli/get-inventory-information/#inventory-used-of-examples","title":"Inventory used of examples","text":"

Let\u2019s consider the following inventory:

---\nanta_inventory:\n  hosts:\n    - host: 172.20.20.101\n      name: DC1-SPINE1\n      tags: [\"SPINE\", \"DC1\"]\n\n    - host: 172.20.20.102\n      name: DC1-SPINE2\n      tags: [\"SPINE\", \"DC1\"]\n\n    - host: 172.20.20.111\n      name: DC1-LEAF1A\n      tags: [\"LEAF\", \"DC1\"]\n\n    - host: 172.20.20.112\n      name: DC1-LEAF1B\n      tags: [\"LEAF\", \"DC1\"]\n\n    - host: 172.20.20.121\n      name: DC1-BL1\n      tags: [\"BL\", \"DC1\"]\n\n    - host: 172.20.20.122\n      name: DC1-BL2\n      tags: [\"BL\", \"DC1\"]\n\n    - host: 172.20.20.201\n      name: DC2-SPINE1\n      tags: [\"SPINE\", \"DC2\"]\n\n    - host: 172.20.20.202\n      name: DC2-SPINE2\n      tags: [\"SPINE\", \"DC2\"]\n\n    - host: 172.20.20.211\n      name: DC2-LEAF1A\n      tags: [\"LEAF\", \"DC2\"]\n\n    - host: 172.20.20.212\n      name: DC2-LEAF1B\n      tags: [\"LEAF\", \"DC2\"]\n\n    - host: 172.20.20.221\n      name: DC2-BL1\n      tags: [\"BL\", \"DC2\"]\n\n    - host: 172.20.20.222\n      name: DC2-BL2\n      tags: [\"BL\", \"DC2\"]\n
"},{"location":"cli/get-inventory-information/#obtaining-all-configured-tags","title":"Obtaining all configured tags","text":"

As most of ANTA\u2019s commands accommodate tag filtering, this particular command is useful for enumerating all tags configured in the inventory. Running the anta get tags command will return a list of all tags that have been configured in the inventory.

"},{"location":"cli/get-inventory-information/#command-overview","title":"Command overview","text":"
anta get tags --help\nUsage: anta get tags [OPTIONS]\n\n  Get list of configured tags in user inventory.\n\nOptions:\n  -u, --username TEXT     Username to connect to EOS  [env var: ANTA_USERNAME;\n                          required]\n  -p, --password TEXT     Password to connect to EOS that must be provided. It\n                          can be prompted using '--prompt' option.  [env var:\n                          ANTA_PASSWORD]\n  --enable-password TEXT  Password to access EOS Privileged EXEC mode. It can\n                          be prompted using '--prompt' option. Requires '--\n                          enable' option.  [env var: ANTA_ENABLE_PASSWORD]\n  --enable                Some commands may require EOS Privileged EXEC mode.\n                          This option tries to access this mode before sending\n                          a command to the device.  [env var: ANTA_ENABLE]\n  -P, --prompt            Prompt for passwords if they are not provided.  [env\n                          var: ANTA_PROMPT]\n  --timeout INTEGER       Global connection timeout  [env var: ANTA_TIMEOUT;\n                          default: 30]\n  --insecure              Disable SSH Host Key validation  [env var:\n                          ANTA_INSECURE]\n  --disable-cache         Disable cache globally  [env var:\n                          ANTA_DISABLE_CACHE]\n  -i, --inventory FILE    Path to the inventory YAML file  [env var:\n                          ANTA_INVENTORY; required]\n  -t, --tags TEXT         List of tags using comma as separator:\n                          tag1,tag2,tag3  [env var: ANTA_TAGS]\n  --help                  Show this message and exit.\n
"},{"location":"cli/get-inventory-information/#example","title":"Example","text":"

To get the list of all configured tags in the inventory, run the following command:

anta get tags\nTags found:\n[\n  \"BL\",\n  \"DC1\",\n  \"DC2\",\n  \"LEAF\",\n  \"SPINE\"\n]\n\n* note that tag all has been added by anta\n

Note

Even if you haven\u2019t explicitly configured the all tag in the inventory, it is automatically added. This default tag allows to execute commands on all devices in the inventory when no tag is specified.

"},{"location":"cli/get-inventory-information/#list-devices-in-inventory","title":"List devices in inventory","text":"

This command will list all devices available in the inventory. Using the --tags option, you can filter this list to only include devices with specific tags. The --connected option allows to display only the devices where a connection has been established.

"},{"location":"cli/get-inventory-information/#command-overview_1","title":"Command overview","text":"
anta get inventory --help\nUsage: anta get inventory [OPTIONS]\n\n  Show inventory loaded in ANTA.\n\nOptions:\n  -u, --username TEXT            Username to connect to EOS  [env var:\n                                 ANTA_USERNAME; required]\n  -p, --password TEXT            Password to connect to EOS that must be\n                                 provided. It can be prompted using '--prompt'\n                                 option.  [env var: ANTA_PASSWORD]\n  --enable-password TEXT         Password to access EOS Privileged EXEC mode.\n                                 It can be prompted using '--prompt' option.\n                                 Requires '--enable' option.  [env var:\n                                 ANTA_ENABLE_PASSWORD]\n  --enable                       Some commands may require EOS Privileged EXEC\n                                 mode. This option tries to access this mode\n                                 before sending a command to the device.  [env\n                                 var: ANTA_ENABLE]\n  -P, --prompt                   Prompt for passwords if they are not\n                                 provided.  [env var: ANTA_PROMPT]\n  --timeout INTEGER              Global connection timeout  [env var:\n                                 ANTA_TIMEOUT; default: 30]\n  --insecure                     Disable SSH Host Key validation  [env var:\n                                 ANTA_INSECURE]\n  --disable-cache                Disable cache globally  [env var:\n                                 ANTA_DISABLE_CACHE]\n  -i, --inventory FILE           Path to the inventory YAML file  [env var:\n                                 ANTA_INVENTORY; required]\n  -t, --tags TEXT                List of tags using comma as separator:\n                                 tag1,tag2,tag3  [env var: ANTA_TAGS]\n  --connected / --not-connected  Display inventory after connection has been\n                                 created\n  --help                         Show this message and exit.\n

Tip

In its default mode, anta get inventory provides only information that doesn\u2019t rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, please use the --connected option.

"},{"location":"cli/get-inventory-information/#example_1","title":"Example","text":"

To retrieve a comprehensive list of all devices along with their details, execute the following command. It will provide all the data loaded into the ANTA inventory from your inventory file.

anta get inventory --tags SPINE\nCurrent inventory content is:\n{\n    'DC1-SPINE1': AsyncEOSDevice(\n        name='DC1-SPINE1',\n        tags=['SPINE', 'DC1'],\n        hw_model=None,\n        is_online=False,\n        established=False,\n        disable_cache=False,\n        host='172.20.20.101',\n        eapi_port=443,\n        username='arista',\n        enable=True,\n        enable_password='arista',\n        insecure=False\n    ),\n    'DC1-SPINE2': AsyncEOSDevice(\n        name='DC1-SPINE2',\n        tags=['SPINE', 'DC1'],\n        hw_model=None,\n        is_online=False,\n        established=False,\n        disable_cache=False,\n        host='172.20.20.102',\n        eapi_port=443,\n        username='arista',\n        enable=True,\n        insecure=False\n    ),\n    'DC2-SPINE1': AsyncEOSDevice(\n        name='DC2-SPINE1',\n        tags=['SPINE', 'DC2'],\n        hw_model=None,\n        is_online=False,\n        established=False,\n        disable_cache=False,\n        host='172.20.20.201',\n        eapi_port=443,\n        username='arista',\n        enable=True,\n        insecure=False\n    ),\n    'DC2-SPINE2': AsyncEOSDevice(\n        name='DC2-SPINE2',\n        tags=['SPINE', 'DC2'],\n        hw_model=None,\n        is_online=False,\n        established=False,\n        disable_cache=False,\n        host='172.20.20.202',\n        eapi_port=443,\n        username='arista',\n        enable=True,\n        insecure=False\n    )\n}\n
"},{"location":"cli/inv-from-ansible/","title":"Inventory from Ansible","text":""},{"location":"cli/inv-from-ansible/#create-an-inventory-from-ansible-inventory","title":"Create an Inventory from Ansible inventory","text":"

In large setups, it might be beneficial to construct your inventory based on your Ansible inventory. The from-ansible entrypoint of the get command enables the user to create an ANTA inventory from Ansible.

"},{"location":"cli/inv-from-ansible/#command-overview","title":"Command overview","text":"
$ anta get from-ansible --help\nUsage: anta get from-ansible [OPTIONS]\n\n  Build ANTA inventory from an ansible inventory YAML file\n\nOptions:\n  -g, --ansible-group TEXT        Ansible group to filter\n  --ansible-inventory FILENAME\n                                  Path to your ansible inventory file to read\n  -o, --output FILENAME           Path to save inventory file\n  -d, --inventory-directory PATH  Directory to save inventory file\n  --help                          Show this message and exit.\n

The output is an inventory where the name of the container is added as a tag for each host:

anta_inventory:\n  hosts:\n  - host: 10.73.252.41\n    name: srv-pod01\n  - host: 10.73.252.42\n    name: srv-pod02\n  - host: 10.73.252.43\n    name: srv-pod03\n

Warning

The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritence when using the --ansible-group option.

By default, if user does not provide --output file, anta will save output to configured anta inventory (anta --inventory). If the output file has content, anta will ask user to overwrite when running in interactive console. This mechanism can be controlled by triggers in case of CI usage: --overwrite to force anta to overwrite file. If not set, anta will exit

"},{"location":"cli/inv-from-ansible/#command-output","title":"Command output","text":"

host value is coming from the ansible_host key in your inventory while name is the name you defined for your host. Below is an ansible inventory example used to generate previous inventory:

---\ntooling:\n  children:\n    endpoints:\n      hosts:\n        srv-pod01:\n          ansible_httpapi_port: 9023\n          ansible_port: 9023\n          ansible_host: 10.73.252.41\n          type: endpoint\n        srv-pod02:\n          ansible_httpapi_port: 9024\n          ansible_port: 9024\n          ansible_host: 10.73.252.42\n          type: endpoint\n        srv-pod03:\n          ansible_httpapi_port: 9025\n          ansible_port: 9025\n          ansible_host: 10.73.252.43\n          type: endpoint\n
"},{"location":"cli/inv-from-cvp/","title":"Inventory from CVP","text":""},{"location":"cli/inv-from-cvp/#create-an-inventory-from-cloudvision","title":"Create an Inventory from CloudVision","text":"

In large setups, it might be beneficial to construct your inventory based on CloudVision. The from-cvp entrypoint of the get command enables the user to create an ANTA inventory from CloudVision.

"},{"location":"cli/inv-from-cvp/#command-overview","title":"Command overview","text":"
anta get from-cvp --help\nUsage: anta get from-cvp [OPTIONS]\n\n  Build ANTA inventory from Cloudvision\n\nOptions:\n  -ip, --cvp-ip TEXT              CVP IP Address  [required]\n  -u, --cvp-username TEXT         CVP Username  [required]\n  -p, --cvp-password TEXT         CVP Password / token  [required]\n  -c, --cvp-container TEXT        Container where devices are configured\n  -d, --inventory-directory PATH  Path to save inventory file\n  --help                          Show this message and exit.\n

The output is an inventory where the name of the container is added as a tag for each host:

anta_inventory:\n  hosts:\n  - host: 192.168.0.13\n    name: leaf2\n    tags:\n    - pod1\n  - host: 192.168.0.15\n    name: leaf4\n    tags:\n    - pod2\n

Warning

The current implementation only considers devices directly attached to a specific container when using the --cvp-container option.

"},{"location":"cli/inv-from-cvp/#creating-an-inventory-from-multiple-containers","title":"Creating an inventory from multiple containers","text":"

If you need to create an inventory from multiple containers, you can use a bash command and then manually concatenate files to create a single inventory file:

$ for container in pod01 pod02 spines; do anta get from-cvp -ip <cvp-ip> -u cvpadmin -p cvpadmin -c $container -d test-inventory; done\n\n[12:25:35] INFO     Getting auth token from cvp.as73.inetsix.net for user tom\n[12:25:36] INFO     Creating inventory folder /home/tom/Projects/arista/network-test-automation/test-inventory\n           WARNING  Using the new api_token parameter. This will override usage of the cvaas_token parameter if both are provided. This is because api_token and cvaas_token parameters\n                    are for the same use case and api_token is more generic\n           INFO     Connected to CVP cvp.as73.inetsix.net\n\n\n[12:25:37] INFO     Getting auth token from cvp.as73.inetsix.net for user tom\n[12:25:38] WARNING  Using the new api_token parameter. This will override usage of the cvaas_token parameter if both are provided. This is because api_token and cvaas_token parameters\n                    are for the same use case and api_token is more generic\n           INFO     Connected to CVP cvp.as73.inetsix.net\n\n\n[12:25:38] INFO     Getting auth token from cvp.as73.inetsix.net for user tom\n[12:25:39] WARNING  Using the new api_token parameter. This will override usage of the cvaas_token parameter if both are provided. This is because api_token and cvaas_token parameters\n                    are for the same use case and api_token is more generic\n           INFO     Connected to CVP cvp.as73.inetsix.net\n\n           INFO     Inventory file has been created in /home/tom/Projects/arista/network-test-automation/test-inventory/inventory-spines.yml\n
"},{"location":"cli/nrfu/","title":"NRFU","text":""},{"location":"cli/nrfu/#execute-network-readiness-for-use-nrfu-testing","title":"Execute Network Readiness For Use (NRFU) Testing","text":"

ANTA provides a set of commands for performing NRFU tests on devices. These commands are under the anta nrfu namespace and offer multiple output format options:

  • Text view
  • Table view
  • JSON view
  • Custom template view
"},{"location":"cli/nrfu/#nrfu-command-overview","title":"NRFU Command overview","text":"
anta nrfu --help\nUsage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n  Run NRFU against inventory devices\n\nOptions:\n  -u, --username TEXT     Username to connect to EOS  [env var: ANTA_USERNAME;\n                          required]\n  -p, --password TEXT     Password to connect to EOS that must be provided. It\n                          can be prompted using '--prompt' option.  [env var:\n                          ANTA_PASSWORD]\n  --enable-password TEXT  Password to access EOS Privileged EXEC mode. It can\n                          be prompted using '--prompt' option. Requires '--\n                          enable' option.  [env var: ANTA_ENABLE_PASSWORD]\n  --enable                Some commands may require EOS Privileged EXEC mode.\n                          This option tries to access this mode before sending\n                          a command to the device.  [env var: ANTA_ENABLE]\n  -P, --prompt            Prompt for passwords if they are not provided.  [env\n                          var: ANTA_PROMPT]\n  --timeout INTEGER       Global connection timeout  [env var: ANTA_TIMEOUT;\n                          default: 30]\n  --insecure              Disable SSH Host Key validation  [env var:\n                          ANTA_INSECURE]\n  --disable-cache         Disable cache globally  [env var:\n                          ANTA_DISABLE_CACHE]\n  -i, --inventory FILE    Path to the inventory YAML file  [env var:\n                          ANTA_INVENTORY; required]\n  -t, --tags TEXT         List of tags using comma as separator:\n                          tag1,tag2,tag3  [env var: ANTA_TAGS]\n  -c, --catalog FILE      Path to the test catalog YAML file  [env var:\n                          ANTA_CATALOG; required]\n  --ignore-status         Always exit with success  [env var:\n                          ANTA_NRFU_IGNORE_STATUS]\n  --ignore-error          Only report failures and not errors  [env var:\n                          ANTA_NRFU_IGNORE_ERROR]\n  --help                  Show this message and exit.\n\nCommands:\n  json        ANTA command to check network state with JSON result\n  table       ANTA command to check network states with table result\n  text        ANTA command to check network states with text result\n  tpl-report  ANTA command to check network state with templated report\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

All commands under the anta nrfu namespace require a catalog yaml file specified with the --catalog option and a device inventory file specified with the --inventory option.

Info

Issuing the command anta nrfu will run anta nrfu table without any option.

"},{"location":"cli/nrfu/#tag-management","title":"Tag management","text":"

The --tags option can be used to target specific devices in your inventory and run only tests configured with this specific tags from your catalog. The default tag is set to all and is implicit. Expected behaviour is provided below:

Command Description none Run all tests on all devices according tag definition in your inventory and test catalog. And tests with no tag are executed on all devices --tags leaf Run all tests marked with leaf tag on all devices configured with leaf tag. All other tags are ignored --tags leaf,spine Run all tests marked with leaf tag on all devices configured with leaf tag.Run all tests marked with spine tag on all devices configured with spine tag. All other tags are ignored

Info

More examples available on this dedicated page.

"},{"location":"cli/nrfu/#performing-nrfu-with-text-rendering","title":"Performing NRFU with text rendering","text":"

The text subcommand provides a straightforward text report for each test executed on all devices in your inventory.

"},{"location":"cli/nrfu/#command-overview","title":"Command overview","text":"
anta nrfu text --help\nUsage: anta nrfu text [OPTIONS]\n\n  ANTA command to check network states with text result\n\nOptions:\n  -s, --search TEXT  Regular expression to search in both name and test\n  --skip-error       Hide tests in errors due to connectivity issue\n  --help             Show this message and exit.\n

The --search option permits filtering based on a regular expression pattern in both the hostname and the test name.

The --skip-error option can be used to exclude tests that failed due to connectivity issues or unsupported commands.

"},{"location":"cli/nrfu/#example","title":"Example","text":"

anta nrfu text --tags LEAF --search DC1-LEAF1A\n

"},{"location":"cli/nrfu/#performing-nrfu-with-table-rendering","title":"Performing NRFU with table rendering","text":"

The table command under the anta nrfu namespace offers a clear and organized table view of the test results, suitable for filtering. It also has its own set of options for better control over the output.

"},{"location":"cli/nrfu/#command-overview_1","title":"Command overview","text":"
anta nrfu table --help\nUsage: anta nrfu table [OPTIONS]\n\n  ANTA command to check network states with table result\n\nOptions:\n  -d, --device TEXT         Show a summary for this device\n  -t, --test TEXT           Show a summary for this test\n  --group-by [device|test]  Group result by test or host. default none\n  --help                    Show this message and exit.\n

The --device and --test options show a summarized view of the test results for a specific host or test case, respectively.

The --group-by option show a summarized view of the test results per host or per test.

"},{"location":"cli/nrfu/#examples","title":"Examples","text":"

anta nrfu --tags LEAF table\n

For larger setups, you can also group the results by host or test to get a summarized view:

anta nrfu table --group-by device\n

anta nrfu table --group-by test\n

To get more specific information, it is possible to filter on a single device or a single test:

anta nrfu table --device spine1\n

anta nrfu table --test VerifyZeroTouch\n

"},{"location":"cli/nrfu/#performing-nrfu-with-json-rendering","title":"Performing NRFU with JSON rendering","text":"

The JSON rendering command in NRFU testing is useful in generating a JSON output that can subsequently be passed on to another tool for reporting purposes.

"},{"location":"cli/nrfu/#command-overview_2","title":"Command overview","text":"
anta nrfu json --help\nUsage: anta nrfu json [OPTIONS]\n\n  ANTA command to check network state with JSON result\n\nOptions:\n  -o, --output FILE  Path to save report as a file  [env var:\n                     ANTA_NRFU_JSON_OUTPUT]\n  --help             Show this message and exit.\n

The --output option allows you to save the JSON report as a file.

"},{"location":"cli/nrfu/#example_1","title":"Example","text":"

anta nrfu --tags LEAF json\n

"},{"location":"cli/nrfu/#performing-nrfu-with-custom-reports","title":"Performing NRFU with custom reports","text":"

ANTA offers a CLI option for creating custom reports. This leverages the Jinja2 template system, allowing you to tailor reports to your specific needs.

"},{"location":"cli/nrfu/#command-overview_3","title":"Command overview","text":"

anta nrfu tpl-report --help\nUsage: anta nrfu tpl-report [OPTIONS]\n\n  ANTA command to check network state with templated report\n\nOptions:\n  -tpl, --template FILE  Path to the template to use for the report  [env var:\n                         ANTA_NRFU_TPL_REPORT_TEMPLATE; required]\n  -o, --output FILE      Path to save report as a file  [env var:\n                         ANTA_NRFU_TPL_REPORT_OUTPUT]\n  --help                 Show this message and exit.\n
The --template option is used to specify the Jinja2 template file for generating the custom report.

The --output option allows you to choose the path where the final report will be saved.

"},{"location":"cli/nrfu/#example_2","title":"Example","text":"

anta nrfu --tags LEAF tpl-report --template ./custom_template.j2\n

The template ./custom_template.j2 is a simple Jinja2 template:

{% for d in data %}\n* {{ d.test }} is [green]{{ d.result | upper}}[/green] for {{ d.name }}\n{% endfor %}\n

The Jinja2 template has access to all TestResult elements and their values, as described in this documentation.

You can also save the report result to a file using the --output option:

anta nrfu --tags LEAF tpl-report --template ./custom_template.j2 --output nrfu-tpl-report.txt\n

The resulting output might look like this:

cat nrfu-tpl-report.txt\n* VerifyMlagStatus is [green]SUCCESS[/green] for DC1-LEAF1A\n* VerifyMlagInterfaces is [green]SUCCESS[/green] for DC1-LEAF1A\n* VerifyMlagConfigSanity is [green]SUCCESS[/green] for DC1-LEAF1A\n* VerifyMlagReloadDelay is [green]SUCCESS[/green] for DC1-LEAF1A\n
"},{"location":"cli/overview/","title":"Overview","text":""},{"location":"cli/overview/#overview-of-antas-command-line-interface-cli","title":"Overview of ANTA\u2019s Command-Line Interface (CLI)","text":"

ANTA provides a powerful Command-Line Interface (CLI) to perform a wide range of operations. This document provides a comprehensive overview of ANTA CLI usage and its commands.

ANTA can also be used as a Python library, allowing you to build your own tools based on it. Visit this page for more details.

To start using the ANTA CLI, open your terminal and type anta.

Warning

The ANTA CLI options have changed after version 0.11 and have moved away from the top level anta and are now required at their respective commands (e.g. anta nrfu). This breaking change occurs after users feedback on making the CLI more intuitive. This change should not affect user experience when using environment variables.

"},{"location":"cli/overview/#invoking-anta-cli","title":"Invoking ANTA CLI","text":"
$ anta --help\nUsage: anta [OPTIONS] COMMAND [ARGS]...\n\n  Arista Network Test Automation (ANTA) CLI\n\nOptions:\n  --version                       Show the version and exit.\n  --log-file FILE                 Send the logs to a file. If logging level is\n                                  DEBUG, only INFO or higher will be sent to\n                                  stdout.  [env var: ANTA_LOG_FILE]\n  -l, --log-level [CRITICAL|ERROR|WARNING|INFO|DEBUG]\n                                  ANTA logging level  [env var:\n                                  ANTA_LOG_LEVEL; default: INFO]\n  --help                          Show this message and exit.\n\nCommands:\n  check  Commands to validate configuration files\n  debug  Commands to execute EOS commands on remote devices\n  exec   Commands to execute various scripts on EOS devices\n  get    Commands to get information from or generate inventories\n  nrfu   Run ANTA tests on devices\n
"},{"location":"cli/overview/#anta-environement-variables","title":"ANTA environement variables","text":"

Certain parameters are required and can be either passed to the ANTA CLI or set as an environment variable (ENV VAR).

To pass the parameters via the CLI:

anta nrfu -u admin -p arista123 -i inventory.yaml -c tests.yaml\n

To set them as environment variables:

export ANTA_USERNAME=admin\nexport ANTA_PASSWORD=arista123\nexport ANTA_INVENTORY=inventory.yml\nexport ANTA_INVENTORY=tests.yml\n

Then, run the CLI without options:

anta nrfu\n

Note

All environement variables may not be needed for every commands. Refer to <command> --help for the comprehensive environment varibles names.

Below are the environement variables usable with the anta nrfu command:

Variable Name Purpose Required ANTA_USERNAME The username to use in the inventory to connect to devices. Yes ANTA_PASSWORD The password to use in the inventory to connect to devices. Yes ANTA_INVENTORY The path to the inventory file. Yes ANTA_CATALOG The path to the catalog file. Yes ANTA_PROMPT The value to pass to the prompt for password is password is not provided No ANTA_INSECURE Whether or not using insecure mode when connecting to the EOS devices HTTP API. No ANTA_DISABLE_CACHE A variable to disable caching for all ANTA tests (enabled by default). No ANTA_ENABLE Whether it is necessary to go to enable mode on devices. No ANTA_ENABLE_PASSWORD The optional enable password, when this variable is set, ANTA_ENABLE or --enable is required. No

Info

Caching can be disabled with the global parameter --disable-cache. For more details about how caching is implemented in ANTA, please refer to Caching in ANTA.

"},{"location":"cli/overview/#anta-exit-codes","title":"ANTA Exit Codes","text":"

ANTA CLI utilizes the following exit codes:

  • Exit code 0 - All tests passed successfully.
  • Exit code 1 - An internal error occurred while executing ANTA.
  • Exit code 2 - A usage error was raised.
  • Exit code 3 - Tests were run, but at least one test returned an error.
  • Exit code 4 - Tests were run, but at least one test returned a failure.

To ignore the test status, use anta nrfu --ignore-status, and the exit code will always be 0.

To ignore errors, use anta nrfu --ignore-error, and the exit code will be 0 if all tests succeeded or 1 if any test failed.

"},{"location":"cli/overview/#shell-completion","title":"Shell Completion","text":"

You can enable shell completion for the ANTA CLI:

ZSHBASH

If you use ZSH shell, add the following line in your ~/.zshrc:

eval \"$(_ANTA_COMPLETE=zsh_source anta)\" > /dev/null\n

With bash, add the following line in your ~/.bashrc:

eval \"$(_ANTA_COMPLETE=bash_source anta)\" > /dev/null\n
"},{"location":"cli/tag-management/","title":"Tag Management","text":""},{"location":"cli/tag-management/#tag-management","title":"Tag management","text":""},{"location":"cli/tag-management/#overview","title":"Overview","text":"

Some of the ANTA commands like anta nrfu command come with a --tags option.

For nrfu, this allows users to specify a set of tests, marked with a given tag, to be run on devices marked with the same tag. For instance, you can run tests dedicated to leaf devices on your leaf devices only and not on other devices.

Tags are string defined by the user and can be anything considered as a string by Python. A default one is present for all tests and devices.

The next table provides a short summary of the scope of tags using CLI

Command Description none Run all tests on all devices according tag definition in your inventory and test catalog. And tests with no tag are executed on all devices --tags leaf Run all tests marked with leaf tag on all devices configured with leaf tag. All other tags are ignored --tags leaf,spine Run all tests marked with leaf tag on all devices configured with leaf tag.Run all tests marked with spine tag on all devices configured with spine tag. All other tags are ignored"},{"location":"cli/tag-management/#inventory-and-catalog-for-tests","title":"Inventory and Catalog for tests","text":"

All commands in this page are based on the following inventory and test catalog.

InventoryTest Catalog
---\nanta_inventory:\n  hosts:\n  - host: 192.168.0.10\n    name: spine01\n    tags: ['fabric', 'spine']\n  - host: 192.168.0.11\n    name: spine02\n    tags: ['fabric', 'spine']\n  - host: 192.168.0.12\n    name: leaf01\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.13\n    name: leaf02\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.14\n    name: leaf03\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.15\n    name: leaf04\n    tags: ['fabric', 'leaf'\n
anta.tests.system:\n  - VerifyUptime:\n      minimum: 10\n      filters:\n        tags: ['fabric']\n  - VerifyReloadCause:\n      tags: ['leaf', spine']\n  - VerifyCoredump:\n  - VerifyAgentLogs:\n  - VerifyCPUUtilization:\n      filters:\n        tags: ['spine', 'leaf']\n  - VerifyMemoryUtilization:\n  - VerifyFileSystemUtilization:\n  - VerifyNTP:\n\nanta.tests.mlag:\n  - VerifyMlagStatus:\n\n\nanta.tests.interfaces:\n  - VerifyL3MTU:\n      mtu: 1500\n      filters:\n        tags: ['demo']\n
"},{"location":"cli/tag-management/#default-tags","title":"Default tags","text":"

By default, ANTA uses a default tag for both devices and tests. This default tag is all and it can be explicit if you want to make it visible in your inventory and also implicit since the framework injects this tag if it is not defined.

So this command will run all tests from your catalog on all devices. With a mapping for tags defined in your inventory and catalog. If no tags configured, then tests are executed against all devices.

$ anta nrfu -c .personal/catalog-class.yml table --group-by device\n\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 Running ANTA tests:                                  \u2502\n\u2502 - ANTA Inventory contains 6 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 10 tests                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\n\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Device  \u2503 # of success \u2503 # of skipped \u2503 # of failure \u2503 # of errors \u2503 List of failed or error test cases \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 spine01 \u2502 5            \u2502 1            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2502 spine02 \u2502 5            \u2502 1            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2502 leaf01  \u2502 6            \u2502 0            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2502 leaf02  \u2502 6            \u2502 0            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2502 leaf03  \u2502 6            \u2502 0            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2502 leaf04  \u2502 6            \u2502 0            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"cli/tag-management/#use-a-single-tag-in-cli","title":"Use a single tag in CLI","text":"

The most used approach is to use a single tag in your CLI to filter tests & devices configured with this one.

In such scenario, ANTA will run tests marked with $tag only on devices marked with $tag. All other tests and devices will be ignored

$ anta nrfu -c .personal/catalog-class.yml --tags leaf text\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 Running ANTA tests:                                  \u2502\n\u2502 - ANTA Inventory contains 6 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 10 tests                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\nleaf01 :: VerifyUptime :: SUCCESS\nleaf01 :: VerifyReloadCause :: SUCCESS\nleaf01 :: VerifyCPUUtilization :: SUCCESS\nleaf02 :: VerifyUptime :: SUCCESS\nleaf02 :: VerifyReloadCause :: SUCCESS\nleaf02 :: VerifyCPUUtilization :: SUCCESS\nleaf03 :: VerifyUptime :: SUCCESS\nleaf03 :: VerifyReloadCause :: SUCCESS\nleaf03 :: VerifyCPUUtilization :: SUCCESS\nleaf04 :: VerifyUptime :: SUCCESS\nleaf04 :: VerifyReloadCause :: SUCCESS\nleaf04 :: VerifyCPUUtilization :: SUCCESS\n

In this case, only leaf devices defined in your inventory are used to run tests marked with leaf in your test catalog

"},{"location":"cli/tag-management/#use-multiple-tags-in-cli","title":"Use multiple tags in CLI","text":"

A more advanced usage of the tag feature is to list multiple tags in your CLI using --tags $tag1,$tag2 syntax.

In such scenario, all devices marked with $tag1 will be selected and ANTA will run tests with $tag1, then devices with $tag2 will be selected and will be tested with tests marked with $tag2

anta nrfu -c .personal/catalog-class.yml --tags leaf,fabric text\n\nspine01 :: VerifyUptime :: SUCCESS\nspine02 :: VerifyUptime :: SUCCESS\nleaf01 :: VerifyUptime :: SUCCESS\nleaf01 :: VerifyReloadCause :: SUCCESS\nleaf01 :: VerifyCPUUtilization :: SUCCESS\nleaf02 :: VerifyUptime :: SUCCESS\nleaf02 :: VerifyReloadCause :: SUCCESS\nleaf02 :: VerifyCPUUtilization :: SUCCESS\nleaf03 :: VerifyUptime :: SUCCESS\nleaf03 :: VerifyReloadCause :: SUCCESS\nleaf03 :: VerifyCPUUtilization :: SUCCESS\nleaf04 :: VerifyUptime :: SUCCESS\nleaf04 :: VerifyReloadCause :: SUCCESS\nleaf04 :: VerifyCPUUtilization :: SUCCESS\n
"}]} \ No newline at end of file +{"config":{"lang":["en"],"separator":"[\\s\\-]+","pipeline":["stopWordFilter"]},"docs":[{"location":"","title":"Home","text":""},{"location":"#arista-network-test-automation-anta-framework","title":"Arista Network Test Automation (ANTA) Framework","text":"Code License GitHub PyPi

ANTA is Python framework that automates tests for Arista devices.

  • ANTA provides a set of tests to validate the state of your network
  • ANTA can be used to:
    • Automate NRFU (Network Ready For Use) test on a preproduction network
    • Automate tests on a live network (periodically or on demand)
  • ANTA can be used with:
    • The ANTA CLI
    • As a Python library in your own application

# Install ANTA CLI\n$ pip install anta\n\n# Run ANTA CLI\n$ anta --help\nUsage: anta [OPTIONS] COMMAND [ARGS]...\n\n  Arista Network Test Automation (ANTA) CLI\n\nOptions:\n  --version                       Show the version and exit.\n  --log-file FILE                 Send the logs to a file. If logging level is\n                                  DEBUG, only INFO or higher will be sent to\n                                  stdout.  [env var: ANTA_LOG_FILE]\n  -l, --log-level [CRITICAL|ERROR|WARNING|INFO|DEBUG]\n                                  ANTA logging level  [env var:\n                                  ANTA_LOG_LEVEL; default: INFO]\n  --help                          Show this message and exit.\n\nCommands:\n  check  Commands to validate configuration files\n  debug  Commands to execute EOS commands on remote devices\n  exec   Commands to execute various scripts on EOS devices\n  get    Commands to get information from or generate inventories\n  nrfu   Run ANTA tests on devices\n

[!WARNING] The ANTA CLI options have changed after version 0.11 and have moved away from the top level anta and are now required at their respective commands (e.g. anta nrfu). This breaking change occurs after users feedback on making the CLI more intuitive. This change should not affect user experience when using environment variables.

"},{"location":"#documentation","title":"Documentation","text":"

The documentation is published on ANTA package website. Also, a demo repository is available to facilitate your journey with ANTA.

"},{"location":"#contribution-guide","title":"Contribution guide","text":"

Contributions are welcome. Please refer to the contribution guide

"},{"location":"#credits","title":"Credits","text":"

Thank you to Ang\u00e9lique Phillipps, Colin MacGiollaE\u00e1in, Khelil Sator, Matthieu Tache, Onur Gashi, Paul Lavelle, Guillaume Mulocher and Thomas Grimonet for their contributions and guidances.

"},{"location":"contribution/","title":"Contributions","text":""},{"location":"contribution/#how-to-contribute-to-anta","title":"How to contribute to ANTA","text":"

Contribution model is based on a fork-model. Don\u2019t push to arista-netdevops-community/anta directly. Always do a branch in your forked repository and create a PR.

To help development, open your PR as soon as possible even in draft mode. It helps other to know on what you are working on and avoid duplicate PRs.

"},{"location":"contribution/#create-a-development-environement","title":"Create a development environement","text":"

Run the following commands to create an ANTA development environement:

# Clone repository\n$ git clone https://github.com/arista-netdevops-community/anta.git\n$ cd anta\n\n# Install ANTA in editable mode and its development tools\n$ pip install -e .[dev]\n\n# Verify installation\n$ pip list -e\nPackage Version Editable project location\n------- ------- -------------------------\nanta    0.13.0   /mnt/lab/projects/anta\n

Then, tox is configued with few environments to run CI locally:

$ tox list -d\ndefault environments:\nclean  -> Erase previous coverage reports\nlint   -> Check the code style\ntype   -> Check typing\npy38   -> Run pytest with py38\npy39   -> Run pytest with py39\npy310  -> Run pytest with py310\npy311  -> Run pytest with py311\nreport -> Generate coverage report\n
"},{"location":"contribution/#code-linting","title":"Code linting","text":"
tox -e lint\n[...]\nlint: commands[0]> black --check --diff --color .\nAll done! \u2728 \ud83c\udf70 \u2728\n104 files would be left unchanged.\nlint: commands[1]> isort --check --diff --color .\nSkipped 7 files\nlint: commands[2]> flake8 --max-line-length=165 --config=/dev/null anta\nlint: commands[3]> flake8 --max-line-length=165 --config=/dev/null tests\nlint: commands[4]> pylint anta\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\n.pkg: _exit> python /Users/guillaumemulocher/.pyenv/versions/3.8.13/envs/anta/lib/python3.8/site-packages/pyproject_api/_backend.py True setuptools.build_meta\n  lint: OK (19.26=setup[5.83]+cmd[1.50,0.76,1.19,1.20,8.77] seconds)\n  congratulations :) (19.56 seconds)\n
"},{"location":"contribution/#code-typing","title":"Code Typing","text":"
tox -e type\n\n[...]\ntype: commands[0]> mypy --config-file=pyproject.toml anta\nSuccess: no issues found in 52 source files\n.pkg: _exit> python /Users/guillaumemulocher/.pyenv/versions/3.8.13/envs/anta/lib/python3.8/site-packages/pyproject_api/_backend.py True setuptools.build_meta\n  type: OK (46.66=setup[24.20]+cmd[22.46] seconds)\n  congratulations :) (47.01 seconds)\n

NOTE: Typing is configured quite strictly, do not hesitate to reach out if you have any questions, struggles, nightmares.

"},{"location":"contribution/#unit-tests","title":"Unit tests","text":"

To keep high quality code, we require to provide a Pytest for every tests implemented in ANTA.

All submodule should have its own pytest section under tests/units/anta_tests/<submodule-name>.py.

"},{"location":"contribution/#how-to-write-a-unit-test-for-an-antatest-subclass","title":"How to write a unit test for an AntaTest subclass","text":"

The Python modules in the tests/units/anta_tests folder define test parameters for AntaTest subclasses unit tests. A generic test function is written for all unit tests in tests.lib.anta module. The pytest_generate_tests function definition in conftest.py is called during test collection. The pytest_generate_tests function will parametrize the generic test function based on the DATA data structure defined in tests.units.anta_tests modules. See https://docs.pytest.org/en/7.3.x/how-to/parametrize.html#basic-pytest-generate-tests-example

The DATA structure is a list of dictionaries used to parametrize the test. The list elements have the following keys: - name (str): Test name as displayed by Pytest. - test (AntaTest): An AntaTest subclass imported in the test module - e.g. VerifyUptime. - eos_data (list[dict]): List of data mocking EOS returned data to be passed to the test. - inputs (dict): Dictionary to instantiate the test inputs as defined in the class from test. - expected (dict): Expected test result structure, a dictionary containing a key result containing one of the allowed status (Literal['success', 'failure', 'unset', 'skipped', 'error']) and optionally a key messages which is a list(str) and each message is expected to be a substring of one of the actual messages in the TestResult object.

In order for your unit tests to be correctly collected, you need to import the generic test function even if not used in the Python module.

Test example for anta.tests.system.VerifyUptime AntaTest.

# Import the generic test function\nfrom tests.lib.anta import test  # noqa: F401\n\n# Import your AntaTest\nfrom anta.tests.system import VerifyUptime\n\n# Define test parameters\nDATA: list[dict[str, Any]] = [\n   {\n        # Arbitrary test name\n        \"name\": \"success\",\n        # Must be an AntaTest definition\n        \"test\": VerifyUptime,\n        # Data returned by EOS on which the AntaTest is tested\n        \"eos_data\": [{\"upTime\": 1186689.15, \"loadAvg\": [0.13, 0.12, 0.09], \"users\": 1, \"currentTime\": 1683186659.139859}],\n        # Dictionary to instantiate VerifyUptime.Input\n        \"inputs\": {\"minimum\": 666},\n        # Expected test result\n        \"expected\": {\"result\": \"success\"},\n    },\n    {\n        \"name\": \"failure\",\n        \"test\": VerifyUptime,\n        \"eos_data\": [{\"upTime\": 665.15, \"loadAvg\": [0.13, 0.12, 0.09], \"users\": 1, \"currentTime\": 1683186659.139859}],\n        \"inputs\": {\"minimum\": 666},\n        # If the test returns messages, it needs to be expected otherwise test will fail.\n        # NB: expected messages only needs to be included in messages returned by the test. Exact match is not required.\n        \"expected\": {\"result\": \"failure\", \"messages\": [\"Device uptime is 665.15 seconds\"]},\n    },\n]\n
"},{"location":"contribution/#git-pre-commit-hook","title":"Git Pre-commit hook","text":"
pip install pre-commit\npre-commit install\n

When running a commit or a pre-commit check:

\u276f echo \"import foobaz\" > test.py && git add test.py\n\u276f pre-commit\npylint...................................................................Failed\n- hook id: pylint\n- exit code: 22\n\n************* Module test\ntest.py:1:0: C0114: Missing module docstring (missing-module-docstring)\ntest.py:1:0: E0401: Unable to import 'foobaz' (import-error)\ntest.py:1:0: W0611: Unused import foobaz (unused-import)\n

NOTE: It could happen that pre-commit and tox disagree on something, in that case please open an issue on Github so we can take a look.. It is most probably wrong configuration on our side.

"},{"location":"contribution/#configure-mypypath","title":"Configure MYPYPATH","text":"

In some cases, mypy can complain about not having MYPYPATH configured in your shell. It is especially the case when you update both an anta test and its unit test. So you can configure this environment variable with:

# Option 1: use local folder\nexport MYPYPATH=.\n\n# Option 2: use absolute path\nexport MYPYPATH=/path/to/your/local/anta/repository\n
"},{"location":"contribution/#documentation","title":"Documentation","text":"

mkdocs is used to generate the documentation. A PR should always update the documentation to avoid documentation debt.

"},{"location":"contribution/#install-documentation-requirements","title":"Install documentation requirements","text":"

Run pip to install the documentation requirements from the root of the repo:

pip install -e .[doc]\n
"},{"location":"contribution/#testing-documentation","title":"Testing documentation","text":"

You can then check locally the documentation using the following command from the root of the repo:

mkdocs serve\n

By default, mkdocs listens to http://127.0.0.1:8000/, if you need to expose the documentation to another IP or port (for instance all IPs on port 8080), use the following command:

mkdocs serve --dev-addr=0.0.0.0:8080\n
"},{"location":"contribution/#build-class-diagram","title":"Build class diagram","text":"

To build class diagram to use in API documentation, you can use pyreverse part of pylint with graphviz installed for jpeg generation.

pyreverse anta --colorized -a1 -s1 -o jpeg -m true -k --output-directory docs/imgs/uml/ -c <FQDN anta class>\n

Image will be generated under docs/imgs/uml/ and can be inserted in your documentation.

"},{"location":"contribution/#checking-links","title":"Checking links","text":"

Writing documentation is crucial but managing links can be cumbersome. To be sure there is no dead links, you can use muffet with the following command:

muffet -c 2 --color=always http://127.0.0.1:8000 -e fonts.gstatic.com -b 8192\n
"},{"location":"contribution/#continuous-integration","title":"Continuous Integration","text":"

GitHub actions is used to test git pushes and pull requests. The workflows are defined in this directory. We can view the results here.

"},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#frequently-asked-questions-faq","title":"Frequently Asked Questions (FAQ)","text":""},{"location":"faq/#importerror-related-to-urllib3","title":"ImportError related to urllib3","text":"ImportError related to urllib3 when running ANTA

When running the anta --help command, some users might encounter the following error:

ImportError: urllib3 v2.0 only supports OpenSSL 1.1.1+, currently the 'ssl' module is compiled with 'OpenSSL 1.0.2k-fips  26 Jan 2017'. See: https://github.com/urllib3/urllib3/issues/2168\n

This error arises due to a compatibility issue between urllib3 v2.0 and older versions of OpenSSL.

"},{"location":"faq/#solution","title":"Solution","text":"
  1. Workaround: Downgrade urllib3

    If you need a quick fix, you can temporarily downgrade the urllib3 package:

    pip3 uninstall urllib3\n\npip3 install urllib3==1.26.15\n
  2. Recommended: Upgrade System or Libraries:

    As per the [urllib3 v2 migration guide](https://urllib3.readthedocs.io/en/latest/v2-migration-guide.html), the root cause of this error is an incompatibility with older OpenSSL versions. For example, users on RHEL7 might consider upgrading to RHEL8, which supports the required OpenSSL version.\n
"},{"location":"faq/#attributeerror-module-lib-has-no-attribute-openssl_add_all_algorithms","title":"AttributeError: module 'lib' has no attribute 'OpenSSL_add_all_algorithms'","text":"AttributeError: module 'lib' has no attribute 'OpenSSL_add_all_algorithms' when running ANTA

When running the anta commands after installation, some users might encounter the following error:

AttributeError: module 'lib' has no attribute 'OpenSSL_add_all_algorithms'\n

The error is a result of incompatibility between cryptography and pyopenssl when installing asyncssh which is a requirement of ANTA.

"},{"location":"faq/#solution_1","title":"Solution","text":"
  1. Upgrade pyopenssl

    pip install -U pyopenssl>22.0\n
"},{"location":"faq/#__nscfconstantstring-initialize-error-on-osx","title":"__NSCFConstantString initialize error on OSX","text":"__NSCFConstantString initialize error on OSX

This error occurs because of added security to restrict multithreading in macOS High Sierra and later versions of macOS. https://www.wefearchange.org/2018/11/forkmacos.rst.html

"},{"location":"faq/#solution_2","title":"Solution","text":"
  1. Set the following environment variable

    export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES\n
"},{"location":"faq/#still-facing-issues","title":"Still facing issues?","text":"

If you\u2019ve tried the above solutions and continue to experience problems, please report the issue in our GitHub repository.

"},{"location":"getting-started/","title":"Getting Started","text":""},{"location":"getting-started/#getting-started","title":"Getting Started","text":"

This section shows how to use ANTA with basic configuration. All examples are based on Arista Test Drive (ATD) topology you can access by reaching out to your preferred SE.

"},{"location":"getting-started/#installation","title":"Installation","text":"

The easiest way to intall ANTA package is to run Python (>=3.8) and its pip package to install:

pip install anta\n

For more details about how to install package, please see the requirements and intallation section.

"},{"location":"getting-started/#configure-arista-eos-devices","title":"Configure Arista EOS devices","text":"

For ANTA to be able to connect to your target devices, you need to configure your management interface

vrf instance MGMT\n!\ninterface Management0\n   description oob_management\n   vrf MGMT\n   ip address 192.168.0.10/24\n!\n

Then, configure access to eAPI:

!\nmanagement api http-commands\n   protocol https port 443\n   no shutdown\n   vrf MGMT\n      no shutdown\n   !\n!\n
"},{"location":"getting-started/#create-your-inventory","title":"Create your inventory","text":"

ANTA uses an inventory to list the target devices for the tests. You can create a file manually with this format:

anta_inventory:\n  hosts:\n  - host: 192.168.0.10\n    name: spine01\n    tags: ['fabric', 'spine']\n  - host: 192.168.0.11\n    name: spine02\n    tags: ['fabric', 'spine']\n  - host: 192.168.0.12\n    name: leaf01\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.13\n    name: leaf02\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.14\n    name: leaf03\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.15\n    name: leaf04\n    tags: ['fabric', 'leaf']\n

You can read more details about how to build your inventory here

"},{"location":"getting-started/#test-catalog","title":"Test Catalog","text":"

To test your network, ANTA relies on a test catalog to list all the tests to run against your inventory. A test catalog references python functions into a yaml file.

The structure to follow is like:

<anta_tests_submodule>:\n  - <anta_tests_submodule function name>:\n      <test function option>:\n        <test function option value>\n

You can read more details about how to build your catalog here

Here is an example for basic tests:

# Load anta.tests.software\nanta.tests.software:\n  - VerifyEOSVersion: # Verifies the device is running one of the allowed EOS version.\n      versions: # List of allowed EOS versions.\n        - 4.25.4M\n        - 4.26.1F\n        - '4.28.3M-28837868.4283M (engineering build)'\n  - VerifyTerminAttrVersion:\n      versions:\n        - v1.22.1\n\nanta.tests.system:\n  - VerifyUptime: # Verifies the device uptime is higher than a value.\n      minimum: 1\n  - VerifyNTP:\n  - VerifySyslog:\n\nanta.tests.mlag:\n  - VerifyMlagStatus:\n  - VerifyMlagInterfaces:\n  - VerifyMlagConfigSanity:\n\nanta.tests.configuration:\n  - VerifyZeroTouch: # Verifies ZeroTouch is disabled.\n  - VerifyRunningConfigDiffs:\n
"},{"location":"getting-started/#test-your-network","title":"Test your network","text":"

ANTA comes with a generic CLI entrypoint to run tests in your network. It requires an inventory file as well as a test catalog.

This entrypoint has multiple options to manage test coverage and reporting.

# Generic ANTA options\n$ anta\nUsage: anta [OPTIONS] COMMAND [ARGS]...\n\n  Arista Network Test Automation (ANTA) CLI\n\nOptions:\n  --version                       Show the version and exit.\n  --log-file FILE                 Send the logs to a file. If logging level is\n                                  DEBUG, only INFO or higher will be sent to\n                                  stdout.  [env var: ANTA_LOG_FILE]\n  -l, --log-level [CRITICAL|ERROR|WARNING|INFO|DEBUG]\n                                  ANTA logging level  [env var:\n                                  ANTA_LOG_LEVEL; default: INFO]\n  --help                          Show this message and exit.\n\nCommands:\n  check  Commands to validate configuration files\n  debug  Commands to execute EOS commands on remote devices\n  exec   Commands to execute various scripts on EOS devices\n  get    Commands to get information from or generate inventories\n  nrfu   Run ANTA tests on devices\n
# NRFU part of ANTA\nUsage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n  Run ANTA tests on devices\n\nOptions:\n  -u, --username TEXT     Username to connect to EOS  [env var: ANTA_USERNAME;\n                          required]\n  -p, --password TEXT     Password to connect to EOS that must be provided. It\n                          can be prompted using '--prompt' option.  [env var:\n                          ANTA_PASSWORD]\n  --enable-password TEXT  Password to access EOS Privileged EXEC mode. It can\n                          be prompted using '--prompt' option. Requires '--\n                          enable' option.  [env var: ANTA_ENABLE_PASSWORD]\n  --enable                Some commands may require EOS Privileged EXEC mode.\n                          This option tries to access this mode before sending\n                          a command to the device.  [env var: ANTA_ENABLE]\n  -P, --prompt            Prompt for passwords if they are not provided.  [env\n                          var: ANTA_PROMPT]\n  --timeout INTEGER       Global connection timeout  [env var: ANTA_TIMEOUT;\n                          default: 30]\n  --insecure              Disable SSH Host Key validation  [env var:\n                          ANTA_INSECURE]\n  --disable-cache         Disable cache globally  [env var:\n                          ANTA_DISABLE_CACHE]\n  -i, --inventory FILE    Path to the inventory YAML file  [env var:\n                          ANTA_INVENTORY; required]\n  -t, --tags TEXT         List of tags using comma as separator:\n                          tag1,tag2,tag3  [env var: ANTA_TAGS]\n  -c, --catalog FILE      Path to the test catalog YAML file  [env var:\n                          ANTA_CATALOG; required]\n  --ignore-status         Always exit with success  [env var:\n                          ANTA_NRFU_IGNORE_STATUS]\n  --ignore-error          Only report failures and not errors  [env var:\n                          ANTA_NRFU_IGNORE_ERROR]\n  --help                  Show this message and exit.\n\nCommands:\n  json        ANTA command to check network state with JSON result\n  table       ANTA command to check network states with table result\n  text        ANTA command to check network states with text result\n  tpl-report  ANTA command to check network state with templated report\n

To run the NRFU, you need to select an output format amongst [\u201cjson\u201d, \u201ctable\u201d, \u201ctext\u201d, \u201ctpl-report\u201d]. For a first usage, table is recommended. By default all test results for all devices are rendered but it can be changed to a report per test case or per host

"},{"location":"getting-started/#default-report-using-table","title":"Default report using table","text":"
anta nrfu \\\n    --username tom \\\n    --password arista123 \\\n    --enable \\\n    --enable-password t \\\n    --inventory .personal/inventory_atd.yml \\\n    --catalog .personal/tests-bases.yml \\\n    table --tags leaf\n\n\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 Running ANTA tests:                                  \u2502\n\u2502 - ANTA Inventory contains 6 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 10 tests                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n[10:17:24] INFO     Running ANTA tests...                                                                                                           runner.py:75\n  \u2022 Running NRFU Tests...100% \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 40/40 \u2022 0:00:02 \u2022 0:00:00\n\n                                                                       All tests results\n\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Device IP \u2503 Test Name                \u2503 Test Status \u2503 Message(s)       \u2503 Test description                                                     \u2503 Test category \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 leaf01    \u2502 VerifyEOSVersion         \u2502 success     \u2502                  \u2502 Verifies the device is running one of the allowed EOS version.       \u2502 software      \u2502\n\u2502 leaf01    \u2502 VerifyTerminAttrVersion  \u2502 success     \u2502                  \u2502 Verifies the device is running one of the allowed TerminAttr         \u2502 software      \u2502\n\u2502           \u2502                          \u2502             \u2502                  \u2502 version.                                                             \u2502               \u2502\n\u2502 leaf01    \u2502 VerifyUptime             \u2502 success     \u2502                  \u2502 Verifies the device uptime is higher than a value.                   \u2502 system        \u2502\n\u2502 leaf01    \u2502 VerifyNTP                \u2502 success     \u2502                  \u2502 Verifies NTP is synchronised.                                        \u2502 system        \u2502\n\u2502 leaf01    \u2502 VerifySyslog             \u2502 success     \u2502                  \u2502 Verifies the device had no syslog message with a severity of warning \u2502 system        \u2502\n\u2502           \u2502                          \u2502             \u2502                  \u2502 (or a more severe message) during the last 7 days.                   \u2502               \u2502\n\u2502 leaf01    \u2502 VerifyMlagStatus         \u2502 skipped     \u2502 MLAG is disabled \u2502 This test verifies the health status of the MLAG configuration.      \u2502 mlag          \u2502\n\u2502 leaf01    \u2502 VerifyMlagInterfaces     \u2502 skipped     \u2502 MLAG is disabled \u2502 This test verifies there are no inactive or active-partial MLAG      \u2502 mlag          \u2502\n[...]\n\u2502 leaf04    \u2502 VerifyMlagConfigSanity   \u2502 skipped     \u2502 MLAG is disabled \u2502 This test verifies there are no MLAG config-sanity inconsistencies.  \u2502 mlag          \u2502\n\u2502 leaf04    \u2502 VerifyZeroTouch          \u2502 success     \u2502                  \u2502 Verifies ZeroTouch is disabled.                                      \u2502 configuration \u2502\n\u2502 leaf04    \u2502 VerifyRunningConfigDiffs \u2502 success     \u2502                  \u2502                                                                      \u2502 configuration \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"getting-started/#report-in-text-mode","title":"Report in text mode","text":"
$ anta nrfu \\\n    --username tom \\\n    --password arista123 \\\n    --enable \\\n    --enable-password t \\\n    --inventory .personal/inventory_atd.yml \\\n    --catalog .personal/tests-bases.yml \\\n    text --tags leaf\n\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 Running ANTA tests:                                  \u2502\n\u2502 - ANTA Inventory contains 6 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 10 tests                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n[10:20:47] INFO     Running ANTA tests...                                                                                                           runner.py:75\n  \u2022 Running NRFU Tests...100% \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 40/40 \u2022 0:00:01 \u2022 0:00:00\nleaf01 :: VerifyEOSVersion :: SUCCESS\nleaf01 :: VerifyTerminAttrVersion :: SUCCESS\nleaf01 :: VerifyUptime :: SUCCESS\nleaf01 :: VerifyNTP :: SUCCESS\nleaf01 :: VerifySyslog :: SUCCESS\nleaf01 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\nleaf01 :: VerifyMlagInterfaces :: SKIPPED (MLAG is disabled)\nleaf01 :: VerifyMlagConfigSanity :: SKIPPED (MLAG is disabled)\n[...]\n
"},{"location":"getting-started/#report-in-json-format","title":"Report in JSON format","text":"
$ anta nrfu \\\n    --username tom \\\n    --password arista123 \\\n    --enable \\\n    --enable-password t \\\n    --inventory .personal/inventory_atd.yml \\\n    --catalog .personal/tests-bases.yml \\\n    json --tags leaf\n\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 Running ANTA tests:                                  \u2502\n\u2502 - ANTA Inventory contains 6 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 10 tests                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n[10:21:51] INFO     Running ANTA tests...                                                                                                           runner.py:75\n  \u2022 Running NRFU Tests...100% \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501 40/40 \u2022 0:00:02 \u2022 0:00:00\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 JSON results of all tests                                                                                                                                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n[\n  {\n    \"name\": \"leaf01\",\n    \"test\": \"VerifyEOSVersion\",\n    \"categories\": [\n      \"software\"\n    ],\n    \"description\": \"Verifies the device is running one of the allowed EOS version.\",\n    \"result\": \"success\",\n    \"messages\": [],\n    \"custom_field\": \"None\",\n  },\n  {\n    \"name\": \"leaf01\",\n    \"test\": \"VerifyTerminAttrVersion\",\n    \"categories\": [\n      \"software\"\n    ],\n    \"description\": \"Verifies the device is running one of the allowed TerminAttr version.\",\n    \"result\": \"success\",\n    \"messages\": [],\n    \"custom_field\": \"None\",\n  },\n[...]\n]\n

You can find more information under the usage section of the website

"},{"location":"requirements-and-installation/","title":"Installation","text":""},{"location":"requirements-and-installation/#anta-requirements","title":"ANTA Requirements","text":""},{"location":"requirements-and-installation/#python-version","title":"Python version","text":"

Python 3 (>=3.8) is required:

python --version\nPython 3.9.9\n
"},{"location":"requirements-and-installation/#install-anta-package","title":"Install ANTA package","text":"

This installation will deploy tests collection, scripts and all their Python requirements.

The ANTA package and the cli require some packages that are not part of the Python standard library. They are indicated in the pyproject.toml file, under dependencies.

"},{"location":"requirements-and-installation/#install-from-pypi-server","title":"Install from Pypi server","text":"
pip install anta\n
"},{"location":"requirements-and-installation/#install-anta-from-github","title":"Install ANTA from github","text":"
pip install git+https://github.com/arista-netdevops-community/anta.git\n\n# You can even specify the branch, tag or commit:\npip install git+https://github.com/arista-netdevops-community/anta.git@<cool-feature-branch>\npip install git+https://github.com/arista-netdevops-community/anta.git@<cool-tag>\npip install git+https://github.com/arista-netdevops-community/anta.git@<more-or-less-cool-hash>\n
"},{"location":"requirements-and-installation/#check-installation","title":"Check installation","text":"

After installing ANTA, verify the installation with the following commands:

# Check ANTA has been installed in your python path\npip list | grep anta\n\n# Check scripts are in your $PATH\n# Path may differ but it means CLI is in your path\nwhich anta\n/home/tom/.pyenv/shims/anta\n

Warning

Before running the anta --version command, please be aware that some users have reported issues related to the urllib3 package. If you encounter an error at this step, please refer to our FAQ page for guidance on resolving it.

# Check ANTA version\nanta --version\nanta, version v0.13.0\n
"},{"location":"requirements-and-installation/#eos-requirements","title":"EOS Requirements","text":"

To get ANTA working, the targetted Arista EOS devices must have the following configuration (assuming you connect to the device using Management interface in MGMT VRF):

configure\n!\nvrf instance MGMT\n!\ninterface Management1\n   description oob_management\n   vrf MGMT\n   ip address 10.73.1.105/24\n!\nend\n

Enable eAPI on the MGMT vrf:

configure\n!\nmanagement api http-commands\n   protocol https port 443\n   no shutdown\n   vrf MGMT\n      no shutdown\n!\nend\n

Now the switch accepts on port 443 in the MGMT VRF HTTPS requests containing a list of CLI commands.

Run these EOS commands to verify:

show management http-server\nshow management api http-commands\n
"},{"location":"usage-inventory-catalog/","title":"Inventory & Tests catalog","text":""},{"location":"usage-inventory-catalog/#inventory-and-catalog","title":"Inventory and Catalog","text":"

The ANTA framework needs 2 important inputs from the user to run: a device inventory and a test catalog.

Both inputs can be defined in a file or programmatically.

"},{"location":"usage-inventory-catalog/#device-inventory","title":"Device Inventory","text":"

A device inventory is an instance of the AntaInventory class.

"},{"location":"usage-inventory-catalog/#device-inventory-file","title":"Device Inventory File","text":"

The ANTA device inventory can easily be defined as a YAML file. The file must comply with the following structure:

anta_inventory:\n  hosts:\n    - host: < ip address value >\n      port: < TCP port for eAPI. Default is 443 (Optional)>\n      name: < name to display in report. Default is host:port (Optional) >\n      tags: < list of tags to use to filter inventory during tests >\n      disable_cache: < Disable cache per hosts. Default is False. >\n  networks:\n    - network: < network using CIDR notation >\n      tags: < list of tags to use to filter inventory during tests >\n      disable_cache: < Disable cache per network. Default is False. >\n  ranges:\n    - start: < first ip address value of the range >\n      end: < last ip address value of the range >\n      tags: < list of tags to use to filter inventory during tests >\n      disable_cache: < Disable cache per range. Default is False. >\n

The inventory file must start with the anta_inventory key then define one or multiple methods:

  • hosts: define each device individually
  • networks: scan a network for devices accesible via eAPI
  • ranges: scan a range for devices accesible via eAPI

A full description of the inventory model is available in API documentation

Info

Caching can be disabled per device, network or range by setting the disable_cache key to True in the inventory file. For more details about how caching is implemented in ANTA, please refer to Caching in ANTA.

"},{"location":"usage-inventory-catalog/#example","title":"Example","text":"
---\nanta_inventory:\n  hosts:\n  - host: 192.168.0.10\n    name: spine01\n    tags: ['fabric', 'spine']\n  - host: 192.168.0.11\n    name: spine02\n    tags: ['fabric', 'spine']\n  networks:\n  - network: '192.168.110.0/24'\n    tags: ['fabric', 'leaf']\n  ranges:\n  - start: 10.0.0.9\n    end: 10.0.0.11\n    tags: ['fabric', 'l2leaf']\n
"},{"location":"usage-inventory-catalog/#test-catalog","title":"Test Catalog","text":"

A test catalog is an instance of the AntaCatalog class.

"},{"location":"usage-inventory-catalog/#test-catalog-file","title":"Test Catalog File","text":"

In addition to the inventory file, you also have to define a catalog of tests to execute against your devices. This catalog list all your tests, their inputs and their tags.

A valid test catalog file must have the following structure:

---\n<Python module>:\n    - <AntaTest subclass>:\n        <AntaTest.Input compliant dictionary>\n

"},{"location":"usage-inventory-catalog/#example_1","title":"Example","text":"
---\nanta.tests.connectivity:\n  - VerifyReachability:\n      hosts:\n        - source: Management0\n          destination: 1.1.1.1\n          vrf: MGMT\n        - source: Management0\n          destination: 8.8.8.8\n          vrf: MGMT\n      filters:\n        tags: ['leaf']\n      result_overwrite:\n        categories:\n          - \"Overwritten category 1\"\n        description: \"Test with overwritten description\"\n        custom_field: \"Test run by John Doe\"\n

It is also possible to nest Python module definition:

anta.tests:\n  connectivity:\n    - VerifyReachability:\n        hosts:\n          - source: Management0\n            destination: 1.1.1.1\n            vrf: MGMT\n          - source: Management0\n            destination: 8.8.8.8\n            vrf: MGMT\n        filters:\n          tags: ['leaf']\n        result_overwrite:\n          categories:\n            - \"Overwritten category 1\"\n          description: \"Test with overwritten description\"\n          custom_field: \"Test run by John Doe\"\n

This test catalog example is maintained with all the tests defined in the anta.tests Python module.

"},{"location":"usage-inventory-catalog/#test-tags","title":"Test tags","text":"

All tests can be defined with a list of user defined tags. These tags will be mapped with device tags: when at least one tag is defined for a test, this test will only be executed on devices with the same tag. If a test is defined in the catalog without any tags, the test will be executed on all devices.

anta.tests.system:\n  - VerifyUptime:\n      minimum: 10\n      filters:\n        tags: ['demo', 'leaf']\n  - VerifyReloadCause:\n  - VerifyCoredump:\n  - VerifyAgentLogs:\n  - VerifyCPUUtilization:\n      filters:\n        tags: ['leaf']\n

Info

When using the CLI, you can filter the NRFU execution using tags. Refer to this section of the CLI documentation.

"},{"location":"usage-inventory-catalog/#tests-available-in-anta","title":"Tests available in ANTA","text":"

All tests available as part of the ANTA framework are defined under the anta.tests Python module and are categorised per family (Python submodule). The complete list of the tests and their respective inputs is available at the tests section of this website.

To run test to verify the EOS software version, you can do:

anta.tests.software:\n  - VerifyEOSVersion:\n

It will load the test VerifyEOSVersion located in anta.tests.software. But since this test has mandatory inputs, we need to provide them as a dictionary in the YAML file:

anta.tests.software:\n  - VerifyEOSVersion:\n      # List of allowed EOS versions.\n      versions:\n        - 4.25.4M\n        - 4.26.1F\n

The following example is a very minimal test catalog:

---\n# Load anta.tests.software\nanta.tests.software:\n  # Verifies the device is running one of the allowed EOS version.\n  - VerifyEOSVersion:\n      # List of allowed EOS versions.\n      versions:\n        - 4.25.4M\n        - 4.26.1F\n\n# Load anta.tests.system\nanta.tests.system:\n  # Verifies the device uptime is higher than a value.\n  - VerifyUptime:\n      minimum: 1\n\n# Load anta.tests.configuration\nanta.tests.configuration:\n  # Verifies ZeroTouch is disabled.\n  - VerifyZeroTouch:\n  - VerifyRunningConfigDiffs:\n
"},{"location":"usage-inventory-catalog/#catalog-with-custom-tests","title":"Catalog with custom tests","text":"

In case you want to leverage your own tests collection, use your own Python package in the test catalog. So for instance, if my custom tests are defined in the titom73.tests.system Python module, the test catalog will be:

titom73.tests.system:\n  - VerifyPlatform:\n    type: ['cEOS-LAB']\n

How to create custom tests

To create your custom tests, you should refer to this documentation

"},{"location":"usage-inventory-catalog/#customize-test-description-and-categories","title":"Customize test description and categories","text":"

It might be interesting to use your own categories and customized test description to build a better report for your environment. ANTA comes with a handy feature to define your own categories and description in the report.

In your test catalog, use result_overwrite dictionary with categories and description to just overwrite this values in your report:

anta.tests.configuration:\n  - VerifyZeroTouch: # Verifies ZeroTouch is disabled.\n      result_overwrite:\n        categories: ['demo', 'pr296']\n        description: A custom test\n  - VerifyRunningConfigDiffs:\nanta.tests.interfaces:\n  - VerifyInterfaceUtilization:\n

Once you run anta nrfu table, you will see following output:

\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Device IP \u2503 Test Name                  \u2503 Test Status \u2503 Message(s) \u2503 Test description                              \u2503 Test category \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 spine01   \u2502 VerifyZeroTouch            \u2502 success     \u2502            \u2502 A custom test                                 \u2502 demo, pr296   \u2502\n\u2502 spine01   \u2502 VerifyRunningConfigDiffs   \u2502 success     \u2502            \u2502                                               \u2502 configuration \u2502\n\u2502 spine01   \u2502 VerifyInterfaceUtilization \u2502 success     \u2502            \u2502 Verifies interfaces utilization is below 75%. \u2502 interfaces    \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"advanced_usages/as-python-lib/","title":"ANTA as a Python Library","text":"

ANTA is a Python library that can be used in user applications. This section describes how you can leverage ANTA Python modules to help you create your own NRFU solution.

Tip

If you are unfamiliar with asyncio, refer to the Python documentation relevant to your Python version - https://docs.python.org/3/library/asyncio.html

"},{"location":"advanced_usages/as-python-lib/#antadevice-abstract-class","title":"AntaDevice Abstract Class","text":"

A device is represented in ANTA as a instance of a subclass of the AntaDevice abstract class. There are few abstract methods that needs to be implemented by child classes:

  • The collect() coroutine is in charge of collecting outputs of AntaCommand instances.
  • The refresh() coroutine is in charge of updating attributes of the AntaDevice instance. These attributes are used by AntaInventory to filter out unreachable devices or by AntaTest to skip devices based on their hardware models.

The copy() coroutine is used to copy files to and from the device. It does not need to be implemented if tests are not using it.

"},{"location":"advanced_usages/as-python-lib/#asynceosdevice-class","title":"AsyncEOSDevice Class","text":"

The AsyncEOSDevice class is an implementation of AntaDevice for Arista EOS. It uses the aio-eapi eAPI client and the AsyncSSH library.

  • The collect() coroutine collects AntaCommand outputs using eAPI.
  • The refresh() coroutine tries to open a TCP connection on the eAPI port and update the is_online attribute accordingly. If the TCP connection succeeds, it sends a show version command to gather the hardware model of the device and updates the established and hw_model attributes.
  • The copy() coroutine copies files to and from the device using the SCP protocol.
"},{"location":"advanced_usages/as-python-lib/#antainventory-class","title":"AntaInventory Class","text":"

The AntaInventory class is a subclass of the standard Python type dict. The keys of this dictionary are the device names, the values are AntaDevice instances.

AntaInventory provides methods to interact with the ANTA inventory:

  • The add_device() method adds an AntaDevice instance to the inventory. Adding an entry to AntaInventory with a key different from the device name is not allowed.
  • The get_inventory() returns a new AntaInventory instance with filtered out devices based on the method inputs.
  • The connect_inventory() coroutine will execute the refresh() coroutines of all the devices in the inventory.
  • The parse() static method creates an AntaInventory instance from a YAML file and returns it. The devices are AsyncEOSDevice instances.

To parse a YAML inventory file and print the devices connection status:

\"\"\"\nExample\n\"\"\"\nimport asyncio\n\nfrom anta.inventory import AntaInventory\n\n\nasync def main(inv: AntaInventory) -> None:\n    \"\"\"\n    Take an AntaInventory and:\n    1. try to connect to every device in the inventory\n    2. print a message for every device connection status\n    \"\"\"\n    await inv.connect_inventory()\n\n    for device in inv.values():\n        if device.established:\n            print(f\"Device {device.name} is online\")\n        else:\n            print(f\"Could not connect to device {device.name}\")\n\nif __name__ == \"__main__\":\n    # Create the AntaInventory instance\n    inventory = AntaInventory.parse(\n        filename=\"inv.yml\",\n        username=\"arista\",\n        password=\"@rista123\",\n        timeout=15,\n    )\n\n    # Run the main coroutine\n    res = asyncio.run(main(inventory))\n
How to create your inventory file

Please visit this dedicated section for how to use inventory and catalog files.

To run an EOS commands list on the reachable devices from the inventory:

\"\"\"\nExample\n\"\"\"\n# This is needed to run the script for python < 3.10 for typing annotations\nfrom __future__ import annotations\n\nimport asyncio\nfrom pprint import pprint\n\nfrom anta.inventory import AntaInventory\nfrom anta.models import AntaCommand\n\n\nasync def main(inv: AntaInventory, commands: list[str]) -> dict[str, list[AntaCommand]]:\n    \"\"\"\n    Take an AntaInventory and a list of commands as string and:\n    1. try to connect to every device in the inventory\n    2. collect the results of the commands from each device\n\n    Returns:\n      a dictionary where key is the device name and the value is the list of AntaCommand ran towards the device\n    \"\"\"\n    await inv.connect_inventory()\n\n    # Make a list of coroutine to run commands towards each connected device\n    coros = []\n    # dict to keep track of the commands per device\n    result_dict = {}\n    for name, device in inv.get_inventory(established_only=True).items():\n        anta_commands = [AntaCommand(command=command, ofmt=\"json\") for command in commands]\n        result_dict[name] = anta_commands\n        coros.append(device.collect_commands(anta_commands))\n\n    # Run the coroutines\n    await asyncio.gather(*coros)\n\n    return result_dict\n\n\nif __name__ == \"__main__\":\n    # Create the AntaInventory instance\n    inventory = AntaInventory.parse(\n        filename=\"inv.yml\",\n        username=\"arista\",\n        password=\"@rista123\",\n        timeout=15,\n    )\n\n    # Create a list of commands with json output\n    commands = [\"show version\", \"show ip bgp summary\"]\n\n    # Run the main asyncio  entry point\n    res = asyncio.run(main(inventory, commands))\n\n    pprint(res)\n

"},{"location":"advanced_usages/as-python-lib/#use-tests-from-anta","title":"Use tests from ANTA","text":"

All the test classes inherit from the same abstract Base Class AntaTest. The Class definition indicates which commands are required for the test and the user should focus only on writing the test function with optional keywords argument. The instance of the class upon creation instantiates a TestResult object that can be accessed later on to check the status of the test ([unset, skipped, success, failure, error]).

"},{"location":"advanced_usages/as-python-lib/#test-structure","title":"Test structure","text":"

All tests are built on a class named AntaTest which provides a complete toolset for a test:

  • Object creation
  • Test definition
  • TestResult definition
  • Abstracted method to collect data

This approach means each time you create a test it will be based on this AntaTest class. Besides that, you will have to provide some elements:

  • name: Name of the test
  • description: A human readable description of your test
  • categories: a list of categories to sort test.
  • commands: a list of command to run. This list must be a list of AntaCommand which is described in the next part of this document.

Here is an example of a hardware test related to device temperature:

from __future__ import annotations\n\nimport logging\nfrom typing import Any, Dict, List, Optional, cast\n\nfrom anta.models import AntaTest, AntaCommand\n\n\nclass VerifyTemperature(AntaTest):\n    \"\"\"\n    Verifies device temparture is currently OK.\n    \"\"\"\n\n    # The test name\n    name = \"VerifyTemperature\"\n    # A small description of the test, usually the first line of the class docstring\n    description = \"Verifies device temparture is currently OK\"\n    # The category of the test, usually the module name\n    categories = [\"hardware\"]\n    # The command(s) used for the test. Could be a template instead\n    commands = [AntaCommand(command=\"show system environment temperature\", ofmt=\"json\")]\n\n    # Decorator\n    @AntaTest.anta_test\n    # abstract method that must be defined by the child Test class\n    def test(self) -> None:\n        \"\"\"Run VerifyTemperature validation\"\"\"\n        command_output = cast(Dict[str, Dict[Any, Any]], self.instance_commands[0].output)\n        temperature_status = command_output[\"systemStatus\"] if \"systemStatus\" in command_output.keys() else \"\"\n        if temperature_status == \"temperatureOk\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device temperature is not OK, systemStatus: {temperature_status }\")\n

When you run the test, object will automatically call its anta.models.AntaTest.collect() method to get device output for each command if no pre-collected data was given to the test. This method does a loop to call anta.inventory.models.InventoryDevice.collect() methods which is in charge of managing device connection and how to get data.

run test offline

You can also pass eos data directly to your test if you want to validate data collected in a different workflow. An example is provided below just for information:

test = VerifyTemperature(device, eos_data=test_data[\"eos_data\"])\nasyncio.run(test.test())\n

The test function is always the same and must be defined with the @AntaTest.anta_test decorator. This function takes at least one argument which is a anta.inventory.models.InventoryDevice object. In some cases a test would rely on some additional inputs from the user, for instance the number of expected peers or some expected numbers. All parameters must come with a default value and the test function should validate the parameters values (at this stage this is the only place where validation can be done but there are future plans to make this better).

class VerifyTemperature(AntaTest):\n    ...\n    @AntaTest.anta_test\n    def test(self) -> None:\n        pass\n\nclass VerifyTransceiversManufacturers(AntaTest):\n    ...\n    @AntaTest.anta_test\n    def test(self, manufacturers: Optional[List[str]] = None) -> None:\n        # validate the manufactures parameter\n        pass\n

The test itself does not return any value, but the result is directly availble from your AntaTest object and exposes a anta.result_manager.models.TestResult object with result, name of the test and optional messages:

  • name (str): Device name where the test has run.
  • test (str): Test name runs on the device.
  • categories (List[str]): List of categories the TestResult belongs to, by default the AntaTest categories.
  • description (str): TestResult description, by default the AntaTest description.
  • results (str): Result of the test. Can be one of [\u201cunset\u201d, \u201csuccess\u201d, \u201cfailure\u201d, \u201cerror\u201d, \u201cskipped\u201d].
  • message (str, optional): Message to report after the test if any.
  • custom_field (str, optional): Custom field to store a string for flexibility in integrating with ANTA
from anta.tests.hardware import VerifyTemperature\n\ntest = VerifyTemperature(device, eos_data=test_data[\"eos_data\"])\nasyncio.run(test.test())\nassert test.result.result == \"success\"\n
"},{"location":"advanced_usages/as-python-lib/#classes-for-commands","title":"Classes for commands","text":"

To make it easier to get data, ANTA defines 2 different classes to manage commands to send to devices:

"},{"location":"advanced_usages/as-python-lib/#antacommand-class","title":"AntaCommand Class","text":"

Represent a command with following information:

  • Command to run
  • Ouput format expected
  • eAPI version
  • Output of the command

Usage example:

from anta.models import AntaCommand\n\ncmd1 = AntaCommand(command=\"show zerotouch\")\ncmd2 = AntaCommand(command=\"show running-config diffs\", ofmt=\"text\")\n

Command revision and version

  • Most of EOS commands return a JSON structure according to a model (some commands may not be modeled hence the necessity to use text outformat sometimes.
  • The model can change across time (adding feature, \u2026 ) and when the model is changed in a non backward-compatible way, the revision number is bumped. The initial model starts with revision 1.
  • A revision applies to a particular CLI command whereas a version is global to an eAPI call. The version is internally translated to a specific revision for each CLI command in the RPC call. The currently supported version vaues are 1 and latest.
  • A revision takes precedence over a version (e.g. if a command is run with version=\u201dlatest\u201d and revision=1, the first revision of the model is returned)
  • By default eAPI returns the first revision of each model to ensure that when upgrading, intergation with existing tools is not broken. This is done by using by default version=1 in eAPI calls.

ANTA uses by default version=\"latest\" in AntaCommand. For some commands, you may want to run them with a different revision or version.

For instance the VerifyRoutingTableSize test leverages the first revision of show bfd peers:

# revision 1 as later revision introduce additional nesting for type\ncommands = [AntaCommand(command=\"show bfd peers\", revision=1)]\n
"},{"location":"advanced_usages/as-python-lib/#antatemplate-class","title":"AntaTemplate Class","text":"

Because some command can require more dynamic than just a command with no parameter provided by user, ANTA supports command template: you define a template in your test class and user provide parameters when creating test object.

class RunArbitraryTemplateCommand(AntaTest):\n    \"\"\"\n    Run an EOS command and return result\n    Based on AntaTest to build relevant output for pytest\n    \"\"\"\n\n    name = \"Run aributrary EOS command\"\n    description = \"To be used only with anta debug commands\"\n    template = AntaTemplate(template=\"show interfaces {ifd}\")\n    categories = [\"debug\"]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        errdisabled_interfaces = [interface for interface, value in response[\"interfaceStatuses\"].items() if value[\"linkStatus\"] == \"errdisabled\"]\n        ...\n\n\nparams = [{\"ifd\": \"Ethernet2\"}, {\"ifd\": \"Ethernet49/1\"}]\nrun_command1 = RunArbitraryTemplateCommand(device_anta, params)\n

In this example, test waits for interfaces to check from user setup and will only check for interfaces in params

"},{"location":"advanced_usages/caching/","title":"Caching in ANTA","text":"

ANTA is a streamlined Python framework designed for efficient interaction with network devices. This section outlines how ANTA incorporates caching mechanisms to collect command outputs from network devices.

"},{"location":"advanced_usages/caching/#configuration","title":"Configuration","text":"

By default, ANTA utilizes aiocache\u2019s memory cache backend, also called SimpleMemoryCache. This library aims for simplicity and supports asynchronous operations to go along with Python asyncio used in ANTA.

The _init_cache() method of the AntaDevice abstract class initializes the cache. Child classes can override this method to tweak the cache configuration:

def _init_cache(self) -> None:\n    \"\"\"\n    Initialize cache for the device, can be overridden by subclasses to manipulate how it works\n    \"\"\"\n    self.cache = Cache(cache_class=Cache.MEMORY, ttl=60, namespace=self.name, plugins=[HitMissRatioPlugin()])\n    self.cache_locks = defaultdict(asyncio.Lock)\n

The cache is also configured with aiocache\u2019s HitMissRatioPlugin plugin to calculate the ratio of hits the cache has and give useful statistics for logging purposes in ANTA.

"},{"location":"advanced_usages/caching/#cache-key-design","title":"Cache key design","text":"

The cache is initialized per AntaDevice and uses the following cache key design:

<device_name>:<uid>

The uid is an attribute of AntaCommand, which is a unique identifier generated from the command, version, revision and output format.

Each UID has its own asyncio lock. This design allows coroutines that need to access the cache for different UIDs to do so concurrently. The locks are managed by the self.cache_locks dictionary.

"},{"location":"advanced_usages/caching/#mechanisms","title":"Mechanisms","text":"

By default, once the cache is initialized, it is used in the collect() method of AntaDevice. The collect() method prioritizes retrieving the output of the command from the cache. If the output is not in the cache, the private _collect() method will retrieve and then store it for future access.

"},{"location":"advanced_usages/caching/#how-to-disable-caching","title":"How to disable caching","text":"

Caching is enabled by default in ANTA following the previous configuration and mechanisms.

There might be scenarios where caching is not wanted. You can disable caching in multiple ways in ANTA:

  1. Caching can be disabled globally, for ALL commands on ALL devices, using the --disable-cache global flag when invoking anta at the CLI:
    anta --disable-cache --username arista --password arista nrfu table\n
  2. Caching can be disabled per device, network or range by setting the disable_cache key to True when definining the ANTA Inventory file:

    anta_inventory:\n  hosts:\n  - host: 172.20.20.101\n    name: DC1-SPINE1\n    tags: [\"SPINE\", \"DC1\"]\n    disable_cache: True  # Set this key to True\n  - host: 172.20.20.102\n    name: DC1-SPINE2\n    tags: [\"SPINE\", \"DC1\"]\n    disable_cache: False # Optional since it's the default\n\n  networks:\n  - network: \"172.21.21.0/24\"\n    disable_cache: True\n\n  ranges:\n  - start: 172.22.22.10\n    end: 172.22.22.19\n    disable_cache: True\n
    This approach effectively disables caching for ALL commands sent to devices targeted by the disable_cache key.

  3. For tests developpers, caching can be disabled for a specific AntaCommand or AntaTemplate by setting the use_cache attribute to False. That means the command output will always be collected on the device and therefore, never use caching.

"},{"location":"advanced_usages/caching/#disable-caching-in-a-child-class-of-antadevice","title":"Disable caching in a child class of AntaDevice","text":"

Since caching is implemented at the AntaDevice abstract class level, all subclasses will inherit that default behavior. As a result, if you need to disable caching in any custom implementation of AntaDevice outside of the ANTA framework, you must initialize AntaDevice with disable_cache set to True:

class AnsibleEOSDevice(AntaDevice):\n  \"\"\"\n  Implementation of an AntaDevice using Ansible HttpApi plugin for EOS.\n  \"\"\"\n  def __init__(self, name: str, connection: ConnectionBase, tags: list = None) -> None:\n      super().__init__(name, tags, disable_cache=True)\n
"},{"location":"advanced_usages/custom-tests/","title":"Developing ANTA tests","text":"

This documentation applies for both creating tests in ANTA or creating your own test package.

ANTA is not only a Python library with a CLI and a collection of built-in tests, it is also a framework you can extend by building your own tests.

"},{"location":"advanced_usages/custom-tests/#generic-approach","title":"Generic approach","text":"

A test is a Python class where a test function is defined and will be run by the framework.

ANTA provides an abstract class AntaTest. This class does the heavy lifting and provide the logic to define, collect and test data. The code below is an example of a simple test in ANTA, which is an AntaTest subclass:

from anta.models import AntaTest, AntaCommand\nfrom anta.decorators import skip_on_platforms\n\n\nclass VerifyTemperature(AntaTest):\n    \"\"\"Verifies if the device temperature is within acceptable limits.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device temperature is currently OK: 'temperatureOk'.\n    * Failure: The test will fail if the device temperature is NOT OK.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyTemperature:\n    ```\n    \"\"\"\n\n    name = \"VerifyTemperature\"\n    description = \"Verifies the device temperature.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTemperature.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        temperature_status = command_output.get(\"systemStatus\", \"\")\n        if temperature_status == \"temperatureOk\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'\")\n

AntaTest also provide more advanced capabilities like AntaCommand templating using the AntaTemplate class or test inputs definition and validation using AntaTest.Input pydantic model. This will be discussed in the sections below.

"},{"location":"advanced_usages/custom-tests/#antatest-structure","title":"AntaTest structure","text":""},{"location":"advanced_usages/custom-tests/#class-attributes","title":"Class Attributes","text":"
  • name (str): Name of the test. Used during reporting.
  • description (str): A human readable description of your test.
  • categories (list[str]): A list of categories in which the test belongs.
  • commands ([list[AntaCommand | AntaTemplate]]): A list of command to collect from devices. This list must be a list of AntaCommand or AntaTemplate instances. Rendering AntaTemplate instances will be discussed later.

Info

All these class attributes are mandatory. If any attribute is missing, a NotImplementedError exception will be raised during class instantiation.

"},{"location":"advanced_usages/custom-tests/#instance-attributes","title":"Instance Attributes","text":"

Info

You can access an instance attribute in your code using the self reference. E.g. you can access the test input values using self.inputs.

Logger object

ANTA already provides comprehensive logging at every steps of a test execution. The AntaTest class also provides a logger attribute that is a Python logger specific to the test instance. See Python documentation for more information.

AntaDevice object

Even if device is not a private attribute, you should not need to access this object in your code.

"},{"location":"advanced_usages/custom-tests/#test-inputs","title":"Test Inputs","text":"

AntaTest.Input is a pydantic model that allow test developers to define their test inputs. pydantic provides out of the box error handling for test input validation based on the type hints defined by the test developer.

The base definition of AntaTest.Input provides common test inputs for all AntaTest instances:

"},{"location":"advanced_usages/custom-tests/#input-model","title":"Input model","text":""},{"location":"advanced_usages/custom-tests/#resultoverwrite-model","title":"ResultOverwrite model","text":"

Attributes:

Name Type Description description overwrite TestResult.description

categories: overwrite TestResult.categories custom_field: a free string that will be included in the TestResult object

Note

The pydantic model is configured using the extra=forbid that will fail input validation if extra fields are provided.

"},{"location":"advanced_usages/custom-tests/#methods","title":"Methods","text":"
  • test(self) -> None: This is an abstract method that must be implemented. It contains the test logic that can access the collected command outputs using the instance_commands instance attribute, access the test inputs using the inputs instance attribute and must set the result instance attribute accordingly. It must be implemented using the AntaTest.anta_test decorator that provides logging and will collect commands before executing the test() method.
  • render(self, template: AntaTemplate) -> list[AntaCommand]: This method only needs to be implemented if AntaTemplate instances are present in the commands class attribute. It will be called for every AntaTemplate occurence and must return a list of AntaCommand using the AntaTemplate.render() method. It can access test inputs using the inputs instance attribute.
"},{"location":"advanced_usages/custom-tests/#test-execution","title":"Test execution","text":"

Below is a high level description of the test execution flow in ANTA:

  1. ANTA will parse the test catalog to get the list of AntaTest subclasses to instantiate and their associated input values. We consider a single AntaTest subclass in the following steps.

  2. ANTA will instantiate the AntaTest subclass and a single device will be provided to the test instance. The Input model defined in the class will also be instantiated at this moment. If any ValidationError is raised, the test execution will be stopped.

  3. If there is any AntaTemplate instance in the commands class attribute, render() will be called for every occurrence. At this moment, the instance_commands attribute has been initialized. If any rendering error occurs, the test execution will be stopped.

  4. The AntaTest.anta_test decorator will collect the commands from the device and update the instance_commands attribute with the outputs. If any collection error occurs, the test execution will be stopped.

  5. The test() method is executed.

"},{"location":"advanced_usages/custom-tests/#writing-an-antatest-subclass","title":"Writing an AntaTest subclass","text":"

In this section, we will go into all the details of writing an AntaTest subclass.

"},{"location":"advanced_usages/custom-tests/#class-definition","title":"Class definition","text":"

Import anta.models.AntaTest and define your own class. Define the mandatory class attributes using anta.models.AntaCommand, anta.models.AntaTemplate or both.

Info

Caching can be disabled per AntaCommand or AntaTemplate by setting the use_cache argument to False. For more details about how caching is implemented in ANTA, please refer to Caching in ANTA.

from anta.models import AntaTest, AntaCommand, AntaTemplate\n\n\nclass <YourTestName>(AntaTest):\n    \"\"\"\n    <a docstring description of your test>\n    \"\"\"\n\n    name = \"YourTestName\"                                           # should be your class name\n    description = \"<test description in human reading format>\"\n    categories = [\"<arbitrary category>\", \"<another arbitrary category>\"]\n    commands = [\n        AntaCommand(\n            command=\"<EOS command to run>\",\n            ofmt=\"<command format output>\",\n            version=\"<eAPI version to use>\",\n            revision=\"<revision to use for the command>\",           # revision has precedence over version\n            use_cache=\"<Use cache for the command>\",\n        ),\n        AntaTemplate(\n            template=\"<Python f-string to render an EOS command>\",\n            ofmt=\"<command format output>\",\n            version=\"<eAPI version to use>\",\n            revision=\"<revision to use for the command>\",           # revision has precedence over version\n            use_cache=\"<Use cache for the command>\",\n        )\n    ]\n
"},{"location":"advanced_usages/custom-tests/#inputs-definition","title":"Inputs definition","text":"

If the user needs to provide inputs for your test, you need to define a pydantic model that defines the schema of the test inputs:

class <YourTestName>(AntaTest):\n    \"\"\"Verifies ...\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if ...\n    * Failure: The test will fail if ...\n\n    Examples\n    --------\n    ```yaml\n    your.module.path:\n      - YourTestName:\n        field_name: example_field_value\n    ```\n    \"\"\"\n    ...\n    class Input(AntaTest.Input):\n        \"\"\"Inputs for my awesome test.\"\"\"\n        <input field name>: <input field type>\n        \"\"\"<input field docstring>\"\"\"\n

To define an input field type, refer to the pydantic documentation about types. You can also leverage anta.custom_types that provides reusable types defined in ANTA tests.

Regarding required, optional and nullable fields, refer to this documentation on how to define them.

Note

All the pydantic features are supported. For instance you can define validators for complex input validation.

"},{"location":"advanced_usages/custom-tests/#template-rendering","title":"Template rendering","text":"

Define the render() method if you have AntaTemplate instances in your commands class attribute:

class <YourTestName>(AntaTest):\n    ...\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        return [template.render(<template param>=input_value) for input_value in self.inputs.<input_field>]\n

You can access test inputs and render as many AntaCommand as desired.

"},{"location":"advanced_usages/custom-tests/#test-definition","title":"Test definition","text":"

Implement the test() method with your test logic:

class <YourTestName>(AntaTest):\n    ...\n    @AntaTest.anta_test\n    def test(self) -> None:\n        pass\n

The logic usually includes the following different stages: 1. Parse the command outputs using the self.instance_commands instance attribute. 2. If needed, access the test inputs using the self.inputs instance attribute and write your conditional logic. 3. Set the result instance attribute to reflect the test result by either calling self.result.is_success() or self.result.is_failure(\"<FAILURE REASON>\"). Sometimes, setting the test result to skipped using self.result.is_skipped(\"<SKIPPED REASON>\") can make sense (e.g. testing the OSPF neighbor states but no neighbor was found). However, you should not need to catch any exception and set the test result to error since the error handling is done by the framework, see below.

The example below is based on the VerifyTemperature test.

class VerifyTemperature(AntaTest):\n    ...\n    @AntaTest.anta_test\n    def test(self) -> None:\n        # Grab output of the collected command\n        command_output = self.instance_commands[0].json_output\n\n        # Do your test: In this example we check a specific field of the JSON output from EOS\n        temperature_status = command_output[\"systemStatus\"] if \"systemStatus\" in command_output.keys() else \"\"\n        if temperature_status == \"temperatureOk\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'\")\n

As you can see there is no error handling to do in your code. Everything is packaged in the AntaTest.anta_tests decorator and below is a simple example of error captured when trying to access a dictionary with an incorrect key:

class VerifyTemperature(AntaTest):\n    ...\n    @AntaTest.anta_test\n    def test(self) -> None:\n        # Grab output of the collected command\n        command_output = self.instance_commands[0].json_output\n\n        # Access the dictionary with an incorrect key\n        command_output['incorrectKey']\n
ERROR    Exception raised for test VerifyTemperature (on device 192.168.0.10) - KeyError ('incorrectKey')\n

Get stack trace for debugging

If you want to access to the full exception stack, you can run ANTA in debug mode by setting the ANTA_DEBUG environment variable to true. Example:

$ ANTA_DEBUG=true anta nrfu --catalog test_custom.yml text\n

"},{"location":"advanced_usages/custom-tests/#test-decorators","title":"Test decorators","text":"

In addition to the required AntaTest.anta_tests decorator, ANTA offers a set of optional decorators for further test customization:

  • anta.decorators.deprecated_test: Use this to log a message of WARNING severity when a test is deprecated.
  • anta.decorators.skip_on_platforms: Use this to skip tests for functionalities that are not supported on specific platforms.
from anta.decorators import skip_on_platforms\n\nclass VerifyTemperature(AntaTest):\n    ...\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        pass\n
"},{"location":"advanced_usages/custom-tests/#access-your-custom-tests-in-the-test-catalog","title":"Access your custom tests in the test catalog","text":"

This section is required only if you are not merging your development into ANTA. Otherwise, just follow contribution guide.

For that, you need to create your own Python package as described in this hitchhiker\u2019s guide to package Python code. We assume it is well known and we won\u2019t focus on this aspect. Thus, your package must be impartable by ANTA hence available in the module search path sys.path (you can use PYTHONPATH for example).

It is very similar to what is documented in catalog section but you have to use your own package name.2

Let say the custom Python package is anta_titom73 and the test is defined in anta_titom73.dc_project Python module, the test catalog would look like:

anta_titom73.dc_project:\n  - VerifyFeatureX:\n      minimum: 1\n
And now you can run your NRFU tests with the CLI:

anta nrfu text --catalog test_custom.yml\nspine01 :: verify_dynamic_vlan :: FAILURE (Device has 0 configured, we expect at least 1)\nspine02 :: verify_dynamic_vlan :: FAILURE (Device has 0 configured, we expect at least 1)\nleaf01 :: verify_dynamic_vlan :: SUCCESS\nleaf02 :: verify_dynamic_vlan :: SUCCESS\nleaf03 :: verify_dynamic_vlan :: SUCCESS\nleaf04 :: verify_dynamic_vlan :: SUCCESS\n
"},{"location":"api/catalog/","title":"Test Catalog","text":""},{"location":"api/catalog/#anta.catalog.AntaCatalog","title":"AntaCatalog","text":"
AntaCatalog(tests: list[AntaTestDefinition] | None = None, filename: str | Path | None = None)\n

Class representing an ANTA Catalog.

It can be instantiated using its contructor or one of the static methods: parse(), from_list() or from_dict()

Args:
tests: A list of AntaTestDefinition instances.\nfilename: The path from which the catalog is loaded.\n
Source code in anta/catalog.py
def __init__(\n    self,\n    tests: list[AntaTestDefinition] | None = None,\n    filename: str | Path | None = None,\n) -> None:\n    \"\"\"Instantiate an AntaCatalog instance.\n\n    Args:\n    ----\n        tests: A list of AntaTestDefinition instances.\n        filename: The path from which the catalog is loaded.\n\n    \"\"\"\n    self._tests: list[AntaTestDefinition] = []\n    if tests is not None:\n        self._tests = tests\n    self._filename: Path | None = None\n    if filename is not None:\n        if isinstance(filename, Path):\n            self._filename = filename\n        else:\n            self._filename = Path(filename)\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalog.filename","title":"filename property","text":"
filename: Path | None\n

Path of the file used to create this AntaCatalog instance.

"},{"location":"api/catalog/#anta.catalog.AntaCatalog.tests","title":"tests property writable","text":"
tests: list[AntaTestDefinition]\n

List of AntaTestDefinition in this catalog.

"},{"location":"api/catalog/#anta.catalog.AntaCatalog.from_dict","title":"from_dict staticmethod","text":"
from_dict(data: RawCatalogInput, filename: str | Path | None = None) -> AntaCatalog\n

Create an AntaCatalog instance from a dictionary data structure.

See RawCatalogInput type alias for details. It is the data structure returned by yaml.load() function of a valid YAML Test Catalog file.

Args:
data: Python dictionary used to instantiate the AntaCatalog instance\nfilename: value to be set as AntaCatalog instance attribute\n
Source code in anta/catalog.py
@staticmethod\ndef from_dict(data: RawCatalogInput, filename: str | Path | None = None) -> AntaCatalog:\n    \"\"\"Create an AntaCatalog instance from a dictionary data structure.\n\n    See RawCatalogInput type alias for details.\n    It is the data structure returned by `yaml.load()` function of a valid\n    YAML Test Catalog file.\n\n    Args:\n    ----\n        data: Python dictionary used to instantiate the AntaCatalog instance\n        filename: value to be set as AntaCatalog instance attribute\n\n    \"\"\"\n    tests: list[AntaTestDefinition] = []\n    if data is None:\n        logger.warning(\"Catalog input data is empty\")\n        return AntaCatalog(filename=filename)\n\n    if not isinstance(data, dict):\n        msg = f\"Wrong input type for catalog data{f' (from {filename})' if filename is not None else ''}, must be a dict, got {type(data).__name__}\"\n        raise TypeError(msg)\n\n    try:\n        catalog_data = AntaCatalogFile(**data)  # type: ignore[arg-type]\n    except ValidationError as e:\n        anta_log_exception(\n            e,\n            f\"Test catalog is invalid!{f' (from {filename})' if filename is not None else ''}\",\n            logger,\n        )\n        raise\n    for t in catalog_data.root.values():\n        tests.extend(t)\n    return AntaCatalog(tests, filename=filename)\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalog.from_list","title":"from_list staticmethod","text":"
from_list(data: ListAntaTestTuples) -> AntaCatalog\n

Create an AntaCatalog instance from a list data structure.

See ListAntaTestTuples type alias for details.

Args:
data: Python list used to instantiate the AntaCatalog instance\n
Source code in anta/catalog.py
@staticmethod\ndef from_list(data: ListAntaTestTuples) -> AntaCatalog:\n    \"\"\"Create an AntaCatalog instance from a list data structure.\n\n    See ListAntaTestTuples type alias for details.\n\n    Args:\n    ----\n        data: Python list used to instantiate the AntaCatalog instance\n\n    \"\"\"\n    tests: list[AntaTestDefinition] = []\n    try:\n        tests.extend(AntaTestDefinition(test=test, inputs=inputs) for test, inputs in data)\n    except ValidationError as e:\n        anta_log_exception(e, \"Test catalog is invalid!\", logger)\n        raise\n    return AntaCatalog(tests)\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalog.get_tests_by_tags","title":"get_tests_by_tags","text":"
get_tests_by_tags(tags: list[str], *, strict: bool = False) -> list[AntaTestDefinition]\n

Return all the tests that have matching tags in their input filters.

If strict=True, returns only tests that match all the tags provided as input. If strict=False, return all the tests that match at least one tag provided as input.

Source code in anta/catalog.py
def get_tests_by_tags(self, tags: list[str], *, strict: bool = False) -> list[AntaTestDefinition]:\n    \"\"\"Return all the tests that have matching tags in their input filters.\n\n    If strict=True, returns only tests that match all the tags provided as input.\n    If strict=False, return all the tests that match at least one tag provided as input.\n    \"\"\"\n    result: list[AntaTestDefinition] = []\n    for test in self.tests:\n        if test.inputs.filters and (f := test.inputs.filters.tags):\n            if strict:\n                if all(t in tags for t in f):\n                    result.append(test)\n            elif any(t in tags for t in f):\n                result.append(test)\n    return result\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalog.parse","title":"parse staticmethod","text":"
parse(filename: str | Path) -> AntaCatalog\n

Create an AntaCatalog instance from a test catalog file.

Args:
filename: Path to test catalog YAML file\n
Source code in anta/catalog.py
@staticmethod\ndef parse(filename: str | Path) -> AntaCatalog:\n    \"\"\"Create an AntaCatalog instance from a test catalog file.\n\n    Args:\n    ----\n        filename: Path to test catalog YAML file\n\n    \"\"\"\n    try:\n        file: Path = filename if isinstance(filename, Path) else Path(filename)\n        with file.open(encoding=\"UTF-8\") as f:\n            data = safe_load(f)\n    except (TypeError, YAMLError, OSError) as e:\n        message = f\"Unable to parse ANTA Test Catalog file '{filename}'\"\n        anta_log_exception(e, message, logger)\n        raise\n\n    return AntaCatalog.from_dict(data, filename=filename)\n
"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition","title":"AntaTestDefinition","text":"
AntaTestDefinition(**data: type[AntaTest] | AntaTest.Input | dict[str, Any] | None)\n

Bases: BaseModel

Define a test with its associated inputs.

test: An AntaTest concrete subclass inputs: The associated AntaTest.Input subclass instance

https://docs.pydantic.dev/2.0/usage/validators/#using-validation-context-with-basemodel-initialization.

Source code in anta/catalog.py
def __init__(self, **data: type[AntaTest] | AntaTest.Input | dict[str, Any] | None) -> None:\n    \"\"\"Inject test in the context to allow to instantiate Input in the BeforeValidator.\n\n    https://docs.pydantic.dev/2.0/usage/validators/#using-validation-context-with-basemodel-initialization.\n    \"\"\"\n    self.__pydantic_validator__.validate_python(\n        data,\n        self_instance=self,\n        context={\"test\": data[\"test\"]},\n    )\n    super(BaseModel, self).__init__()\n
"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.check_inputs","title":"check_inputs","text":"
check_inputs() -> AntaTestDefinition\n

Check the inputs field typing.

The inputs class attribute needs to be an instance of the AntaTest.Input subclass defined in the class test.

Source code in anta/catalog.py
@model_validator(mode=\"after\")\ndef check_inputs(self) -> AntaTestDefinition:\n    \"\"\"Check the `inputs` field typing.\n\n    The `inputs` class attribute needs to be an instance of the AntaTest.Input subclass defined in the class `test`.\n    \"\"\"\n    if not isinstance(self.inputs, self.test.Input):\n        msg = f\"Test input has type {self.inputs.__class__.__qualname__} but expected type {self.test.Input.__qualname__}\"\n        raise ValueError(msg)  # noqa: TRY004 pydantic catches ValueError or AssertionError, no TypeError\n    return self\n
"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.instantiate_inputs","title":"instantiate_inputs classmethod","text":"
instantiate_inputs(data: AntaTest.Input | dict[str, Any] | None, info: ValidationInfo) -> AntaTest.Input\n

Ensure the test inputs can be instantiated and thus are valid.

If the test has no inputs, allow the user to omit providing the inputs field. If the test has inputs, allow the user to provide a valid dictionary of the input fields. This model validator will instantiate an Input class from the test class field.

Source code in anta/catalog.py
@field_validator(\"inputs\", mode=\"before\")\n@classmethod\ndef instantiate_inputs(\n    cls: type[AntaTestDefinition],\n    data: AntaTest.Input | dict[str, Any] | None,\n    info: ValidationInfo,\n) -> AntaTest.Input:\n    \"\"\"Ensure the test inputs can be instantiated and thus are valid.\n\n    If the test has no inputs, allow the user to omit providing the `inputs` field.\n    If the test has inputs, allow the user to provide a valid dictionary of the input fields.\n    This model validator will instantiate an Input class from the `test` class field.\n    \"\"\"\n    if info.context is None:\n        msg = \"Could not validate inputs as no test class could be identified\"\n        raise ValueError(msg)\n    # Pydantic guarantees at this stage that test_class is a subclass of AntaTest because of the ordering\n    # of fields in the class definition - so no need to check for this\n    test_class = info.context[\"test\"]\n    if not (isclass(test_class) and issubclass(test_class, AntaTest)):\n        msg = f\"Could not validate inputs as no test class {test_class} is not a subclass of AntaTest\"\n        raise ValueError(msg)\n\n    if isinstance(data, AntaTest.Input):\n        return data\n    try:\n        if data is None:\n            return test_class.Input()\n        if isinstance(data, dict):\n            return test_class.Input(**data)\n    except ValidationError as e:\n        inputs_msg = str(e).replace(\"\\n\", \"\\n\\t\")\n        err_type = \"wrong_test_inputs\"\n        raise PydanticCustomError(\n            err_type,\n            f\"{test_class.name} test inputs are not valid: {inputs_msg}\\n\",\n            {\"errors\": e.errors()},\n        ) from e\n    msg = f\"Coud not instantiate inputs as type {type(data).__name__} is not valid\"\n    raise ValueError(msg)\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile","title":"AntaCatalogFile","text":"

Bases: RootModel[Dict[ImportString[Any], List[AntaTestDefinition]]]

Represents an ANTA Test Catalog File.

Example:
A valid test catalog file must have the following structure:\n```\n<Python module>:\n    - <AntaTest subclass>:\n        <AntaTest.Input compliant dictionary>\n```\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.check_tests","title":"check_tests classmethod","text":"
check_tests(data: Any) -> Any\n

Allow the user to provide a Python data structure that only has string values.

This validator will try to flatten and import Python modules, check if the tests classes are actually defined in their respective Python module and instantiate Input instances with provided value to validate test inputs.

Source code in anta/catalog.py
@model_validator(mode=\"before\")\n@classmethod\ndef check_tests(cls: type[AntaCatalogFile], data: Any) -> Any:  # noqa: ANN401\n    \"\"\"Allow the user to provide a Python data structure that only has string values.\n\n    This validator will try to flatten and import Python modules, check if the tests classes\n    are actually defined in their respective Python module and instantiate Input instances\n    with provided value to validate test inputs.\n    \"\"\"\n    if isinstance(data, dict):\n        typed_data: dict[ModuleType, list[Any]] = AntaCatalogFile.flatten_modules(data)\n        for module, tests in typed_data.items():\n            test_definitions: list[AntaTestDefinition] = []\n            for test_definition in tests:\n                if not isinstance(test_definition, dict):\n                    msg = f\"Syntax error when parsing: {test_definition}\\nIt must be a dictionary. Check the test catalog.\"\n                    raise ValueError(msg)  # noqa: TRY004 pydantic catches ValueError or AssertionError, no TypeError\n                if len(test_definition) != 1:\n                    msg = (\n                        f\"Syntax error when parsing: {test_definition}\\n\"\n                        \"It must be a dictionary with a single entry. Check the indentation in the test catalog.\"\n                    )\n                    raise ValueError(msg)\n                for test_name, test_inputs in test_definition.copy().items():\n                    test: type[AntaTest] | None = getattr(module, test_name, None)\n                    if test is None:\n                        msg = (\n                            f\"{test_name} is not defined in Python module {module.__name__}\"\n                            f\"{f' (from {module.__file__})' if module.__file__ is not None else ''}\"\n                        )\n                        raise ValueError(msg)\n                    test_definitions.append(AntaTestDefinition(test=test, inputs=test_inputs))\n            typed_data[module] = test_definitions\n    return typed_data\n
"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.flatten_modules","title":"flatten_modules staticmethod","text":"
flatten_modules(data: dict[str, Any], package: str | None = None) -> dict[ModuleType, list[Any]]\n

Allow the user to provide a data structure with nested Python modules.

Example:
```\nanta.tests.routing:\n  generic:\n    - <AntaTestDefinition>\n  bgp:\n    - <AntaTestDefinition>\n```\n`anta.tests.routing.generic` and `anta.tests.routing.bgp` are importable Python modules.\n
Source code in anta/catalog.py
@staticmethod\ndef flatten_modules(data: dict[str, Any], package: str | None = None) -> dict[ModuleType, list[Any]]:\n    \"\"\"Allow the user to provide a data structure with nested Python modules.\n\n    Example:\n    -------\n        ```\n        anta.tests.routing:\n          generic:\n            - <AntaTestDefinition>\n          bgp:\n            - <AntaTestDefinition>\n        ```\n        `anta.tests.routing.generic` and `anta.tests.routing.bgp` are importable Python modules.\n\n    \"\"\"\n    modules: dict[ModuleType, list[Any]] = {}\n    for module_name, tests in data.items():\n        if package and not module_name.startswith(\".\"):\n            # PLW2901 - we redefine the loop variable on purpose here.\n            module_name = f\".{module_name}\"  # noqa: PLW2901\n        try:\n            module: ModuleType = importlib.import_module(name=module_name, package=package)\n        except Exception as e:  # pylint: disable=broad-exception-caught\n            # A test module is potentially user-defined code.\n            # We need to catch everything if we want to have meaningful logs\n            module_str = f\"{module_name[1:] if module_name.startswith('.') else module_name}{f' from package {package}' if package else ''}\"\n            message = f\"Module named {module_str} cannot be imported. Verify that the module exists and there is no Python syntax issues.\"\n            anta_log_exception(e, message, logger)\n            raise ValueError(message) from e\n        if isinstance(tests, dict):\n            # This is an inner Python module\n            modules.update(AntaCatalogFile.flatten_modules(data=tests, package=module.__name__))\n        else:\n            if not isinstance(tests, list):\n                msg = f\"Syntax error when parsing: {tests}\\nIt must be a list of ANTA tests. Check the test catalog.\"\n                raise ValueError(msg)  # noqa: TRY004 pydantic catches ValueError or AssertionError, no TypeError\n            # This is a list of AntaTestDefinition\n            modules[module] = tests\n    return modules\n
"},{"location":"api/device/","title":"Device","text":""},{"location":"api/device/#antadevice-base-class","title":"AntaDevice base class","text":""},{"location":"api/device/#uml-representation","title":"UML representation","text":""},{"location":"api/device/#anta.device.AntaDevice","title":"AntaDevice","text":"
AntaDevice(name: str, tags: list[str] | None = None, *, disable_cache: bool = False)\n

Bases: ABC

Abstract class representing a device in ANTA.

An implementation of this class must override the abstract coroutines _collect() and refresh().

Attributes:

Name Type Description name Device name

is_online: True if the device IP is reachable and a port can be open established: True if remote command execution succeeds hw_model: Hardware model of the device tags: List of tags for this device cache: In-memory cache from aiocache library for this device (None if cache is disabled) cache_locks: Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled

Args:
name: Device name\ntags: List of tags for this device\ndisable_cache: Disable caching for all commands for this device. Defaults to False.\n
Source code in anta/device.py
def __init__(self, name: str, tags: list[str] | None = None, *, disable_cache: bool = False) -> None:\n    \"\"\"Initialize an AntaDevice.\n\n    Args:\n    ----\n        name: Device name\n        tags: List of tags for this device\n        disable_cache: Disable caching for all commands for this device. Defaults to False.\n\n    \"\"\"\n    self.name: str = name\n    self.hw_model: str | None = None\n    self.tags: list[str] = tags if tags is not None else []\n    # A device always has its own name as tag\n    self.tags.append(self.name)\n    self.is_online: bool = False\n    self.established: bool = False\n    self.cache: Cache | None = None\n    self.cache_locks: defaultdict[str, asyncio.Lock] | None = None\n\n    # Initialize cache if not disabled\n    if not disable_cache:\n        self._init_cache()\n
"},{"location":"api/device/#anta.device.AntaDevice.cache_statistics","title":"cache_statistics property","text":"
cache_statistics: dict[str, Any] | None\n

Returns the device cache statistics for logging purposes.

"},{"location":"api/device/#anta.device.AntaDevice.__hash__","title":"__hash__","text":"
__hash__() -> int\n

Implement hashing for AntaDevice objects.

Source code in anta/device.py
def __hash__(self) -> int:\n    \"\"\"Implement hashing for AntaDevice objects.\"\"\"\n    return hash(self._keys)\n
"},{"location":"api/device/#anta.device.AntaDevice.collect","title":"collect async","text":"
collect(command: AntaCommand) -> None\n

Collect the output for a specified command.

When caching is activated on both the device and the command, this method prioritizes retrieving the output from the cache. In cases where the output isn\u2019t cached yet, it will be freshly collected and then stored in the cache for future access. The method employs asynchronous locks based on the command\u2019s UID to guarantee exclusive access to the cache.

When caching is NOT enabled, either at the device or command level, the method directly collects the output via the private _collect method without interacting with the cache.

Args:
command (AntaCommand): The command to process.\n
Source code in anta/device.py
async def collect(self, command: AntaCommand) -> None:\n    \"\"\"Collect the output for a specified command.\n\n    When caching is activated on both the device and the command,\n    this method prioritizes retrieving the output from the cache. In cases where the output isn't cached yet,\n    it will be freshly collected and then stored in the cache for future access.\n    The method employs asynchronous locks based on the command's UID to guarantee exclusive access to the cache.\n\n    When caching is NOT enabled, either at the device or command level, the method directly collects the output\n    via the private `_collect` method without interacting with the cache.\n\n    Args:\n    ----\n        command (AntaCommand): The command to process.\n\n    \"\"\"\n    # Need to ignore pylint no-member as Cache is a proxy class and pylint is not smart enough\n    # https://github.com/pylint-dev/pylint/issues/7258\n    if self.cache is not None and self.cache_locks is not None and command.use_cache:\n        async with self.cache_locks[command.uid]:\n            cached_output = await self.cache.get(command.uid)  # pylint: disable=no-member\n\n            if cached_output is not None:\n                logger.debug(\"Cache hit for %s on %s\", command.command, self.name)\n                command.output = cached_output\n            else:\n                await self._collect(command=command)\n                await self.cache.set(command.uid, command.output)  # pylint: disable=no-member\n    else:\n        await self._collect(command=command)\n
"},{"location":"api/device/#anta.device.AntaDevice.collect_commands","title":"collect_commands async","text":"
collect_commands(commands: list[AntaCommand]) -> None\n

Collect multiple commands.

Args:
commands: the commands to collect\n
Source code in anta/device.py
async def collect_commands(self, commands: list[AntaCommand]) -> None:\n    \"\"\"Collect multiple commands.\n\n    Args:\n    ----\n        commands: the commands to collect\n\n    \"\"\"\n    await asyncio.gather(*(self.collect(command=command) for command in commands))\n
"},{"location":"api/device/#anta.device.AntaDevice.copy","title":"copy async","text":"
copy(sources: list[Path], destination: Path, direction: Literal['to', 'from'] = 'from') -> None\n

Copy files to and from the device, usually through SCP.

It is not mandatory to implement this for a valid AntaDevice subclass.

Args:
sources: List of files to copy to or from the device.\ndestination: Local or remote destination when copying the files. Can be a folder.\ndirection: Defines if this coroutine copies files to or from the device.\n
Source code in anta/device.py
async def copy(self, sources: list[Path], destination: Path, direction: Literal[\"to\", \"from\"] = \"from\") -> None:\n    \"\"\"Copy files to and from the device, usually through SCP.\n\n    It is not mandatory to implement this for a valid AntaDevice subclass.\n\n    Args:\n    ----\n        sources: List of files to copy to or from the device.\n        destination: Local or remote destination when copying the files. Can be a folder.\n        direction: Defines if this coroutine copies files to or from the device.\n\n    \"\"\"\n    _ = (sources, destination, direction)\n    msg = f\"copy() method has not been implemented in {self.__class__.__name__} definition\"\n    raise NotImplementedError(msg)\n
"},{"location":"api/device/#anta.device.AntaDevice.refresh","title":"refresh abstractmethod async","text":"
refresh() -> None\n

Update attributes of an AntaDevice instance.

This coroutine must update the following attributes of AntaDevice: - is_online: When the device IP is reachable and a port can be open - established: When a command execution succeeds - hw_model: The hardware model of the device

Source code in anta/device.py
@abstractmethod\nasync def refresh(self) -> None:\n    \"\"\"Update attributes of an AntaDevice instance.\n\n    This coroutine must update the following attributes of AntaDevice:\n        - `is_online`: When the device IP is reachable and a port can be open\n        - `established`: When a command execution succeeds\n        - `hw_model`: The hardware model of the device\n    \"\"\"\n
"},{"location":"api/device/#anta.device.AntaDevice.supports","title":"supports","text":"
supports(command: AntaCommand) -> bool\n

Return True if the command is supported on the device hardware platform, False otherwise.

Source code in anta/device.py
def supports(self, command: AntaCommand) -> bool:\n    \"\"\"Return True if the command is supported on the device hardware platform, False otherwise.\"\"\"\n    unsupported = any(\"not supported on this hardware platform\" in e for e in command.errors)\n    logger.debug(command)\n    if unsupported:\n        logger.debug(\"%s is not supported on %s\", command.command, self.hw_model)\n    return not unsupported\n
"},{"location":"api/device/#async-eos-device-class","title":"Async EOS device class","text":""},{"location":"api/device/#uml-representation_1","title":"UML representation","text":""},{"location":"api/device/#anta.device.AsyncEOSDevice","title":"AsyncEOSDevice","text":"
AsyncEOSDevice(host: str, username: str, password: str, name: str | None = None, enable_password: str | None = None, port: int | None = None, ssh_port: int | None = 22, tags: list[str] | None = None, timeout: float | None = None, proto: Literal['http', 'https'] = 'https', *, enable: bool = False, insecure: bool = False, disable_cache: bool = False)\n

Bases: AntaDevice

Implementation of AntaDevice for EOS using aio-eapi.

Attributes:

Name Type Description name Device name

is_online: True if the device IP is reachable and a port can be open established: True if remote command execution succeeds hw_model: Hardware model of the device tags: List of tags for this device

Args:
host: Device FQDN or IP\nusername: Username to connect to eAPI and SSH\npassword: Password to connect to eAPI and SSH\nname: Device name\nenable: Device needs privileged access\nenable_password: Password used to gain privileged access on EOS\nport: eAPI port. Defaults to 80 is proto is 'http' or 443 if proto is 'https'.\nssh_port: SSH port\ntags: List of tags for this device\ntimeout: Timeout value in seconds for outgoing connections. Default to 10 secs.\ninsecure: Disable SSH Host Key validation\nproto: eAPI protocol. Value can be 'http' or 'https'\ndisable_cache: Disable caching for all commands for this device. Defaults to False.\n
Source code in anta/device.py
def __init__(\n    self,\n    host: str,\n    username: str,\n    password: str,\n    name: str | None = None,\n    enable_password: str | None = None,\n    port: int | None = None,\n    ssh_port: int | None = 22,\n    tags: list[str] | None = None,\n    timeout: float | None = None,\n    proto: Literal[\"http\", \"https\"] = \"https\",\n    *,\n    enable: bool = False,\n    insecure: bool = False,\n    disable_cache: bool = False,\n) -> None:\n    \"\"\"Instantiate an AsyncEOSDevice.\n\n    Args:\n    ----\n        host: Device FQDN or IP\n        username: Username to connect to eAPI and SSH\n        password: Password to connect to eAPI and SSH\n        name: Device name\n        enable: Device needs privileged access\n        enable_password: Password used to gain privileged access on EOS\n        port: eAPI port. Defaults to 80 is proto is 'http' or 443 if proto is 'https'.\n        ssh_port: SSH port\n        tags: List of tags for this device\n        timeout: Timeout value in seconds for outgoing connections. Default to 10 secs.\n        insecure: Disable SSH Host Key validation\n        proto: eAPI protocol. Value can be 'http' or 'https'\n        disable_cache: Disable caching for all commands for this device. Defaults to False.\n\n    \"\"\"\n    if host is None:\n        message = \"'host' is required to create an AsyncEOSDevice\"\n        logger.error(message)\n        raise ValueError(message)\n    if name is None:\n        name = f\"{host}{f':{port}' if port else ''}\"\n    super().__init__(name, tags, disable_cache=disable_cache)\n    if username is None:\n        message = f\"'username' is required to instantiate device '{self.name}'\"\n        logger.error(message)\n        raise ValueError(message)\n    if password is None:\n        message = f\"'password' is required to instantiate device '{self.name}'\"\n        logger.error(message)\n        raise ValueError(message)\n    self.enable = enable\n    self._enable_password = enable_password\n    self._session: aioeapi.Device = aioeapi.Device(host=host, port=port, username=username, password=password, proto=proto, timeout=timeout)\n    ssh_params: dict[str, Any] = {}\n    if insecure:\n        ssh_params[\"known_hosts\"] = None\n    self._ssh_opts: SSHClientConnectionOptions = SSHClientConnectionOptions(host=host, port=ssh_port, username=username, password=password, **ssh_params)\n
"},{"location":"api/device/#anta.device.AsyncEOSDevice.copy","title":"copy async","text":"
copy(sources: list[Path], destination: Path, direction: Literal['to', 'from'] = 'from') -> None\n

Copy files to and from the device using asyncssh.scp().

Args:
sources: List of files to copy to or from the device.\ndestination: Local or remote destination when copying the files. Can be a folder.\ndirection: Defines if this coroutine copies files to or from the device.\n
Source code in anta/device.py
async def copy(self, sources: list[Path], destination: Path, direction: Literal[\"to\", \"from\"] = \"from\") -> None:\n    \"\"\"Copy files to and from the device using asyncssh.scp().\n\n    Args:\n    ----\n        sources: List of files to copy to or from the device.\n        destination: Local or remote destination when copying the files. Can be a folder.\n        direction: Defines if this coroutine copies files to or from the device.\n\n    \"\"\"\n    async with asyncssh.connect(\n        host=self._ssh_opts.host,\n        port=self._ssh_opts.port,\n        tunnel=self._ssh_opts.tunnel,\n        family=self._ssh_opts.family,\n        local_addr=self._ssh_opts.local_addr,\n        options=self._ssh_opts,\n    ) as conn:\n        src: list[tuple[SSHClientConnection, Path]] | list[Path]\n        dst: tuple[SSHClientConnection, Path] | Path\n        if direction == \"from\":\n            src = [(conn, file) for file in sources]\n            dst = destination\n            for file in sources:\n                message = f\"Copying '{file}' from device {self.name} to '{destination}' locally\"\n                logger.info(message)\n\n        elif direction == \"to\":\n            src = sources\n            dst = conn, destination\n            for file in src:\n                message = f\"Copying '{file}' to device {self.name} to '{destination}' remotely\"\n                logger.info(message)\n\n        else:\n            logger.critical(\"'direction' argument to copy() fonction is invalid: %s\", direction)\n\n            return\n        await asyncssh.scp(src, dst)\n
"},{"location":"api/device/#anta.device.AsyncEOSDevice.refresh","title":"refresh async","text":"
refresh() -> None\n

Update attributes of an AsyncEOSDevice instance.

This coroutine must update the following attributes of AsyncEOSDevice: - is_online: When a device IP is reachable and a port can be open - established: When a command execution succeeds - hw_model: The hardware model of the device

Source code in anta/device.py
async def refresh(self) -> None:\n    \"\"\"Update attributes of an AsyncEOSDevice instance.\n\n    This coroutine must update the following attributes of AsyncEOSDevice:\n    - is_online: When a device IP is reachable and a port can be open\n    - established: When a command execution succeeds\n    - hw_model: The hardware model of the device\n    \"\"\"\n    logger.debug(\"Refreshing device %s\", self.name)\n    self.is_online = await self._session.check_connection()\n    if self.is_online:\n        show_version = \"show version\"\n        hw_model_key = \"modelName\"\n        try:\n            response = await self._session.cli(command=show_version)\n        except aioeapi.EapiCommandError as e:\n            logger.warning(\"Cannot get hardware information from device %s: %s\", self.name, e.errmsg)\n\n        except (HTTPError, ConnectError) as e:\n            logger.warning(\"Cannot get hardware information from device %s: %s\", self.name, exc_to_str(e))\n\n        else:\n            if hw_model_key in response:\n                self.hw_model = response[hw_model_key]\n            else:\n                logger.warning(\"Cannot get hardware information from device %s: cannot parse '%s'\", self.name, show_version)\n\n    else:\n        logger.warning(\"Could not connect to device %s: cannot open eAPI port\", self.name)\n\n    self.established = bool(self.is_online and self.hw_model)\n
"},{"location":"api/inventory/","title":"Inventory module","text":""},{"location":"api/inventory/#anta.inventory.AntaInventory","title":"AntaInventory","text":"

Bases: Dict[str, AntaDevice]

Inventory abstraction for ANTA framework.

"},{"location":"api/inventory/#anta.inventory.AntaInventory.__setitem__","title":"__setitem__","text":"
__setitem__(key: str, value: AntaDevice) -> None\n

Set a device in the inventory.

Source code in anta/inventory/__init__.py
def __setitem__(self, key: str, value: AntaDevice) -> None:\n    \"\"\"Set a device in the inventory.\"\"\"\n    if key != value.name:\n        msg = f\"The key must be the device name for device '{value.name}'. Use AntaInventory.add_device().\"\n        raise RuntimeError(msg)\n    return super().__setitem__(key, value)\n
"},{"location":"api/inventory/#anta.inventory.AntaInventory.add_device","title":"add_device","text":"
add_device(device: AntaDevice) -> None\n

Add a device to final inventory.

Args:
device: Device object to be added\n
Source code in anta/inventory/__init__.py
def add_device(self, device: AntaDevice) -> None:\n    \"\"\"Add a device to final inventory.\n\n    Args:\n    ----\n        device: Device object to be added\n\n    \"\"\"\n    self[device.name] = device\n
"},{"location":"api/inventory/#anta.inventory.AntaInventory.connect_inventory","title":"connect_inventory async","text":"
connect_inventory() -> None\n

Run refresh() coroutines for all AntaDevice objects in this inventory.

Source code in anta/inventory/__init__.py
async def connect_inventory(self) -> None:\n    \"\"\"Run `refresh()` coroutines for all AntaDevice objects in this inventory.\"\"\"\n    logger.debug(\"Refreshing devices...\")\n    results = await asyncio.gather(\n        *(device.refresh() for device in self.values()),\n        return_exceptions=True,\n    )\n    for r in results:\n        if isinstance(r, Exception):\n            message = \"Error when refreshing inventory\"\n            anta_log_exception(r, message, logger)\n
"},{"location":"api/inventory/#anta.inventory.AntaInventory.get_inventory","title":"get_inventory","text":"
get_inventory(*, established_only: bool = False, tags: list[str] | None = None) -> AntaInventory\n

Return a filtered inventory.

Args:
established_only: Whether or not to include only established devices. Default False.\ntags: List of tags to filter devices.\n

Returns:

Type Description AntaInventory: An inventory with filtered AntaDevice objects. Source code in anta/inventory/__init__.py
def get_inventory(self, *, established_only: bool = False, tags: list[str] | None = None) -> AntaInventory:\n    \"\"\"Return a filtered inventory.\n\n    Args:\n    ----\n        established_only: Whether or not to include only established devices. Default False.\n        tags: List of tags to filter devices.\n\n    Returns\n    -------\n        AntaInventory: An inventory with filtered AntaDevice objects.\n\n    \"\"\"\n\n    def _filter_devices(device: AntaDevice) -> bool:\n        \"\"\"Select the devices based on the input tags and the requirement for an established connection.\"\"\"\n        if tags is not None and all(tag not in tags for tag in device.tags):\n            return False\n        return bool(not established_only or device.established)\n\n    devices: list[AntaDevice] = list(filter(_filter_devices, self.values()))\n    result = AntaInventory()\n    for device in devices:\n        result.add_device(device)\n    return result\n
"},{"location":"api/inventory/#anta.inventory.AntaInventory.parse","title":"parse staticmethod","text":"
parse(filename: str | Path, username: str, password: str, enable_password: str | None = None, timeout: float | None = None, *, enable: bool = False, insecure: bool = False, disable_cache: bool = False) -> AntaInventory\n

Create an AntaInventory instance from an inventory file.

The inventory devices are AsyncEOSDevice instances.

Args:
filename (str): Path to device inventory YAML file\nusername (str): Username to use to connect to devices\npassword (str): Password to use to connect to devices\nenable (bool): Whether or not the commands need to be run in enable mode towards the devices\nenable_password (str, optional): Enable password to use if required\ntimeout (float, optional): timeout in seconds for every API call.\ninsecure (bool): Disable SSH Host Key validation\ndisable_cache (bool): Disable cache globally\n

Raises:

Type Description InventoryRootKeyError: Root key of inventory is missing.

InventoryIncorrectSchemaError: Inventory file is not following AntaInventory Schema.

Source code in anta/inventory/__init__.py
@staticmethod\ndef parse(\n    filename: str | Path,\n    username: str,\n    password: str,\n    enable_password: str | None = None,\n    timeout: float | None = None,\n    *,\n    enable: bool = False,\n    insecure: bool = False,\n    disable_cache: bool = False,\n) -> AntaInventory:\n    \"\"\"Create an AntaInventory instance from an inventory file.\n\n    The inventory devices are AsyncEOSDevice instances.\n\n    Args:\n    ----\n        filename (str): Path to device inventory YAML file\n        username (str): Username to use to connect to devices\n        password (str): Password to use to connect to devices\n        enable (bool): Whether or not the commands need to be run in enable mode towards the devices\n        enable_password (str, optional): Enable password to use if required\n        timeout (float, optional): timeout in seconds for every API call.\n        insecure (bool): Disable SSH Host Key validation\n        disable_cache (bool): Disable cache globally\n\n    Raises\n    ------\n        InventoryRootKeyError: Root key of inventory is missing.\n        InventoryIncorrectSchemaError: Inventory file is not following AntaInventory Schema.\n\n    \"\"\"\n    inventory = AntaInventory()\n    kwargs: dict[str, Any] = {\n        \"username\": username,\n        \"password\": password,\n        \"enable\": enable,\n        \"enable_password\": enable_password,\n        \"timeout\": timeout,\n        \"insecure\": insecure,\n        \"disable_cache\": disable_cache,\n    }\n    if username is None:\n        message = \"'username' is required to create an AntaInventory\"\n        logger.error(message)\n        raise ValueError(message)\n    if password is None:\n        message = \"'password' is required to create an AntaInventory\"\n        logger.error(message)\n        raise ValueError(message)\n\n    try:\n        filename = Path(filename)\n        with filename.open(encoding=\"UTF-8\") as file:\n            data = safe_load(file)\n    except (TypeError, YAMLError, OSError) as e:\n        message = f\"Unable to parse ANTA Device Inventory file '{filename}'\"\n        anta_log_exception(e, message, logger)\n        raise\n\n    if AntaInventory.INVENTORY_ROOT_KEY not in data:\n        exc = InventoryRootKeyError(f\"Inventory root key ({AntaInventory.INVENTORY_ROOT_KEY}) is not defined in your inventory\")\n        anta_log_exception(exc, f\"Device inventory is invalid! (from {filename})\", logger)\n        raise exc\n\n    try:\n        inventory_input = AntaInventoryInput(**data[AntaInventory.INVENTORY_ROOT_KEY])\n    except ValidationError as e:\n        anta_log_exception(e, f\"Device inventory is invalid! (from {filename})\", logger)\n        raise\n\n    # Read data from input\n    AntaInventory._parse_hosts(inventory_input, inventory, **kwargs)\n    AntaInventory._parse_networks(inventory_input, inventory, **kwargs)\n    AntaInventory._parse_ranges(inventory_input, inventory, **kwargs)\n\n    return inventory\n
"},{"location":"api/inventory/#anta.inventory.exceptions","title":"exceptions","text":"

Manage Exception in Inventory module.

"},{"location":"api/inventory/#anta.inventory.exceptions.InventoryIncorrectSchemaError","title":"InventoryIncorrectSchemaError","text":"

Bases: Exception

Error when user data does not follow ANTA schema.

"},{"location":"api/inventory/#anta.inventory.exceptions.InventoryRootKeyError","title":"InventoryRootKeyError","text":"

Bases: Exception

Error raised when inventory root key is not found.

"},{"location":"api/inventory.models.input/","title":"Inventory models","text":""},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryInput","title":"AntaInventoryInput","text":"

Bases: BaseModel

User\u2019s inventory model.

Attributes:

Name Type Description networks (list[AntaInventoryNetwork],Optional) List of AntaInventoryNetwork objects for networks.

hosts (list[AntaInventoryHost],Optional): List of AntaInventoryHost objects for hosts. range (list[AntaInventoryRange],Optional): List of AntaInventoryRange objects for ranges.

"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryHost","title":"AntaInventoryHost","text":"

Bases: BaseModel

Host definition for user\u2019s inventory.

Attributes:

Name Type Description host (IPvAnyAddress) IPv4 or IPv6 address of the device

port (int): (Optional) eAPI port to use Default is 443. name (str): (Optional) Name to display during tests report. Default is hostname:port tags (list[str]): List of attached tags read from inventory file. disable_cache (bool): Disable cache per host. Defaults to False.

"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryNetwork","title":"AntaInventoryNetwork","text":"

Bases: BaseModel

Network definition for user\u2019s inventory.

Attributes:

Name Type Description network (IPvAnyNetwork) Subnet to use for testing.

tags (list[str]): List of attached tags read from inventory file. disable_cache (bool): Disable cache per network. Defaults to False.

"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryRange","title":"AntaInventoryRange","text":"

Bases: BaseModel

IP Range definition for user\u2019s inventory.

Attributes:

Name Type Description start (IPvAnyAddress) IPv4 or IPv6 address for the begining of the range.

stop (IPvAnyAddress): IPv4 or IPv6 address for the end of the range. tags (list[str]): List of attached tags read from inventory file. disable_cache (bool): Disable cache per range of hosts. Defaults to False.

"},{"location":"api/models/","title":"Test models","text":""},{"location":"api/models/#test-definition","title":"Test definition","text":""},{"location":"api/models/#uml-diagram","title":"UML Diagram","text":""},{"location":"api/models/#anta.models.AntaTest","title":"AntaTest","text":"
AntaTest(device: AntaDevice, inputs: dict[str, Any] | AntaTest.Input | None = None, eos_data: list[dict[Any, Any] | str] | None = None)\n

Bases: ABC

Abstract class defining a test in ANTA.

The goal of this class is to handle the heavy lifting and make writing a test as simple as possible.

Examples

The following is an example of an AntaTest subclass implementation:

    class VerifyReachability(AntaTest):\n        name = \"VerifyReachability\"\n        description = \"Test the network reachability to one or many destination IP(s).\"\n        categories = [\"connectivity\"]\n        commands = [AntaTemplate(template=\"ping vrf {vrf} {dst} source {src} repeat 2\")]\n\n        class Input(AntaTest.Input):\n            hosts: list[Host]\n            class Host(BaseModel):\n                dst: IPv4Address\n                src: IPv4Address\n                vrf: str = \"default\"\n\n        def render(self, template: AntaTemplate) -> list[AntaCommand]:\n            return [template.render({\"dst\": host.dst, \"src\": host.src, \"vrf\": host.vrf}) for host in self.inputs.hosts]\n\n        @AntaTest.anta_test\n        def test(self) -> None:\n            failures = []\n            for command in self.instance_commands:\n                if command.params and (\"src\" and \"dst\") in command.params:\n                    src, dst = command.params[\"src\"], command.params[\"dst\"]\n                if \"2 received\" not in command.json_output[\"messages\"][0]:\n                    failures.append((str(src), str(dst)))\n            if not failures:\n                self.result.is_success()\n            else:\n                self.result.is_failure(f\"Connectivity test failed for the following source-destination pairs: {failures}\")\n
Attributes: device: AntaDevice instance on which this test is run inputs: AntaTest.Input instance carrying the test inputs instance_commands: List of AntaCommand instances of this test result: TestResult instance representing the result of this test logger: Python logger for this test instance

Args:
device: AntaDevice instance on which the test will be run\ninputs: dictionary of attributes used to instantiate the AntaTest.Input instance\neos_data: Populate outputs of the test commands instead of collecting from devices.\n          This list must have the same length and order than the `instance_commands` instance attribute.\n
Source code in anta/models.py
def __init__(\n    self,\n    device: AntaDevice,\n    inputs: dict[str, Any] | AntaTest.Input | None = None,\n    eos_data: list[dict[Any, Any] | str] | None = None,\n) -> None:\n    \"\"\"AntaTest Constructor.\n\n    Args:\n    ----\n        device: AntaDevice instance on which the test will be run\n        inputs: dictionary of attributes used to instantiate the AntaTest.Input instance\n        eos_data: Populate outputs of the test commands instead of collecting from devices.\n                  This list must have the same length and order than the `instance_commands` instance attribute.\n\n    \"\"\"\n    self.logger: logging.Logger = logging.getLogger(f\"{self.__module__}.{self.__class__.__name__}\")\n    self.device: AntaDevice = device\n    self.inputs: AntaTest.Input\n    self.instance_commands: list[AntaCommand] = []\n    self.result: TestResult = TestResult(\n        name=device.name,\n        test=self.name,\n        categories=self.categories,\n        description=self.description,\n    )\n    self._init_inputs(inputs)\n    if self.result.result == \"unset\":\n        self._init_commands(eos_data)\n
"},{"location":"api/models/#anta.models.AntaTest.blocked","title":"blocked property","text":"
blocked: bool\n

Check if CLI commands contain a blocked keyword.

"},{"location":"api/models/#anta.models.AntaTest.collected","title":"collected property","text":"
collected: bool\n

Returns True if all commands for this test have been collected.

"},{"location":"api/models/#anta.models.AntaTest.failed_commands","title":"failed_commands property","text":"
failed_commands: list[AntaCommand]\n

Returns a list of all the commands that have failed.

"},{"location":"api/models/#anta.models.AntaTest.Input","title":"Input","text":"

Bases: BaseModel

Class defining inputs for a test in ANTA.

Examples

A valid test catalog will look like the following:

<Python module>:\n- <AntaTest subclass>:\n    result_overwrite:\n        categories:\n        - \"Overwritten category 1\"\n        description: \"Test with overwritten description\"\n        custom_field: \"Test run by John Doe\"\n
Attributes: result_overwrite: Define fields to overwrite in the TestResult object

"},{"location":"api/models/#anta.models.AntaTest.Input.Filters","title":"Filters","text":"

Bases: BaseModel

Runtime filters to map tests with list of tags or devices.

Attributes:

Name Type Description tags List of device's tags for the test."},{"location":"api/models/#anta.models.AntaTest.Input.ResultOverwrite","title":"ResultOverwrite","text":"

Bases: BaseModel

Test inputs model to overwrite result fields.

Attributes:

Name Type Description description overwrite TestResult.description

categories: overwrite TestResult.categories custom_field: a free string that will be included in the TestResult object

"},{"location":"api/models/#anta.models.AntaTest.Input.__hash__","title":"__hash__","text":"
__hash__() -> int\n

Implement generic hashing for AntaTest.Input.

This will work in most cases but this does not consider 2 lists with different ordering as equal.

Source code in anta/models.py
def __hash__(self) -> int:\n    \"\"\"Implement generic hashing for AntaTest.Input.\n\n    This will work in most cases but this does not consider 2 lists with different ordering as equal.\n    \"\"\"\n    return hash(self.model_dump_json())\n
"},{"location":"api/models/#anta.models.AntaTest.anta_test","title":"anta_test staticmethod","text":"
anta_test(function: F) -> Callable[..., Coroutine[Any, Any, TestResult]]\n

Decorate the test() method in child classes.

This decorator implements (in this order):

  1. Instantiate the command outputs if eos_data is provided to the test() method
  2. Collect the commands from the device
  3. Run the test() method
  4. Catches any exception in test() user code and set the result instance attribute
Source code in anta/models.py
@staticmethod\ndef anta_test(function: F) -> Callable[..., Coroutine[Any, Any, TestResult]]:\n    \"\"\"Decorate the `test()` method in child classes.\n\n    This decorator implements (in this order):\n\n    1. Instantiate the command outputs if `eos_data` is provided to the `test()` method\n    2. Collect the commands from the device\n    3. Run the `test()` method\n    4. Catches any exception in `test()` user code and set the `result` instance attribute\n    \"\"\"\n\n    @wraps(function)\n    async def wrapper(\n        self: AntaTest,\n        eos_data: list[dict[Any, Any] | str] | None = None,\n        **kwargs: dict[str, Any],\n    ) -> TestResult:\n        \"\"\"Inner function for the anta_test decorator.\n\n        Args:\n        ----\n            self: The test instance.\n            eos_data: Populate outputs of the test commands instead of collecting from devices.\n                      This list must have the same length and order than the `instance_commands` instance attribute.\n            kwargs: Any keyword argument to pass to the test.\n\n        Returns\n        -------\n            result: TestResult instance attribute populated with error status if any\n\n        \"\"\"\n\n        def format_td(seconds: float, digits: int = 3) -> str:\n            isec, fsec = divmod(round(seconds * 10**digits), 10**digits)\n            return f\"{timedelta(seconds=isec)}.{fsec:0{digits}.0f}\"\n\n        start_time = time.time()\n        if self.result.result != \"unset\":\n            return self.result\n\n        # Data\n        if eos_data is not None:\n            self.save_commands_data(eos_data)\n            self.logger.debug(\"Test %s initialized with input data %s\", self.name, eos_data)\n\n        # If some data is missing, try to collect\n        if not self.collected:\n            await self.collect()\n            if self.result.result != \"unset\":\n                return self.result\n\n            if cmds := self.failed_commands:\n                self.logger.debug(self.device.supports)\n                unsupported_commands = [f\"Skipped because {c.command} is not supported on {self.device.hw_model}\" for c in cmds if not self.device.supports(c)]\n                self.logger.debug(unsupported_commands)\n                if unsupported_commands:\n                    msg = f\"Test {self.name} has been skipped because it is not supported on {self.device.hw_model}: {GITHUB_SUGGESTION}\"\n                    self.logger.warning(msg)\n                    self.result.is_skipped(\"\\n\".join(unsupported_commands))\n                    return self.result\n                self.result.is_error(message=\"\\n\".join([f\"{c.command} has failed: {', '.join(c.errors)}\" for c in cmds]))\n                return self.result\n\n        try:\n            function(self, **kwargs)\n        except Exception as e:  # pylint: disable=broad-exception-caught\n            # test() is user-defined code.\n            # We need to catch everything if we want the AntaTest object\n            # to live until the reporting\n            message = f\"Exception raised for test {self.name} (on device {self.device.name})\"\n            anta_log_exception(e, message, self.logger)\n            self.result.is_error(message=exc_to_str(e))\n\n        test_duration = time.time() - start_time\n        msg = f\"Executing test {self.name} on device {self.device.name} took {format_td(test_duration)}\"\n        self.logger.debug(msg)\n\n        AntaTest.update_progress()\n        return self.result\n\n    return wrapper\n
"},{"location":"api/models/#anta.models.AntaTest.collect","title":"collect async","text":"
collect() -> None\n

Collect outputs of all commands of this test class from the device of this test instance.

Source code in anta/models.py
async def collect(self) -> None:\n    \"\"\"Collect outputs of all commands of this test class from the device of this test instance.\"\"\"\n    try:\n        if self.blocked is False:\n            await self.device.collect_commands(self.instance_commands)\n    except Exception as e:  # pylint: disable=broad-exception-caught\n        # device._collect() is user-defined code.\n        # We need to catch everything if we want the AntaTest object\n        # to live until the reporting\n        message = f\"Exception raised while collecting commands for test {self.name} (on device {self.device.name})\"\n        anta_log_exception(e, message, self.logger)\n        self.result.is_error(message=exc_to_str(e))\n
"},{"location":"api/models/#anta.models.AntaTest.render","title":"render","text":"
render(template: AntaTemplate) -> list[AntaCommand]\n

Render an AntaTemplate instance of this AntaTest using the provided AntaTest.Input instance at self.inputs.

This is not an abstract method because it does not need to be implemented if there is no AntaTemplate for this test.

Source code in anta/models.py
def render(self, template: AntaTemplate) -> list[AntaCommand]:  # pylint: disable=W0613  # noqa: ARG002\n    \"\"\"Render an AntaTemplate instance of this AntaTest using the provided AntaTest.Input instance at self.inputs.\n\n    This is not an abstract method because it does not need to be implemented if there is\n    no AntaTemplate for this test.\n    \"\"\"\n    msg = f\"AntaTemplate are provided but render() method has not been implemented for {self.__module__}.{self.name}\"\n    raise NotImplementedError(msg)\n
"},{"location":"api/models/#anta.models.AntaTest.save_commands_data","title":"save_commands_data","text":"
save_commands_data(eos_data: list[dict[str, Any] | str]) -> None\n

Populate output of all AntaCommand instances in instance_commands.

Source code in anta/models.py
def save_commands_data(self, eos_data: list[dict[str, Any] | str]) -> None:\n    \"\"\"Populate output of all AntaCommand instances in `instance_commands`.\"\"\"\n    if len(eos_data) > len(self.instance_commands):\n        self.result.is_error(message=\"Test initialization error: Trying to save more data than there are commands for the test\")\n        return\n    if len(eos_data) < len(self.instance_commands):\n        self.result.is_error(message=\"Test initialization error: Trying to save less data than there are commands for the test\")\n        return\n    for index, data in enumerate(eos_data or []):\n        self.instance_commands[index].output = data\n
"},{"location":"api/models/#anta.models.AntaTest.test","title":"test abstractmethod","text":"
test() -> Coroutine[Any, Any, TestResult]\n

Core of the test logic.

This is an abstractmethod that must be implemented by child classes. It must set the correct status of the result instance attribute with the appropriate outcome of the test.

Examples

It must be implemented using the AntaTest.anta_test decorator:

@AntaTest.anta_test\ndef test(self) -> None:\n    self.result.is_success()\n    for command in self.instance_commands:\n        if not self._test_command(command): # _test_command() is an arbitrary test logic\n            self.result.is_failure(\"Failure reson\")\n

Source code in anta/models.py
@abstractmethod\ndef test(self) -> Coroutine[Any, Any, TestResult]:\n    \"\"\"Core of the test logic.\n\n    This is an abstractmethod that must be implemented by child classes.\n    It must set the correct status of the `result` instance attribute with the appropriate outcome of the test.\n\n    Examples\n    --------\n    It must be implemented using the `AntaTest.anta_test` decorator:\n        ```python\n        @AntaTest.anta_test\n        def test(self) -> None:\n            self.result.is_success()\n            for command in self.instance_commands:\n                if not self._test_command(command): # _test_command() is an arbitrary test logic\n                    self.result.is_failure(\"Failure reson\")\n        ```\n\n    \"\"\"\n
"},{"location":"api/models/#command-definition","title":"Command definition","text":""},{"location":"api/models/#uml-diagram_1","title":"UML Diagram","text":"

Warning

CLI commands are protected to avoid execution of critical commands such as reload or write erase.

  • Reload command: ^reload\\s*\\w*
  • Configure mode: ^conf\\w*\\s*(terminal|session)*
  • Write: ^wr\\w*\\s*\\w+
"},{"location":"api/models/#anta.models.AntaCommand","title":"AntaCommand","text":"

Bases: BaseModel

Class to define a command.

Info

eAPI models are revisioned, this means that if a model is modified in a non-backwards compatible way, then its revision will be bumped up (revisions are numbers, default value is 1).

By default an eAPI request will return revision 1 of the model instance, this ensures that older management software will not suddenly stop working when a switch is upgraded. A revision applies to a particular CLI command whereas a version is global and is internally translated to a specific revision for each CLI command in the RPC.

Revision has precedence over version.

Attributes:

Name Type Description command Device command

version: eAPI version - valid values are 1 or \u201clatest\u201d - default is \u201clatest\u201d revision: eAPI revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt: eAPI output - json or text - default is json output: Output of the command populated by the collect() function template: AntaTemplate object used to render this command params: Dictionary of variables with string values to render the template errors: If the command execution fails, eAPI returns a list of strings detailing the error use_cache: Enable or disable caching for this AntaCommand if the AntaDevice supports it - default is True

"},{"location":"api/models/#anta.models.AntaCommand.collected","title":"collected property","text":"
collected: bool\n

Return True if the command has been collected.

"},{"location":"api/models/#anta.models.AntaCommand.json_output","title":"json_output property","text":"
json_output: dict[str, Any]\n

Get the command output as JSON.

"},{"location":"api/models/#anta.models.AntaCommand.text_output","title":"text_output property","text":"
text_output: str\n

Get the command output as a string.

"},{"location":"api/models/#anta.models.AntaCommand.uid","title":"uid property","text":"
uid: str\n

Generate a unique identifier for this command.

"},{"location":"api/models/#template-definition","title":"Template definition","text":""},{"location":"api/models/#uml-diagram_2","title":"UML Diagram","text":""},{"location":"api/models/#anta.models.AntaTemplate","title":"AntaTemplate","text":"

Bases: BaseModel

Class to define a command template as Python f-string.

Can render a command from parameters.

Attributes:

Name Type Description template Python f-string. Example: 'show vlan {vlan_id}'

version: eAPI version - valid values are 1 or \u201clatest\u201d - default is \u201clatest\u201d revision: Revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt: eAPI output - json or text - default is json use_cache: Enable or disable caching for this AntaTemplate if the AntaDevice supports it - default is True

"},{"location":"api/models/#anta.models.AntaTemplate.render","title":"render","text":"
render(**params: dict[str, Any]) -> AntaCommand\n

Render an AntaCommand from an AntaTemplate instance.

Keep the parameters used in the AntaTemplate instance.

Args:
params: dictionary of variables with string values to render the Python f-string\n

Returns:

Type Description command: The rendered AntaCommand.

This AntaCommand instance have a template attribute that references this AntaTemplate instance.

Source code in anta/models.py
def render(self, **params: dict[str, Any]) -> AntaCommand:\n    \"\"\"Render an AntaCommand from an AntaTemplate instance.\n\n    Keep the parameters used in the AntaTemplate instance.\n\n    Args:\n    ----\n        params: dictionary of variables with string values to render the Python f-string\n\n    Returns\n    -------\n        command: The rendered AntaCommand.\n                 This AntaCommand instance have a template attribute that references this\n                 AntaTemplate instance.\n\n    \"\"\"\n    try:\n        return AntaCommand(\n            command=self.template.format(**params),\n            ofmt=self.ofmt,\n            version=self.version,\n            revision=self.revision,\n            template=self,\n            params=params,\n            use_cache=self.use_cache,\n        )\n    except KeyError as e:\n        raise AntaTemplateRenderError(self, e.args[0]) from e\n
"},{"location":"api/report_manager/","title":"Report Manager","text":""},{"location":"api/report_manager/#anta.reporter.ReportTable","title":"ReportTable","text":"

TableReport Generate a Table based on TestResult.

"},{"location":"api/report_manager/#anta.reporter.ReportTable.report_all","title":"report_all","text":"
report_all(result_manager: ResultManager, host: str | None = None, testcase: str | None = None, title: str = 'All tests results') -> Table\n

Create a table report with all tests for one or all devices.

Create table with full output: Host / Test / Status / Message

Args:
result_manager (ResultManager): A manager with a list of tests.\nhost (str, optional): IP Address of a host to search for. Defaults to None.\ntestcase (str, optional): A test name to search for. Defaults to None.\ntitle (str, optional): Title for the report. Defaults to 'All tests results'.\n

Returns:

Type Description Table: A fully populated rich Table Source code in anta/reporter/__init__.py
def report_all(\n    self,\n    result_manager: ResultManager,\n    host: str | None = None,\n    testcase: str | None = None,\n    title: str = \"All tests results\",\n) -> Table:\n    \"\"\"Create a table report with all tests for one or all devices.\n\n    Create table with full output: Host / Test / Status / Message\n\n    Args:\n    ----\n        result_manager (ResultManager): A manager with a list of tests.\n        host (str, optional): IP Address of a host to search for. Defaults to None.\n        testcase (str, optional): A test name to search for. Defaults to None.\n        title (str, optional): Title for the report. Defaults to 'All tests results'.\n\n    Returns\n    -------\n        Table: A fully populated rich Table\n\n    \"\"\"\n    table = Table(title=title, show_lines=True)\n    headers = [\"Device\", \"Test Name\", \"Test Status\", \"Message(s)\", \"Test description\", \"Test category\"]\n    table = self._build_headers(headers=headers, table=table)\n\n    for result in result_manager.get_results():\n        # pylint: disable=R0916\n        if (host is None and testcase is None) or (host is not None and str(result.name) == host) or (testcase is not None and testcase == str(result.test)):\n            state = self._color_result(result.result)\n            message = self._split_list_to_txt_list(result.messages) if len(result.messages) > 0 else \"\"\n            categories = \", \".join(result.categories)\n            table.add_row(str(result.name), result.test, state, message, result.description, categories)\n    return table\n
"},{"location":"api/report_manager/#anta.reporter.ReportTable.report_summary_hosts","title":"report_summary_hosts","text":"
report_summary_hosts(result_manager: ResultManager, host: str | None = None, title: str = 'Summary per host') -> Table\n

Create a table report with result agregated per host.

Create table with full output: Host / Number of success / Number of failure / Number of error / List of nodes in error or failure

Args:
result_manager (ResultManager): A manager with a list of tests.\nhost (str, optional): IP Address of a host to search for. Defaults to None.\ntitle (str, optional): Title for the report. Defaults to 'All tests results'.\n

Returns:

Type Description Table: A fully populated rich Table Source code in anta/reporter/__init__.py
def report_summary_hosts(\n    self,\n    result_manager: ResultManager,\n    host: str | None = None,\n    title: str = \"Summary per host\",\n) -> Table:\n    \"\"\"Create a table report with result agregated per host.\n\n    Create table with full output: Host / Number of success / Number of failure / Number of error / List of nodes in error or failure\n\n    Args:\n    ----\n        result_manager (ResultManager): A manager with a list of tests.\n        host (str, optional): IP Address of a host to search for. Defaults to None.\n        title (str, optional): Title for the report. Defaults to 'All tests results'.\n\n    Returns\n    -------\n        Table: A fully populated rich Table\n\n    \"\"\"\n    table = Table(title=title, show_lines=True)\n    headers = [\n        \"Device\",\n        \"# of success\",\n        \"# of skipped\",\n        \"# of failure\",\n        \"# of errors\",\n        \"List of failed or error test cases\",\n    ]\n    table = self._build_headers(headers=headers, table=table)\n    for host_read in result_manager.get_hosts():\n        if host is None or str(host_read) == host:\n            results = result_manager.get_result_by_host(host_read)\n            logger.debug(\"data to use for computation\")\n            logger.debug(\"%s: %s\", host, results)\n            nb_failure = len([result for result in results if result.result == \"failure\"])\n            nb_error = len([result for result in results if result.result == \"error\"])\n            list_failure = [str(result.test) for result in results if result.result in [\"failure\", \"error\"]]\n            nb_success = len([result for result in results if result.result == \"success\"])\n            nb_skipped = len([result for result in results if result.result == \"skipped\"])\n            table.add_row(\n                str(host_read),\n                str(nb_success),\n                str(nb_skipped),\n                str(nb_failure),\n                str(nb_error),\n                str(list_failure),\n            )\n    return table\n
"},{"location":"api/report_manager/#anta.reporter.ReportTable.report_summary_tests","title":"report_summary_tests","text":"
report_summary_tests(result_manager: ResultManager, testcase: str | None = None, title: str = 'Summary per test case') -> Table\n

Create a table report with result agregated per test.

Create table with full output: Test / Number of success / Number of failure / Number of error / List of nodes in error or failure

Args:
result_manager (ResultManager): A manager with a list of tests.\ntestcase (str, optional): A test name to search for. Defaults to None.\ntitle (str, optional): Title for the report. Defaults to 'All tests results'.\n

Returns:

Type Description Table: A fully populated rich Table Source code in anta/reporter/__init__.py
def report_summary_tests(\n    self,\n    result_manager: ResultManager,\n    testcase: str | None = None,\n    title: str = \"Summary per test case\",\n) -> Table:\n    \"\"\"Create a table report with result agregated per test.\n\n    Create table with full output: Test / Number of success / Number of failure / Number of error / List of nodes in error or failure\n\n    Args:\n    ----\n        result_manager (ResultManager): A manager with a list of tests.\n        testcase (str, optional): A test name to search for. Defaults to None.\n        title (str, optional): Title for the report. Defaults to 'All tests results'.\n\n    Returns\n    -------\n        Table: A fully populated rich Table\n\n    \"\"\"\n    # sourcery skip: class-extract-method\n    table = Table(title=title, show_lines=True)\n    headers = [\n        \"Test Case\",\n        \"# of success\",\n        \"# of skipped\",\n        \"# of failure\",\n        \"# of errors\",\n        \"List of failed or error nodes\",\n    ]\n    table = self._build_headers(headers=headers, table=table)\n    for testcase_read in result_manager.get_testcases():\n        if testcase is None or str(testcase_read) == testcase:\n            results = result_manager.get_result_by_test(testcase_read)\n            nb_failure = len([result for result in results if result.result == \"failure\"])\n            nb_error = len([result for result in results if result.result == \"error\"])\n            list_failure = [str(result.name) for result in results if result.result in [\"failure\", \"error\"]]\n            nb_success = len([result for result in results if result.result == \"success\"])\n            nb_skipped = len([result for result in results if result.result == \"skipped\"])\n            table.add_row(\n                testcase_read,\n                str(nb_success),\n                str(nb_skipped),\n                str(nb_failure),\n                str(nb_error),\n                str(list_failure),\n            )\n    return table\n
"},{"location":"api/result_manager/","title":"Result Manager module","text":""},{"location":"api/result_manager/#result-manager-definition","title":"Result Manager definition","text":""},{"location":"api/result_manager/#uml-diagram","title":"UML Diagram","text":""},{"location":"api/result_manager/#anta.result_manager.ResultManager","title":"ResultManager","text":"
ResultManager()\n

Helper to manage Test Results and generate reports.

Examples
Create Inventory:\n\n    inventory_anta = AntaInventory.parse(\n        filename='examples/inventory.yml',\n        username='ansible',\n        password='ansible',\n        timeout=0.5\n    )\n\nCreate Result Manager:\n\n    manager = ResultManager()\n\nRun tests for all connected devices:\n\n    for device in inventory_anta.get_inventory():\n        manager.add_test_result(\n            VerifyNTP(device=device).test()\n        )\n        manager.add_test_result(\n            VerifyEOSVersion(device=device).test(version='4.28.3M')\n        )\n\nPrint result in native format:\n\n    manager.get_results()\n    [\n        TestResult(\n            host=IPv4Address('192.168.0.10'),\n            test='VerifyNTP',\n            result='failure',\n            message=\"device is not running NTP correctly\"\n        ),\n        TestResult(\n            host=IPv4Address('192.168.0.10'),\n            test='VerifyEOSVersion',\n            result='success',\n            message=None\n        ),\n    ]\n

The status of the class is initialized to \u201cunset\u201d

Then when adding a test with a status that is NOT \u2018error\u2019 the following table shows the updated status:

Current Status Added test Status Updated Status unset Any Any skipped unset, skipped skipped skipped success success skipped failure failure success unset, skipped, success success success failure failure failure unset, skipped success, failure failure

If the status of the added test is error, the status is untouched and the error_status is set to True.

Source code in anta/result_manager/__init__.py
def __init__(self) -> None:\n    \"\"\"Class constructor.\n\n    The status of the class is initialized to \"unset\"\n\n    Then when adding a test with a status that is NOT 'error' the following\n    table shows the updated status:\n\n    | Current Status |         Added test Status       | Updated Status |\n    | -------------- | ------------------------------- | -------------- |\n    |      unset     |              Any                |       Any      |\n    |     skipped    |         unset, skipped          |     skipped    |\n    |     skipped    |            success              |     success    |\n    |     skipped    |            failure              |     failure    |\n    |     success    |     unset, skipped, success     |     success    |\n    |     success    |            failure              |     failure    |\n    |     failure    | unset, skipped success, failure |     failure    |\n\n    If the status of the added test is error, the status is untouched and the\n    error_status is set to True.\n    \"\"\"\n    self._result_entries: list[TestResult] = []\n    # Initialize status\n    self.status: TestStatus = \"unset\"\n    self.error_status = False\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.add_test_result","title":"add_test_result","text":"
add_test_result(entry: TestResult) -> None\n

Add a result to the list.

Args:
entry (TestResult): TestResult data to add to the report\n
Source code in anta/result_manager/__init__.py
def add_test_result(self, entry: TestResult) -> None:\n    \"\"\"Add a result to the list.\n\n    Args:\n    ----\n        entry (TestResult): TestResult data to add to the report\n\n    \"\"\"\n    logger.debug(entry)\n    self._result_entries.append(entry)\n    self._update_status(entry.result)\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.add_test_results","title":"add_test_results","text":"
add_test_results(entries: list[TestResult]) -> None\n

Add a list of results to the list.

Args:
entries (list[TestResult]): List of TestResult data to add to the report\n
Source code in anta/result_manager/__init__.py
def add_test_results(self, entries: list[TestResult]) -> None:\n    \"\"\"Add a list of results to the list.\n\n    Args:\n    ----\n        entries (list[TestResult]): List of TestResult data to add to the report\n\n    \"\"\"\n    for e in entries:\n        self.add_test_result(e)\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_hosts","title":"get_hosts","text":"
get_hosts() -> list[str]\n

Get list of IP addresses in current manager.

Returns:

Type Description list[str]: List of IP addresses. Source code in anta/result_manager/__init__.py
def get_hosts(self) -> list[str]:\n    \"\"\"Get list of IP addresses in current manager.\n\n    Returns\n    -------\n        list[str]: List of IP addresses.\n\n    \"\"\"\n    result_list = []\n    for testcase in self._result_entries:\n        if str(testcase.name) not in result_list:\n            result_list.append(str(testcase.name))\n    return result_list\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_json_results","title":"get_json_results","text":"
get_json_results() -> str\n

Expose list of all test results in JSON.

Returns:

Type Description str: JSON dumps of the list of results Source code in anta/result_manager/__init__.py
def get_json_results(self) -> str:\n    \"\"\"Expose list of all test results in JSON.\n\n    Returns\n    -------\n        str: JSON dumps of the list of results\n\n    \"\"\"\n    result = [result.model_dump() for result in self._result_entries]\n    return json.dumps(result, indent=4)\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_result_by_host","title":"get_result_by_host","text":"
get_result_by_host(host_ip: str) -> list[TestResult]\n

Get list of test result for a given host.

Args:
host_ip (str): IP Address of the host to use to filter results.\n

Returns:

Type Description list[TestResult]: List of results related to the host. Source code in anta/result_manager/__init__.py
def get_result_by_host(self, host_ip: str) -> list[TestResult]:\n    \"\"\"Get list of test result for a given host.\n\n    Args:\n    ----\n        host_ip (str): IP Address of the host to use to filter results.\n\n    Returns\n    -------\n        list[TestResult]: List of results related to the host.\n\n    \"\"\"\n    return [result for result in self._result_entries if str(result.name) == host_ip]\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_result_by_test","title":"get_result_by_test","text":"
get_result_by_test(test_name: str) -> list[TestResult]\n

Get list of test result for a given test.

Args:
test_name (str): Test name to use to filter results\n

Returns:

Type Description list[TestResult]: List of results related to the test. Source code in anta/result_manager/__init__.py
def get_result_by_test(self, test_name: str) -> list[TestResult]:\n    \"\"\"Get list of test result for a given test.\n\n    Args:\n    ----\n        test_name (str): Test name to use to filter results\n\n    Returns\n    -------\n        list[TestResult]: List of results related to the test.\n\n    \"\"\"\n    return [result for result in self._result_entries if str(result.test) == test_name]\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_results","title":"get_results","text":"
get_results() -> list[TestResult]\n

Expose list of all test results in different format.

Returns:

Type Description any: List of results. Source code in anta/result_manager/__init__.py
def get_results(self) -> list[TestResult]:\n    \"\"\"Expose list of all test results in different format.\n\n    Returns\n    -------\n        any: List of results.\n\n    \"\"\"\n    return self._result_entries\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_status","title":"get_status","text":"
get_status(*, ignore_error: bool = False) -> str\n

Return the current status including error_status if ignore_error is False.

Source code in anta/result_manager/__init__.py
def get_status(self, *, ignore_error: bool = False) -> str:\n    \"\"\"Return the current status including error_status if ignore_error is False.\"\"\"\n    return \"error\" if self.error_status and not ignore_error else self.status\n
"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_testcases","title":"get_testcases","text":"
get_testcases() -> list[str]\n

Get list of name of all test cases in current manager.

Returns:

Type Description list[str]: List of names for all tests. Source code in anta/result_manager/__init__.py
def get_testcases(self) -> list[str]:\n    \"\"\"Get list of name of all test cases in current manager.\n\n    Returns\n    -------\n        list[str]: List of names for all tests.\n\n    \"\"\"\n    result_list = []\n    for testcase in self._result_entries:\n        if str(testcase.test) not in result_list:\n            result_list.append(str(testcase.test))\n    return result_list\n
"},{"location":"api/result_manager_models/","title":"Result Manager models","text":""},{"location":"api/result_manager_models/#test-result-model","title":"Test Result model","text":""},{"location":"api/result_manager_models/#uml-diagram","title":"UML Diagram","text":""},{"location":"api/result_manager_models/#anta.result_manager.models.TestResult","title":"TestResult","text":"

Bases: BaseModel

Describe the result of a test from a single device.

Attributes:

Name Type Description name Device name where the test has run.

test: Test name runs on the device. categories: List of categories the TestResult belongs to, by default the AntaTest categories. description: TestResult description, by default the AntaTest description. result: Result of the test. Can be one of \u201cunset\u201d, \u201csuccess\u201d, \u201cfailure\u201d, \u201cerror\u201d or \u201cskipped\u201d. messages: Message to report after the test if any. custom_field: Custom field to store a string for flexibility in integrating with ANTA

"},{"location":"api/result_manager_models/#anta.result_manager.models.TestResult.is_error","title":"is_error","text":"
is_error(message: str | None = None) -> None\n

Set status to error.

Args:
message: Optional message related to the test\n
Source code in anta/result_manager/models.py
def is_error(self, message: str | None = None) -> None:\n    \"\"\"Set status to error.\n\n    Args:\n    ----\n        message: Optional message related to the test\n\n    \"\"\"\n    self._set_status(\"error\", message)\n
"},{"location":"api/result_manager_models/#anta.result_manager.models.TestResult.is_failure","title":"is_failure","text":"
is_failure(message: str | None = None) -> None\n

Set status to failure.

Args:
message: Optional message related to the test\n
Source code in anta/result_manager/models.py
def is_failure(self, message: str | None = None) -> None:\n    \"\"\"Set status to failure.\n\n    Args:\n    ----\n        message: Optional message related to the test\n\n    \"\"\"\n    self._set_status(\"failure\", message)\n
"},{"location":"api/result_manager_models/#anta.result_manager.models.TestResult.is_skipped","title":"is_skipped","text":"
is_skipped(message: str | None = None) -> None\n

Set status to skipped.

Args:
message: Optional message related to the test\n
Source code in anta/result_manager/models.py
def is_skipped(self, message: str | None = None) -> None:\n    \"\"\"Set status to skipped.\n\n    Args:\n    ----\n        message: Optional message related to the test\n\n    \"\"\"\n    self._set_status(\"skipped\", message)\n
"},{"location":"api/result_manager_models/#anta.result_manager.models.TestResult.is_success","title":"is_success","text":"
is_success(message: str | None = None) -> None\n

Set status to success.

Args:
message: Optional message related to the test\n
Source code in anta/result_manager/models.py
def is_success(self, message: str | None = None) -> None:\n    \"\"\"Set status to success.\n\n    Args:\n    ----\n        message: Optional message related to the test\n\n    \"\"\"\n    self._set_status(\"success\", message)\n
"},{"location":"api/tests.aaa/","title":"AAA","text":""},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAcctConsoleMethods","title":"VerifyAcctConsoleMethods","text":"

Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x).

Expected Results
  • Success: The test will pass if the provided AAA accounting console method list is matching in the configured accounting types.
  • Failure: The test will fail if the provided AAA accounting console method list is NOT matching in the configured accounting types.
Examples
anta.tests.aaa:\n  - VerifyAcctConsoleMethods:\n      methods:\n        - local\n        - none\n        - logging\n      types:\n        - system\n        - exec\n        - commands\n        - dot1x\n
Source code in anta/tests/aaa.py
class VerifyAcctConsoleMethods(AntaTest):\n    \"\"\"Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided AAA accounting console method list is matching in the configured accounting types.\n    * Failure: The test will fail if the provided AAA accounting console method list is NOT matching in the configured accounting types.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyAcctConsoleMethods:\n          methods:\n            - local\n            - none\n            - logging\n          types:\n            - system\n            - exec\n            - commands\n            - dot1x\n    ```\n    \"\"\"\n\n    name = \"VerifyAcctConsoleMethods\"\n    description = \"Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x).\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyAcctConsoleMethods test.\"\"\"\n\n        methods: list[AAAAuthMethod]\n        \"\"\"List of AAA accounting console methods. Methods should be in the right order.\"\"\"\n        types: set[Literal[\"commands\", \"exec\", \"system\", \"dot1x\"]]\n        \"\"\"List of accounting console types to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAcctConsoleMethods.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        not_matching = []\n        not_configured = []\n        for k, v in command_output.items():\n            acct_type = k.replace(\"AcctMethods\", \"\")\n            if acct_type not in self.inputs.types:\n                # We do not need to verify this accounting type\n                continue\n            for methods in v.values():\n                if \"consoleAction\" not in methods:\n                    not_configured.append(acct_type)\n                if methods[\"consoleMethods\"] != self.inputs.methods:\n                    not_matching.append(acct_type)\n        if not_configured:\n            self.result.is_failure(f\"AAA console accounting is not configured for {not_configured}\")\n            return\n        if not not_matching:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"AAA accounting console methods {self.inputs.methods} are not matching for {not_matching}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAcctConsoleMethods-attributes","title":"Inputs","text":"Name Type Description Default methods list[AAAAuthMethod] List of AAA accounting console methods. Methods should be in the right order. - types set[Literal['commands', 'exec', 'system', 'dot1x']] List of accounting console types to verify. -"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAcctDefaultMethods","title":"VerifyAcctDefaultMethods","text":"

Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x).

Expected Results
  • Success: The test will pass if the provided AAA accounting default method list is matching in the configured accounting types.
  • Failure: The test will fail if the provided AAA accounting default method list is NOT matching in the configured accounting types.
Examples
anta.tests.aaa:\n  - VerifyAcctDefaultMethods:\n      methods:\n        - local\n        - none\n        - logging\n      types:\n        - system\n        - exec\n        - commands\n        - dot1x\n
Source code in anta/tests/aaa.py
class VerifyAcctDefaultMethods(AntaTest):\n    \"\"\"Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided AAA accounting default method list is matching in the configured accounting types.\n    * Failure: The test will fail if the provided AAA accounting default method list is NOT matching in the configured accounting types.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyAcctDefaultMethods:\n          methods:\n            - local\n            - none\n            - logging\n          types:\n            - system\n            - exec\n            - commands\n            - dot1x\n    ```\n    \"\"\"\n\n    name = \"VerifyAcctDefaultMethods\"\n    description = \"Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x).\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyAcctDefaultMethods test.\"\"\"\n\n        methods: list[AAAAuthMethod]\n        \"\"\"List of AAA accounting methods. Methods should be in the right order.\"\"\"\n        types: set[Literal[\"commands\", \"exec\", \"system\", \"dot1x\"]]\n        \"\"\"List of accounting types to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAcctDefaultMethods.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        not_matching = []\n        not_configured = []\n        for k, v in command_output.items():\n            acct_type = k.replace(\"AcctMethods\", \"\")\n            if acct_type not in self.inputs.types:\n                # We do not need to verify this accounting type\n                continue\n            for methods in v.values():\n                if \"defaultAction\" not in methods:\n                    not_configured.append(acct_type)\n                if methods[\"defaultMethods\"] != self.inputs.methods:\n                    not_matching.append(acct_type)\n        if not_configured:\n            self.result.is_failure(f\"AAA default accounting is not configured for {not_configured}\")\n            return\n        if not not_matching:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"AAA accounting default methods {self.inputs.methods} are not matching for {not_matching}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAcctDefaultMethods-attributes","title":"Inputs","text":"Name Type Description Default methods list[AAAAuthMethod] List of AAA accounting methods. Methods should be in the right order. - types set[Literal['commands', 'exec', 'system', 'dot1x']] List of accounting types to verify. -"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAuthenMethods","title":"VerifyAuthenMethods","text":"

Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x).

Expected Results
  • Success: The test will pass if the provided AAA authentication method list is matching in the configured authentication types.
  • Failure: The test will fail if the provided AAA authentication method list is NOT matching in the configured authentication types.
Examples
anta.tests.aaa:\n  - VerifyAuthenMethods:\n    methods:\n      - local\n      - none\n      - logging\n    types:\n      - login\n      - enable\n      - dot1x\n
Source code in anta/tests/aaa.py
class VerifyAuthenMethods(AntaTest):\n    \"\"\"Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided AAA authentication method list is matching in the configured authentication types.\n    * Failure: The test will fail if the provided AAA authentication method list is NOT matching in the configured authentication types.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyAuthenMethods:\n        methods:\n          - local\n          - none\n          - logging\n        types:\n          - login\n          - enable\n          - dot1x\n    ```\n    \"\"\"\n\n    name = \"VerifyAuthenMethods\"\n    description = \"Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x).\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authentication\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyAuthenMethods test.\"\"\"\n\n        methods: list[AAAAuthMethod]\n        \"\"\"List of AAA authentication methods. Methods should be in the right order.\"\"\"\n        types: set[Literal[\"login\", \"enable\", \"dot1x\"]]\n        \"\"\"List of authentication types to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAuthenMethods.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        not_matching: list[str] = []\n        for k, v in command_output.items():\n            auth_type = k.replace(\"AuthenMethods\", \"\")\n            if auth_type not in self.inputs.types:\n                # We do not need to verify this accounting type\n                continue\n            if auth_type == \"login\":\n                if \"login\" not in v:\n                    self.result.is_failure(\"AAA authentication methods are not configured for login console\")\n                    return\n                if v[\"login\"][\"methods\"] != self.inputs.methods:\n                    self.result.is_failure(f\"AAA authentication methods {self.inputs.methods} are not matching for login console\")\n                    return\n            not_matching.extend(auth_type for methods in v.values() if methods[\"methods\"] != self.inputs.methods)\n\n        if not not_matching:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"AAA authentication methods {self.inputs.methods} are not matching for {not_matching}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAuthenMethods-attributes","title":"Inputs","text":"Name Type Description Default methods list[AAAAuthMethod] List of AAA authentication methods. Methods should be in the right order. - types set[Literal['login', 'enable', 'dot1x']] List of authentication types to verify. -"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAuthzMethods","title":"VerifyAuthzMethods","text":"

Verifies the AAA authorization method lists for different authorization types (commands, exec).

Expected Results
  • Success: The test will pass if the provided AAA authorization method list is matching in the configured authorization types.
  • Failure: The test will fail if the provided AAA authorization method list is NOT matching in the configured authorization types.
Examples
anta.tests.aaa:\n  - VerifyAuthzMethods:\n      methods:\n        - local\n        - none\n        - logging\n      types:\n        - commands\n        - exec\n
Source code in anta/tests/aaa.py
class VerifyAuthzMethods(AntaTest):\n    \"\"\"Verifies the AAA authorization method lists for different authorization types (commands, exec).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided AAA authorization method list is matching in the configured authorization types.\n    * Failure: The test will fail if the provided AAA authorization method list is NOT matching in the configured authorization types.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyAuthzMethods:\n          methods:\n            - local\n            - none\n            - logging\n          types:\n            - commands\n            - exec\n    ```\n    \"\"\"\n\n    name = \"VerifyAuthzMethods\"\n    description = \"Verifies the AAA authorization method lists for different authorization types (commands, exec).\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authorization\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyAuthzMethods test.\"\"\"\n\n        methods: list[AAAAuthMethod]\n        \"\"\"List of AAA authorization methods. Methods should be in the right order.\"\"\"\n        types: set[Literal[\"commands\", \"exec\"]]\n        \"\"\"List of authorization types to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAuthzMethods.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        not_matching: list[str] = []\n        for k, v in command_output.items():\n            authz_type = k.replace(\"AuthzMethods\", \"\")\n            if authz_type not in self.inputs.types:\n                # We do not need to verify this accounting type\n                continue\n            not_matching.extend(authz_type for methods in v.values() if methods[\"methods\"] != self.inputs.methods)\n\n        if not not_matching:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"AAA authorization methods {self.inputs.methods} are not matching for {not_matching}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyAuthzMethods-attributes","title":"Inputs","text":"Name Type Description Default methods list[AAAAuthMethod] List of AAA authorization methods. Methods should be in the right order. - types set[Literal['commands', 'exec']] List of authorization types to verify. -"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsServerGroups","title":"VerifyTacacsServerGroups","text":"

Verifies if the provided TACACS server group(s) are configured.

Expected Results
  • Success: The test will pass if the provided TACACS server group(s) are configured.
  • Failure: The test will fail if one or all the provided TACACS server group(s) are NOT configured.
Examples
anta.tests.aaa:\n  - VerifyTacacsServerGroups:\n      groups:\n        - TACACS-GROUP1\n        - TACACS-GROUP2\n
Source code in anta/tests/aaa.py
class VerifyTacacsServerGroups(AntaTest):\n    \"\"\"Verifies if the provided TACACS server group(s) are configured.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided TACACS server group(s) are configured.\n    * Failure: The test will fail if one or all the provided TACACS server group(s) are NOT configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyTacacsServerGroups:\n          groups:\n            - TACACS-GROUP1\n            - TACACS-GROUP2\n    ```\n    \"\"\"\n\n    name = \"VerifyTacacsServerGroups\"\n    description = \"Verifies if the provided TACACS server group(s) are configured.\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTacacsServerGroups test.\"\"\"\n\n        groups: list[str]\n        \"\"\"List of TACACS server groups.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTacacsServerGroups.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        tacacs_groups = command_output[\"groups\"]\n        if not tacacs_groups:\n            self.result.is_failure(\"No TACACS server group(s) are configured\")\n            return\n        not_configured = [group for group in self.inputs.groups if group not in tacacs_groups]\n        if not not_configured:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"TACACS server group(s) {not_configured} are not configured\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsServerGroups-attributes","title":"Inputs","text":"Name Type Description Default groups list[str] List of TACACS server groups. -"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsServers","title":"VerifyTacacsServers","text":"

Verifies TACACS servers are configured for a specified VRF.

Expected Results
  • Success: The test will pass if the provided TACACS servers are configured in the specified VRF.
  • Failure: The test will fail if the provided TACACS servers are NOT configured in the specified VRF.
Examples
anta.tests.aaa:\n  - VerifyTacacsServers:\n      servers:\n        - 10.10.10.21\n        - 10.10.10.22\n      vrf: MGMT\n
Source code in anta/tests/aaa.py
class VerifyTacacsServers(AntaTest):\n    \"\"\"Verifies TACACS servers are configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided TACACS servers are configured in the specified VRF.\n    * Failure: The test will fail if the provided TACACS servers are NOT configured in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyTacacsServers:\n          servers:\n            - 10.10.10.21\n            - 10.10.10.22\n          vrf: MGMT\n    ```\n    \"\"\"\n\n    name = \"VerifyTacacsServers\"\n    description = \"Verifies TACACS servers are configured for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTacacsServers test.\"\"\"\n\n        servers: list[IPv4Address]\n        \"\"\"List of TACACS servers.\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF to transport TACACS messages. Defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTacacsServers.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        tacacs_servers = command_output[\"tacacsServers\"]\n        if not tacacs_servers:\n            self.result.is_failure(\"No TACACS servers are configured\")\n            return\n        not_configured = [\n            str(server)\n            for server in self.inputs.servers\n            if not any(\n                str(server) == tacacs_server[\"serverInfo\"][\"hostname\"] and self.inputs.vrf == tacacs_server[\"serverInfo\"][\"vrf\"] for tacacs_server in tacacs_servers\n            )\n        ]\n        if not not_configured:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"TACACS servers {not_configured} are not configured in VRF {self.inputs.vrf}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsServers-attributes","title":"Inputs","text":"Name Type Description Default servers list[IPv4Address] List of TACACS servers. - vrf str The name of the VRF to transport TACACS messages. Defaults to `default`. 'default'"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsSourceIntf","title":"VerifyTacacsSourceIntf","text":"

Verifies TACACS source-interface for a specified VRF.

Expected Results
  • Success: The test will pass if the provided TACACS source-interface is configured in the specified VRF.
  • Failure: The test will fail if the provided TACACS source-interface is NOT configured in the specified VRF.
Examples
anta.tests.aaa:\n  - VerifyTacacsSourceIntf:\n      intf: Management0\n      vrf: MGMT\n
Source code in anta/tests/aaa.py
class VerifyTacacsSourceIntf(AntaTest):\n    \"\"\"Verifies TACACS source-interface for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided TACACS source-interface is configured in the specified VRF.\n    * Failure: The test will fail if the provided TACACS source-interface is NOT configured in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.aaa:\n      - VerifyTacacsSourceIntf:\n          intf: Management0\n          vrf: MGMT\n    ```\n    \"\"\"\n\n    name = \"VerifyTacacsSourceIntf\"\n    description = \"Verifies TACACS source-interface for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"aaa\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTacacsSourceIntf test.\"\"\"\n\n        intf: str\n        \"\"\"Source-interface to use as source IP of TACACS messages.\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF to transport TACACS messages. Defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTacacsSourceIntf.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        try:\n            if command_output[\"srcIntf\"][self.inputs.vrf] == self.inputs.intf:\n                self.result.is_success()\n            else:\n                self.result.is_failure(f\"Wrong source-interface configured in VRF {self.inputs.vrf}\")\n        except KeyError:\n            self.result.is_failure(f\"Source-interface {self.inputs.intf} is not configured in VRF {self.inputs.vrf}\")\n
"},{"location":"api/tests.aaa/#anta.tests.aaa.VerifyTacacsSourceIntf-attributes","title":"Inputs","text":"Name Type Description Default intf str Source-interface to use as source IP of TACACS messages. - vrf str The name of the VRF to transport TACACS messages. Defaults to `default`. 'default'"},{"location":"api/tests.bfd/","title":"BFD","text":""},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersHealth","title":"VerifyBFDPeersHealth","text":"

Verifies the health of IPv4 BFD peers across all VRFs.

It checks that no BFD peer is in the down state and that the discriminator value of the remote system is not zero.

Optionally, it can also verify that BFD peers have not been down before a specified threshold of hours.

Expected Results
  • Success: The test will pass if all IPv4 BFD peers are up, the discriminator value of each remote system is non-zero, and the last downtime of each peer is above the defined threshold.
  • Failure: The test will fail if any IPv4 BFD peer is down, the discriminator value of any remote system is zero, or the last downtime of any peer is below the defined threshold.
Examples
anta.tests.bfd:\n  - VerifyBFDPeersHealth:\n      down_threshold: 2\n
Source code in anta/tests/bfd.py
class VerifyBFDPeersHealth(AntaTest):\n    \"\"\"Verifies the health of IPv4 BFD peers across all VRFs.\n\n    It checks that no BFD peer is in the down state and that the discriminator value of the remote system is not zero.\n\n    Optionally, it can also verify that BFD peers have not been down before a specified threshold of hours.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all IPv4 BFD peers are up, the discriminator value of each remote system is non-zero,\n               and the last downtime of each peer is above the defined threshold.\n    * Failure: The test will fail if any IPv4 BFD peer is down, the discriminator value of any remote system is zero,\n               or the last downtime of any peer is below the defined threshold.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.bfd:\n      - VerifyBFDPeersHealth:\n          down_threshold: 2\n    ```\n    \"\"\"\n\n    name = \"VerifyBFDPeersHealth\"\n    description = \"Verifies the health of all IPv4 BFD peers.\"\n    categories: ClassVar[list[str]] = [\"bfd\"]\n    # revision 1 as later revision introduces additional nesting for type\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaCommand(command=\"show bfd peers\", revision=1),\n        AntaCommand(command=\"show clock\"),\n    ]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBFDPeersHealth test.\"\"\"\n\n        down_threshold: int | None = Field(default=None, gt=0)\n        \"\"\"Optional down threshold in hours to check if a BFD peer was down before those hours or not.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBFDPeersHealth.\"\"\"\n        # Initialize failure strings\n        down_failures = []\n        up_failures = []\n\n        # Extract the current timestamp and command output\n        clock_output = self.instance_commands[1].json_output\n        current_timestamp = clock_output[\"utcTime\"]\n        bfd_output = self.instance_commands[0].json_output\n\n        # set the initial result\n        self.result.is_success()\n\n        # Check if any IPv4 BFD peer is configured\n        ipv4_neighbors_exist = any(vrf_data[\"ipv4Neighbors\"] for vrf_data in bfd_output[\"vrfs\"].values())\n        if not ipv4_neighbors_exist:\n            self.result.is_failure(\"No IPv4 BFD peers are configured for any VRF.\")\n            return\n\n        # Iterate over IPv4 BFD peers\n        for vrf, vrf_data in bfd_output[\"vrfs\"].items():\n            for peer, neighbor_data in vrf_data[\"ipv4Neighbors\"].items():\n                for peer_data in neighbor_data[\"peerStats\"].values():\n                    peer_status = peer_data[\"status\"]\n                    remote_disc = peer_data[\"remoteDisc\"]\n                    remote_disc_info = f\" with remote disc {remote_disc}\" if remote_disc == 0 else \"\"\n                    last_down = peer_data[\"lastDown\"]\n                    hours_difference = (\n                        datetime.fromtimestamp(current_timestamp, tz=timezone.utc) - datetime.fromtimestamp(last_down, tz=timezone.utc)\n                    ).total_seconds() / 3600\n\n                    # Check if peer status is not up\n                    if peer_status != \"up\":\n                        down_failures.append(f\"{peer} is {peer_status} in {vrf} VRF{remote_disc_info}.\")\n\n                    # Check if the last down is within the threshold\n                    elif self.inputs.down_threshold and hours_difference < self.inputs.down_threshold:\n                        up_failures.append(f\"{peer} in {vrf} VRF was down {round(hours_difference)} hours ago{remote_disc_info}.\")\n\n                    # Check if remote disc is 0\n                    elif remote_disc == 0:\n                        up_failures.append(f\"{peer} in {vrf} VRF has remote disc {remote_disc}.\")\n\n        # Check if there are any failures\n        if down_failures:\n            down_failures_str = \"\\n\".join(down_failures)\n            self.result.is_failure(f\"Following BFD peers are not up:\\n{down_failures_str}\")\n        if up_failures:\n            up_failures_str = \"\\n\".join(up_failures)\n            self.result.is_failure(f\"\\nFollowing BFD peers were down:\\n{up_failures_str}\")\n
"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersHealth-attributes","title":"Inputs","text":"Name Type Description Default down_threshold int | None Optional down threshold in hours to check if a BFD peer was down before those hours or not. Field(default=None, gt=0)"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersIntervals","title":"VerifyBFDPeersIntervals","text":"

Verifies the timers of the IPv4 BFD peers in the specified VRF.

Expected Results
  • Success: The test will pass if the timers of the IPv4 BFD peers are correct in the specified VRF.
  • Failure: The test will fail if the IPv4 BFD peers are not found or their timers are incorrect in the specified VRF.
Examples
anta.tests.bfd:\n  - VerifyBFDPeersIntervals:\n      bfd_peers:\n        - peer_address: 192.0.255.8\n          vrf: default\n          tx_interval: 1200\n          rx_interval: 1200\n          multiplier: 3\n        - peer_address: 192.0.255.7\n          vrf: default\n          tx_interval: 1200\n          rx_interval: 1200\n          multiplier: 3\n
Source code in anta/tests/bfd.py
class VerifyBFDPeersIntervals(AntaTest):\n    \"\"\"Verifies the timers of the IPv4 BFD peers in the specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the timers of the IPv4 BFD peers are correct in the specified VRF.\n    * Failure: The test will fail if the IPv4 BFD peers are not found or their timers are incorrect in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.bfd:\n      - VerifyBFDPeersIntervals:\n          bfd_peers:\n            - peer_address: 192.0.255.8\n              vrf: default\n              tx_interval: 1200\n              rx_interval: 1200\n              multiplier: 3\n            - peer_address: 192.0.255.7\n              vrf: default\n              tx_interval: 1200\n              rx_interval: 1200\n              multiplier: 3\n    ```\n    \"\"\"\n\n    name = \"VerifyBFDPeersIntervals\"\n    description = \"Verifies the timers of the IPv4 BFD peers in the specified VRF.\"\n    categories: ClassVar[list[str]] = [\"bfd\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBFDPeersIntervals test.\"\"\"\n\n        bfd_peers: list[BFDPeer]\n        \"\"\"List of BFD peers.\"\"\"\n\n        class BFDPeer(BaseModel):\n            \"\"\"Model for an IPv4 BFD peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BFD peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BFD peer. If not provided, it defaults to `default`.\"\"\"\n            tx_interval: BfdInterval\n            \"\"\"Tx interval of BFD peer in milliseconds.\"\"\"\n            rx_interval: BfdInterval\n            \"\"\"Rx interval of BFD peer in milliseconds.\"\"\"\n            multiplier: BfdMultiplier\n            \"\"\"Multiplier of BFD peer.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBFDPeersIntervals.\"\"\"\n        failures: dict[Any, Any] = {}\n\n        # Iterating over BFD peers\n        for bfd_peers in self.inputs.bfd_peers:\n            peer = str(bfd_peers.peer_address)\n            vrf = bfd_peers.vrf\n\n            # Converting milliseconds intervals into actual value\n            tx_interval = bfd_peers.tx_interval * 1000\n            rx_interval = bfd_peers.rx_interval * 1000\n            multiplier = bfd_peers.multiplier\n            bfd_output = get_value(\n                self.instance_commands[0].json_output,\n                f\"vrfs..{vrf}..ipv4Neighbors..{peer}..peerStats..\",\n                separator=\"..\",\n            )\n\n            # Check if BFD peer configured\n            if not bfd_output:\n                failures[peer] = {vrf: \"Not Configured\"}\n                continue\n\n            bfd_details = bfd_output.get(\"peerStatsDetail\", {})\n            intervals_ok = (\n                bfd_details.get(\"operTxInterval\") == tx_interval and bfd_details.get(\"operRxInterval\") == rx_interval and bfd_details.get(\"detectMult\") == multiplier\n            )\n\n            # Check timers of BFD peer\n            if not intervals_ok:\n                failures[peer] = {\n                    vrf: {\n                        \"tx_interval\": bfd_details.get(\"operTxInterval\"),\n                        \"rx_interval\": bfd_details.get(\"operRxInterval\"),\n                        \"multiplier\": bfd_details.get(\"detectMult\"),\n                    }\n                }\n\n        # Check if any failures\n        if not failures:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BFD peers are not configured or timers are not correct:\\n{failures}\")\n
"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersIntervals-attributes","title":"Inputs","text":"Name Type Description Default bfd_peers list[BFDPeer] List of BFD peers. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersIntervals-attributes","title":"BFDPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BFD peer. - vrf str Optional VRF for BFD peer. If not provided, it defaults to `default`. 'default' tx_interval BfdInterval Tx interval of BFD peer in milliseconds. - rx_interval BfdInterval Rx interval of BFD peer in milliseconds. - multiplier BfdMultiplier Multiplier of BFD peer. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDSpecificPeers","title":"VerifyBFDSpecificPeers","text":"

Verifies if the IPv4 BFD peer\u2019s sessions are UP and remote disc is non-zero in the specified VRF.

Expected Results
  • Success: The test will pass if IPv4 BFD peers are up and remote disc is non-zero in the specified VRF.
  • Failure: The test will fail if IPv4 BFD peers are not found, the status is not UP or remote disc is zero in the specified VRF.
Examples
anta.tests.bfd:\n  - VerifyBFDSpecificPeers:\n      bfd_peers:\n        - peer_address: 192.0.255.8\n          vrf: default\n        - peer_address: 192.0.255.7\n          vrf: default\n
Source code in anta/tests/bfd.py
class VerifyBFDSpecificPeers(AntaTest):\n    \"\"\"Verifies if the IPv4 BFD peer's sessions are UP and remote disc is non-zero in the specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if IPv4 BFD peers are up and remote disc is non-zero in the specified VRF.\n    * Failure: The test will fail if IPv4 BFD peers are not found, the status is not UP or remote disc is zero in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.bfd:\n      - VerifyBFDSpecificPeers:\n          bfd_peers:\n            - peer_address: 192.0.255.8\n              vrf: default\n            - peer_address: 192.0.255.7\n              vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyBFDSpecificPeers\"\n    description = \"Verifies the IPv4 BFD peer's sessions and remote disc in the specified VRF.\"\n    categories: ClassVar[list[str]] = [\"bfd\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBFDSpecificPeers test.\"\"\"\n\n        bfd_peers: list[BFDPeer]\n        \"\"\"List of IPv4 BFD peers.\"\"\"\n\n        class BFDPeer(BaseModel):\n            \"\"\"Model for an IPv4 BFD peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BFD peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BFD peer. If not provided, it defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBFDSpecificPeers.\"\"\"\n        failures: dict[Any, Any] = {}\n\n        # Iterating over BFD peers\n        for bfd_peer in self.inputs.bfd_peers:\n            peer = str(bfd_peer.peer_address)\n            vrf = bfd_peer.vrf\n            bfd_output = get_value(\n                self.instance_commands[0].json_output,\n                f\"vrfs..{vrf}..ipv4Neighbors..{peer}..peerStats..\",\n                separator=\"..\",\n            )\n\n            # Check if BFD peer configured\n            if not bfd_output:\n                failures[peer] = {vrf: \"Not Configured\"}\n                continue\n\n            # Check BFD peer status and remote disc\n            if not (bfd_output.get(\"status\") == \"up\" and bfd_output.get(\"remoteDisc\") != 0):\n                failures[peer] = {\n                    vrf: {\n                        \"status\": bfd_output.get(\"status\"),\n                        \"remote_disc\": bfd_output.get(\"remoteDisc\"),\n                    }\n                }\n\n        if not failures:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BFD peers are not configured, status is not up or remote disc is zero:\\n{failures}\")\n
"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDSpecificPeers-attributes","title":"Inputs","text":"Name Type Description Default bfd_peers list[BFDPeer] List of IPv4 BFD peers. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDSpecificPeers-attributes","title":"BFDPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BFD peer. - vrf str Optional VRF for BFD peer. If not provided, it defaults to `default`. 'default'"},{"location":"api/tests.configuration/","title":"Configuration","text":""},{"location":"api/tests.configuration/#anta.tests.configuration.VerifyRunningConfigDiffs","title":"VerifyRunningConfigDiffs","text":"

Verifies there is no difference between the running-config and the startup-config.

Expected Results
  • Success: The test will pass if there is no difference between the running-config and the startup-config.
  • Failure: The test will fail if there is a difference between the running-config and the startup-config.
Examples
anta.tests.configuration:\n  - VerifyRunningConfigDiffs:\n
Source code in anta/tests/configuration.py
class VerifyRunningConfigDiffs(AntaTest):\n    \"\"\"Verifies there is no difference between the running-config and the startup-config.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there is no difference between the running-config and the startup-config.\n    * Failure: The test will fail if there is a difference between the running-config and the startup-config.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.configuration:\n      - VerifyRunningConfigDiffs:\n    ```\n    \"\"\"\n\n    name = \"VerifyRunningConfigDiffs\"\n    description = \"Verifies there is no difference between the running-config and the startup-config\"\n    categories: ClassVar[list[str]] = [\"configuration\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show running-config diffs\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyRunningConfigDiffs.\"\"\"\n        command_output = self.instance_commands[0].text_output\n        if command_output == \"\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(command_output)\n
"},{"location":"api/tests.configuration/#anta.tests.configuration.VerifyZeroTouch","title":"VerifyZeroTouch","text":"

Verifies ZeroTouch is disabled.

Expected Results
  • Success: The test will pass if ZeroTouch is disabled.
  • Failure: The test will fail if ZeroTouch is enabled.
Examples
anta.tests.configuration:\n  - VerifyZeroTouch:\n
Source code in anta/tests/configuration.py
class VerifyZeroTouch(AntaTest):\n    \"\"\"Verifies ZeroTouch is disabled.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if ZeroTouch is disabled.\n    * Failure: The test will fail if ZeroTouch is enabled.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.configuration:\n      - VerifyZeroTouch:\n    ```\n    \"\"\"\n\n    name = \"VerifyZeroTouch\"\n    description = \"Verifies ZeroTouch is disabled\"\n    categories: ClassVar[list[str]] = [\"configuration\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show zerotouch\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyZeroTouch.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"mode\"] == \"disabled\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"ZTP is NOT disabled\")\n
"},{"location":"api/tests.connectivity/","title":"Connectivity","text":""},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors","title":"VerifyLLDPNeighbors","text":"

Verifies that the provided LLDP neighbors are present and connected with the correct configuration.

Expected Results
  • Success: The test will pass if each of the provided LLDP neighbors is present and connected to the specified port and device.
  • Failure: The test will fail if any of the following conditions are met:
    • The provided LLDP neighbor is not found.
    • The system name or port of the LLDP neighbor does not match the provided information.
Examples
anta.tests.connectivity:\n  - VerifyLLDPNeighbors:\n      neighbors:\n        - port: Ethernet1\n          neighbor_device: DC1-SPINE1\n          neighbor_port: Ethernet1\n        - port: Ethernet2\n          neighbor_device: DC1-SPINE2\n          neighbor_port: Ethernet1\n
Source code in anta/tests/connectivity.py
class VerifyLLDPNeighbors(AntaTest):\n    \"\"\"Verifies that the provided LLDP neighbors are present and connected with the correct configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if each of the provided LLDP neighbors is present and connected to the specified port and device.\n    * Failure: The test will fail if any of the following conditions are met:\n        - The provided LLDP neighbor is not found.\n        - The system name or port of the LLDP neighbor does not match the provided information.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.connectivity:\n      - VerifyLLDPNeighbors:\n          neighbors:\n            - port: Ethernet1\n              neighbor_device: DC1-SPINE1\n              neighbor_port: Ethernet1\n            - port: Ethernet2\n              neighbor_device: DC1-SPINE2\n              neighbor_port: Ethernet1\n    ```\n    \"\"\"\n\n    name = \"VerifyLLDPNeighbors\"\n    description = \"Verifies that the provided LLDP neighbors are connected properly.\"\n    categories: ClassVar[list[str]] = [\"connectivity\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show lldp neighbors detail\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyLLDPNeighbors test.\"\"\"\n\n        neighbors: list[Neighbor]\n        \"\"\"List of LLDP neighbors.\"\"\"\n\n        class Neighbor(BaseModel):\n            \"\"\"Model for an LLDP neighbor.\"\"\"\n\n            port: Interface\n            \"\"\"LLDP port.\"\"\"\n            neighbor_device: str\n            \"\"\"LLDP neighbor device.\"\"\"\n            neighbor_port: Interface\n            \"\"\"LLDP neighbor port.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLLDPNeighbors.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        failures: dict[str, list[str]] = {}\n\n        for neighbor in self.inputs.neighbors:\n            if neighbor.port not in command_output[\"lldpNeighbors\"]:\n                failures.setdefault(\"port_not_configured\", []).append(neighbor.port)\n            elif len(lldp_neighbor_info := command_output[\"lldpNeighbors\"][neighbor.port][\"lldpNeighborInfo\"]) == 0:\n                failures.setdefault(\"no_lldp_neighbor\", []).append(neighbor.port)\n            elif (\n                lldp_neighbor_info[0][\"systemName\"] != neighbor.neighbor_device\n                or lldp_neighbor_info[0][\"neighborInterfaceInfo\"][\"interfaceId_v2\"] != neighbor.neighbor_port\n            ):\n                failures.setdefault(\"wrong_lldp_neighbor\", []).append(neighbor.port)\n\n        if not failures:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following port(s) have issues: {failures}\")\n
"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors-attributes","title":"Inputs","text":"Name Type Description Default neighbors list[Neighbor] List of LLDP neighbors. -"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors-attributes","title":"Neighbor","text":"Name Type Description Default port Interface LLDP port. - neighbor_device str LLDP neighbor device. - neighbor_port Interface LLDP neighbor port. -"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyReachability","title":"VerifyReachability","text":"

Test network reachability to one or many destination IP(s).

Expected Results
  • Success: The test will pass if all destination IP(s) are reachable.
  • Failure: The test will fail if one or many destination IP(s) are unreachable.
Examples
anta.tests.connectivity:\n  - VerifyReachability:\n      hosts:\n        - source: Management0\n          destination: 1.1.1.1\n          vrf: MGMT\n        - source: Management0\n          destination: 8.8.8.8\n          vrf: MGMT\n
Source code in anta/tests/connectivity.py
class VerifyReachability(AntaTest):\n    \"\"\"Test network reachability to one or many destination IP(s).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all destination IP(s) are reachable.\n    * Failure: The test will fail if one or many destination IP(s) are unreachable.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.connectivity:\n      - VerifyReachability:\n          hosts:\n            - source: Management0\n              destination: 1.1.1.1\n              vrf: MGMT\n            - source: Management0\n              destination: 8.8.8.8\n              vrf: MGMT\n    ```\n    \"\"\"\n\n    name = \"VerifyReachability\"\n    description = \"Test the network reachability to one or many destination IP(s).\"\n    categories: ClassVar[list[str]] = [\"connectivity\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"ping vrf {vrf} {destination} source {source} repeat {repeat}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyReachability test.\"\"\"\n\n        hosts: list[Host]\n        \"\"\"List of host to ping.\"\"\"\n\n        class Host(BaseModel):\n            \"\"\"Model for a remote host to ping.\"\"\"\n\n            destination: IPv4Address\n            \"\"\"IPv4 address to ping.\"\"\"\n            source: IPv4Address | Interface\n            \"\"\"IPv4 address source IP or egress interface to use.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"VRF context. Defaults to `default`.\"\"\"\n            repeat: int = 2\n            \"\"\"Number of ping repetition. Defaults to 2.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each host in the input list.\"\"\"\n        return [template.render(destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat) for host in self.inputs.hosts]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyReachability.\"\"\"\n        failures = []\n        for command in self.instance_commands:\n            src = command.params.get(\"source\")\n            dst = command.params.get(\"destination\")\n            repeat = command.params.get(\"repeat\")\n\n            if any(elem is None for elem in (src, dst, repeat)):\n                msg = f\"A parameter is missing to execute the test for command {command}\"\n                raise AntaMissingParamError(msg)\n\n            if f\"{repeat} received\" not in command.json_output[\"messages\"][0]:\n                failures.append((str(src), str(dst)))\n\n        if not failures:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Connectivity test failed for the following source-destination pairs: {failures}\")\n
"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyReachability-attributes","title":"Inputs","text":"Name Type Description Default hosts list[Host] List of host to ping. -"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyReachability-attributes","title":"Host","text":"Name Type Description Default destination IPv4Address IPv4 address to ping. - source IPv4Address | Interface IPv4 address source IP or egress interface to use. - vrf str VRF context. Defaults to `default`. 'default' repeat int Number of ping repetition. Defaults to 2. 2"},{"location":"api/tests.field_notices/","title":"Field Notices","text":""},{"location":"api/tests.field_notices/#anta.tests.field_notices.VerifyFieldNotice44Resolution","title":"VerifyFieldNotice44Resolution","text":"

Verifies if the device is using an Aboot version that fixes the bug discussed in the Field Notice 44.

Aboot manages system settings prior to EOS initialization.

Reference: https://www.arista.com/en/support/advisories-notices/field-notice/8756-field-notice-44

Expected Results
  • Success: The test will pass if the device is using an Aboot version that fixes the bug discussed in the Field Notice 44.
  • Failure: The test will fail if the device is not using an Aboot version that fixes the bug discussed in the Field Notice 44.
Examples
anta.tests.field_notices:\n  - VerifyFieldNotice44Resolution:\n
Source code in anta/tests/field_notices.py
class VerifyFieldNotice44Resolution(AntaTest):\n    \"\"\"Verifies if the device is using an Aboot version that fixes the bug discussed in the Field Notice 44.\n\n    Aboot manages system settings prior to EOS initialization.\n\n    Reference: https://www.arista.com/en/support/advisories-notices/field-notice/8756-field-notice-44\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is using an Aboot version that fixes the bug discussed in the Field Notice 44.\n    * Failure: The test will fail if the device is not using an Aboot version that fixes the bug discussed in the Field Notice 44.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.field_notices:\n      - VerifyFieldNotice44Resolution:\n    ```\n    \"\"\"\n\n    name = \"VerifyFieldNotice44Resolution\"\n    description = \"Verifies that the device is using the correct Aboot version per FN0044.\"\n    categories: ClassVar[list[str]] = [\"field notices\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version detail\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyFieldNotice44Resolution.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        devices = [\n            \"DCS-7010T-48\",\n            \"DCS-7010T-48-DC\",\n            \"DCS-7050TX-48\",\n            \"DCS-7050TX-64\",\n            \"DCS-7050TX-72\",\n            \"DCS-7050TX-72Q\",\n            \"DCS-7050TX-96\",\n            \"DCS-7050TX2-128\",\n            \"DCS-7050SX-64\",\n            \"DCS-7050SX-72\",\n            \"DCS-7050SX-72Q\",\n            \"DCS-7050SX2-72Q\",\n            \"DCS-7050SX-96\",\n            \"DCS-7050SX2-128\",\n            \"DCS-7050QX-32S\",\n            \"DCS-7050QX2-32S\",\n            \"DCS-7050SX3-48YC12\",\n            \"DCS-7050CX3-32S\",\n            \"DCS-7060CX-32S\",\n            \"DCS-7060CX2-32S\",\n            \"DCS-7060SX2-48YC6\",\n            \"DCS-7160-48YC6\",\n            \"DCS-7160-48TC6\",\n            \"DCS-7160-32CQ\",\n            \"DCS-7280SE-64\",\n            \"DCS-7280SE-68\",\n            \"DCS-7280SE-72\",\n            \"DCS-7150SC-24-CLD\",\n            \"DCS-7150SC-64-CLD\",\n            \"DCS-7020TR-48\",\n            \"DCS-7020TRA-48\",\n            \"DCS-7020SR-24C2\",\n            \"DCS-7020SRG-24C2\",\n            \"DCS-7280TR-48C6\",\n            \"DCS-7280TRA-48C6\",\n            \"DCS-7280SR-48C6\",\n            \"DCS-7280SRA-48C6\",\n            \"DCS-7280SRAM-48C6\",\n            \"DCS-7280SR2K-48C6-M\",\n            \"DCS-7280SR2-48YC6\",\n            \"DCS-7280SR2A-48YC6\",\n            \"DCS-7280SRM-40CX2\",\n            \"DCS-7280QR-C36\",\n            \"DCS-7280QRA-C36S\",\n        ]\n        variants = [\"-SSD-F\", \"-SSD-R\", \"-M-F\", \"-M-R\", \"-F\", \"-R\"]\n\n        model = command_output[\"modelName\"]\n        for variant in variants:\n            model = model.replace(variant, \"\")\n        if model not in devices:\n            self.result.is_skipped(\"device is not impacted by FN044\")\n            return\n\n        for component in command_output[\"details\"][\"components\"]:\n            if component[\"name\"] == \"Aboot\":\n                aboot_version = component[\"version\"].split(\"-\")[2]\n        self.result.is_success()\n        incorrect_aboot_version = (\n            aboot_version.startswith(\"4.0.\")\n            and int(aboot_version.split(\".\")[2]) < 7\n            or aboot_version.startswith(\"4.1.\")\n            and int(aboot_version.split(\".\")[2]) < 1\n            or (\n                aboot_version.startswith(\"6.0.\")\n                and int(aboot_version.split(\".\")[2]) < 9\n                or aboot_version.startswith(\"6.1.\")\n                and int(aboot_version.split(\".\")[2]) < 7\n            )\n        )\n        if incorrect_aboot_version:\n            self.result.is_failure(f\"device is running incorrect version of aboot ({aboot_version})\")\n
"},{"location":"api/tests.field_notices/#anta.tests.field_notices.VerifyFieldNotice72Resolution","title":"VerifyFieldNotice72Resolution","text":"

Verifies if the device is potentially exposed to Field Notice 72, and if the issue has been mitigated.

Reference: https://www.arista.com/en/support/advisories-notices/field-notice/17410-field-notice-0072

Expected Results
  • Success: The test will pass if the device is not exposed to FN72 and the issue has been mitigated.
  • Failure: The test will fail if the device is exposed to FN72 and the issue has not been mitigated.
Examples
anta.tests.field_notices:\n  - VerifyFieldNotice72Resolution:\n
Source code in anta/tests/field_notices.py
class VerifyFieldNotice72Resolution(AntaTest):\n    \"\"\"Verifies if the device is potentially exposed to Field Notice 72, and if the issue has been mitigated.\n\n    Reference: https://www.arista.com/en/support/advisories-notices/field-notice/17410-field-notice-0072\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is not exposed to FN72 and the issue has been mitigated.\n    * Failure: The test will fail if the device is exposed to FN72 and the issue has not been mitigated.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.field_notices:\n      - VerifyFieldNotice72Resolution:\n    ```\n    \"\"\"\n\n    name = \"VerifyFieldNotice72Resolution\"\n    description = \"Verifies if the device is exposed to FN0072, and if the issue has been mitigated.\"\n    categories: ClassVar[list[str]] = [\"field notices\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version detail\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyFieldNotice72Resolution.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        devices = [\"DCS-7280SR3-48YC8\", \"DCS-7280SR3K-48YC8\"]\n        variants = [\"-SSD-F\", \"-SSD-R\", \"-M-F\", \"-M-R\", \"-F\", \"-R\"]\n        model = command_output[\"modelName\"]\n\n        for variant in variants:\n            model = model.replace(variant, \"\")\n        if model not in devices:\n            self.result.is_skipped(\"Platform is not impacted by FN072\")\n            return\n\n        serial = command_output[\"serialNumber\"]\n        number = int(serial[3:7])\n\n        if \"JPE\" not in serial and \"JAS\" not in serial:\n            self.result.is_skipped(\"Device not exposed\")\n            return\n\n        if model == \"DCS-7280SR3-48YC8\" and \"JPE\" in serial and number >= 2131:\n            self.result.is_skipped(\"Device not exposed\")\n            return\n\n        if model == \"DCS-7280SR3-48YC8\" and \"JAS\" in serial and number >= 2041:\n            self.result.is_skipped(\"Device not exposed\")\n            return\n\n        if model == \"DCS-7280SR3K-48YC8\" and \"JPE\" in serial and number >= 2134:\n            self.result.is_skipped(\"Device not exposed\")\n            return\n\n        if model == \"DCS-7280SR3K-48YC8\" and \"JAS\" in serial and number >= 2041:\n            self.result.is_skipped(\"Device not exposed\")\n            return\n\n        # Because each of the if checks above will return if taken, we only run the long check if we get this far\n        for entry in command_output[\"details\"][\"components\"]:\n            if entry[\"name\"] == \"FixedSystemvrm1\":\n                if int(entry[\"version\"]) < 7:\n                    self.result.is_failure(\"Device is exposed to FN72\")\n                else:\n                    self.result.is_success(\"FN72 is mitigated\")\n                return\n        # We should never hit this point\n        self.result.is_error(\"Error in running test - FixedSystemvrm1 not found\")\n        return\n
"},{"location":"api/tests.greent/","title":"GreenT","text":""},{"location":"api/tests.greent/#anta.tests.greent.VerifyGreenT","title":"VerifyGreenT","text":"

Verifies if a GreenT (GRE Encapsulated Telemetry) policy other than the default is created.

Expected Results
  • Success: The test will pass if a GreenT policy is created other than the default one.
  • Failure: The test will fail if no other GreenT policy is created.
Examples
anta.tests.greent:\n  - VerifyGreenTCounters:\n
Source code in anta/tests/greent.py
class VerifyGreenT(AntaTest):\n    \"\"\"Verifies if a GreenT (GRE Encapsulated Telemetry) policy other than the default is created.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if a GreenT policy is created other than the default one.\n    * Failure: The test will fail if no other GreenT policy is created.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.greent:\n      - VerifyGreenTCounters:\n    ```\n    \"\"\"\n\n    name = \"VerifyGreenT\"\n    description = \"Verifies if a GreenT policy is created.\"\n    categories: ClassVar[list[str]] = [\"greent\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show monitor telemetry postcard policy profile\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyGreenT.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        profiles = [profile for profile in command_output[\"profiles\"] if profile != \"default\"]\n\n        if profiles:\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"No GreenT policy is created\")\n
"},{"location":"api/tests.greent/#anta.tests.greent.VerifyGreenTCounters","title":"VerifyGreenTCounters","text":"

Verifies if the GreenT (GRE Encapsulated Telemetry) counters are incremented.

Expected Results
  • Success: The test will pass if the GreenT counters are incremented.
  • Failure: The test will fail if the GreenT counters are not incremented.
Examples
anta.tests.greent:\n  - VerifyGreenT:\n
Source code in anta/tests/greent.py
class VerifyGreenTCounters(AntaTest):\n    \"\"\"Verifies if the GreenT (GRE Encapsulated Telemetry) counters are incremented.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the GreenT counters are incremented.\n    * Failure: The test will fail if the GreenT counters are not incremented.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.greent:\n      - VerifyGreenT:\n    ```\n    \"\"\"\n\n    name = \"VerifyGreenTCounters\"\n    description = \"Verifies if the GreenT counters are incremented.\"\n    categories: ClassVar[list[str]] = [\"greent\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show monitor telemetry postcard counters\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyGreenTCounters.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        if command_output[\"grePktSent\"] > 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"GreenT counters are not incremented\")\n
"},{"location":"api/tests.hardware/","title":"Hardware","text":""},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyAdverseDrops","title":"VerifyAdverseDrops","text":"

Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches (Arad/Jericho chips).

Expected Results
  • Success: The test will pass if there are no adverse drops.
  • Failure: The test will fail if there are adverse drops.
Examples
anta.tests.hardware:\n  - VerifyAdverseDrops:\n
Source code in anta/tests/hardware.py
class VerifyAdverseDrops(AntaTest):\n    \"\"\"Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches (Arad/Jericho chips).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are no adverse drops.\n    * Failure: The test will fail if there are adverse drops.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyAdverseDrops:\n    ```\n    \"\"\"\n\n    name = \"VerifyAdverseDrops\"\n    description = \"Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hardware counter drop\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAdverseDrops.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        total_adverse_drop = command_output.get(\"totalAdverseDrops\", \"\")\n        if total_adverse_drop == 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device totalAdverseDrops counter is: '{total_adverse_drop}'\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyEnvironmentCooling","title":"VerifyEnvironmentCooling","text":"

Verifies the status of power supply fans and all fan trays.

Expected Results
  • Success: The test will pass if the fans status are within the accepted states list.
  • Failure: The test will fail if some fans status is not within the accepted states list.
Examples
anta.tests.hardware:\n  - VerifyEnvironmentCooling:\n      states:\n        - ok\n
Source code in anta/tests/hardware.py
class VerifyEnvironmentCooling(AntaTest):\n    \"\"\"Verifies the status of power supply fans and all fan trays.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the fans status are within the accepted states list.\n    * Failure: The test will fail if some fans status is not within the accepted states list.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyEnvironmentCooling:\n          states:\n            - ok\n    ```\n    \"\"\"\n\n    name = \"VerifyEnvironmentCooling\"\n    description = \"Verifies the status of power supply fans and all fan trays.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment cooling\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyEnvironmentCooling test.\"\"\"\n\n        states: list[str]\n        \"\"\"List of accepted states of fan status.\"\"\"\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEnvironmentCooling.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        self.result.is_success()\n        # First go through power supplies fans\n        for power_supply in command_output.get(\"powerSupplySlots\", []):\n            for fan in power_supply.get(\"fans\", []):\n                if (state := fan[\"status\"]) not in self.inputs.states:\n                    self.result.is_failure(f\"Fan {fan['label']} on PowerSupply {power_supply['label']} is: '{state}'\")\n        # Then go through fan trays\n        for fan_tray in command_output.get(\"fanTraySlots\", []):\n            for fan in fan_tray.get(\"fans\", []):\n                if (state := fan[\"status\"]) not in self.inputs.states:\n                    self.result.is_failure(f\"Fan {fan['label']} on Fan Tray {fan_tray['label']} is: '{state}'\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyEnvironmentCooling-attributes","title":"Inputs","text":"Name Type Description Default states list[str] List of accepted states of fan status. -"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyEnvironmentPower","title":"VerifyEnvironmentPower","text":"

Verifies the power supplies status.

Expected Results
  • Success: The test will pass if the power supplies status are within the accepted states list.
  • Failure: The test will fail if some power supplies status is not within the accepted states list.
Examples
anta.tests.hardware:\n  - VerifyEnvironmentPower:\n      states:\n        - ok\n
Source code in anta/tests/hardware.py
class VerifyEnvironmentPower(AntaTest):\n    \"\"\"Verifies the power supplies status.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the power supplies status are within the accepted states list.\n    * Failure: The test will fail if some power supplies status is not within the accepted states list.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyEnvironmentPower:\n          states:\n            - ok\n    ```\n    \"\"\"\n\n    name = \"VerifyEnvironmentPower\"\n    description = \"Verifies the power supplies status.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment power\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyEnvironmentPower test.\"\"\"\n\n        states: list[str]\n        \"\"\"List of accepted states list of power supplies status.\"\"\"\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEnvironmentPower.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        power_supplies = command_output.get(\"powerSupplies\", \"{}\")\n        wrong_power_supplies = {\n            powersupply: {\"state\": value[\"state\"]} for powersupply, value in dict(power_supplies).items() if value[\"state\"] not in self.inputs.states\n        }\n        if not wrong_power_supplies:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following power supplies status are not in the accepted states list: {wrong_power_supplies}\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyEnvironmentPower-attributes","title":"Inputs","text":"Name Type Description Default states list[str] List of accepted states list of power supplies status. -"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyEnvironmentSystemCooling","title":"VerifyEnvironmentSystemCooling","text":"

Verifies the device\u2019s system cooling status.

Expected Results
  • Success: The test will pass if the system cooling status is OK: \u2018coolingOk\u2019.
  • Failure: The test will fail if the system cooling status is NOT OK.
Examples
anta.tests.hardware:\n  - VerifyEnvironmentSystemCooling:\n
Source code in anta/tests/hardware.py
class VerifyEnvironmentSystemCooling(AntaTest):\n    \"\"\"Verifies the device's system cooling status.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the system cooling status is OK: 'coolingOk'.\n    * Failure: The test will fail if the system cooling status is NOT OK.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyEnvironmentSystemCooling:\n    ```\n    \"\"\"\n\n    name = \"VerifyEnvironmentSystemCooling\"\n    description = \"Verifies the system cooling status.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment cooling\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEnvironmentSystemCooling.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        sys_status = command_output.get(\"systemStatus\", \"\")\n        self.result.is_success()\n        if sys_status != \"coolingOk\":\n            self.result.is_failure(f\"Device system cooling is not OK: '{sys_status}'\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyTemperature","title":"VerifyTemperature","text":"

Verifies if the device temperature is within acceptable limits.

Expected Results
  • Success: The test will pass if the device temperature is currently OK: \u2018temperatureOk\u2019.
  • Failure: The test will fail if the device temperature is NOT OK.
Examples
anta.tests.hardware:\n  - VerifyTemperature:\n
Source code in anta/tests/hardware.py
class VerifyTemperature(AntaTest):\n    \"\"\"Verifies if the device temperature is within acceptable limits.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device temperature is currently OK: 'temperatureOk'.\n    * Failure: The test will fail if the device temperature is NOT OK.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyTemperature:\n    ```\n    \"\"\"\n\n    name = \"VerifyTemperature\"\n    description = \"Verifies the device temperature.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTemperature.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        temperature_status = command_output.get(\"systemStatus\", \"\")\n        if temperature_status == \"temperatureOk\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device temperature exceeds acceptable limits. Current system status: '{temperature_status}'\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyTransceiversManufacturers","title":"VerifyTransceiversManufacturers","text":"

Verifies if all the transceivers come from approved manufacturers.

Expected Results
  • Success: The test will pass if all transceivers are from approved manufacturers.
  • Failure: The test will fail if some transceivers are from unapproved manufacturers.
Examples
anta.tests.hardware:\n  - VerifyTransceiversManufacturers:\n      manufacturers:\n        - Not Present\n        - Arista Networks\n        - Arastra, Inc.\n
Source code in anta/tests/hardware.py
class VerifyTransceiversManufacturers(AntaTest):\n    \"\"\"Verifies if all the transceivers come from approved manufacturers.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all transceivers are from approved manufacturers.\n    * Failure: The test will fail if some transceivers are from unapproved manufacturers.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyTransceiversManufacturers:\n          manufacturers:\n            - Not Present\n            - Arista Networks\n            - Arastra, Inc.\n    ```\n    \"\"\"\n\n    name = \"VerifyTransceiversManufacturers\"\n    description = \"Verifies if all transceivers come from approved manufacturers.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show inventory\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTransceiversManufacturers test.\"\"\"\n\n        manufacturers: list[str]\n        \"\"\"List of approved transceivers manufacturers.\"\"\"\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTransceiversManufacturers.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        wrong_manufacturers = {\n            interface: value[\"mfgName\"] for interface, value in command_output[\"xcvrSlots\"].items() if value[\"mfgName\"] not in self.inputs.manufacturers\n        }\n        if not wrong_manufacturers:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Some transceivers are from unapproved manufacturers: {wrong_manufacturers}\")\n
"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyTransceiversManufacturers-attributes","title":"Inputs","text":"Name Type Description Default manufacturers list[str] List of approved transceivers manufacturers. -"},{"location":"api/tests.hardware/#anta.tests.hardware.VerifyTransceiversTemperature","title":"VerifyTransceiversTemperature","text":"

Verifies if all the transceivers are operating at an acceptable temperature.

Expected Results
  • Success: The test will pass if all transceivers status are OK: \u2018ok\u2019.
  • Failure: The test will fail if some transceivers are NOT OK.
Examples
anta.tests.hardware:\n  - VerifyTransceiversTemperature:\n
Source code in anta/tests/hardware.py
class VerifyTransceiversTemperature(AntaTest):\n    \"\"\"Verifies if all the transceivers are operating at an acceptable temperature.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all transceivers status are OK: 'ok'.\n    * Failure: The test will fail if some transceivers are NOT OK.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.hardware:\n      - VerifyTransceiversTemperature:\n    ```\n    \"\"\"\n\n    name = \"VerifyTransceiversTemperature\"\n    description = \"Verifies the transceivers temperature.\"\n    categories: ClassVar[list[str]] = [\"hardware\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature transceiver\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTransceiversTemperature.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        sensors = command_output.get(\"tempSensors\", \"\")\n        wrong_sensors = {\n            sensor[\"name\"]: {\n                \"hwStatus\": sensor[\"hwStatus\"],\n                \"alertCount\": sensor[\"alertCount\"],\n            }\n            for sensor in sensors\n            if sensor[\"hwStatus\"] != \"ok\" or sensor[\"alertCount\"] != 0\n        }\n        if not wrong_sensors:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following sensors are operating outside the acceptable temperature range or have raised alerts: {wrong_sensors}\")\n
"},{"location":"api/tests.interfaces/","title":"Interfaces","text":""},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyIPProxyARP","title":"VerifyIPProxyARP","text":"

Verifies if Proxy-ARP is enabled for the provided list of interface(s).

Expected Results
  • Success: The test will pass if Proxy-ARP is enabled on the specified interface(s).
  • Failure: The test will fail if Proxy-ARP is disabled on the specified interface(s).
Examples
anta.tests.interfaces:\n  - VerifyIPProxyARP:\n      interfaces:\n        - Ethernet1\n        - Ethernet2\n
Source code in anta/tests/interfaces.py
class VerifyIPProxyARP(AntaTest):\n    \"\"\"Verifies if Proxy-ARP is enabled for the provided list of interface(s).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if Proxy-ARP is enabled on the specified interface(s).\n    * Failure: The test will fail if Proxy-ARP is disabled on the specified interface(s).\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyIPProxyARP:\n          interfaces:\n            - Ethernet1\n            - Ethernet2\n    ```\n    \"\"\"\n\n    name = \"VerifyIPProxyARP\"\n    description = \"Verifies if Proxy ARP is enabled.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip interface {intf}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyIPProxyARP test.\"\"\"\n\n        interfaces: list[str]\n        \"\"\"List of interfaces to be tested.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each interface in the input list.\"\"\"\n        return [template.render(intf=intf) for intf in self.inputs.interfaces]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIPProxyARP.\"\"\"\n        disabled_intf = []\n        for command in self.instance_commands:\n            if \"intf\" in command.params:\n                intf = command.params[\"intf\"]\n            if not command.json_output[\"interfaces\"][intf][\"proxyArp\"]:\n                disabled_intf.append(intf)\n        if disabled_intf:\n            self.result.is_failure(f\"The following interface(s) have Proxy-ARP disabled: {disabled_intf}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyIPProxyARP-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[str] List of interfaces to be tested. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyIllegalLACP","title":"VerifyIllegalLACP","text":"

Verifies there are no illegal LACP packets in all port channels.

Expected Results
  • Success: The test will pass if there are no illegal LACP packets received.
  • Failure: The test will fail if there is at least one illegal LACP packet received.
Examples
anta.tests.interfaces:\n  - VerifyIllegalLACP:\n
Source code in anta/tests/interfaces.py
class VerifyIllegalLACP(AntaTest):\n    \"\"\"Verifies there are no illegal LACP packets in all port channels.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are no illegal LACP packets received.\n    * Failure: The test will fail if there is at least one illegal LACP packet received.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyIllegalLACP:\n    ```\n    \"\"\"\n\n    name = \"VerifyIllegalLACP\"\n    description = \"Verifies there are no illegal LACP packets in all port channels.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show lacp counters all-ports\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIllegalLACP.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        po_with_illegal_lacp: list[dict[str, dict[str, int]]] = []\n        for portchannel, portchannel_dict in command_output[\"portChannels\"].items():\n            po_with_illegal_lacp.extend(\n                {portchannel: interface} for interface, interface_dict in portchannel_dict[\"interfaces\"].items() if interface_dict[\"illegalRxCount\"] != 0\n            )\n        if not po_with_illegal_lacp:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following port-channels have received illegal LACP packets on the following ports: {po_with_illegal_lacp}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceDiscards","title":"VerifyInterfaceDiscards","text":"

Verifies that the interfaces packet discard counters are equal to zero.

Expected Results
  • Success: The test will pass if all interfaces have discard counters equal to zero.
  • Failure: The test will fail if one or more interfaces have non-zero discard counters.
Examples
anta.tests.interfaces:\n  - VerifyInterfaceDiscards:\n
Source code in anta/tests/interfaces.py
class VerifyInterfaceDiscards(AntaTest):\n    \"\"\"Verifies that the interfaces packet discard counters are equal to zero.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all interfaces have discard counters equal to zero.\n    * Failure: The test will fail if one or more interfaces have non-zero discard counters.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfaceDiscards:\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfaceDiscards\"\n    description = \"Verifies there are no interface discard counters.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters discards\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfaceDiscards.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        wrong_interfaces: list[dict[str, dict[str, int]]] = []\n        for interface, outer_v in command_output[\"interfaces\"].items():\n            wrong_interfaces.extend({interface: outer_v} for value in outer_v.values() if value > 0)\n        if not wrong_interfaces:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following interfaces have non 0 discard counter(s): {wrong_interfaces}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceErrDisabled","title":"VerifyInterfaceErrDisabled","text":"

Verifies there are no interfaces in the errdisabled state.

Expected Results
  • Success: The test will pass if there are no interfaces in the errdisabled state.
  • Failure: The test will fail if there is at least one interface in the errdisabled state.
Examples
anta.tests.interfaces:\n  - VerifyInterfaceErrDisabled:\n
Source code in anta/tests/interfaces.py
class VerifyInterfaceErrDisabled(AntaTest):\n    \"\"\"Verifies there are no interfaces in the errdisabled state.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are no interfaces in the errdisabled state.\n    * Failure: The test will fail if there is at least one interface in the errdisabled state.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfaceErrDisabled:\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfaceErrDisabled\"\n    description = \"Verifies there are no interfaces in the errdisabled state.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces status\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfaceErrDisabled.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        errdisabled_interfaces = [interface for interface, value in command_output[\"interfaceStatuses\"].items() if value[\"linkStatus\"] == \"errdisabled\"]\n        if errdisabled_interfaces:\n            self.result.is_failure(f\"The following interfaces are in error disabled state: {errdisabled_interfaces}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceErrors","title":"VerifyInterfaceErrors","text":"

Verifies that the interfaces error counters are equal to zero.

Expected Results
  • Success: The test will pass if all interfaces have error counters equal to zero.
  • Failure: The test will fail if one or more interfaces have non-zero error counters.
Examples
anta.tests.interfaces:\n  - VerifyInterfaceErrors:\n
Source code in anta/tests/interfaces.py
class VerifyInterfaceErrors(AntaTest):\n    \"\"\"Verifies that the interfaces error counters are equal to zero.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all interfaces have error counters equal to zero.\n    * Failure: The test will fail if one or more interfaces have non-zero error counters.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfaceErrors:\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfaceErrors\"\n    description = \"Verifies there are no interface error counters.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters errors\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfaceErrors.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        wrong_interfaces: list[dict[str, dict[str, int]]] = []\n        for interface, counters in command_output[\"interfaceErrorCounters\"].items():\n            if any(value > 0 for value in counters.values()) and all(interface not in wrong_interface for wrong_interface in wrong_interfaces):\n                wrong_interfaces.append({interface: counters})\n        if not wrong_interfaces:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following interface(s) have non-zero error counters: {wrong_interfaces}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceIPv4","title":"VerifyInterfaceIPv4","text":"

Verifies if an interface is configured with a correct primary and list of optional secondary IPv4 addresses.

Expected Results
  • Success: The test will pass if an interface is configured with a correct primary and secondary IPv4 address.
  • Failure: The test will fail if an interface is not found or the primary and secondary IPv4 addresses do not match with the input.
Examples
anta.tests.interfaces:\n  - VerifyInterfaceIPv4:\n      interfaces:\n        - name: Ethernet2\n          primary_ip: 172.30.11.0/31\n          secondary_ips:\n            - 10.10.10.0/31\n            - 10.10.10.10/31\n
Source code in anta/tests/interfaces.py
class VerifyInterfaceIPv4(AntaTest):\n    \"\"\"Verifies if an interface is configured with a correct primary and list of optional secondary IPv4 addresses.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if an interface is configured with a correct primary and secondary IPv4 address.\n    * Failure: The test will fail if an interface is not found or the primary and secondary IPv4 addresses do not match with the input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfaceIPv4:\n          interfaces:\n            - name: Ethernet2\n              primary_ip: 172.30.11.0/31\n              secondary_ips:\n                - 10.10.10.0/31\n                - 10.10.10.10/31\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfaceIPv4\"\n    description = \"Verifies the interface IPv4 addresses.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip interface {interface}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyInterfaceIPv4 test.\"\"\"\n\n        interfaces: list[InterfaceDetail]\n        \"\"\"List of interfaces with their details.\"\"\"\n\n        class InterfaceDetail(BaseModel):\n            \"\"\"Model for an interface detail.\"\"\"\n\n            name: Interface\n            \"\"\"Name of the interface.\"\"\"\n            primary_ip: IPv4Network\n            \"\"\"Primary IPv4 address in CIDR notation.\"\"\"\n            secondary_ips: list[IPv4Network] | None = None\n            \"\"\"Optional list of secondary IPv4 addresses in CIDR notation.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each interface in the input list.\"\"\"\n        return [\n            template.render(interface=interface.name, primary_ip=interface.primary_ip, secondary_ips=interface.secondary_ips) for interface in self.inputs.interfaces\n        ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfaceIPv4.\"\"\"\n        self.result.is_success()\n        for command in self.instance_commands:\n            intf = command.params[\"interface\"]\n            input_primary_ip = str(command.params[\"primary_ip\"])\n            failed_messages = []\n\n            # Check if the interface has an IP address configured\n            if not (interface_output := get_value(command.json_output, f\"interfaces.{intf}.interfaceAddress\")):\n                self.result.is_failure(f\"For interface `{intf}`, IP address is not configured.\")\n                continue\n\n            primary_ip = get_value(interface_output, \"primaryIp\")\n\n            # Combine IP address and subnet for primary IP\n            actual_primary_ip = f\"{primary_ip['address']}/{primary_ip['maskLen']}\"\n\n            # Check if the primary IP address matches the input\n            if actual_primary_ip != input_primary_ip:\n                failed_messages.append(f\"The expected primary IP address is `{input_primary_ip}`, but the actual primary IP address is `{actual_primary_ip}`.\")\n\n            if command.params[\"secondary_ips\"] is not None:\n                input_secondary_ips = sorted([str(network) for network in command.params[\"secondary_ips\"]])\n                secondary_ips = get_value(interface_output, \"secondaryIpsOrderedList\")\n\n                # Combine IP address and subnet for secondary IPs\n                actual_secondary_ips = sorted([f\"{secondary_ip['address']}/{secondary_ip['maskLen']}\" for secondary_ip in secondary_ips])\n\n                # Check if the secondary IP address is configured\n                if not actual_secondary_ips:\n                    failed_messages.append(\n                        f\"The expected secondary IP addresses are `{input_secondary_ips}`, but the actual secondary IP address is not configured.\"\n                    )\n\n                # Check if the secondary IP addresses match the input\n                elif actual_secondary_ips != input_secondary_ips:\n                    failed_messages.append(\n                        f\"The expected secondary IP addresses are `{input_secondary_ips}`, but the actual secondary IP addresses are `{actual_secondary_ips}`.\"\n                    )\n\n            if failed_messages:\n                self.result.is_failure(f\"For interface `{intf}`, \" + \" \".join(failed_messages))\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceIPv4-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceDetail] List of interfaces with their details. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceIPv4-attributes","title":"InterfaceDetail","text":"Name Type Description Default name Interface Name of the interface. - primary_ip IPv4Network Primary IPv4 address in CIDR notation. - secondary_ips list[IPv4Network] | None Optional list of secondary IPv4 addresses in CIDR notation. None"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceUtilization","title":"VerifyInterfaceUtilization","text":"

Verifies that the utilization of interfaces is below a certain threshold.

Load interval (default to 5 minutes) is defined in device configuration.

Expected Results
  • Success: The test will pass if all interfaces have a usage below the threshold.
  • Failure: The test will fail if one or more interfaces have a usage above the threshold.
Examples
anta.tests.interfaces:\n  - VerifyInterfaceUtilization:\n      threshold: 70.0\n
Source code in anta/tests/interfaces.py
class VerifyInterfaceUtilization(AntaTest):\n    \"\"\"Verifies that the utilization of interfaces is below a certain threshold.\n\n    Load interval (default to 5 minutes) is defined in device configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all interfaces have a usage below the threshold.\n    * Failure: The test will fail if one or more interfaces have a usage above the threshold.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfaceUtilization:\n          threshold: 70.0\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfaceUtilization\"\n    description = \"Verifies that the utilization of interfaces is below a certain threshold.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters rates\"), AntaCommand(command=\"show interfaces\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyInterfaceUtilization test.\"\"\"\n\n        threshold: Percent = 75.0\n        \"\"\"Interface utilization threshold above which the test will fail. Defaults to 75%.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfaceUtilization.\"\"\"\n        duplex_full = \"duplexFull\"\n        failed_interfaces: dict[str, dict[str, float]] = {}\n        rates = self.instance_commands[0].json_output\n        interfaces = self.instance_commands[1].json_output\n\n        for intf, rate in rates[\"interfaces\"].items():\n            # Assuming the interface is full-duplex in the logic below\n            if \"duplex\" in interfaces[\"interfaces\"][intf]:\n                if interfaces[\"interfaces\"][intf][\"duplex\"] != duplex_full:\n                    self.result.is_error(f\"Interface {intf} is not Full-Duplex, VerifyInterfaceUtilization has not been implemented in ANTA\")\n                    return\n            elif \"memberInterfaces\" in interfaces[\"interfaces\"][intf]:\n                # This is a Port-Channel\n                for member, stats in interfaces[\"interfaces\"][intf][\"memberInterfaces\"].items():\n                    if stats[\"duplex\"] != duplex_full:\n                        self.result.is_error(f\"Member {member} of {intf} is not Full-Duplex, VerifyInterfaceUtilization has not been implemented in ANTA\")\n                        return\n\n            bandwidth = interfaces[\"interfaces\"][intf][\"bandwidth\"]\n\n            for bps_rate in (\"inBpsRate\", \"outBpsRate\"):\n                usage = rate[bps_rate] / bandwidth * 100\n                if usage > self.inputs.threshold:\n                    failed_interfaces.setdefault(intf, {})[bps_rate] = usage\n\n        if not failed_interfaces:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following interfaces have a usage > {self.inputs.threshold}%: {failed_interfaces}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfaceUtilization-attributes","title":"Inputs","text":"Name Type Description Default threshold Percent Interface utilization threshold above which the test will fail. Defaults to 75%. 75.0"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus","title":"VerifyInterfacesStatus","text":"

Verifies if the provided list of interfaces are all in the expected state.

  • If line protocol status is provided, prioritize checking against both status and line protocol status
  • If line protocol status is not provided and interface status is \u201cup\u201d, expect both status and line protocol to be \u201cup\u201d
  • If interface status is not \u201cup\u201d, check only the interface status without considering line protocol status
Expected Results
  • Success: The test will pass if the provided interfaces are all in the expected state.
  • Failure: The test will fail if any interface is not in the expected state.
Examples
anta.tests.interfaces:\n  - VerifyInterfacesStatus:\n      interfaces:\n        - name: Ethernet1\n          status: up\n        - name: Port-Channel100\n          status: down\n          line_protocol_status: lowerLayerDown\n        - name: Ethernet49/1\n          status: adminDown\n          line_protocol_status: notPresent\n
Source code in anta/tests/interfaces.py
class VerifyInterfacesStatus(AntaTest):\n    \"\"\"Verifies if the provided list of interfaces are all in the expected state.\n\n    - If line protocol status is provided, prioritize checking against both status and line protocol status\n    - If line protocol status is not provided and interface status is \"up\", expect both status and line protocol to be \"up\"\n    - If interface status is not \"up\", check only the interface status without considering line protocol status\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided interfaces are all in the expected state.\n    * Failure: The test will fail if any interface is not in the expected state.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyInterfacesStatus:\n          interfaces:\n            - name: Ethernet1\n              status: up\n            - name: Port-Channel100\n              status: down\n              line_protocol_status: lowerLayerDown\n            - name: Ethernet49/1\n              status: adminDown\n              line_protocol_status: notPresent\n    ```\n    \"\"\"\n\n    name = \"VerifyInterfacesStatus\"\n    description = \"Verifies the status of the provided interfaces.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyInterfacesStatus test.\"\"\"\n\n        interfaces: list[InterfaceState]\n        \"\"\"List of interfaces with their expected state.\"\"\"\n\n        class InterfaceState(BaseModel):\n            \"\"\"Model for an interface state.\"\"\"\n\n            name: Interface\n            \"\"\"Interface to validate.\"\"\"\n            status: Literal[\"up\", \"down\", \"adminDown\"]\n            \"\"\"Expected status of the interface.\"\"\"\n            line_protocol_status: Literal[\"up\", \"down\", \"testing\", \"unknown\", \"dormant\", \"notPresent\", \"lowerLayerDown\"] | None = None\n            \"\"\"Expected line protocol status of the interface.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyInterfacesStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        self.result.is_success()\n\n        intf_not_configured = []\n        intf_wrong_state = []\n\n        for interface in self.inputs.interfaces:\n            if (intf_status := get_value(command_output[\"interfaceDescriptions\"], interface.name, separator=\"..\")) is None:\n                intf_not_configured.append(interface.name)\n                continue\n\n            status = \"up\" if intf_status[\"interfaceStatus\"] in {\"up\", \"connected\"} else intf_status[\"interfaceStatus\"]\n            proto = \"up\" if intf_status[\"lineProtocolStatus\"] in {\"up\", \"connected\"} else intf_status[\"lineProtocolStatus\"]\n\n            # If line protocol status is provided, prioritize checking against both status and line protocol status\n            if interface.line_protocol_status:\n                if interface.status != status or interface.line_protocol_status != proto:\n                    intf_wrong_state.append(f\"{interface.name} is {status}/{proto}\")\n\n            # If line protocol status is not provided and interface status is \"up\", expect both status and proto to be \"up\"\n            # If interface status is not \"up\", check only the interface status without considering line protocol status\n            elif (interface.status == \"up\" and (status != \"up\" or proto != \"up\")) or (interface.status != status):\n                intf_wrong_state.append(f\"{interface.name} is {status}/{proto}\")\n\n        if intf_not_configured:\n            self.result.is_failure(f\"The following interface(s) are not configured: {intf_not_configured}\")\n\n        if intf_wrong_state:\n            self.result.is_failure(f\"The following interface(s) are not in the expected state: {intf_wrong_state}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceState] List of interfaces with their expected state. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus-attributes","title":"InterfaceState","text":"Name Type Description Default name Interface Interface to validate. - status Literal['up', 'down', 'adminDown'] Expected status of the interface. - line_protocol_status Literal['up', 'down', 'testing', 'unknown', 'dormant', 'notPresent', 'lowerLayerDown'] | None Expected line protocol status of the interface. None"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyIpVirtualRouterMac","title":"VerifyIpVirtualRouterMac","text":"

Verifies the IP virtual router MAC address.

Expected Results
  • Success: The test will pass if the IP virtual router MAC address matches the input.
  • Failure: The test will fail if the IP virtual router MAC address does not match the input.
Examples
anta.tests.interfaces:\n  - VerifyIpVirtualRouterMac:\n      mac_address: 00:1c:73:00:dc:01\n
Source code in anta/tests/interfaces.py
class VerifyIpVirtualRouterMac(AntaTest):\n    \"\"\"Verifies the IP virtual router MAC address.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the IP virtual router MAC address matches the input.\n    * Failure: The test will fail if the IP virtual router MAC address does not match the input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyIpVirtualRouterMac:\n          mac_address: 00:1c:73:00:dc:01\n    ```\n    \"\"\"\n\n    name = \"VerifyIpVirtualRouterMac\"\n    description = \"Verifies the IP virtual router MAC address.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip virtual-router\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyIpVirtualRouterMac test.\"\"\"\n\n        mac_address: MacAddress\n        \"\"\"IP virtual router MAC address.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIpVirtualRouterMac.\"\"\"\n        command_output = self.instance_commands[0].json_output[\"virtualMacs\"]\n        mac_address_found = get_item(command_output, \"macAddress\", self.inputs.mac_address)\n\n        if mac_address_found is None:\n            self.result.is_failure(f\"IP virtual router MAC address `{self.inputs.mac_address}` is not configured.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyIpVirtualRouterMac-attributes","title":"Inputs","text":"Name Type Description Default mac_address MacAddress IP virtual router MAC address. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyL2MTU","title":"VerifyL2MTU","text":"

Verifies the global layer 2 Maximum Transfer Unit (MTU) for all L2 interfaces.

Test that L2 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces. You can define a global MTU to check and also an MTU per interface and also ignored some interfaces.

Expected Results
  • Success: The test will pass if all layer 2 interfaces have the proper MTU configured.
  • Failure: The test will fail if one or many layer 2 interfaces have the wrong MTU configured.
Examples
anta.tests.interfaces:\n  - VerifyL2MTU:\n      mtu: 1500\n      ignored_interfaces:\n        - Management1\n        - Vxlan1\n      specific_mtu:\n        - Ethernet1/1: 1500\n
Source code in anta/tests/interfaces.py
class VerifyL2MTU(AntaTest):\n    \"\"\"Verifies the global layer 2 Maximum Transfer Unit (MTU) for all L2 interfaces.\n\n    Test that L2 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces.\n    You can define a global MTU to check and also an MTU per interface and also ignored some interfaces.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all layer 2 interfaces have the proper MTU configured.\n    * Failure: The test will fail if one or many layer 2 interfaces have the wrong MTU configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyL2MTU:\n          mtu: 1500\n          ignored_interfaces:\n            - Management1\n            - Vxlan1\n          specific_mtu:\n            - Ethernet1/1: 1500\n    ```\n    \"\"\"\n\n    name = \"VerifyL2MTU\"\n    description = \"Verifies the global L2 MTU of all L2 interfaces.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyL2MTU test.\"\"\"\n\n        mtu: int = 9214\n        \"\"\"Default MTU we should have configured on all non-excluded interfaces. Defaults to 9214.\"\"\"\n        ignored_interfaces: list[str] = Field(default=[\"Management\", \"Loopback\", \"Vxlan\", \"Tunnel\"])\n        \"\"\"A list of L2 interfaces to ignore. Defaults to [\"Management\", \"Loopback\", \"Vxlan\", \"Tunnel\"]\"\"\"\n        specific_mtu: list[dict[str, int]] = Field(default=[])\n        \"\"\"A list of dictionary of L2 interfaces with their specific MTU configured\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyL2MTU.\"\"\"\n        # Parameter to save incorrect interface settings\n        wrong_l2mtu_intf: list[dict[str, int]] = []\n        command_output = self.instance_commands[0].json_output\n        # Set list of interfaces with specific settings\n        specific_interfaces: list[str] = []\n        if self.inputs.specific_mtu:\n            for d in self.inputs.specific_mtu:\n                specific_interfaces.extend(d)\n        for interface, values in command_output[\"interfaces\"].items():\n            catch_interface = re.findall(r\"^[e,p][a-zA-Z]+[-,a-zA-Z]*\\d+\\/*\\d*\", interface, re.IGNORECASE)\n            if len(catch_interface) and catch_interface[0] not in self.inputs.ignored_interfaces and values[\"forwardingModel\"] == \"bridged\":\n                if interface in specific_interfaces:\n                    wrong_l2mtu_intf.extend({interface: values[\"mtu\"]} for custom_data in self.inputs.specific_mtu if values[\"mtu\"] != custom_data[interface])\n                # Comparison with generic setting\n                elif values[\"mtu\"] != self.inputs.mtu:\n                    wrong_l2mtu_intf.append({interface: values[\"mtu\"]})\n        if wrong_l2mtu_intf:\n            self.result.is_failure(f\"Some L2 interfaces do not have correct MTU configured:\\n{wrong_l2mtu_intf}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyL2MTU-attributes","title":"Inputs","text":"Name Type Description Default mtu int Default MTU we should have configured on all non-excluded interfaces. Defaults to 9214. 9214 ignored_interfaces list[str] A list of L2 interfaces to ignore. Defaults to [\"Management\", \"Loopback\", \"Vxlan\", \"Tunnel\"] Field(default=['Management', 'Loopback', 'Vxlan', 'Tunnel']) specific_mtu list[dict[str, int]] A list of dictionary of L2 interfaces with their specific MTU configured Field(default=[])"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyL3MTU","title":"VerifyL3MTU","text":"

Verifies the global layer 3 Maximum Transfer Unit (MTU) for all L3 interfaces.

Test that L3 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces.

You can define a global MTU to check, or an MTU per interface and you can also ignored some interfaces.

Expected Results
  • Success: The test will pass if all layer 3 interfaces have the proper MTU configured.
  • Failure: The test will fail if one or many layer 3 interfaces have the wrong MTU configured.
Examples
anta.tests.interfaces:\n  - VerifyL3MTU:\n      mtu: 1500\n      ignored_interfaces:\n          - Vxlan1\n      specific_mtu:\n          - Ethernet1: 2500\n
Source code in anta/tests/interfaces.py
class VerifyL3MTU(AntaTest):\n    \"\"\"Verifies the global layer 3 Maximum Transfer Unit (MTU) for all L3 interfaces.\n\n    Test that L3 interfaces are configured with the correct MTU. It supports Ethernet, Port Channel and VLAN interfaces.\n\n    You can define a global MTU to check, or an MTU per interface and you can also ignored some interfaces.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all layer 3 interfaces have the proper MTU configured.\n    * Failure: The test will fail if one or many layer 3 interfaces have the wrong MTU configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyL3MTU:\n          mtu: 1500\n          ignored_interfaces:\n              - Vxlan1\n          specific_mtu:\n              - Ethernet1: 2500\n    ```\n    \"\"\"\n\n    name = \"VerifyL3MTU\"\n    description = \"Verifies the global L3 MTU of all L3 interfaces.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyL3MTU test.\"\"\"\n\n        mtu: int = 1500\n        \"\"\"Default MTU we should have configured on all non-excluded interfaces. Defaults to 1500.\"\"\"\n        ignored_interfaces: list[str] = Field(default=[\"Management\", \"Loopback\", \"Vxlan\", \"Tunnel\"])\n        \"\"\"A list of L3 interfaces to ignore\"\"\"\n        specific_mtu: list[dict[str, int]] = Field(default=[])\n        \"\"\"A list of dictionary of L3 interfaces with their specific MTU configured\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyL3MTU.\"\"\"\n        # Parameter to save incorrect interface settings\n        wrong_l3mtu_intf: list[dict[str, int]] = []\n        command_output = self.instance_commands[0].json_output\n        # Set list of interfaces with specific settings\n        specific_interfaces: list[str] = []\n        if self.inputs.specific_mtu:\n            for d in self.inputs.specific_mtu:\n                specific_interfaces.extend(d)\n        for interface, values in command_output[\"interfaces\"].items():\n            if re.findall(r\"[a-z]+\", interface, re.IGNORECASE)[0] not in self.inputs.ignored_interfaces and values[\"forwardingModel\"] == \"routed\":\n                if interface in specific_interfaces:\n                    wrong_l3mtu_intf.extend({interface: values[\"mtu\"]} for custom_data in self.inputs.specific_mtu if values[\"mtu\"] != custom_data[interface])\n                # Comparison with generic setting\n                elif values[\"mtu\"] != self.inputs.mtu:\n                    wrong_l3mtu_intf.append({interface: values[\"mtu\"]})\n        if wrong_l3mtu_intf:\n            self.result.is_failure(f\"Some interfaces do not have correct MTU configured:\\n{wrong_l3mtu_intf}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyL3MTU-attributes","title":"Inputs","text":"Name Type Description Default mtu int Default MTU we should have configured on all non-excluded interfaces. Defaults to 1500. 1500 ignored_interfaces list[str] A list of L3 interfaces to ignore Field(default=['Management', 'Loopback', 'Vxlan', 'Tunnel']) specific_mtu list[dict[str, int]] A list of dictionary of L3 interfaces with their specific MTU configured Field(default=[])"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLoopbackCount","title":"VerifyLoopbackCount","text":"

Verifies that the device has the expected number of loopback interfaces and all are operational.

Expected Results
  • Success: The test will pass if the device has the correct number of loopback interfaces and none are down.
  • Failure: The test will fail if the loopback interface count is incorrect or any are non-operational.
Examples
anta.tests.interfaces:\n  - VerifyLoopbackCount:\n      number: 3\n
Source code in anta/tests/interfaces.py
class VerifyLoopbackCount(AntaTest):\n    \"\"\"Verifies that the device has the expected number of loopback interfaces and all are operational.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device has the correct number of loopback interfaces and none are down.\n    * Failure: The test will fail if the loopback interface count is incorrect or any are non-operational.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyLoopbackCount:\n          number: 3\n    ```\n    \"\"\"\n\n    name = \"VerifyLoopbackCount\"\n    description = \"Verifies the number of loopback interfaces and their status.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip interface brief\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyLoopbackCount test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"Number of loopback interfaces expected to be present.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoopbackCount.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        loopback_count = 0\n        down_loopback_interfaces = []\n        for interface in command_output[\"interfaces\"]:\n            interface_dict = command_output[\"interfaces\"][interface]\n            if \"Loopback\" in interface:\n                loopback_count += 1\n                if not (interface_dict[\"lineProtocolStatus\"] == \"up\" and interface_dict[\"interfaceStatus\"] == \"connected\"):\n                    down_loopback_interfaces.append(interface)\n        if loopback_count == self.inputs.number and len(down_loopback_interfaces) == 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure()\n            if loopback_count != self.inputs.number:\n                self.result.is_failure(f\"Found {loopback_count} Loopbacks when expecting {self.inputs.number}\")\n            elif len(down_loopback_interfaces) != 0:\n                self.result.is_failure(f\"The following Loopbacks are not up: {down_loopback_interfaces}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLoopbackCount-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger Number of loopback interfaces expected to be present. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyPortChannels","title":"VerifyPortChannels","text":"

Verifies there are no inactive ports in all port channels.

Expected Results
  • Success: The test will pass if there are no inactive ports in all port channels.
  • Failure: The test will fail if there is at least one inactive port in a port channel.
Examples
anta.tests.interfaces:\n  - VerifyPortChannels:\n
Source code in anta/tests/interfaces.py
class VerifyPortChannels(AntaTest):\n    \"\"\"Verifies there are no inactive ports in all port channels.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are no inactive ports in all port channels.\n    * Failure: The test will fail if there is at least one inactive port in a port channel.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyPortChannels:\n    ```\n    \"\"\"\n\n    name = \"VerifyPortChannels\"\n    description = \"Verifies there are no inactive ports in all port channels.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show port-channel\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPortChannels.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        po_with_inactive_ports: list[dict[str, str]] = []\n        for portchannel, portchannel_dict in command_output[\"portChannels\"].items():\n            if len(portchannel_dict[\"inactivePorts\"]) != 0:\n                po_with_inactive_ports.extend({portchannel: portchannel_dict[\"inactivePorts\"]})\n        if not po_with_inactive_ports:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following port-channels have inactive port(s): {po_with_inactive_ports}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifySVI","title":"VerifySVI","text":"

Verifies the status of all SVIs.

Expected Results
  • Success: The test will pass if all SVIs are up.
  • Failure: The test will fail if one or many SVIs are not up.
Examples
anta.tests.interfaces:\n  - VerifySVI:\n
Source code in anta/tests/interfaces.py
class VerifySVI(AntaTest):\n    \"\"\"Verifies the status of all SVIs.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all SVIs are up.\n    * Failure: The test will fail if one or many SVIs are not up.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifySVI:\n    ```\n    \"\"\"\n\n    name = \"VerifySVI\"\n    description = \"Verifies the status of all SVIs.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip interface brief\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySVI.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        down_svis = []\n        for interface in command_output[\"interfaces\"]:\n            interface_dict = command_output[\"interfaces\"][interface]\n            if \"Vlan\" in interface and not (interface_dict[\"lineProtocolStatus\"] == \"up\" and interface_dict[\"interfaceStatus\"] == \"connected\"):\n                down_svis.append(interface)\n        if len(down_svis) == 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following SVIs are not up: {down_svis}\")\n
"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyStormControlDrops","title":"VerifyStormControlDrops","text":"

Verifies there are no interface storm-control drop counters.

Expected Results
  • Success: The test will pass if there are no storm-control drop counters.
  • Failure: The test will fail if there is at least one storm-control drop counter.
Examples
anta.tests.interfaces:\n  - VerifyStormControlDrops:\n
Source code in anta/tests/interfaces.py
class VerifyStormControlDrops(AntaTest):\n    \"\"\"Verifies there are no interface storm-control drop counters.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are no storm-control drop counters.\n    * Failure: The test will fail if there is at least one storm-control drop counter.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.interfaces:\n      - VerifyStormControlDrops:\n    ```\n    \"\"\"\n\n    name = \"VerifyStormControlDrops\"\n    description = \"Verifies there are no interface storm-control drop counters.\"\n    categories: ClassVar[list[str]] = [\"interfaces\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show storm-control\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyStormControlDrops.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        storm_controlled_interfaces: dict[str, dict[str, Any]] = {}\n        for interface, interface_dict in command_output[\"interfaces\"].items():\n            for traffic_type, traffic_type_dict in interface_dict[\"trafficTypes\"].items():\n                if \"drop\" in traffic_type_dict and traffic_type_dict[\"drop\"] != 0:\n                    storm_controlled_interface_dict = storm_controlled_interfaces.setdefault(interface, {})\n                    storm_controlled_interface_dict.update({traffic_type: traffic_type_dict[\"drop\"]})\n        if not storm_controlled_interfaces:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following interfaces have none 0 storm-control drop counters {storm_controlled_interfaces}\")\n
"},{"location":"api/tests.lanz/","title":"LANZ","text":""},{"location":"api/tests.lanz/#anta.tests.lanz.VerifyLANZ","title":"VerifyLANZ","text":"

Verifies if LANZ (Latency Analyzer) is enabled.

Expected Results
  • Success: The test will pass if LANZ is enabled.
  • Failure: The test will fail if LANZ is disabled.
Examples
anta.tests.lanz:\n  - VerifyLANZ:\n
Source code in anta/tests/lanz.py
class VerifyLANZ(AntaTest):\n    \"\"\"Verifies if LANZ (Latency Analyzer) is enabled.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if LANZ is enabled.\n    * Failure: The test will fail if LANZ is disabled.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.lanz:\n      - VerifyLANZ:\n    ```\n    \"\"\"\n\n    name = \"VerifyLANZ\"\n    description = \"Verifies if LANZ is enabled.\"\n    categories: ClassVar[list[str]] = [\"lanz\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show queue-monitor length status\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLANZ.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        if command_output[\"lanzEnabled\"] is not True:\n            self.result.is_failure(\"LANZ is not enabled\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.logging/","title":"Logging","text":""},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingAccounting","title":"VerifyLoggingAccounting","text":"

Verifies if AAA accounting logs are generated.

Expected Results
  • Success: The test will pass if AAA accounting logs are generated.
  • Failure: The test will fail if AAA accounting logs are NOT generated.
Examples
anta.tests.logging:\n  - VerifyLoggingAccounting:\n
Source code in anta/tests/logging.py
class VerifyLoggingAccounting(AntaTest):\n    \"\"\"Verifies if AAA accounting logs are generated.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if AAA accounting logs are generated.\n    * Failure: The test will fail if AAA accounting logs are NOT generated.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingAccounting:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingAccounting\"\n    description = \"Verifies if AAA accounting logs are generated.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa accounting logs | tail\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingAccounting.\"\"\"\n        pattern = r\"cmd=show aaa accounting logs\"\n        output = self.instance_commands[0].text_output\n        if re.search(pattern, output):\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"AAA accounting logs are not generated\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingErrors","title":"VerifyLoggingErrors","text":"

Verifies there are no syslog messages with a severity of ERRORS or higher.

Expected Results
  • Success: The test will pass if there are NO syslog messages with a severity of ERRORS or higher.
  • Failure: The test will fail if ERRORS or higher syslog messages are present.
Examples
anta.tests.logging:\n  - VerifyLoggingErrors:\n
Source code in anta/tests/logging.py
class VerifyLoggingErrors(AntaTest):\n    \"\"\"Verifies there are no syslog messages with a severity of ERRORS or higher.\n\n    Expected Results\n    ----------------\n      * Success: The test will pass if there are NO syslog messages with a severity of ERRORS or higher.\n      * Failure: The test will fail if ERRORS or higher syslog messages are present.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingErrors:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingErrors\"\n    description = \"Verifies there are no syslog messages with a severity of ERRORS or higher.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show logging threshold errors\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingErrors.\"\"\"\n        command_output = self.instance_commands[0].text_output\n\n        if len(command_output) == 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"Device has reported syslog messages with a severity of ERRORS or higher\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingHostname","title":"VerifyLoggingHostname","text":"

Verifies if logs are generated with the device FQDN.

Expected Results
  • Success: The test will pass if logs are generated with the device FQDN.
  • Failure: The test will fail if logs are NOT generated with the device FQDN.
Examples
anta.tests.logging:\n  - VerifyLoggingHostname:\n
Source code in anta/tests/logging.py
class VerifyLoggingHostname(AntaTest):\n    \"\"\"Verifies if logs are generated with the device FQDN.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if logs are generated with the device FQDN.\n    * Failure: The test will fail if logs are NOT generated with the device FQDN.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingHostname:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingHostname\"\n    description = \"Verifies if logs are generated with the device FQDN.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaCommand(command=\"show hostname\"),\n        AntaCommand(command=\"send log level informational message ANTA VerifyLoggingHostname validation\"),\n        AntaCommand(command=\"show logging informational last 30 seconds | grep ANTA\", ofmt=\"text\", use_cache=False),\n    ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingHostname.\"\"\"\n        output_hostname = self.instance_commands[0].json_output\n        output_logging = self.instance_commands[2].text_output\n        fqdn = output_hostname[\"fqdn\"]\n        lines = output_logging.strip().split(\"\\n\")[::-1]\n        log_pattern = r\"ANTA VerifyLoggingHostname validation\"\n        last_line_with_pattern = \"\"\n        for line in lines:\n            if re.search(log_pattern, line):\n                last_line_with_pattern = line\n                break\n        if fqdn in last_line_with_pattern:\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"Logs are not generated with the device FQDN\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingHosts","title":"VerifyLoggingHosts","text":"

Verifies logging hosts (syslog servers) for a specified VRF.

Expected Results
  • Success: The test will pass if the provided syslog servers are configured in the specified VRF.
  • Failure: The test will fail if the provided syslog servers are NOT configured in the specified VRF.
Examples
anta.tests.logging:\n  - VerifyLoggingHosts:\n      hosts:\n        - 1.1.1.1\n        - 2.2.2.2\n      vrf: default\n
Source code in anta/tests/logging.py
class VerifyLoggingHosts(AntaTest):\n    \"\"\"Verifies logging hosts (syslog servers) for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided syslog servers are configured in the specified VRF.\n    * Failure: The test will fail if the provided syslog servers are NOT configured in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingHosts:\n          hosts:\n            - 1.1.1.1\n            - 2.2.2.2\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingHosts\"\n    description = \"Verifies logging hosts (syslog servers) for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show logging\", ofmt=\"text\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyLoggingHosts test.\"\"\"\n\n        hosts: list[IPv4Address]\n        \"\"\"List of hosts (syslog servers) IP addresses.\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF to transport log messages. Defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingHosts.\"\"\"\n        output = self.instance_commands[0].text_output\n        not_configured = []\n        for host in self.inputs.hosts:\n            pattern = rf\"Logging to '{host!s}'.*VRF {self.inputs.vrf}\"\n            if not re.search(pattern, _get_logging_states(self.logger, output)):\n                not_configured.append(str(host))\n\n        if not not_configured:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Syslog servers {not_configured} are not configured in VRF {self.inputs.vrf}\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingHosts-attributes","title":"Inputs","text":"Name Type Description Default hosts list[IPv4Address] List of hosts (syslog servers) IP addresses. - vrf str The name of the VRF to transport log messages. Defaults to `default`. 'default'"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingLogsGeneration","title":"VerifyLoggingLogsGeneration","text":"

Verifies if logs are generated.

Expected Results
  • Success: The test will pass if logs are generated.
  • Failure: The test will fail if logs are NOT generated.
Examples
anta.tests.logging:\n  - VerifyLoggingLogsGeneration:\n
Source code in anta/tests/logging.py
class VerifyLoggingLogsGeneration(AntaTest):\n    \"\"\"Verifies if logs are generated.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if logs are generated.\n    * Failure: The test will fail if logs are NOT generated.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingLogsGeneration:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingLogsGeneration\"\n    description = \"Verifies if logs are generated.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaCommand(command=\"send log level informational message ANTA VerifyLoggingLogsGeneration validation\"),\n        AntaCommand(command=\"show logging informational last 30 seconds | grep ANTA\", ofmt=\"text\", use_cache=False),\n    ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingLogsGeneration.\"\"\"\n        log_pattern = r\"ANTA VerifyLoggingLogsGeneration validation\"\n        output = self.instance_commands[1].text_output\n        lines = output.strip().split(\"\\n\")[::-1]\n        for line in lines:\n            if re.search(log_pattern, line):\n                self.result.is_success()\n                return\n        self.result.is_failure(\"Logs are not generated\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingPersistent","title":"VerifyLoggingPersistent","text":"

Verifies if logging persistent is enabled and logs are saved in flash.

Expected Results
  • Success: The test will pass if logging persistent is enabled and logs are in flash.
  • Failure: The test will fail if logging persistent is disabled or no logs are saved in flash.
Examples
anta.tests.logging:\n  - VerifyLoggingPersistent:\n
Source code in anta/tests/logging.py
class VerifyLoggingPersistent(AntaTest):\n    \"\"\"Verifies if logging persistent is enabled and logs are saved in flash.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if logging persistent is enabled and logs are in flash.\n    * Failure: The test will fail if logging persistent is disabled or no logs are saved in flash.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingPersistent:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingPersistent\"\n    description = \"Verifies if logging persistent is enabled and logs are saved in flash.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaCommand(command=\"show logging\", ofmt=\"text\"),\n        AntaCommand(command=\"dir flash:/persist/messages\", ofmt=\"text\"),\n    ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingPersistent.\"\"\"\n        self.result.is_success()\n        log_output = self.instance_commands[0].text_output\n        dir_flash_output = self.instance_commands[1].text_output\n        if \"Persistent logging: disabled\" in _get_logging_states(self.logger, log_output):\n            self.result.is_failure(\"Persistent logging is disabled\")\n            return\n        pattern = r\"-rw-\\s+(\\d+)\"\n        persist_logs = re.search(pattern, dir_flash_output)\n        if not persist_logs or int(persist_logs.group(1)) == 0:\n            self.result.is_failure(\"No persistent logs are saved in flash\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingSourceIntf","title":"VerifyLoggingSourceIntf","text":"

Verifies logging source-interface for a specified VRF.

Expected Results
  • Success: The test will pass if the provided logging source-interface is configured in the specified VRF.
  • Failure: The test will fail if the provided logging source-interface is NOT configured in the specified VRF.
Examples
anta.tests.logging:\n  - VerifyLoggingSourceIntf:\n      interface: Management0\n      vrf: default\n
Source code in anta/tests/logging.py
class VerifyLoggingSourceIntf(AntaTest):\n    \"\"\"Verifies logging source-interface for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided logging source-interface is configured in the specified VRF.\n    * Failure: The test will fail if the provided logging source-interface is NOT configured in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingSourceIntf:\n          interface: Management0\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingSourceInt\"\n    description = \"Verifies logging source-interface for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show logging\", ofmt=\"text\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyLoggingSourceInt test.\"\"\"\n\n        interface: str\n        \"\"\"Source-interface to use as source IP of log messages.\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF to transport log messages. Defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingSourceInt.\"\"\"\n        output = self.instance_commands[0].text_output\n        pattern = rf\"Logging source-interface '{self.inputs.interface}'.*VRF {self.inputs.vrf}\"\n        if re.search(pattern, _get_logging_states(self.logger, output)):\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Source-interface '{self.inputs.interface}' is not configured in VRF {self.inputs.vrf}\")\n
"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingSourceIntf-attributes","title":"Inputs","text":"Name Type Description Default interface str Source-interface to use as source IP of log messages. - vrf str The name of the VRF to transport log messages. Defaults to `default`. 'default'"},{"location":"api/tests.logging/#anta.tests.logging.VerifyLoggingTimestamp","title":"VerifyLoggingTimestamp","text":"

Verifies if logs are generated with the approprate timestamp.

Expected Results
  • Success: The test will pass if logs are generated with the appropriated timestamp.
  • Failure: The test will fail if logs are NOT generated with the appropriated timestamp.
Examples
anta.tests.logging:\n  - VerifyLoggingTimestamp:\n
Source code in anta/tests/logging.py
class VerifyLoggingTimestamp(AntaTest):\n    \"\"\"Verifies if logs are generated with the approprate timestamp.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if logs are generated with the appropriated timestamp.\n    * Failure: The test will fail if logs are NOT generated with the appropriated timestamp.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.logging:\n      - VerifyLoggingTimestamp:\n    ```\n    \"\"\"\n\n    name = \"VerifyLoggingTimestamp\"\n    description = \"Verifies if logs are generated with the appropriate timestamp.\"\n    categories: ClassVar[list[str]] = [\"logging\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaCommand(command=\"send log level informational message ANTA VerifyLoggingTimestamp validation\"),\n        AntaCommand(command=\"show logging informational last 30 seconds | grep ANTA\", ofmt=\"text\", use_cache=False),\n    ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyLoggingTimestamp.\"\"\"\n        log_pattern = r\"ANTA VerifyLoggingTimestamp validation\"\n        timestamp_pattern = r\"\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}\\.\\d{6}-\\d{2}:\\d{2}\"\n        output = self.instance_commands[1].text_output\n        lines = output.strip().split(\"\\n\")[::-1]\n        last_line_with_pattern = \"\"\n        for line in lines:\n            if re.search(log_pattern, line):\n                last_line_with_pattern = line\n                break\n        if re.search(timestamp_pattern, last_line_with_pattern):\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"Logs are not generated with the appropriate timestamp format\")\n
"},{"location":"api/tests.logging/#anta.tests.logging._get_logging_states","title":"_get_logging_states","text":"
_get_logging_states(logger: logging.Logger, command_output: str) -> str\n

Parse show logging output and gets operational logging states used in the tests in this module.

Args:
logger: The logger object.\ncommand_output: The `show logging` output.\n

Returns:

Type Description str: The operational logging states. Source code in anta/tests/logging.py
def _get_logging_states(logger: logging.Logger, command_output: str) -> str:\n    \"\"\"Parse `show logging` output and gets operational logging states used in the tests in this module.\n\n    Args:\n    ----\n        logger: The logger object.\n        command_output: The `show logging` output.\n\n    Returns\n    -------\n        str: The operational logging states.\n\n    \"\"\"\n    log_states = command_output.partition(\"\\n\\nExternal configuration:\")[0]\n    logger.debug(\"Device logging states:\\n%s\", log_states)\n    return log_states\n
"},{"location":"api/tests/","title":"Overview","text":""},{"location":"api/tests/#anta-tests-landing-page","title":"ANTA Tests Landing Page","text":"

This section describes all the available tests provided by the ANTA package.

"},{"location":"api/tests/#available-tests","title":"Available Tests","text":"

Here are the tests that we currently provide:

  • AAA
  • BFD
  • Configuration
  • Connectivity
  • Field Notice
  • GreenT
  • Hardware
  • Interfaces
  • LANZ
  • Logging
  • MLAG
  • Multicast
  • Profiles
  • PTP
  • Routing Generic
  • Routing BGP
  • Routing OSPF
  • Security
  • Services
  • SNMP
  • Software
  • STP
  • System
  • VLAN
  • VXLAN
"},{"location":"api/tests/#using-the-tests","title":"Using the Tests","text":"

All these tests can be imported in a catalog to be used by the ANTA CLI or in your own framework.

"},{"location":"api/tests.mlag/","title":"MLAG","text":""},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagConfigSanity","title":"VerifyMlagConfigSanity","text":"

Verifies there are no MLAG config-sanity inconsistencies.

Expected Results
  • Success: The test will pass if there are NO MLAG config-sanity inconsistencies.
  • Failure: The test will fail if there are MLAG config-sanity inconsistencies.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
  • Error: The test will give an error if \u2018mlagActive\u2019 is not found in the JSON response.
Examples
anta.tests.mlag:\n  - VerifyMlagConfigSanity:\n
Source code in anta/tests/mlag.py
class VerifyMlagConfigSanity(AntaTest):\n    \"\"\"Verifies there are no MLAG config-sanity inconsistencies.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO MLAG config-sanity inconsistencies.\n    * Failure: The test will fail if there are MLAG config-sanity inconsistencies.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n    * Error: The test will give an error if 'mlagActive' is not found in the JSON response.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagConfigSanity:\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagConfigSanity\"\n    description = \"Verifies there are no MLAG config-sanity inconsistencies.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag config-sanity\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagConfigSanity.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if (mlag_status := get_value(command_output, \"mlagActive\")) is None:\n            self.result.is_error(message=\"Incorrect JSON response - 'mlagActive' state was not found\")\n            return\n        if mlag_status is False:\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n        keys_to_verify = [\"globalConfiguration\", \"interfaceConfiguration\"]\n        verified_output = {key: get_value(command_output, key) for key in keys_to_verify}\n        if not any(verified_output.values()):\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"MLAG config-sanity returned inconsistencies: {verified_output}\")\n
"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagDualPrimary","title":"VerifyMlagDualPrimary","text":"

Verifies the dual-primary detection and its parameters of the MLAG configuration.

Expected Results
  • Success: The test will pass if the dual-primary detection is enabled and its parameters are configured properly.
  • Failure: The test will fail if the dual-primary detection is NOT enabled or its parameters are NOT configured properly.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
Examples
anta.tests.mlag:\n  - VerifyMlagDualPrimary:\n      detection_delay: 200\n      errdisabled: True\n      recovery_delay: 60\n      recovery_delay_non_mlag: 0\n
Source code in anta/tests/mlag.py
class VerifyMlagDualPrimary(AntaTest):\n    \"\"\"Verifies the dual-primary detection and its parameters of the MLAG configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the dual-primary detection is enabled and its parameters are configured properly.\n    * Failure: The test will fail if the dual-primary detection is NOT enabled or its parameters are NOT configured properly.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagDualPrimary:\n          detection_delay: 200\n          errdisabled: True\n          recovery_delay: 60\n          recovery_delay_non_mlag: 0\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagDualPrimary\"\n    description = \"Verifies the MLAG dual-primary detection parameters.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag detail\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyMlagDualPrimary test.\"\"\"\n\n        detection_delay: PositiveInteger\n        \"\"\"Delay detection (seconds).\"\"\"\n        errdisabled: bool = False\n        \"\"\"Errdisabled all interfaces when dual-primary is detected.\"\"\"\n        recovery_delay: PositiveInteger\n        \"\"\"Delay (seconds) after dual-primary detection resolves until non peer-link ports that are part of an MLAG are enabled.\"\"\"\n        recovery_delay_non_mlag: PositiveInteger\n        \"\"\"Delay (seconds) after dual-primary detection resolves until ports that are not part of an MLAG are enabled.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagDualPrimary.\"\"\"\n        errdisabled_action = \"errdisableAllInterfaces\" if self.inputs.errdisabled else \"none\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"state\"] == \"disabled\":\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n        if command_output[\"dualPrimaryDetectionState\"] == \"disabled\":\n            self.result.is_failure(\"Dual-primary detection is disabled\")\n            return\n        keys_to_verify = [\"detail.dualPrimaryDetectionDelay\", \"detail.dualPrimaryAction\", \"dualPrimaryMlagRecoveryDelay\", \"dualPrimaryNonMlagRecoveryDelay\"]\n        verified_output = {key: get_value(command_output, key) for key in keys_to_verify}\n        if (\n            verified_output[\"detail.dualPrimaryDetectionDelay\"] == self.inputs.detection_delay\n            and verified_output[\"detail.dualPrimaryAction\"] == errdisabled_action\n            and verified_output[\"dualPrimaryMlagRecoveryDelay\"] == self.inputs.recovery_delay\n            and verified_output[\"dualPrimaryNonMlagRecoveryDelay\"] == self.inputs.recovery_delay_non_mlag\n        ):\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The dual-primary parameters are not configured properly: {verified_output}\")\n
"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagDualPrimary-attributes","title":"Inputs","text":"Name Type Description Default detection_delay PositiveInteger Delay detection (seconds). - errdisabled bool Errdisabled all interfaces when dual-primary is detected. False recovery_delay PositiveInteger Delay (seconds) after dual-primary detection resolves until non peer-link ports that are part of an MLAG are enabled. - recovery_delay_non_mlag PositiveInteger Delay (seconds) after dual-primary detection resolves until ports that are not part of an MLAG are enabled. -"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagInterfaces","title":"VerifyMlagInterfaces","text":"

Verifies there are no inactive or active-partial MLAG ports.

Expected Results
  • Success: The test will pass if there are NO inactive or active-partial MLAG ports.
  • Failure: The test will fail if there are inactive or active-partial MLAG ports.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
Examples
anta.tests.mlag:\n  - VerifyMlagInterfaces:\n
Source code in anta/tests/mlag.py
class VerifyMlagInterfaces(AntaTest):\n    \"\"\"Verifies there are no inactive or active-partial MLAG ports.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO inactive or active-partial MLAG ports.\n    * Failure: The test will fail if there are inactive or active-partial MLAG ports.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagInterfaces:\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagInterfaces\"\n    description = \"Verifies there are no inactive or active-partial MLAG ports.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagInterfaces.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"state\"] == \"disabled\":\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n        if command_output[\"mlagPorts\"][\"Inactive\"] == 0 and command_output[\"mlagPorts\"][\"Active-partial\"] == 0:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"MLAG status is not OK: {command_output['mlagPorts']}\")\n
"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagPrimaryPriority","title":"VerifyMlagPrimaryPriority","text":"

Verify the MLAG (Multi-Chassis Link Aggregation) primary priority.

Expected Results
  • Success: The test will pass if the MLAG state is set as \u2018primary\u2019 and the priority matches the input.
  • Failure: The test will fail if the MLAG state is not \u2018primary\u2019 or the priority doesn\u2019t match the input.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
Examples
anta.tests.mlag:\n  - VerifyMlagPrimaryPriority:\n      primary_priority: 3276\n
Source code in anta/tests/mlag.py
class VerifyMlagPrimaryPriority(AntaTest):\n    \"\"\"Verify the MLAG (Multi-Chassis Link Aggregation) primary priority.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the MLAG state is set as 'primary' and the priority matches the input.\n    * Failure: The test will fail if the MLAG state is not 'primary' or the priority doesn't match the input.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagPrimaryPriority:\n          primary_priority: 3276\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagPrimaryPriority\"\n    description = \"Verifies the configuration of the MLAG primary priority.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag detail\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyMlagPrimaryPriority test.\"\"\"\n\n        primary_priority: MlagPriority\n        \"\"\"The expected MLAG primary priority.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagPrimaryPriority.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        self.result.is_success()\n        # Skip the test if MLAG is disabled\n        if command_output[\"state\"] == \"disabled\":\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n\n        mlag_state = get_value(command_output, \"detail.mlagState\")\n        primary_priority = get_value(command_output, \"detail.primaryPriority\")\n\n        # Check MLAG state\n        if mlag_state != \"primary\":\n            self.result.is_failure(\"The device is not set as MLAG primary.\")\n\n        # Check primary priority\n        if primary_priority != self.inputs.primary_priority:\n            self.result.is_failure(\n                f\"The primary priority does not match expected. Expected `{self.inputs.primary_priority}`, but found `{primary_priority}` instead.\",\n            )\n
"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagPrimaryPriority-attributes","title":"Inputs","text":"Name Type Description Default primary_priority MlagPriority The expected MLAG primary priority. -"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagReloadDelay","title":"VerifyMlagReloadDelay","text":"

Verifies the reload-delay parameters of the MLAG configuration.

Expected Results
  • Success: The test will pass if the reload-delay parameters are configured properly.
  • Failure: The test will fail if the reload-delay parameters are NOT configured properly.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
Examples
anta.tests.mlag:\n  - VerifyMlagReloadDelay:\n      reload_delay: 300\n      reload_delay_non_mlag: 330\n
Source code in anta/tests/mlag.py
class VerifyMlagReloadDelay(AntaTest):\n    \"\"\"Verifies the reload-delay parameters of the MLAG configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the reload-delay parameters are configured properly.\n    * Failure: The test will fail if the reload-delay parameters are NOT configured properly.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagReloadDelay:\n          reload_delay: 300\n          reload_delay_non_mlag: 330\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagReloadDelay\"\n    description = \"Verifies the MLAG reload-delay parameters.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyMlagReloadDelay test.\"\"\"\n\n        reload_delay: PositiveInteger\n        \"\"\"Delay (seconds) after reboot until non peer-link ports that are part of an MLAG are enabled.\"\"\"\n        reload_delay_non_mlag: PositiveInteger\n        \"\"\"Delay (seconds) after reboot until ports that are not part of an MLAG are enabled.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagReloadDelay.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"state\"] == \"disabled\":\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n        keys_to_verify = [\"reloadDelay\", \"reloadDelayNonMlag\"]\n        verified_output = {key: get_value(command_output, key) for key in keys_to_verify}\n        if verified_output[\"reloadDelay\"] == self.inputs.reload_delay and verified_output[\"reloadDelayNonMlag\"] == self.inputs.reload_delay_non_mlag:\n            self.result.is_success()\n\n        else:\n            self.result.is_failure(f\"The reload-delay parameters are not configured properly: {verified_output}\")\n
"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagReloadDelay-attributes","title":"Inputs","text":"Name Type Description Default reload_delay PositiveInteger Delay (seconds) after reboot until non peer-link ports that are part of an MLAG are enabled. - reload_delay_non_mlag PositiveInteger Delay (seconds) after reboot until ports that are not part of an MLAG are enabled. -"},{"location":"api/tests.mlag/#anta.tests.mlag.VerifyMlagStatus","title":"VerifyMlagStatus","text":"

Verifies the health status of the MLAG configuration.

Expected Results
  • Success: The test will pass if the MLAG state is \u2018active\u2019, negotiation status is \u2018connected\u2019, peer-link status and local interface status are \u2018up\u2019.
  • Failure: The test will fail if the MLAG state is not \u2018active\u2019, negotiation status is not \u2018connected\u2019, peer-link status or local interface status are not \u2018up\u2019.
  • Skipped: The test will be skipped if MLAG is \u2018disabled\u2019.
Examples
anta.tests.mlag:\n  - VerifyMlagStatus:\n
Source code in anta/tests/mlag.py
class VerifyMlagStatus(AntaTest):\n    \"\"\"Verifies the health status of the MLAG configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the MLAG state is 'active', negotiation status is 'connected',\n                   peer-link status and local interface status are 'up'.\n    * Failure: The test will fail if the MLAG state is not 'active', negotiation status is not 'connected',\n                   peer-link status or local interface status are not 'up'.\n    * Skipped: The test will be skipped if MLAG is 'disabled'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.mlag:\n      - VerifyMlagStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyMlagStatus\"\n    description = \"Verifies the health status of the MLAG configuration.\"\n    categories: ClassVar[list[str]] = [\"mlag\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMlagStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"state\"] == \"disabled\":\n            self.result.is_skipped(\"MLAG is disabled\")\n            return\n        keys_to_verify = [\"state\", \"negStatus\", \"localIntfStatus\", \"peerLinkStatus\"]\n        verified_output = {key: get_value(command_output, key) for key in keys_to_verify}\n        if (\n            verified_output[\"state\"] == \"active\"\n            and verified_output[\"negStatus\"] == \"connected\"\n            and verified_output[\"localIntfStatus\"] == \"up\"\n            and verified_output[\"peerLinkStatus\"] == \"up\"\n        ):\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"MLAG status is not OK: {verified_output}\")\n
"},{"location":"api/tests.multicast/","title":"Multicast","text":""},{"location":"api/tests.multicast/#anta.tests.multicast.VerifyIGMPSnoopingGlobal","title":"VerifyIGMPSnoopingGlobal","text":"

Verifies the IGMP snooping global status.

Expected Results
  • Success: The test will pass if the IGMP snooping global status matches the expected status.
  • Failure: The test will fail if the IGMP snooping global status does not match the expected status.
Examples
anta.tests.multicast:\n  - VerifyIGMPSnoopingGlobal:\n      enabled: True\n
Source code in anta/tests/multicast.py
class VerifyIGMPSnoopingGlobal(AntaTest):\n    \"\"\"Verifies the IGMP snooping global status.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the IGMP snooping global status matches the expected status.\n    * Failure: The test will fail if the IGMP snooping global status does not match the expected status.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.multicast:\n      - VerifyIGMPSnoopingGlobal:\n          enabled: True\n    ```\n    \"\"\"\n\n    name = \"VerifyIGMPSnoopingGlobal\"\n    description = \"Verifies the IGMP snooping global configuration.\"\n    categories: ClassVar[list[str]] = [\"multicast\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyIGMPSnoopingGlobal test.\"\"\"\n\n        enabled: bool\n        \"\"\"Whether global IGMP snopping must be enabled (True) or disabled (False).\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIGMPSnoopingGlobal.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        self.result.is_success()\n        igmp_state = command_output[\"igmpSnoopingState\"]\n        if igmp_state != \"enabled\" if self.inputs.enabled else igmp_state != \"disabled\":\n            self.result.is_failure(f\"IGMP state is not valid: {igmp_state}\")\n
"},{"location":"api/tests.multicast/#anta.tests.multicast.VerifyIGMPSnoopingGlobal-attributes","title":"Inputs","text":"Name Type Description Default enabled bool Whether global IGMP snopping must be enabled (True) or disabled (False). -"},{"location":"api/tests.multicast/#anta.tests.multicast.VerifyIGMPSnoopingVlans","title":"VerifyIGMPSnoopingVlans","text":"

Verifies the IGMP snooping status for the provided VLANs.

Expected Results
  • Success: The test will pass if the IGMP snooping status matches the expected status for the provided VLANs.
  • Failure: The test will fail if the IGMP snooping status does not match the expected status for the provided VLANs.
Examples
anta.tests.multicast:\n  - VerifyIGMPSnoopingVlans:\n      vlans:\n        10: False\n        12: False\n
Source code in anta/tests/multicast.py
class VerifyIGMPSnoopingVlans(AntaTest):\n    \"\"\"Verifies the IGMP snooping status for the provided VLANs.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the IGMP snooping status matches the expected status for the provided VLANs.\n    * Failure: The test will fail if the IGMP snooping status does not match the expected status for the provided VLANs.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.multicast:\n      - VerifyIGMPSnoopingVlans:\n          vlans:\n            10: False\n            12: False\n    ```\n    \"\"\"\n\n    name = \"VerifyIGMPSnoopingVlans\"\n    description = \"Verifies the IGMP snooping status for the provided VLANs.\"\n    categories: ClassVar[list[str]] = [\"multicast\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyIGMPSnoopingVlans test.\"\"\"\n\n        vlans: dict[Vlan, bool]\n        \"\"\"Dictionary with VLAN ID and whether IGMP snooping must be enabled (True) or disabled (False).\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIGMPSnoopingVlans.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        self.result.is_success()\n        for vlan, enabled in self.inputs.vlans.items():\n            if str(vlan) not in command_output[\"vlans\"]:\n                self.result.is_failure(f\"Supplied vlan {vlan} is not present on the device.\")\n                continue\n\n            igmp_state = command_output[\"vlans\"][str(vlan)][\"igmpSnoopingState\"]\n            if igmp_state != \"enabled\" if enabled else igmp_state != \"disabled\":\n                self.result.is_failure(f\"IGMP state for vlan {vlan} is {igmp_state}\")\n
"},{"location":"api/tests.multicast/#anta.tests.multicast.VerifyIGMPSnoopingVlans-attributes","title":"Inputs","text":"Name Type Description Default vlans dict[Vlan, bool] Dictionary with VLAN ID and whether IGMP snooping must be enabled (True) or disabled (False). -"},{"location":"api/tests.profiles/","title":"Profiles","text":""},{"location":"api/tests.profiles/#anta.tests.profiles.VerifyTcamProfile","title":"VerifyTcamProfile","text":"

Verifies that the device is using the provided Ternary Content-Addressable Memory (TCAM) profile.

Expected Results
  • Success: The test will pass if the provided TCAM profile is actually running on the device.
  • Failure: The test will fail if the provided TCAM profile is not running on the device.
Examples
anta.tests.profiles:\n  - VerifyTcamProfile:\n      profile: vxlan-routing\n
Source code in anta/tests/profiles.py
class VerifyTcamProfile(AntaTest):\n    \"\"\"Verifies that the device is using the provided Ternary Content-Addressable Memory (TCAM) profile.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided TCAM profile is actually running on the device.\n    * Failure: The test will fail if the provided TCAM profile is not running on the device.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.profiles:\n      - VerifyTcamProfile:\n          profile: vxlan-routing\n    ```\n    \"\"\"\n\n    name = \"VerifyTcamProfile\"\n    description = \"Verifies the device TCAM profile.\"\n    categories: ClassVar[list[str]] = [\"profiles\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hardware tcam profile\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTcamProfile test.\"\"\"\n\n        profile: str\n        \"\"\"Expected TCAM profile.\"\"\"\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTcamProfile.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"pmfProfiles\"][\"FixedSystem\"][\"status\"] == command_output[\"pmfProfiles\"][\"FixedSystem\"][\"config\"] == self.inputs.profile:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Incorrect profile running on device: {command_output['pmfProfiles']['FixedSystem']['status']}\")\n
"},{"location":"api/tests.profiles/#anta.tests.profiles.VerifyTcamProfile-attributes","title":"Inputs","text":"Name Type Description Default profile str Expected TCAM profile. -"},{"location":"api/tests.profiles/#anta.tests.profiles.VerifyUnifiedForwardingTableMode","title":"VerifyUnifiedForwardingTableMode","text":"

Verifies the device is using the expected UFT (Unified Forwarding Table) mode.

Expected Results
  • Success: The test will pass if the device is using the expected UFT mode.
  • Failure: The test will fail if the device is not using the expected UFT mode.
Examples
anta.tests.profiles:\n  - VerifyUnifiedForwardingTableMode:\n      mode: 3\n
Source code in anta/tests/profiles.py
class VerifyUnifiedForwardingTableMode(AntaTest):\n    \"\"\"Verifies the device is using the expected UFT (Unified Forwarding Table) mode.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is using the expected UFT mode.\n    * Failure: The test will fail if the device is not using the expected UFT mode.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.profiles:\n      - VerifyUnifiedForwardingTableMode:\n          mode: 3\n    ```\n    \"\"\"\n\n    name = \"VerifyUnifiedForwardingTableMode\"\n    description = \"Verifies the device is using the expected UFT mode.\"\n    categories: ClassVar[list[str]] = [\"profiles\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show platform trident forwarding-table partition\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyUnifiedForwardingTableMode test.\"\"\"\n\n        mode: Literal[0, 1, 2, 3, 4, \"flexible\"]\n        \"\"\"Expected UFT mode. Valid values are 0, 1, 2, 3, 4, or \"flexible\".\"\"\"\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyUnifiedForwardingTableMode.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"uftMode\"] == str(self.inputs.mode):\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device is not running correct UFT mode (expected: {self.inputs.mode} / running: {command_output['uftMode']})\")\n
"},{"location":"api/tests.profiles/#anta.tests.profiles.VerifyUnifiedForwardingTableMode-attributes","title":"Inputs","text":"Name Type Description Default mode Literal[0, 1, 2, 3, 4, 'flexible'] Expected UFT mode. Valid values are 0, 1, 2, 3, 4, or \"flexible\". -"},{"location":"api/tests.ptp/","title":"PTP","text":""},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpGMStatus","title":"VerifyPtpGMStatus","text":"

Verifies that the device is locked to a valid Precision Time Protocol (PTP) Grandmaster (GM).

To test PTP failover, re-run the test with a secondary GMID configured.

Expected Results
  • Success: The test will pass if the device is locked to the provided Grandmaster.
  • Failure: The test will fail if the device is not locked to the provided Grandmaster.
  • Error: The test will error if the \u2018gmClockIdentity\u2019 variable is not present in the command output.
Examples
anta.tests.ptp:\n  - VerifyPtpGMStatus:\n      gmid: 0xec:46:70:ff:fe:00:ff:a9\n
Source code in anta/tests/ptp.py
class VerifyPtpGMStatus(AntaTest):\n    \"\"\"Verifies that the device is locked to a valid Precision Time Protocol (PTP) Grandmaster (GM).\n\n    To test PTP failover, re-run the test with a secondary GMID configured.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is locked to the provided Grandmaster.\n    * Failure: The test will fail if the device is not locked to the provided Grandmaster.\n    * Error: The test will error if the 'gmClockIdentity' variable is not present in the command output.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.ptp:\n      - VerifyPtpGMStatus:\n          gmid: 0xec:46:70:ff:fe:00:ff:a9\n    ```\n    \"\"\"\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyPtpGMStatus test.\"\"\"\n\n        gmid: str\n        \"\"\"Identifier of the Grandmaster to which the device should be locked.\"\"\"\n\n    name = \"VerifyPtpGMStatus\"\n    description = \"Verifies that the device is locked to a valid PTP Grandmaster.\"\n    categories: ClassVar[list[str]] = [\"ptp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPtpGMStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        if (ptp_clock_summary := command_output.get(\"ptpClockSummary\")) is None:\n            self.result.is_error(\"'ptpClockSummary' variable is not present in the command output\")\n            return\n\n        if ptp_clock_summary[\"gmClockIdentity\"] != self.inputs.gmid:\n            self.result.is_failure(\n                f\"The device is locked to the following Grandmaster: '{ptp_clock_summary['gmClockIdentity']}', which differ from the expected one.\",\n            )\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpGMStatus-attributes","title":"Inputs","text":"Name Type Description Default gmid str Identifier of the Grandmaster to which the device should be locked. -"},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpLockStatus","title":"VerifyPtpLockStatus","text":"

Verifies that the device was locked to the upstream Precision Time Protocol (PTP) Grandmaster (GM) in the last minute.

Expected Results
  • Success: The test will pass if the device was locked to the upstream GM in the last minute.
  • Failure: The test will fail if the device was not locked to the upstream GM in the last minute.
  • Error: The test will error if the \u2018lastSyncTime\u2019 variable is not present in the command output.
Examples
anta.tests.ptp:\n  - VerifyPtpLockStatus:\n
Source code in anta/tests/ptp.py
class VerifyPtpLockStatus(AntaTest):\n    \"\"\"Verifies that the device was locked to the upstream Precision Time Protocol (PTP) Grandmaster (GM) in the last minute.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device was locked to the upstream GM in the last minute.\n    * Failure: The test will fail if the device was not locked to the upstream GM in the last minute.\n    * Error: The test will error if the 'lastSyncTime' variable is not present in the command output.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.ptp:\n      - VerifyPtpLockStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyPtpLockStatus\"\n    description = \"Verifies that the device was locked to the upstream PTP GM in the last minute.\"\n    categories: ClassVar[list[str]] = [\"ptp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPtpLockStatus.\"\"\"\n        threshold = 60\n        command_output = self.instance_commands[0].json_output\n\n        if (ptp_clock_summary := command_output.get(\"ptpClockSummary\")) is None:\n            self.result.is_error(\"'ptpClockSummary' variable is not present in the command output\")\n            return\n\n        time_difference = ptp_clock_summary[\"currentPtpSystemTime\"] - ptp_clock_summary[\"lastSyncTime\"]\n\n        if time_difference >= threshold:\n            self.result.is_failure(f\"The device lock is more than {threshold}s old: {time_difference}s\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpModeStatus","title":"VerifyPtpModeStatus","text":"

Verifies that the device is configured as a Precision Time Protocol (PTP) Boundary Clock (BC).

Expected Results
  • Success: The test will pass if the device is a BC.
  • Failure: The test will fail if the device is not a BC.
  • Error: The test will error if the \u2018ptpMode\u2019 variable is not present in the command output.
Examples
anta.tests.ptp:\n  - VerifyPtpModeStatus:\n
Source code in anta/tests/ptp.py
class VerifyPtpModeStatus(AntaTest):\n    \"\"\"Verifies that the device is configured as a Precision Time Protocol (PTP) Boundary Clock (BC).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is a BC.\n    * Failure: The test will fail if the device is not a BC.\n    * Error: The test will error if the 'ptpMode' variable is not present in the command output.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.ptp:\n      - VerifyPtpModeStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyPtpModeStatus\"\n    description = \"Verifies that the device is configured as a PTP Boundary Clock.\"\n    categories: ClassVar[list[str]] = [\"ptp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPtpModeStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        if (ptp_mode := command_output.get(\"ptpMode\")) is None:\n            self.result.is_error(\"'ptpMode' variable is not present in the command output\")\n            return\n\n        if ptp_mode != \"ptpBoundaryClock\":\n            self.result.is_failure(f\"The device is not configured as a PTP Boundary Clock: '{ptp_mode}'\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpOffset","title":"VerifyPtpOffset","text":"

Verifies that the Precision Time Protocol (PTP) timing offset is within \u00b1 1000ns from the master clock.

Expected Results
  • Success: The test will pass if the PTP timing offset is within \u00b1 1000ns from the master clock.
  • Failure: The test will fail if the PTP timing offset is greater than \u00b1 1000ns from the master clock.
  • Skipped: The test will be skipped if PTP is not configured.
Examples
anta.tests.ptp:\n  - VerifyPtpOffset:\n
Source code in anta/tests/ptp.py
class VerifyPtpOffset(AntaTest):\n    \"\"\"Verifies that the Precision Time Protocol (PTP) timing offset is within +/- 1000ns from the master clock.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the PTP timing offset is within +/- 1000ns from the master clock.\n    * Failure: The test will fail if the PTP timing offset is greater than +/- 1000ns from the master clock.\n    * Skipped: The test will be skipped if PTP is not configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.ptp:\n      - VerifyPtpOffset:\n    ```\n    \"\"\"\n\n    name = \"VerifyPtpOffset\"\n    description = \"Verifies that the PTP timing offset is within +/- 1000ns from the master clock.\"\n    categories: ClassVar[list[str]] = [\"ptp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp monitor\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPtpOffset.\"\"\"\n        threshold = 1000\n        offset_interfaces: dict[str, list[int]] = {}\n        command_output = self.instance_commands[0].json_output\n\n        if not command_output[\"ptpMonitorData\"]:\n            self.result.is_skipped(\"PTP is not configured\")\n            return\n\n        for interface in command_output[\"ptpMonitorData\"]:\n            if abs(interface[\"offsetFromMaster\"]) > threshold:\n                offset_interfaces.setdefault(interface[\"intf\"], []).append(interface[\"offsetFromMaster\"])\n\n        if offset_interfaces:\n            self.result.is_failure(f\"The device timing offset from master is greater than +/- {threshold}ns: {offset_interfaces}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.ptp/#anta.tests.ptp.VerifyPtpPortModeStatus","title":"VerifyPtpPortModeStatus","text":"

Verifies that all interfaces are in a valid Precision Time Protocol (PTP) state.

The interfaces can be in one of the following state: Master, Slave, Passive, or Disabled.

Expected Results
  • Success: The test will pass if all PTP enabled interfaces are in a valid state.
  • Failure: The test will fail if there are no PTP enabled interfaces or if some interfaces are not in a valid state.
Examples
anta.tests.ptp:\n  - VerifyPtpPortModeStatus:\n
Source code in anta/tests/ptp.py
class VerifyPtpPortModeStatus(AntaTest):\n    \"\"\"Verifies that all interfaces are in a valid Precision Time Protocol (PTP) state.\n\n    The interfaces can be in one of the following state: Master, Slave, Passive, or Disabled.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all PTP enabled interfaces are in a valid state.\n    * Failure: The test will fail if there are no PTP enabled interfaces or if some interfaces are not in a valid state.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.ptp:\n      - VerifyPtpPortModeStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyPtpPortModeStatus\"\n    description = \"Verifies the PTP interfaces state.\"\n    categories: ClassVar[list[str]] = [\"ptp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", ofmt=\"json\")]\n\n    @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\", \"cEOSCloudLab\"])\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyPtpPortModeStatus.\"\"\"\n        valid_state = (\"psMaster\", \"psSlave\", \"psPassive\", \"psDisabled\")\n        command_output = self.instance_commands[0].json_output\n\n        if not command_output[\"ptpIntfSummaries\"]:\n            self.result.is_failure(\"No interfaces are PTP enabled\")\n            return\n\n        invalid_interfaces = [\n            interface\n            for interface in command_output[\"ptpIntfSummaries\"]\n            for vlan in command_output[\"ptpIntfSummaries\"][interface][\"ptpIntfVlanSummaries\"]\n            if vlan[\"portState\"] not in valid_state\n        ]\n\n        if not invalid_interfaces:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following interface(s) are not in a valid PTP state: '{invalid_interfaces}'\")\n
"},{"location":"api/tests.routing.bgp/","title":"BGP","text":""},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPAdvCommunities","title":"VerifyBGPAdvCommunities","text":"

Verifies if the advertised communities of BGP peers are standard, extended, and large in the specified VRF.

Expected Results
  • Success: The test will pass if the advertised communities of BGP peers are standard, extended, and large in the specified VRF.
  • Failure: The test will fail if the advertised communities of BGP peers are not standard, extended, and large in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPAdvCommunities:\n        bgp_peers:\n          - peer_address: 172.30.11.17\n            vrf: default\n          - peer_address: 172.30.11.21\n            vrf: default\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPAdvCommunities(AntaTest):\n    \"\"\"Verifies if the advertised communities of BGP peers are standard, extended, and large in the specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the advertised communities of BGP peers are standard, extended, and large in the specified VRF.\n    * Failure: The test will fail if the advertised communities of BGP peers are not standard, extended, and large in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPAdvCommunities:\n            bgp_peers:\n              - peer_address: 172.30.11.17\n                vrf: default\n              - peer_address: 172.30.11.21\n                vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPAdvCommunities\"\n    description = \"Verifies the advertised communities of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPAdvCommunities test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of BGP peers.\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPAdvCommunities.\"\"\"\n        failures: dict[str, Any] = {\"bgp_peers\": {}}\n\n        # Iterate over each bgp peer\n        for bgp_peer in self.inputs.bgp_peers:\n            peer = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            failure: dict[str, dict[str, dict[str, Any]]] = {\"bgp_peers\": {peer: {vrf: {}}}}\n\n            # Verify BGP peer\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer)) is None\n            ):\n                failure[\"bgp_peers\"][peer][vrf] = {\"status\": \"Not configured\"}\n                failures = deep_update(failures, failure)\n                continue\n\n            # Verify BGP peer's advertised communities\n            bgp_output = bgp_output.get(\"advertisedCommunities\")\n            if not bgp_output[\"standard\"] or not bgp_output[\"extended\"] or not bgp_output[\"large\"]:\n                failure[\"bgp_peers\"][peer][vrf] = {\"advertised_communities\": bgp_output}\n                failures = deep_update(failures, failure)\n\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peers are not configured or advertised communities are not standard, extended, and large:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPAdvCommunities-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of BGP peers. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPAdvCommunities-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default'"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPExchangedRoutes","title":"VerifyBGPExchangedRoutes","text":"

Verifies if the BGP peers have correctly advertised and received routes.

The route type should be \u2018valid\u2019 and \u2018active\u2019 for a specified VRF.

Expected Results
  • Success: If the BGP peers have correctly advertised and received routes of type \u2018valid\u2019 and \u2018active\u2019 for a specified VRF.
  • Failure: If a BGP peer is not found, the expected advertised/received routes are not found, or the routes are not \u2018valid\u2019 or \u2018active\u2019.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPExchangedRoutes:\n        bgp_peers:\n          - peer_address: 172.30.255.5\n            vrf: default\n            advertised_routes:\n              - 192.0.254.5/32\n            received_routes:\n              - 192.0.255.4/32\n          - peer_address: 172.30.255.1\n            vrf: default\n            advertised_routes:\n              - 192.0.255.1/32\n              - 192.0.254.5/32\n            received_routes:\n              - 192.0.254.3/32\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPExchangedRoutes(AntaTest):\n    \"\"\"Verifies if the BGP peers have correctly advertised and received routes.\n\n    The route type should be 'valid' and 'active' for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: If the BGP peers have correctly advertised and received routes of type 'valid' and 'active' for a specified VRF.\n    * Failure: If a BGP peer is not found, the expected advertised/received routes are not found, or the routes are not 'valid' or 'active'.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPExchangedRoutes:\n            bgp_peers:\n              - peer_address: 172.30.255.5\n                vrf: default\n                advertised_routes:\n                  - 192.0.254.5/32\n                received_routes:\n                  - 192.0.255.4/32\n              - peer_address: 172.30.255.1\n                vrf: default\n                advertised_routes:\n                  - 192.0.255.1/32\n                  - 192.0.254.5/32\n                received_routes:\n                  - 192.0.254.3/32\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPExchangedRoutes\"\n    description = \"Verifies the advertised and received routes of BGP peers.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaTemplate(template=\"show bgp neighbors {peer} advertised-routes vrf {vrf}\"),\n        AntaTemplate(template=\"show bgp neighbors {peer} routes vrf {vrf}\"),\n    ]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPExchangedRoutes test.\"\"\"\n\n        bgp_peers: list[BgpNeighbor]\n        \"\"\"List of BGP neighbors.\"\"\"\n\n        class BgpNeighbor(BaseModel):\n            \"\"\"Model for a BGP neighbor.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n            advertised_routes: list[IPv4Network]\n            \"\"\"List of advertised routes in CIDR format.\"\"\"\n            received_routes: list[IPv4Network]\n            \"\"\"List of received routes in CIDR format.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each BGP neighbor in the input list.\"\"\"\n        return [\n            template.render(peer=bgp_peer.peer_address, vrf=bgp_peer.vrf, advertised_routes=bgp_peer.advertised_routes, received_routes=bgp_peer.received_routes)\n            for bgp_peer in self.inputs.bgp_peers\n        ]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPExchangedRoutes.\"\"\"\n        failures: dict[str, dict[str, Any]] = {\"bgp_peers\": {}}\n\n        # Iterating over command output for different peers\n        for command in self.instance_commands:\n            peer = str(command.params[\"peer\"])\n            vrf = command.params[\"vrf\"]\n            advertised_routes = command.params[\"advertised_routes\"]\n            received_routes = command.params[\"received_routes\"]\n            failure = {vrf: \"\"}\n\n            # Verify if a BGP peer is configured with the provided vrf\n            if not (bgp_routes := get_value(command.json_output, f\"vrfs.{vrf}.bgpRouteEntries\")):\n                failure[vrf] = \"Not configured\"\n                failures[\"bgp_peers\"][peer] = failure\n                continue\n\n            # Validate advertised routes\n            if \"advertised-routes\" in command.command:\n                failure_routes = _add_bgp_routes_failure(advertised_routes, bgp_routes, peer, vrf)\n\n            # Validate received routes\n            else:\n                failure_routes = _add_bgp_routes_failure(received_routes, bgp_routes, peer, vrf, route_type=\"received_routes\")\n            failures = deep_update(failures, failure_routes)\n\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peers are not found or routes are not exchanged properly:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPExchangedRoutes-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpNeighbor] List of BGP neighbors. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPExchangedRoutes-attributes","title":"BgpNeighbor","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default' advertised_routes list[IPv4Network] List of advertised routes in CIDR format. - received_routes list[IPv4Network] List of received routes in CIDR format. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerASNCap","title":"VerifyBGPPeerASNCap","text":"

Verifies the four octet asn capabilities of a BGP peer in a specified VRF.

Expected Results
  • Success: The test will pass if BGP peer\u2019s four octet asn capabilities are advertised, received, and enabled in the specified VRF.
  • Failure: The test will fail if BGP peers are not found or four octet asn capabilities are not advertised, received, and enabled in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeerASNCap:\n        bgp_peers:\n          - peer_address: 172.30.11.1\n            vrf: default\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeerASNCap(AntaTest):\n    \"\"\"Verifies the four octet asn capabilities of a BGP peer in a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if BGP peer's four octet asn capabilities are advertised, received, and enabled in the specified VRF.\n    * Failure: The test will fail if BGP peers are not found or four octet asn capabilities are not advertised, received, and enabled in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeerASNCap:\n            bgp_peers:\n              - peer_address: 172.30.11.1\n                vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeerASNCap\"\n    description = \"Verifies the four octet asn capabilities of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeerASNCap test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of BGP peers.\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeerASNCap.\"\"\"\n        failures: dict[str, Any] = {\"bgp_peers\": {}}\n\n        # Iterate over each bgp peer\n        for bgp_peer in self.inputs.bgp_peers:\n            peer = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            failure: dict[str, dict[str, dict[str, Any]]] = {\"bgp_peers\": {peer: {vrf: {}}}}\n\n            # Check if BGP output exists\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer)) is None\n            ):\n                failure[\"bgp_peers\"][peer][vrf] = {\"status\": \"Not configured\"}\n                failures = deep_update(failures, failure)\n                continue\n\n            bgp_output = get_value(bgp_output, \"neighborCapabilities.fourOctetAsnCap\")\n\n            # Check if  four octet asn capabilities are found\n            if not bgp_output:\n                failure[\"bgp_peers\"][peer][vrf] = {\"fourOctetAsnCap\": \"not found\"}\n                failures = deep_update(failures, failure)\n\n            # Check if capabilities are not advertised, received, or enabled\n            elif not all(bgp_output.get(prop, False) for prop in [\"advertised\", \"received\", \"enabled\"]):\n                failure[\"bgp_peers\"][peer][vrf] = {\"fourOctetAsnCap\": bgp_output}\n                failures = deep_update(failures, failure)\n\n        # Check if there are any failures\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peer four octet asn capabilities are not found or not ok:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerASNCap-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of BGP peers. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerASNCap-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default'"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount","title":"VerifyBGPPeerCount","text":"

Verifies the count of BGP peers for a given address family.

It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).

For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.

Please refer to the Input class attributes below for details.

Expected Results
  • Success: If the count of BGP peers matches the expected count for each address family and VRF.
  • Failure: If the count of BGP peers does not match the expected count, or if BGP is not configured for an expected VRF or address family.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeerCount:\n        address_families:\n          - afi: \"evpn\"\n            num_peers: 2\n          - afi: \"ipv4\"\n            safi: \"unicast\"\n            vrf: \"PROD\"\n            num_peers: 2\n          - afi: \"ipv4\"\n            safi: \"unicast\"\n            vrf: \"default\"\n            num_peers: 3\n          - afi: \"ipv4\"\n            safi: \"multicast\"\n            vrf: \"DEV\"\n            num_peers: 3\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeerCount(AntaTest):\n    \"\"\"Verifies the count of BGP peers for a given address family.\n\n    It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).\n\n    For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.\n\n    Please refer to the Input class attributes below for details.\n\n    Expected Results\n    ----------------\n    * Success: If the count of BGP peers matches the expected count for each address family and VRF.\n    * Failure: If the count of BGP peers does not match the expected count, or if BGP is not configured for an expected VRF or address family.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeerCount:\n            address_families:\n              - afi: \"evpn\"\n                num_peers: 2\n              - afi: \"ipv4\"\n                safi: \"unicast\"\n                vrf: \"PROD\"\n                num_peers: 2\n              - afi: \"ipv4\"\n                safi: \"unicast\"\n                vrf: \"default\"\n                num_peers: 3\n              - afi: \"ipv4\"\n                safi: \"multicast\"\n                vrf: \"DEV\"\n                num_peers: 3\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeerCount\"\n    description = \"Verifies the count of BGP peers.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaTemplate(template=\"show bgp {afi} {safi} summary vrf {vrf}\"),\n        AntaTemplate(template=\"show bgp {afi} summary\"),\n    ]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeerCount test.\"\"\"\n\n        address_families: list[BgpAfi]\n        \"\"\"List of BGP address families (BgpAfi).\"\"\"\n\n        class BgpAfi(BaseModel):\n            \"\"\"Model for a BGP address family (AFI) and subsequent address family (SAFI).\"\"\"\n\n            afi: Afi\n            \"\"\"BGP address family (AFI).\"\"\"\n            safi: Safi | None = None\n            \"\"\"Optional BGP subsequent service family (SAFI).\n\n            If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided.\n            \"\"\"\n            vrf: str = \"default\"\n            \"\"\"\n            Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`.\n\n            If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`.\n            \"\"\"\n            num_peers: PositiveInt\n            \"\"\"Number of expected BGP peer(s).\"\"\"\n\n            @model_validator(mode=\"after\")\n            def validate_inputs(self: BaseModel) -> BaseModel:\n                \"\"\"Validate the inputs provided to the BgpAfi class.\n\n                If afi is either ipv4 or ipv6, safi must be provided.\n\n                If afi is not ipv4 or ipv6, safi must not be provided and vrf must be default.\n                \"\"\"\n                if self.afi in [\"ipv4\", \"ipv6\"]:\n                    if self.safi is None:\n                        msg = \"'safi' must be provided when afi is ipv4 or ipv6\"\n                        raise ValueError(msg)\n                elif self.safi is not None:\n                    msg = \"'safi' must not be provided when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                elif self.vrf != \"default\":\n                    msg = \"'vrf' must be default when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                return self\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each BGP address family in the input list.\"\"\"\n        commands = []\n        for afi in self.inputs.address_families:\n            if template == VerifyBGPPeerCount.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi != \"sr-te\":\n                commands.append(template.render(afi=afi.afi, safi=afi.safi, vrf=afi.vrf, num_peers=afi.num_peers))\n\n            # For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6\n            elif template == VerifyBGPPeerCount.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi == \"sr-te\":\n                commands.append(template.render(afi=afi.safi, safi=afi.afi, vrf=afi.vrf, num_peers=afi.num_peers))\n            elif template == VerifyBGPPeerCount.commands[1] and afi.afi not in [\"ipv4\", \"ipv6\"]:\n                commands.append(template.render(afi=afi.afi, vrf=afi.vrf, num_peers=afi.num_peers))\n        return commands\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeerCount.\"\"\"\n        self.result.is_success()\n\n        failures: dict[tuple[str, Any], dict[str, Any]] = {}\n\n        for command in self.instance_commands:\n            peer_count = 0\n            command_output = command.json_output\n\n            afi = cast(Afi, command.params.get(\"afi\"))\n            safi = cast(Optional[Safi], command.params.get(\"safi\"))\n            afi_vrf = cast(str, command.params.get(\"vrf\"))\n            num_peers = cast(PositiveInt, command.params.get(\"num_peers\"))\n\n            # Swaping AFI and SAFI in case of SR-TE\n            if afi == \"sr-te\":\n                afi, safi = safi, afi\n\n            if not (vrfs := command_output.get(\"vrfs\")):\n                _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=\"Not Configured\")\n                continue\n\n            if afi_vrf == \"all\":\n                for vrf_data in vrfs.values():\n                    peer_count += len(vrf_data[\"peers\"])\n            else:\n                peer_count += len(command_output[\"vrfs\"][afi_vrf][\"peers\"])\n\n            if peer_count != num_peers:\n                _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=f\"Expected: {num_peers}, Actual: {peer_count}\")\n\n        if failures:\n            self.result.is_failure(f\"Failures: {list(failures.values())}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAfi] List of BGP address families (BgpAfi). -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount-attributes","title":"BgpAfi","text":"Name Type Description Default afi Afi BGP address family (AFI). - safi Safi | None Optional BGP subsequent service family (SAFI). If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided. None vrf str Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`. If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`. 'default' num_peers PositiveInt Number of expected BGP peer(s). -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMD5Auth","title":"VerifyBGPPeerMD5Auth","text":"

Verifies the MD5 authentication and state of IPv4 BGP peers in a specified VRF.

Expected Results
  • Success: The test will pass if IPv4 BGP peers are configured with MD5 authentication and state as established in the specified VRF.
  • Failure: The test will fail if IPv4 BGP peers are not found, state is not as established or MD5 authentication is not enabled in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeerMD5Auth:\n        bgp_peers:\n          - peer_address: 172.30.11.1\n            vrf: default\n          - peer_address: 172.30.11.5\n            vrf: default\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeerMD5Auth(AntaTest):\n    \"\"\"Verifies the MD5 authentication and state of IPv4 BGP peers in a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if IPv4 BGP peers are configured with MD5 authentication and state as established in the specified VRF.\n    * Failure: The test will fail if IPv4 BGP peers are not found, state is not as established or MD5 authentication is not enabled in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeerMD5Auth:\n            bgp_peers:\n              - peer_address: 172.30.11.1\n                vrf: default\n              - peer_address: 172.30.11.5\n                vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeerMD5Auth\"\n    description = \"Verifies the MD5 authentication and state of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeerMD5Auth test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of IPv4 BGP peers.\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeerMD5Auth.\"\"\"\n        failures: dict[str, Any] = {\"bgp_peers\": {}}\n\n        # Iterate over each command\n        for bgp_peer in self.inputs.bgp_peers:\n            peer = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            failure: dict[str, dict[str, dict[str, Any]]] = {\"bgp_peers\": {peer: {vrf: {}}}}\n\n            # Check if BGP output exists\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer)) is None\n            ):\n                failure[\"bgp_peers\"][peer][vrf] = {\"status\": \"Not configured\"}\n                failures = deep_update(failures, failure)\n                continue\n\n            # Check if BGP peer state and authentication\n            state = bgp_output.get(\"state\")\n            md5_auth_enabled = bgp_output.get(\"md5AuthEnabled\")\n            if state != \"Established\" or not md5_auth_enabled:\n                failure[\"bgp_peers\"][peer][vrf] = {\"state\": state, \"md5_auth_enabled\": md5_auth_enabled}\n                failures = deep_update(failures, failure)\n\n        # Check if there are any failures\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peers are not configured, not established or MD5 authentication is not enabled:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMD5Auth-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of IPv4 BGP peers. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMD5Auth-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default'"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMPCaps","title":"VerifyBGPPeerMPCaps","text":"

Verifies the multiprotocol capabilities of a BGP peer in a specified VRF.

Expected Results
  • Success: The test will pass if the BGP peer\u2019s multiprotocol capabilities are advertised, received, and enabled in the specified VRF.
  • Failure: The test will fail if BGP peers are not found or multiprotocol capabilities are not advertised, received, and enabled in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeerMPCaps:\n        bgp_peers:\n          - peer_address: 172.30.11.1\n            vrf: default\n            capabilities:\n              - ipv4Unicast\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeerMPCaps(AntaTest):\n    \"\"\"Verifies the multiprotocol capabilities of a BGP peer in a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the BGP peer's multiprotocol capabilities are advertised, received, and enabled in the specified VRF.\n    * Failure: The test will fail if BGP peers are not found or multiprotocol capabilities are not advertised, received, and enabled in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeerMPCaps:\n            bgp_peers:\n              - peer_address: 172.30.11.1\n                vrf: default\n                capabilities:\n                  - ipv4Unicast\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeerMPCaps\"\n    description = \"Verifies the multiprotocol capabilities of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeerMPCaps test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of BGP peers\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n            capabilities: list[MultiProtocolCaps]\n            \"\"\"List of multiprotocol capabilities to be verified.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeerMPCaps.\"\"\"\n        failures: dict[str, Any] = {\"bgp_peers\": {}}\n\n        # Iterate over each bgp peer\n        for bgp_peer in self.inputs.bgp_peers:\n            peer = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            capabilities = bgp_peer.capabilities\n            failure: dict[str, dict[str, dict[str, Any]]] = {\"bgp_peers\": {peer: {vrf: {}}}}\n\n            # Check if BGP output exists\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer)) is None\n            ):\n                failure[\"bgp_peers\"][peer][vrf] = {\"status\": \"Not configured\"}\n                failures = deep_update(failures, failure)\n                continue\n\n            # Check each capability\n            bgp_output = get_value(bgp_output, \"neighborCapabilities.multiprotocolCaps\")\n            for capability in capabilities:\n                capability_output = bgp_output.get(capability)\n\n                # Check if capabilities are missing\n                if not capability_output:\n                    failure[\"bgp_peers\"][peer][vrf][capability] = \"not found\"\n                    failures = deep_update(failures, failure)\n\n                # Check if capabilities are not advertised, received, or enabled\n                elif not all(capability_output.get(prop, False) for prop in [\"advertised\", \"received\", \"enabled\"]):\n                    failure[\"bgp_peers\"][peer][vrf][capability] = capability_output\n                    failures = deep_update(failures, failure)\n\n        # Check if there are any failures\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peer multiprotocol capabilities are not found or not ok:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMPCaps-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of BGP peers -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerMPCaps-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default' capabilities list[MultiProtocolCaps] List of multiprotocol capabilities to be verified. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteRefreshCap","title":"VerifyBGPPeerRouteRefreshCap","text":"

Verifies the route refresh capabilities of a BGP peer in a specified VRF.

Expected Results
  • Success: The test will pass if the BGP peer\u2019s route refresh capabilities are advertised, received, and enabled in the specified VRF.
  • Failure: The test will fail if BGP peers are not found or route refresh capabilities are not advertised, received, and enabled in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeerRouteRefreshCap:\n        bgp_peers:\n          - peer_address: 172.30.11.1\n            vrf: default\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeerRouteRefreshCap(AntaTest):\n    \"\"\"Verifies the route refresh capabilities of a BGP peer in a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the BGP peer's route refresh capabilities are advertised, received, and enabled in the specified VRF.\n    * Failure: The test will fail if BGP peers are not found or route refresh capabilities are not advertised, received, and enabled in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeerRouteRefreshCap:\n            bgp_peers:\n              - peer_address: 172.30.11.1\n                vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeerRouteRefreshCap\"\n    description = \"Verifies the route refresh capabilities of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeerRouteRefreshCap test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of BGP peers\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeerRouteRefreshCap.\"\"\"\n        failures: dict[str, Any] = {\"bgp_peers\": {}}\n\n        # Iterate over each bgp peer\n        for bgp_peer in self.inputs.bgp_peers:\n            peer = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            failure: dict[str, dict[str, dict[str, Any]]] = {\"bgp_peers\": {peer: {vrf: {}}}}\n\n            # Check if BGP output exists\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer)) is None\n            ):\n                failure[\"bgp_peers\"][peer][vrf] = {\"status\": \"Not configured\"}\n                failures = deep_update(failures, failure)\n                continue\n\n            bgp_output = get_value(bgp_output, \"neighborCapabilities.routeRefreshCap\")\n\n            # Check if route refresh capabilities are found\n            if not bgp_output:\n                failure[\"bgp_peers\"][peer][vrf] = {\"routeRefreshCap\": \"not found\"}\n                failures = deep_update(failures, failure)\n\n            # Check if capabilities are not advertised, received, or enabled\n            elif not all(bgp_output.get(prop, False) for prop in [\"advertised\", \"received\", \"enabled\"]):\n                failure[\"bgp_peers\"][peer][vrf] = {\"routeRefreshCap\": bgp_output}\n                failures = deep_update(failures, failure)\n\n        # Check if there are any failures\n        if not failures[\"bgp_peers\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peer route refresh capabilities are not found or not ok:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteRefreshCap-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of BGP peers -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteRefreshCap-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default'"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth","title":"VerifyBGPPeersHealth","text":"

Verifies the health of BGP peers.

It will validate that all BGP sessions are established and all message queues for these BGP sessions are empty for a given address family.

It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).

For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.

Please refer to the Input class attributes below for details.

Expected Results
  • Success: If all BGP sessions are established and all messages queues are empty for each address family and VRF.
  • Failure: If there are issues with any of the BGP sessions, or if BGP is not configured for an expected VRF or address family.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPPeersHealth:\n        address_families:\n          - afi: \"evpn\"\n          - afi: \"ipv4\"\n            safi: \"unicast\"\n            vrf: \"default\"\n          - afi: \"ipv6\"\n            safi: \"unicast\"\n            vrf: \"DEV\"\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPPeersHealth(AntaTest):\n    \"\"\"Verifies the health of BGP peers.\n\n    It will validate that all BGP sessions are established and all message queues for these BGP sessions are empty for a given address family.\n\n    It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).\n\n    For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.\n\n    Please refer to the Input class attributes below for details.\n\n    Expected Results\n    ----------------\n    * Success: If all BGP sessions are established and all messages queues are empty for each address family and VRF.\n    * Failure: If there are issues with any of the BGP sessions, or if BGP is not configured for an expected VRF or address family.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPPeersHealth:\n            address_families:\n              - afi: \"evpn\"\n              - afi: \"ipv4\"\n                safi: \"unicast\"\n                vrf: \"default\"\n              - afi: \"ipv6\"\n                safi: \"unicast\"\n                vrf: \"DEV\"\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPPeersHealth\"\n    description = \"Verifies the health of BGP peers\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaTemplate(template=\"show bgp {afi} {safi} summary vrf {vrf}\"),\n        AntaTemplate(template=\"show bgp {afi} summary\"),\n    ]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPPeersHealth test.\"\"\"\n\n        address_families: list[BgpAfi]\n        \"\"\"List of BGP address families (BgpAfi).\"\"\"\n\n        class BgpAfi(BaseModel):\n            \"\"\"Model for a BGP address family (AFI) and subsequent address family (SAFI).\"\"\"\n\n            afi: Afi\n            \"\"\"BGP address family (AFI).\"\"\"\n            safi: Safi | None = None\n            \"\"\"Optional BGP subsequent service family (SAFI).\n\n            If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided.\n            \"\"\"\n            vrf: str = \"default\"\n            \"\"\"\n            Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`.\n\n            If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`.\n            \"\"\"\n\n            @model_validator(mode=\"after\")\n            def validate_inputs(self: BaseModel) -> BaseModel:\n                \"\"\"Validate the inputs provided to the BgpAfi class.\n\n                If afi is either ipv4 or ipv6, safi must be provided.\n\n                If afi is not ipv4 or ipv6, safi must not be provided and vrf must be default.\n                \"\"\"\n                if self.afi in [\"ipv4\", \"ipv6\"]:\n                    if self.safi is None:\n                        msg = \"'safi' must be provided when afi is ipv4 or ipv6\"\n                        raise ValueError(msg)\n                elif self.safi is not None:\n                    msg = \"'safi' must not be provided when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                elif self.vrf != \"default\":\n                    msg = \"'vrf' must be default when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                return self\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each BGP address family in the input list.\"\"\"\n        commands = []\n        for afi in self.inputs.address_families:\n            if template == VerifyBGPPeersHealth.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi != \"sr-te\":\n                commands.append(template.render(afi=afi.afi, safi=afi.safi, vrf=afi.vrf))\n\n            # For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6\n            elif template == VerifyBGPPeersHealth.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi == \"sr-te\":\n                commands.append(template.render(afi=afi.safi, safi=afi.afi, vrf=afi.vrf))\n            elif template == VerifyBGPPeersHealth.commands[1] and afi.afi not in [\"ipv4\", \"ipv6\"]:\n                commands.append(template.render(afi=afi.afi, vrf=afi.vrf))\n        return commands\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPPeersHealth.\"\"\"\n        self.result.is_success()\n\n        failures: dict[tuple[str, Any], dict[str, Any]] = {}\n\n        for command in self.instance_commands:\n            command_output = command.json_output\n\n            afi = cast(Afi, command.params.get(\"afi\"))\n            safi = cast(Optional[Safi], command.params.get(\"safi\"))\n\n            # Swaping AFI and SAFI in case of SR-TE\n            if afi == \"sr-te\":\n                afi, safi = safi, afi\n            afi_vrf = cast(str, command.params.get(\"vrf\"))\n\n            if not (vrfs := command_output.get(\"vrfs\")):\n                _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=\"Not Configured\")\n                continue\n\n            for vrf, vrf_data in vrfs.items():\n                if not (peers := vrf_data.get(\"peers\")):\n                    _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=\"No Peers\")\n                    continue\n\n                peer_issues = {}\n                for peer, peer_data in peers.items():\n                    issues = _check_peer_issues(peer_data)\n\n                    if issues:\n                        peer_issues[peer] = issues\n\n                if peer_issues:\n                    _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=vrf, issue=peer_issues)\n\n        if failures:\n            self.result.is_failure(f\"Failures: {list(failures.values())}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAfi] List of BGP address families (BgpAfi). -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth-attributes","title":"BgpAfi","text":"Name Type Description Default afi Afi BGP address family (AFI). - safi Safi | None Optional BGP subsequent service family (SAFI). If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided. None vrf str Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`. If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`. 'default'"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers","title":"VerifyBGPSpecificPeers","text":"

Verifies the health of specific BGP peer(s).

It will validate that the BGP session is established and all message queues for this BGP session are empty for the given peer(s).

It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).

For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.

Please refer to the Input class attributes below for details.

Expected Results
  • Success: If the BGP session is established and all messages queues are empty for each given peer.
  • Failure: If the BGP session has issues or is not configured, or if BGP is not configured for an expected VRF or address family.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPSpecificPeers:\n        address_families:\n          - afi: \"evpn\"\n            peers:\n              - 10.1.0.1\n              - 10.1.0.2\n          - afi: \"ipv4\"\n            safi: \"unicast\"\n            peers:\n              - 10.1.254.1\n              - 10.1.255.0\n              - 10.1.255.2\n              - 10.1.255.4\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPSpecificPeers(AntaTest):\n    \"\"\"Verifies the health of specific BGP peer(s).\n\n    It will validate that the BGP session is established and all message queues for this BGP session are empty for the given peer(s).\n\n    It supports multiple types of Address Families Identifiers (AFI) and Subsequent Address Family Identifiers (SAFI).\n\n    For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6 (AFI) which is handled automatically in this test.\n\n    Please refer to the Input class attributes below for details.\n\n    Expected Results\n    ----------------\n    * Success: If the BGP session is established and all messages queues are empty for each given peer.\n    * Failure: If the BGP session has issues or is not configured, or if BGP is not configured for an expected VRF or address family.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPSpecificPeers:\n            address_families:\n              - afi: \"evpn\"\n                peers:\n                  - 10.1.0.1\n                  - 10.1.0.2\n              - afi: \"ipv4\"\n                safi: \"unicast\"\n                peers:\n                  - 10.1.254.1\n                  - 10.1.255.0\n                  - 10.1.255.2\n                  - 10.1.255.4\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPSpecificPeers\"\n    description = \"Verifies the health of specific BGP peer(s).\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n        AntaTemplate(template=\"show bgp {afi} {safi} summary vrf {vrf}\"),\n        AntaTemplate(template=\"show bgp {afi} summary\"),\n    ]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPSpecificPeers test.\"\"\"\n\n        address_families: list[BgpAfi]\n        \"\"\"List of BGP address families (BgpAfi).\"\"\"\n\n        class BgpAfi(BaseModel):\n            \"\"\"Model for a BGP address family (AFI) and subsequent address family (SAFI).\"\"\"\n\n            afi: Afi\n            \"\"\"BGP address family (AFI).\"\"\"\n            safi: Safi | None = None\n            \"\"\"Optional BGP subsequent service family (SAFI).\n\n            If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided.\n            \"\"\"\n            vrf: str = \"default\"\n            \"\"\"\n            Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`.\n\n            `all` is NOT supported.\n\n            If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`.\n            \"\"\"\n            peers: list[IPv4Address | IPv6Address]\n            \"\"\"List of BGP IPv4 or IPv6 peer.\"\"\"\n\n            @model_validator(mode=\"after\")\n            def validate_inputs(self: BaseModel) -> BaseModel:\n                \"\"\"Validate the inputs provided to the BgpAfi class.\n\n                If afi is either ipv4 or ipv6, safi must be provided and vrf must NOT be all.\n\n                If afi is not ipv4 or ipv6, safi must not be provided and vrf must be default.\n                \"\"\"\n                if self.afi in [\"ipv4\", \"ipv6\"]:\n                    if self.safi is None:\n                        msg = \"'safi' must be provided when afi is ipv4 or ipv6\"\n                        raise ValueError(msg)\n                    if self.vrf == \"all\":\n                        msg = \"'all' is not supported in this test. Use VerifyBGPPeersHealth test instead.\"\n                        raise ValueError(msg)\n                elif self.safi is not None:\n                    msg = \"'safi' must not be provided when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                elif self.vrf != \"default\":\n                    msg = \"'vrf' must be default when afi is not ipv4 or ipv6\"\n                    raise ValueError(msg)\n                return self\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each BGP address family in the input list.\"\"\"\n        commands = []\n\n        for afi in self.inputs.address_families:\n            if template == VerifyBGPSpecificPeers.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi != \"sr-te\":\n                commands.append(template.render(afi=afi.afi, safi=afi.safi, vrf=afi.vrf, peers=afi.peers))\n\n            # For SR-TE SAFI, the EOS command supports sr-te first then ipv4/ipv6\n            elif template == VerifyBGPSpecificPeers.commands[0] and afi.afi in [\"ipv4\", \"ipv6\"] and afi.safi == \"sr-te\":\n                commands.append(template.render(afi=afi.safi, safi=afi.afi, vrf=afi.vrf, peers=afi.peers))\n            elif template == VerifyBGPSpecificPeers.commands[1] and afi.afi not in [\"ipv4\", \"ipv6\"]:\n                commands.append(template.render(afi=afi.afi, vrf=afi.vrf, peers=afi.peers))\n        return commands\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPSpecificPeers.\"\"\"\n        self.result.is_success()\n\n        failures: dict[tuple[str, Any], dict[str, Any]] = {}\n\n        for command in self.instance_commands:\n            command_output = command.json_output\n\n            afi = cast(Afi, command.params.get(\"afi\"))\n            safi = cast(Optional[Safi], command.params.get(\"safi\"))\n\n            # Swaping AFI and SAFI in case of SR-TE\n            if afi == \"sr-te\":\n                afi, safi = safi, afi\n            afi_vrf = cast(str, command.params.get(\"vrf\"))\n            afi_peers = cast(List[Union[IPv4Address, IPv6Address]], command.params.get(\"peers\", []))\n\n            if not (vrfs := command_output.get(\"vrfs\")):\n                _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=\"Not Configured\")\n                continue\n\n            peer_issues = {}\n            for peer in afi_peers:\n                peer_ip = str(peer)\n                peer_data = get_value(dictionary=vrfs, key=f\"{afi_vrf}_peers_{peer_ip}\", separator=\"_\")\n                issues = _check_peer_issues(peer_data)\n                if issues:\n                    peer_issues[peer_ip] = issues\n\n            if peer_issues:\n                _add_bgp_failures(failures=failures, afi=afi, safi=safi, vrf=afi_vrf, issue=peer_issues)\n\n        if failures:\n            self.result.is_failure(f\"Failures: {list(failures.values())}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAfi] List of BGP address families (BgpAfi). -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers-attributes","title":"BgpAfi","text":"Name Type Description Default afi Afi BGP address family (AFI). - safi Safi | None Optional BGP subsequent service family (SAFI). If the input `afi` is `ipv4` or `ipv6`, a valid `safi` must be provided. None vrf str Optional VRF for IPv4 and IPv6. If not provided, it defaults to `default`. `all` is NOT supported. If the input `afi` is not `ipv4` or `ipv6`, e.g. `evpn`, `vrf` must be `default`. 'default' peers list[IPv4Address | IPv6Address] List of BGP IPv4 or IPv6 peer. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPTimers","title":"VerifyBGPTimers","text":"

Verifies if the BGP peers are configured with the correct hold and keep-alive timers in the specified VRF.

Expected Results
  • Success: The test will pass if the hold and keep-alive timers are correct for BGP peers in the specified VRF.
  • Failure: The test will fail if BGP peers are not found or hold and keep-alive timers are not correct in the specified VRF.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyBGPTimers:\n        bgp_peers:\n          - peer_address: 172.30.11.1\n            vrf: default\n            hold_time: 180\n            keep_alive_time: 60\n          - peer_address: 172.30.11.5\n            vrf: default\n            hold_time: 180\n            keep_alive_time: 60\n
Source code in anta/tests/routing/bgp.py
class VerifyBGPTimers(AntaTest):\n    \"\"\"Verifies if the BGP peers are configured with the correct hold and keep-alive timers in the specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the hold and keep-alive timers are correct for BGP peers in the specified VRF.\n    * Failure: The test will fail if BGP peers are not found or hold and keep-alive timers are not correct in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyBGPTimers:\n            bgp_peers:\n              - peer_address: 172.30.11.1\n                vrf: default\n                hold_time: 180\n                keep_alive_time: 60\n              - peer_address: 172.30.11.5\n                vrf: default\n                hold_time: 180\n                keep_alive_time: 60\n    ```\n    \"\"\"\n\n    name = \"VerifyBGPTimers\"\n    description = \"Verifies the timers of a BGP peer.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBGPTimers test.\"\"\"\n\n        bgp_peers: list[BgpPeer]\n        \"\"\"List of BGP peers\"\"\"\n\n        class BgpPeer(BaseModel):\n            \"\"\"Model for a BGP peer.\"\"\"\n\n            peer_address: IPv4Address\n            \"\"\"IPv4 address of a BGP peer.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"Optional VRF for BGP peer. If not provided, it defaults to `default`.\"\"\"\n            hold_time: int = Field(ge=3, le=7200)\n            \"\"\"BGP hold time in seconds.\"\"\"\n            keep_alive_time: int = Field(ge=0, le=3600)\n            \"\"\"BGP keep-alive time in seconds.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBGPTimers.\"\"\"\n        failures: dict[str, Any] = {}\n\n        # Iterate over each bgp peer\n        for bgp_peer in self.inputs.bgp_peers:\n            peer_address = str(bgp_peer.peer_address)\n            vrf = bgp_peer.vrf\n            hold_time = bgp_peer.hold_time\n            keep_alive_time = bgp_peer.keep_alive_time\n\n            # Verify BGP peer\n            if (\n                not (bgp_output := get_value(self.instance_commands[0].json_output, f\"vrfs.{vrf}.peerList\"))\n                or (bgp_output := get_item(bgp_output, \"peerAddress\", peer_address)) is None\n            ):\n                failures[peer_address] = {vrf: \"Not configured\"}\n                continue\n\n            # Verify BGP peer's hold and keep alive timers\n            if bgp_output.get(\"holdTime\") != hold_time or bgp_output.get(\"keepaliveTime\") != keep_alive_time:\n                failures[peer_address] = {vrf: {\"hold_time\": bgp_output.get(\"holdTime\"), \"keep_alive_time\": bgp_output.get(\"keepaliveTime\")}}\n\n        if not failures:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Following BGP peers are not configured or hold and keep-alive timers are not correct:\\n{failures}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPTimers-attributes","title":"Inputs","text":"Name Type Description Default bgp_peers list[BgpPeer] List of BGP peers -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPTimers-attributes","title":"BgpPeer","text":"Name Type Description Default peer_address IPv4Address IPv4 address of a BGP peer. - vrf str Optional VRF for BGP peer. If not provided, it defaults to `default`. 'default' hold_time int BGP hold time in seconds. Field(ge=3, le=7200) keep_alive_time int BGP keep-alive time in seconds. Field(ge=0, le=3600)"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyEVPNType2Route","title":"VerifyEVPNType2Route","text":"

Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI.

Expected Results
  • Success: If all provided VXLAN endpoints have at least one valid and active path to their EVPN Type-2 routes.
  • Failure: If any of the provided VXLAN endpoints do not have at least one valid and active path to their EVPN Type-2 routes.
Examples
anta.tests.routing:\n  bgp:\n    - VerifyEVPNType2Route:\n        vxlan_endpoints:\n          - address: 192.168.20.102\n            vni: 10020\n          - address: aac1.ab5d.b41e\n            vni: 10010\n
Source code in anta/tests/routing/bgp.py
class VerifyEVPNType2Route(AntaTest):\n    \"\"\"Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI.\n\n    Expected Results\n    ----------------\n    * Success: If all provided VXLAN endpoints have at least one valid and active path to their EVPN Type-2 routes.\n    * Failure: If any of the provided VXLAN endpoints do not have at least one valid and active path to their EVPN Type-2 routes.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      bgp:\n        - VerifyEVPNType2Route:\n            vxlan_endpoints:\n              - address: 192.168.20.102\n                vni: 10020\n              - address: aac1.ab5d.b41e\n                vni: 10010\n    ```\n    \"\"\"\n\n    name = \"VerifyEVPNType2Route\"\n    description = \"Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI.\"\n    categories: ClassVar[list[str]] = [\"bgp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp evpn route-type mac-ip {address} vni {vni}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyEVPNType2Route test.\"\"\"\n\n        vxlan_endpoints: list[VxlanEndpoint]\n        \"\"\"List of VXLAN endpoints to verify.\"\"\"\n\n        class VxlanEndpoint(BaseModel):\n            \"\"\"Model for a VXLAN endpoint.\"\"\"\n\n            address: IPv4Address | MacAddress\n            \"\"\"IPv4 or MAC address of the VXLAN endpoint.\"\"\"\n            vni: Vni\n            \"\"\"VNI of the VXLAN endpoint.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each VXLAN endpoint in the input list.\"\"\"\n        return [template.render(address=endpoint.address, vni=endpoint.vni) for endpoint in self.inputs.vxlan_endpoints]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEVPNType2Route.\"\"\"\n        self.result.is_success()\n        no_evpn_routes = []\n        bad_evpn_routes = []\n\n        for command in self.instance_commands:\n            address = str(command.params[\"address\"])\n            vni = command.params[\"vni\"]\n            # Verify that the VXLAN endpoint is in the BGP EVPN table\n            evpn_routes = command.json_output[\"evpnRoutes\"]\n            if not evpn_routes:\n                no_evpn_routes.append((address, vni))\n                continue\n            # Verify that each EVPN route has at least one valid and active path\n            for route, route_data in evpn_routes.items():\n                has_active_path = False\n                for path in route_data[\"evpnRoutePaths\"]:\n                    if path[\"routeType\"][\"valid\"] is True and path[\"routeType\"][\"active\"] is True:\n                        # At least one path is valid and active, no need to check the other paths\n                        has_active_path = True\n                        break\n                if not has_active_path:\n                    bad_evpn_routes.append(route)\n\n        if no_evpn_routes:\n            self.result.is_failure(f\"The following VXLAN endpoint do not have any EVPN Type-2 route: {no_evpn_routes}\")\n        if bad_evpn_routes:\n            self.result.is_failure(f\"The following EVPN Type-2 routes do not have at least one valid and active path: {bad_evpn_routes}\")\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyEVPNType2Route-attributes","title":"Inputs","text":"Name Type Description Default vxlan_endpoints list[VxlanEndpoint] List of VXLAN endpoints to verify. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyEVPNType2Route-attributes","title":"VxlanEndpoint","text":"Name Type Description Default address IPv4Address | MacAddress IPv4 or MAC address of the VXLAN endpoint. - vni Vni VNI of the VXLAN endpoint. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp._add_bgp_failures","title":"_add_bgp_failures","text":"
_add_bgp_failures(failures: dict[tuple[str, str | None], dict[str, Any]], afi: Afi, safi: Safi | None, vrf: str, issue: str | dict[str, Any]) -> None\n

Add a BGP failure entry to the given failures dictionary.

Note: This function modifies failures in-place.

Args:
failures (dict): The dictionary to which the failure will be added.\nafi (Afi): The address family identifier.\nvrf (str): The VRF name.\nsafi (Safi, optional): The subsequent address family identifier.\nissue (Any): A description of the issue. Can be of any type.\n
Example:

The failures dictionnary will have the following structure: { (\u2018afi1\u2019, \u2018safi1\u2019): { \u2018afi\u2019: \u2018afi1\u2019, \u2018safi\u2019: \u2018safi1\u2019, \u2018vrfs\u2019: { \u2018vrf1\u2019: issue1, \u2018vrf2\u2019: issue2 } }, (\u2018afi2\u2019, None): { \u2018afi\u2019: \u2018afi2\u2019, \u2018vrfs\u2019: { \u2018vrf1\u2019: issue3 } } }

Source code in anta/tests/routing/bgp.py
def _add_bgp_failures(failures: dict[tuple[str, str | None], dict[str, Any]], afi: Afi, safi: Safi | None, vrf: str, issue: str | dict[str, Any]) -> None:\n    \"\"\"Add a BGP failure entry to the given `failures` dictionary.\n\n    Note: This function modifies `failures` in-place.\n\n    Args:\n    ----\n        failures (dict): The dictionary to which the failure will be added.\n        afi (Afi): The address family identifier.\n        vrf (str): The VRF name.\n        safi (Safi, optional): The subsequent address family identifier.\n        issue (Any): A description of the issue. Can be of any type.\n\n    Example:\n    -------\n    The `failures` dictionnary will have the following structure:\n        {\n            ('afi1', 'safi1'): {\n                'afi': 'afi1',\n                'safi': 'safi1',\n                'vrfs': {\n                    'vrf1': issue1,\n                    'vrf2': issue2\n                }\n            },\n            ('afi2', None): {\n                'afi': 'afi2',\n                'vrfs': {\n                    'vrf1': issue3\n                }\n            }\n        }\n\n    \"\"\"\n    key = (afi, safi)\n\n    failure_entry = failures.setdefault(key, {\"afi\": afi, \"safi\": safi, \"vrfs\": {}}) if safi else failures.setdefault(key, {\"afi\": afi, \"vrfs\": {}})\n\n    failure_entry[\"vrfs\"][vrf] = issue\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp._add_bgp_routes_failure","title":"_add_bgp_routes_failure","text":"
_add_bgp_routes_failure(bgp_routes: list[str], bgp_output: dict[str, Any], peer: str, vrf: str, route_type: str = 'advertised_routes') -> dict[str, dict[str, dict[str, dict[str, list[str]]]]]\n

Identify missing BGP routes and invalid or inactive route entries.

This function checks the BGP output from the device against the expected routes.

It identifies any missing routes as well as any routes that are invalid or inactive. The results are returned in a dictionary.

Args:
bgp_routes (list[str]): The list of expected routes.\nbgp_output (dict[str, Any]): The BGP output from the device.\npeer (str): The IP address of the BGP peer.\nvrf (str): The name of the VRF for which the routes need to be verified.\nroute_type (str, optional): The type of BGP routes. Defaults to 'advertised_routes'.\n

Returns:

Type Description dict[str, dict[str, dict[str, dict[str, list[str]]]]]: A dictionary containing the missing routes and invalid or inactive routes. Source code in anta/tests/routing/bgp.py
def _add_bgp_routes_failure(\n    bgp_routes: list[str],\n    bgp_output: dict[str, Any],\n    peer: str,\n    vrf: str,\n    route_type: str = \"advertised_routes\",\n) -> dict[str, dict[str, dict[str, dict[str, list[str]]]]]:\n    \"\"\"Identify missing BGP routes and invalid or inactive route entries.\n\n    This function checks the BGP output from the device against the expected routes.\n\n    It identifies any missing routes as well as any routes that are invalid or inactive. The results are returned in a dictionary.\n\n    Args:\n    ----\n        bgp_routes (list[str]): The list of expected routes.\n        bgp_output (dict[str, Any]): The BGP output from the device.\n        peer (str): The IP address of the BGP peer.\n        vrf (str): The name of the VRF for which the routes need to be verified.\n        route_type (str, optional): The type of BGP routes. Defaults to 'advertised_routes'.\n\n    Returns\n    -------\n        dict[str, dict[str, dict[str, dict[str, list[str]]]]]: A dictionary containing the missing routes and invalid or inactive routes.\n\n    \"\"\"\n    # Prepare the failure routes dictionary\n    failure_routes: dict[str, dict[str, Any]] = {}\n\n    # Iterate over the expected BGP routes\n    for route in bgp_routes:\n        str_route = str(route)\n        failure = {\"bgp_peers\": {peer: {vrf: {route_type: {str_route: Any}}}}}\n\n        # Check if the route is missing in the BGP output\n        if str_route not in bgp_output:\n            # If missing, add it to the failure routes dictionary\n            failure[\"bgp_peers\"][peer][vrf][route_type][str_route] = \"Not found\"\n            failure_routes = deep_update(failure_routes, failure)\n            continue\n\n        # Check if the route is active and valid\n        is_active = bgp_output[str_route][\"bgpRoutePaths\"][0][\"routeType\"][\"valid\"]\n        is_valid = bgp_output[str_route][\"bgpRoutePaths\"][0][\"routeType\"][\"active\"]\n\n        # If the route is either inactive or invalid, add it to the failure routes dictionary\n        if not is_active or not is_valid:\n            failure[\"bgp_peers\"][peer][vrf][route_type][str_route] = {\"valid\": is_valid, \"active\": is_active}\n            failure_routes = deep_update(failure_routes, failure)\n\n    return failure_routes\n
"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp._check_peer_issues","title":"_check_peer_issues","text":"
_check_peer_issues(peer_data: dict[str, Any] | None) -> dict[str, Any]\n

Check for issues in BGP peer data.

Args:
peer_data (dict, optional): The BGP peer data dictionary nested in the `show bgp <afi> <safi> summary` command.\n

Returns:

Type Description dict: Dictionary with keys indicating issues or an empty dictionary if no issues.

Raises:

Type Description ValueError: If any of the required keys (\"peerState\", \"inMsgQueue\", \"outMsgQueue\") are missing in `peer_data`, i.e. invalid BGP peer data. Example:
{\"peerNotFound\": True}\n{\"peerState\": \"Idle\", \"inMsgQueue\": 2, \"outMsgQueue\": 0}\n{}\n
Source code in anta/tests/routing/bgp.py
def _check_peer_issues(peer_data: dict[str, Any] | None) -> dict[str, Any]:\n    \"\"\"Check for issues in BGP peer data.\n\n    Args:\n    ----\n        peer_data (dict, optional): The BGP peer data dictionary nested in the `show bgp <afi> <safi> summary` command.\n\n    Returns\n    -------\n        dict: Dictionary with keys indicating issues or an empty dictionary if no issues.\n\n    Raises\n    ------\n        ValueError: If any of the required keys (\"peerState\", \"inMsgQueue\", \"outMsgQueue\") are missing in `peer_data`, i.e. invalid BGP peer data.\n\n    Example:\n    -------\n        {\"peerNotFound\": True}\n        {\"peerState\": \"Idle\", \"inMsgQueue\": 2, \"outMsgQueue\": 0}\n        {}\n\n    \"\"\"\n    if peer_data is None:\n        return {\"peerNotFound\": True}\n\n    if any(key not in peer_data for key in [\"peerState\", \"inMsgQueue\", \"outMsgQueue\"]):\n        msg = \"Provided BGP peer data is invalid.\"\n        raise ValueError(msg)\n\n    if peer_data[\"peerState\"] != \"Established\" or peer_data[\"inMsgQueue\"] != 0 or peer_data[\"outMsgQueue\"] != 0:\n        return {\"peerState\": peer_data[\"peerState\"], \"inMsgQueue\": peer_data[\"inMsgQueue\"], \"outMsgQueue\": peer_data[\"outMsgQueue\"]}\n\n    return {}\n
"},{"location":"api/tests.routing.generic/","title":"Generic","text":""},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingProtocolModel","title":"VerifyRoutingProtocolModel","text":"

Verifies the configured routing protocol model is the one we expect.

Expected Results
  • Success: The test will pass if the configured routing protocol model is the one we expect.
  • Failure: The test will fail if the configured routing protocol model is not the one we expect.
Examples
anta.tests.routing:\n  generic:\n    - VerifyRoutingProtocolModel:\n        model: multi-agent\n
Source code in anta/tests/routing/generic.py
class VerifyRoutingProtocolModel(AntaTest):\n    \"\"\"Verifies the configured routing protocol model is the one we expect.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the configured routing protocol model is the one we expect.\n    * Failure: The test will fail if the configured routing protocol model is not the one we expect.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      generic:\n        - VerifyRoutingProtocolModel:\n            model: multi-agent\n    ```\n    \"\"\"\n\n    name = \"VerifyRoutingProtocolModel\"\n    description = \"Verifies the configured routing protocol model.\"\n    categories: ClassVar[list[str]] = [\"routing\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip route summary\", revision=3)]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyRoutingProtocolModel test.\"\"\"\n\n        model: Literal[\"multi-agent\", \"ribd\"] = \"multi-agent\"\n        \"\"\"Expected routing protocol model. Defaults to `multi-agent`.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyRoutingProtocolModel.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        configured_model = command_output[\"protoModelStatus\"][\"configuredProtoModel\"]\n        operating_model = command_output[\"protoModelStatus\"][\"operatingProtoModel\"]\n        if configured_model == operating_model == self.inputs.model:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"routing model is misconfigured: configured: {configured_model} - operating: {operating_model} - expected: {self.inputs.model}\")\n
"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingProtocolModel-attributes","title":"Inputs","text":"Name Type Description Default model Literal['multi-agent', 'ribd'] Expected routing protocol model. Defaults to `multi-agent`. 'multi-agent'"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableEntry","title":"VerifyRoutingTableEntry","text":"

Verifies that the provided routes are present in the routing table of a specified VRF.

Expected Results
  • Success: The test will pass if the provided routes are present in the routing table.
  • Failure: The test will fail if one or many provided routes are missing from the routing table.
Examples
anta.tests.routing:\n  generic:\n    - VerifyRoutingTableEntry:\n        vrf: default\n        routes:\n          - 10.1.0.1\n          - 10.1.0.2\n
Source code in anta/tests/routing/generic.py
class VerifyRoutingTableEntry(AntaTest):\n    \"\"\"Verifies that the provided routes are present in the routing table of a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the provided routes are present in the routing table.\n    * Failure: The test will fail if one or many provided routes are missing from the routing table.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      generic:\n        - VerifyRoutingTableEntry:\n            vrf: default\n            routes:\n              - 10.1.0.1\n              - 10.1.0.2\n    ```\n    \"\"\"\n\n    name = \"VerifyRoutingTableEntry\"\n    description = \"Verifies that the provided routes are present in the routing table of a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"routing\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip route vrf {vrf} {route}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyRoutingTableEntry test.\"\"\"\n\n        vrf: str = \"default\"\n        \"\"\"VRF context. Defaults to `default` VRF.\"\"\"\n        routes: list[IPv4Address]\n        \"\"\"List of routes to verify.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each route in the input list.\"\"\"\n        return [template.render(vrf=self.inputs.vrf, route=route) for route in self.inputs.routes]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyRoutingTableEntry.\"\"\"\n        missing_routes = []\n\n        for command in self.instance_commands:\n            if \"vrf\" in command.params and \"route\" in command.params:\n                vrf, route = command.params[\"vrf\"], command.params[\"route\"]\n                if len(routes := command.json_output[\"vrfs\"][vrf][\"routes\"]) == 0 or route != ip_interface(next(iter(routes))).ip:\n                    missing_routes.append(str(route))\n\n        if not missing_routes:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"The following route(s) are missing from the routing table of VRF {self.inputs.vrf}: {missing_routes}\")\n
"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableEntry-attributes","title":"Inputs","text":"Name Type Description Default vrf str VRF context. Defaults to `default` VRF. 'default' routes list[IPv4Address] List of routes to verify. -"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableSize","title":"VerifyRoutingTableSize","text":"

Verifies the size of the IP routing table of the default VRF.

Expected Results
  • Success: The test will pass if the routing table size is between the provided minimum and maximum values.
  • Failure: The test will fail if the routing table size is not between the provided minimum and maximum values.
Examples
anta.tests.routing:\n  generic:\n    - VerifyRoutingTableSize:\n        minimum: 2\n        maximum: 20\n
Source code in anta/tests/routing/generic.py
class VerifyRoutingTableSize(AntaTest):\n    \"\"\"Verifies the size of the IP routing table of the default VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the routing table size is between the provided minimum and maximum values.\n    * Failure: The test will fail if the routing table size is not between the provided minimum and maximum values.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      generic:\n        - VerifyRoutingTableSize:\n            minimum: 2\n            maximum: 20\n    ```\n    \"\"\"\n\n    name = \"VerifyRoutingTableSize\"\n    description = \"Verifies the size of the IP routing table of the default VRF.\"\n    categories: ClassVar[list[str]] = [\"routing\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip route summary\", revision=3)]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyRoutingTableSize test.\"\"\"\n\n        minimum: int\n        \"\"\"Expected minimum routing table size.\"\"\"\n        maximum: int\n        \"\"\"Expected maximum routing table size.\"\"\"\n\n        @model_validator(mode=\"after\")  # type: ignore[misc]\n        def check_min_max(self) -> AntaTest.Input:\n            \"\"\"Validate that maximum is greater than minimum.\"\"\"\n            if self.minimum > self.maximum:\n                msg = f\"Minimum {self.minimum} is greater than maximum {self.maximum}\"\n                raise ValueError(msg)\n            return self\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyRoutingTableSize.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        total_routes = int(command_output[\"vrfs\"][\"default\"][\"totalRoutes\"])\n        if self.inputs.minimum <= total_routes <= self.inputs.maximum:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"routing-table has {total_routes} routes and not between min ({self.inputs.minimum}) and maximum ({self.inputs.maximum})\")\n
"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableSize-attributes","title":"Inputs","text":"Name Type Description Default minimum int Expected minimum routing table size. - maximum int Expected maximum routing table size. -"},{"location":"api/tests.routing.ospf/","title":"OSPF","text":""},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFNeighborCount","title":"VerifyOSPFNeighborCount","text":"

Verifies the number of OSPF neighbors in FULL state is the one we expect.

Expected Results
  • Success: The test will pass if the number of OSPF neighbors in FULL state is the one we expect.
  • Failure: The test will fail if the number of OSPF neighbors in FULL state is not the one we expect.
  • Skipped: The test will be skipped if no OSPF neighbor is found.
Examples
anta.tests.routing:\n  ospf:\n    - VerifyOSPFNeighborCount:\n        number: 3\n
Source code in anta/tests/routing/ospf.py
class VerifyOSPFNeighborCount(AntaTest):\n    \"\"\"Verifies the number of OSPF neighbors in FULL state is the one we expect.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the number of OSPF neighbors in FULL state is the one we expect.\n    * Failure: The test will fail if the number of OSPF neighbors in FULL state is not the one we expect.\n    * Skipped: The test will be skipped if no OSPF neighbor is found.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      ospf:\n        - VerifyOSPFNeighborCount:\n            number: 3\n    ```\n    \"\"\"\n\n    name = \"VerifyOSPFNeighborCount\"\n    description = \"Verifies the number of OSPF neighbors in FULL state is the one we expect.\"\n    categories: ClassVar[list[str]] = [\"ospf\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyOSPFNeighborCount test.\"\"\"\n\n        number: int\n        \"\"\"The expected number of OSPF neighbors in FULL state.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyOSPFNeighborCount.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if (neighbor_count := _count_ospf_neighbor(command_output)) == 0:\n            self.result.is_skipped(\"no OSPF neighbor found\")\n            return\n        self.result.is_success()\n        if neighbor_count != self.inputs.number:\n            self.result.is_failure(f\"device has {neighbor_count} neighbors (expected {self.inputs.number})\")\n        not_full_neighbors = _get_not_full_ospf_neighbors(command_output)\n        if not_full_neighbors:\n            self.result.is_failure(f\"Some neighbors are not correctly configured: {not_full_neighbors}.\")\n
"},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFNeighborCount-attributes","title":"Inputs","text":"Name Type Description Default number int The expected number of OSPF neighbors in FULL state. -"},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFNeighborState","title":"VerifyOSPFNeighborState","text":"

Verifies all OSPF neighbors are in FULL state.

Expected Results
  • Success: The test will pass if all OSPF neighbors are in FULL state.
  • Failure: The test will fail if some OSPF neighbors are not in FULL state.
  • Skipped: The test will be skipped if no OSPF neighbor is found.
Examples
anta.tests.routing:\n  ospf:\n    - VerifyOSPFNeighborState:\n
Source code in anta/tests/routing/ospf.py
class VerifyOSPFNeighborState(AntaTest):\n    \"\"\"Verifies all OSPF neighbors are in FULL state.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all OSPF neighbors are in FULL state.\n    * Failure: The test will fail if some OSPF neighbors are not in FULL state.\n    * Skipped: The test will be skipped if no OSPF neighbor is found.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.routing:\n      ospf:\n        - VerifyOSPFNeighborState:\n    ```\n    \"\"\"\n\n    name = \"VerifyOSPFNeighborState\"\n    description = \"Verifies all OSPF neighbors are in FULL state.\"\n    categories: ClassVar[list[str]] = [\"ospf\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyOSPFNeighborState.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if _count_ospf_neighbor(command_output) == 0:\n            self.result.is_skipped(\"no OSPF neighbor found\")\n            return\n        self.result.is_success()\n        not_full_neighbors = _get_not_full_ospf_neighbors(command_output)\n        if not_full_neighbors:\n            self.result.is_failure(f\"Some neighbors are not correctly configured: {not_full_neighbors}.\")\n
"},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf._count_ospf_neighbor","title":"_count_ospf_neighbor","text":"
_count_ospf_neighbor(ospf_neighbor_json: dict[str, Any]) -> int\n

Count the number of OSPF neighbors.

Args:

ospf_neighbor_json (dict[str, Any]): The JSON output of the show ip ospf neighbor command.

Returns:

Type Description int: The number of OSPF neighbors. Source code in anta/tests/routing/ospf.py
def _count_ospf_neighbor(ospf_neighbor_json: dict[str, Any]) -> int:\n    \"\"\"Count the number of OSPF neighbors.\n\n    Args:\n    ----\n      ospf_neighbor_json (dict[str, Any]): The JSON output of the `show ip ospf neighbor` command.\n\n    Returns\n    -------\n      int: The number of OSPF neighbors.\n\n    \"\"\"\n    count = 0\n    for vrf_data in ospf_neighbor_json[\"vrfs\"].values():\n        for instance_data in vrf_data[\"instList\"].values():\n            count += len(instance_data.get(\"ospfNeighborEntries\", []))\n    return count\n
"},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf._get_not_full_ospf_neighbors","title":"_get_not_full_ospf_neighbors","text":"
_get_not_full_ospf_neighbors(ospf_neighbor_json: dict[str, Any]) -> list[dict[str, Any]]\n

Return the OSPF neighbors whose adjacency state is not full.

Args:

ospf_neighbor_json (dict[str, Any]): The JSON output of the show ip ospf neighbor command.

Returns:

Type Description list[dict[str, Any]]: A list of OSPF neighbors whose adjacency state is not `full`. Source code in anta/tests/routing/ospf.py
def _get_not_full_ospf_neighbors(ospf_neighbor_json: dict[str, Any]) -> list[dict[str, Any]]:\n    \"\"\"Return the OSPF neighbors whose adjacency state is not `full`.\n\n    Args:\n    ----\n      ospf_neighbor_json (dict[str, Any]): The JSON output of the `show ip ospf neighbor` command.\n\n    Returns\n    -------\n      list[dict[str, Any]]: A list of OSPF neighbors whose adjacency state is not `full`.\n\n    \"\"\"\n    return [\n        {\n            \"vrf\": vrf,\n            \"instance\": instance,\n            \"neighbor\": neighbor_data[\"routerId\"],\n            \"state\": state,\n        }\n        for vrf, vrf_data in ospf_neighbor_json[\"vrfs\"].items()\n        for instance, instance_data in vrf_data[\"instList\"].items()\n        for neighbor_data in instance_data.get(\"ospfNeighborEntries\", [])\n        if (state := neighbor_data[\"adjacencyState\"]) != \"full\"\n    ]\n
"},{"location":"api/tests.security/","title":"Security","text":""},{"location":"api/tests.security/#anta.tests.security.VerifyAPIHttpStatus","title":"VerifyAPIHttpStatus","text":"

Verifies if eAPI HTTP server is disabled globally.

Expected Results
  • Success: The test will pass if eAPI HTTP server is disabled globally.
  • Failure: The test will fail if eAPI HTTP server is NOT disabled globally.
Examples
anta.tests.security:\n  - VerifyAPIHttpStatus:\n
Source code in anta/tests/security.py
class VerifyAPIHttpStatus(AntaTest):\n    \"\"\"Verifies if eAPI HTTP server is disabled globally.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if eAPI HTTP server is disabled globally.\n    * Failure: The test will fail if eAPI HTTP server is NOT disabled globally.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyAPIHttpStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyAPIHttpStatus\"\n    description = \"Verifies if eAPI HTTP server is disabled globally.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAPIHttpStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"enabled\"] and not command_output[\"httpServer\"][\"running\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"eAPI HTTP server is enabled globally\")\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIHttpsSSL","title":"VerifyAPIHttpsSSL","text":"

Verifies if eAPI HTTPS server SSL profile is configured and valid.

Expected Results
  • Success: The test will pass if the eAPI HTTPS server SSL profile is configured and valid.
  • Failure: The test will fail if the eAPI HTTPS server SSL profile is NOT configured, misconfigured or invalid.
Examples
anta.tests.security:\n  - VerifyAPIHttpsSSL:\n      profile: default\n
Source code in anta/tests/security.py
class VerifyAPIHttpsSSL(AntaTest):\n    \"\"\"Verifies if eAPI HTTPS server SSL profile is configured and valid.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the eAPI HTTPS server SSL profile is configured and valid.\n    * Failure: The test will fail if the eAPI HTTPS server SSL profile is NOT configured, misconfigured or invalid.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyAPIHttpsSSL:\n          profile: default\n    ```\n    \"\"\"\n\n    name = \"VerifyAPIHttpsSSL\"\n    description = \"Verifies if the eAPI has a valid SSL profile.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyAPIHttpsSSL test.\"\"\"\n\n        profile: str\n        \"\"\"SSL profile to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAPIHttpsSSL.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        try:\n            if command_output[\"sslProfile\"][\"name\"] == self.inputs.profile and command_output[\"sslProfile\"][\"state\"] == \"valid\":\n                self.result.is_success()\n            else:\n                self.result.is_failure(f\"eAPI HTTPS server SSL profile ({self.inputs.profile}) is misconfigured or invalid\")\n\n        except KeyError:\n            self.result.is_failure(f\"eAPI HTTPS server SSL profile ({self.inputs.profile}) is not configured\")\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIHttpsSSL-attributes","title":"Inputs","text":"Name Type Description Default profile str SSL profile to verify. -"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIIPv4Acl","title":"VerifyAPIIPv4Acl","text":"

Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if eAPI has the provided number of IPv4 ACL(s) in the specified VRF.
  • Failure: The test will fail if eAPI has not the right number of IPv4 ACL(s) in the specified VRF.
Examples
anta.tests.security:\n  - VerifyAPIIPv4Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/security.py
class VerifyAPIIPv4Acl(AntaTest):\n    \"\"\"Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if eAPI has the provided number of IPv4 ACL(s) in the specified VRF.\n    * Failure: The test will fail if eAPI has not the right number of IPv4 ACL(s) in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyAPIIPv4Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyAPIIPv4Acl\"\n    description = \"Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ip access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input parameters for the VerifyAPIIPv4Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv4 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for eAPI. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAPIIPv4Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv4_acl_list = command_output[\"ipAclList\"][\"aclList\"]\n        ipv4_acl_number = len(ipv4_acl_list)\n        if ipv4_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} eAPI IPv4 ACL(s) in vrf {self.inputs.vrf} but got {ipv4_acl_number}\")\n            return\n\n        not_configured_acl = [acl[\"name\"] for acl in ipv4_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if not_configured_acl:\n            self.result.is_failure(f\"eAPI IPv4 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIIPv4Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv4 ACL(s). - vrf str The name of the VRF in which to check for eAPI. Defaults to `default` VRF. 'default'"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIIPv6Acl","title":"VerifyAPIIPv6Acl","text":"

Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if eAPI has the provided number of IPv6 ACL(s) in the specified VRF.
  • Failure: The test will fail if eAPI has not the right number of IPv6 ACL(s) in the specified VRF.
  • Skipped: The test will be skipped if the number of IPv6 ACL(s) or VRF parameter is not provided.
Examples
anta.tests.security:\n  - VerifyAPIIPv6Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/security.py
class VerifyAPIIPv6Acl(AntaTest):\n    \"\"\"Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if eAPI has the provided number of IPv6 ACL(s) in the specified VRF.\n    * Failure: The test will fail if eAPI has not the right number of IPv6 ACL(s) in the specified VRF.\n    * Skipped: The test will be skipped if the number of IPv6 ACL(s) or VRF parameter is not provided.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyAPIIPv6Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifyAPIIPv6Acl\"\n    description = \"Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ipv6 access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input parameters for the VerifyAPIIPv6Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv6 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for eAPI. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAPIIPv6Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv6_acl_list = command_output[\"ipv6AclList\"][\"aclList\"]\n        ipv6_acl_number = len(ipv6_acl_list)\n        if ipv6_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} eAPI IPv6 ACL(s) in vrf {self.inputs.vrf} but got {ipv6_acl_number}\")\n            return\n\n        not_configured_acl = [acl[\"name\"] for acl in ipv6_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if not_configured_acl:\n            self.result.is_failure(f\"eAPI IPv6 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyAPIIPv6Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv6 ACL(s). - vrf str The name of the VRF in which to check for eAPI. Defaults to `default` VRF. 'default'"},{"location":"api/tests.security/#anta.tests.security.VerifyAPISSLCertificate","title":"VerifyAPISSLCertificate","text":"

Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size.

Expected Results
  • Success: The test will pass if the certificate\u2019s expiry date is greater than the threshold, and the certificate has the correct name, encryption algorithm, and key size.
  • Failure: The test will fail if the certificate is expired or is going to expire, or if the certificate has an incorrect name, encryption algorithm, or key size.
Examples
anta.tests.security:\n  - VerifyAPISSLCertificate:\n      certificates:\n        - certificate_name: ARISTA_SIGNING_CA.crt\n          expiry_threshold: 30\n          common_name: AristaIT-ICA ECDSA Issuing Cert Authority\n          encryption_algorithm: ECDSA\n          key_size: 256\n        - certificate_name: ARISTA_ROOT_CA.crt\n          expiry_threshold: 30\n          common_name: Arista Networks Internal IT Root Cert Authority\n          encryption_algorithm: RSA\n          key_size: 4096\n
Source code in anta/tests/security.py
class VerifyAPISSLCertificate(AntaTest):\n    \"\"\"Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the certificate's expiry date is greater than the threshold,\n                   and the certificate has the correct name, encryption algorithm, and key size.\n    * Failure: The test will fail if the certificate is expired or is going to expire,\n                   or if the certificate has an incorrect name, encryption algorithm, or key size.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyAPISSLCertificate:\n          certificates:\n            - certificate_name: ARISTA_SIGNING_CA.crt\n              expiry_threshold: 30\n              common_name: AristaIT-ICA ECDSA Issuing Cert Authority\n              encryption_algorithm: ECDSA\n              key_size: 256\n            - certificate_name: ARISTA_ROOT_CA.crt\n              expiry_threshold: 30\n              common_name: Arista Networks Internal IT Root Cert Authority\n              encryption_algorithm: RSA\n              key_size: 4096\n    ```\n    \"\"\"\n\n    name = \"VerifyAPISSLCertificate\"\n    description = \"Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management security ssl certificate\"), AntaCommand(command=\"show clock\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input parameters for the VerifyAPISSLCertificate test.\"\"\"\n\n        certificates: list[APISSLCertificate]\n        \"\"\"List of API SSL certificates.\"\"\"\n\n        class APISSLCertificate(BaseModel):\n            \"\"\"Model for an API SSL certificate.\"\"\"\n\n            certificate_name: str\n            \"\"\"The name of the certificate to be verified.\"\"\"\n            expiry_threshold: int\n            \"\"\"The expiry threshold of the certificate in days.\"\"\"\n            common_name: str\n            \"\"\"The common subject name of the certificate.\"\"\"\n            encryption_algorithm: EncryptionAlgorithm\n            \"\"\"The encryption algorithm of the certificate.\"\"\"\n            key_size: RsaKeySize | EcdsaKeySize\n            \"\"\"The encryption algorithm key size of the certificate.\"\"\"\n\n            @model_validator(mode=\"after\")\n            def validate_inputs(self: BaseModel) -> BaseModel:\n                \"\"\"Validate the key size provided to the APISSLCertificates class.\n\n                If encryption_algorithm is RSA then key_size should be in {2048, 3072, 4096}.\n\n                If encryption_algorithm is ECDSA then key_size should be in {256, 384, 521}.\n                \"\"\"\n                if self.encryption_algorithm == \"RSA\" and self.key_size not in RsaKeySize.__args__:\n                    msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for RSA encryption. Allowed sizes are {RsaKeySize.__args__}.\"\n                    raise ValueError(msg)\n\n                if self.encryption_algorithm == \"ECDSA\" and self.key_size not in EcdsaKeySize.__args__:\n                    msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for ECDSA encryption. Allowed sizes are {EcdsaKeySize.__args__}.\"\n                    raise ValueError(msg)\n\n                return self\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAPISSLCertificate.\"\"\"\n        # Mark the result as success by default\n        self.result.is_success()\n\n        # Extract certificate and clock output\n        certificate_output = self.instance_commands[0].json_output\n        clock_output = self.instance_commands[1].json_output\n        current_timestamp = clock_output[\"utcTime\"]\n\n        # Iterate over each API SSL certificate\n        for certificate in self.inputs.certificates:\n            # Collecting certificate expiry time and current EOS time.\n            # These times are used to calculate the number of days until the certificate expires.\n            if not (certificate_data := get_value(certificate_output, f\"certificates..{certificate.certificate_name}\", separator=\"..\")):\n                self.result.is_failure(f\"SSL certificate '{certificate.certificate_name}', is not configured.\\n\")\n                continue\n\n            expiry_time = certificate_data[\"notAfter\"]\n            day_difference = (datetime.fromtimestamp(expiry_time, tz=timezone.utc) - datetime.fromtimestamp(current_timestamp, tz=timezone.utc)).days\n\n            # Verify certificate expiry\n            if 0 < day_difference < certificate.expiry_threshold:\n                self.result.is_failure(f\"SSL certificate `{certificate.certificate_name}` is about to expire in {day_difference} days.\\n\")\n            elif day_difference < 0:\n                self.result.is_failure(f\"SSL certificate `{certificate.certificate_name}` is expired.\\n\")\n\n            # Verify certificate common subject name, encryption algorithm and key size\n            keys_to_verify = [\"subject.commonName\", \"publicKey.encryptionAlgorithm\", \"publicKey.size\"]\n            actual_certificate_details = {key: get_value(certificate_data, key) for key in keys_to_verify}\n\n            expected_certificate_details = {\n                \"subject.commonName\": certificate.common_name,\n                \"publicKey.encryptionAlgorithm\": certificate.encryption_algorithm,\n                \"publicKey.size\": certificate.key_size,\n            }\n\n            if actual_certificate_details != expected_certificate_details:\n                failed_log = f\"SSL certificate `{certificate.certificate_name}` is not configured properly:\"\n                failed_log += get_failed_logs(expected_certificate_details, actual_certificate_details)\n                self.result.is_failure(f\"{failed_log}\\n\")\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyAPISSLCertificate-attributes","title":"Inputs","text":"Name Type Description Default certificates list[APISSLCertificate] List of API SSL certificates. -"},{"location":"api/tests.security/#anta.tests.security.VerifyAPISSLCertificate-attributes","title":"APISSLCertificate","text":"Name Type Description Default certificate_name str The name of the certificate to be verified. - expiry_threshold int The expiry threshold of the certificate in days. - common_name str The common subject name of the certificate. - encryption_algorithm EncryptionAlgorithm The encryption algorithm of the certificate. - key_size RsaKeySize | EcdsaKeySize The encryption algorithm key size of the certificate. -"},{"location":"api/tests.security/#anta.tests.security.VerifyBannerLogin","title":"VerifyBannerLogin","text":"

Verifies the login banner of a device.

Expected Results
  • Success: The test will pass if the login banner matches the provided input.
  • Failure: The test will fail if the login banner does not match the provided input.
Examples
anta.tests.security:\n  - VerifyBannerLogin:\n        login_banner: |\n            # Copyright (c) 2023-2024 Arista Networks, Inc.\n            # Use of this source code is governed by the Apache License 2.0\n            # that can be found in the LICENSE file.\n
Source code in anta/tests/security.py
class VerifyBannerLogin(AntaTest):\n    \"\"\"Verifies the login banner of a device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the login banner matches the provided input.\n    * Failure: The test will fail if the login banner does not match the provided input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyBannerLogin:\n            login_banner: |\n                # Copyright (c) 2023-2024 Arista Networks, Inc.\n                # Use of this source code is governed by the Apache License 2.0\n                # that can be found in the LICENSE file.\n    ```\n    \"\"\"\n\n    name = \"VerifyBannerLogin\"\n    description = \"Verifies the login banner of a device.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner login\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBannerLogin test.\"\"\"\n\n        login_banner: str\n        \"\"\"Expected login banner of the device.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBannerLogin.\"\"\"\n        login_banner = self.instance_commands[0].json_output[\"loginBanner\"]\n\n        # Remove leading and trailing whitespaces from each line\n        cleaned_banner = \"\\n\".join(line.strip() for line in self.inputs.login_banner.split(\"\\n\"))\n        if login_banner != cleaned_banner:\n            self.result.is_failure(f\"Expected `{cleaned_banner}` as the login banner, but found `{login_banner}` instead.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyBannerLogin-attributes","title":"Inputs","text":"Name Type Description Default login_banner str Expected login banner of the device. -"},{"location":"api/tests.security/#anta.tests.security.VerifyBannerMotd","title":"VerifyBannerMotd","text":"

Verifies the motd banner of a device.

Expected Results
  • Success: The test will pass if the motd banner matches the provided input.
  • Failure: The test will fail if the motd banner does not match the provided input.
Examples
anta.tests.security:\n  - VerifyBannerMotd:\n        motd_banner: |\n            # Copyright (c) 2023-2024 Arista Networks, Inc.\n            # Use of this source code is governed by the Apache License 2.0\n            # that can be found in the LICENSE file.\n
Source code in anta/tests/security.py
class VerifyBannerMotd(AntaTest):\n    \"\"\"Verifies the motd banner of a device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the motd banner matches the provided input.\n    * Failure: The test will fail if the motd banner does not match the provided input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyBannerMotd:\n            motd_banner: |\n                # Copyright (c) 2023-2024 Arista Networks, Inc.\n                # Use of this source code is governed by the Apache License 2.0\n                # that can be found in the LICENSE file.\n    ```\n    \"\"\"\n\n    name = \"VerifyBannerMotd\"\n    description = \"Verifies the motd banner of a device.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner motd\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyBannerMotd test.\"\"\"\n\n        motd_banner: str\n        \"\"\"Expected motd banner of the device.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyBannerMotd.\"\"\"\n        motd_banner = self.instance_commands[0].json_output[\"motd\"]\n\n        # Remove leading and trailing whitespaces from each line\n        cleaned_banner = \"\\n\".join(line.strip() for line in self.inputs.motd_banner.split(\"\\n\"))\n        if motd_banner != cleaned_banner:\n            self.result.is_failure(f\"Expected `{cleaned_banner}` as the motd banner, but found `{motd_banner}` instead.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyBannerMotd-attributes","title":"Inputs","text":"Name Type Description Default motd_banner str Expected motd banner of the device. -"},{"location":"api/tests.security/#anta.tests.security.VerifyIPv4ACL","title":"VerifyIPv4ACL","text":"

Verifies the configuration of IPv4 ACLs.

Expected Results
  • Success: The test will pass if an IPv4 ACL is configured with the correct sequence entries.
  • Failure: The test will fail if an IPv4 ACL is not configured or entries are not in sequence.
Examples
anta.tests.security:\n  - VerifyIPv4ACL:\n      ipv4_access_lists:\n        - name: default-control-plane-acl\n          entries:\n            - sequence: 10\n              action: permit icmp any any\n            - sequence: 20\n              action: permit ip any any tracked\n            - sequence: 30\n              action: permit udp any any eq bfd ttl eq 255\n        - name: LabTest\n          entries:\n            - sequence: 10\n              action: permit icmp any any\n            - sequence: 20\n              action: permit tcp any any range 5900 5910\n
Source code in anta/tests/security.py
class VerifyIPv4ACL(AntaTest):\n    \"\"\"Verifies the configuration of IPv4 ACLs.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if an IPv4 ACL is configured with the correct sequence entries.\n    * Failure: The test will fail if an IPv4 ACL is not configured or entries are not in sequence.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyIPv4ACL:\n          ipv4_access_lists:\n            - name: default-control-plane-acl\n              entries:\n                - sequence: 10\n                  action: permit icmp any any\n                - sequence: 20\n                  action: permit ip any any tracked\n                - sequence: 30\n                  action: permit udp any any eq bfd ttl eq 255\n            - name: LabTest\n              entries:\n                - sequence: 10\n                  action: permit icmp any any\n                - sequence: 20\n                  action: permit tcp any any range 5900 5910\n    ```\n    \"\"\"\n\n    name = \"VerifyIPv4ACL\"\n    description = \"Verifies the configuration of IPv4 ACLs.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip access-lists {acl}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyIPv4ACL test.\"\"\"\n\n        ipv4_access_lists: list[IPv4ACL]\n        \"\"\"List of IPv4 ACLs to verify.\"\"\"\n\n        class IPv4ACL(BaseModel):\n            \"\"\"Model for an IPv4 ACL.\"\"\"\n\n            name: str\n            \"\"\"Name of IPv4 ACL.\"\"\"\n\n            entries: list[IPv4ACLEntry]\n            \"\"\"List of IPv4 ACL entries.\"\"\"\n\n            class IPv4ACLEntry(BaseModel):\n                \"\"\"Model for an IPv4 ACL entry.\"\"\"\n\n                sequence: int = Field(ge=1, le=4294967295)\n                \"\"\"Sequence number of an ACL entry.\"\"\"\n                action: str\n                \"\"\"Action of an ACL entry.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each input ACL.\"\"\"\n        return [template.render(acl=acl.name, entries=acl.entries) for acl in self.inputs.ipv4_access_lists]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyIPv4ACL.\"\"\"\n        self.result.is_success()\n        for command_output in self.instance_commands:\n            # Collecting input ACL details\n            acl_name = command_output.params[\"acl\"]\n            acl_entries = command_output.params[\"entries\"]\n\n            # Check if ACL is configured\n            ipv4_acl_list = command_output.json_output[\"aclList\"]\n            if not ipv4_acl_list:\n                self.result.is_failure(f\"{acl_name}: Not found\")\n                continue\n\n            # Check if the sequence number is configured and has the correct action applied\n            failed_log = f\"{acl_name}:\\n\"\n            for acl_entry in acl_entries:\n                acl_seq = acl_entry.sequence\n                acl_action = acl_entry.action\n                if (actual_entry := get_item(ipv4_acl_list[0][\"sequence\"], \"sequenceNumber\", acl_seq)) is None:\n                    failed_log += f\"Sequence number `{acl_seq}` is not found.\\n\"\n                    continue\n\n                if actual_entry[\"text\"] != acl_action:\n                    failed_log += f\"Expected `{acl_action}` as sequence number {acl_seq} action but found `{actual_entry['text']}` instead.\\n\"\n\n            if failed_log != f\"{acl_name}:\\n\":\n                self.result.is_failure(f\"{failed_log}\")\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyIPv4ACL-attributes","title":"Inputs","text":"Name Type Description Default ipv4_access_lists list[IPv4ACL] List of IPv4 ACLs to verify. -"},{"location":"api/tests.security/#anta.tests.security.VerifyIPv4ACL-attributes","title":"IPv4ACL","text":"Name Type Description Default name str Name of IPv4 ACL. - entries list[IPv4ACLEntry] List of IPv4 ACL entries. -"},{"location":"api/tests.security/#anta.tests.security.VerifyIPv4ACL-attributes","title":"IPv4ACLEntry","text":"Name Type Description Default sequence int Sequence number of an ACL entry. Field(ge=1, le=4294967295) action str Action of an ACL entry. -"},{"location":"api/tests.security/#anta.tests.security.VerifySSHIPv4Acl","title":"VerifySSHIPv4Acl","text":"

Verifies if the SSHD agent has the right number IPv4 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if the SSHD agent has the provided number of IPv4 ACL(s) in the specified VRF.
  • Failure: The test will fail if the SSHD agent has not the right number of IPv4 ACL(s) in the specified VRF.
Examples
anta.tests.security:\n  - VerifySSHIPv4Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/security.py
class VerifySSHIPv4Acl(AntaTest):\n    \"\"\"Verifies if the SSHD agent has the right number IPv4 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SSHD agent has the provided number of IPv4 ACL(s) in the specified VRF.\n    * Failure: The test will fail if the SSHD agent has not the right number of IPv4 ACL(s) in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifySSHIPv4Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifySSHIPv4Acl\"\n    description = \"Verifies if the SSHD agent has IPv4 ACL(s) configured.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management ssh ip access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySSHIPv4Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv4 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for the SSHD agent. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySSHIPv4Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv4_acl_list = command_output[\"ipAclList\"][\"aclList\"]\n        ipv4_acl_number = len(ipv4_acl_list)\n        if ipv4_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} SSH IPv4 ACL(s) in vrf {self.inputs.vrf} but got {ipv4_acl_number}\")\n            return\n\n        not_configured_acl = [acl[\"name\"] for acl in ipv4_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if not_configured_acl:\n            self.result.is_failure(f\"SSH IPv4 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifySSHIPv4Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv4 ACL(s). - vrf str The name of the VRF in which to check for the SSHD agent. Defaults to `default` VRF. 'default'"},{"location":"api/tests.security/#anta.tests.security.VerifySSHIPv6Acl","title":"VerifySSHIPv6Acl","text":"

Verifies if the SSHD agent has the right number IPv6 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if the SSHD agent has the provided number of IPv6 ACL(s) in the specified VRF.
  • Failure: The test will fail if the SSHD agent has not the right number of IPv6 ACL(s) in the specified VRF.
Examples
anta.tests.security:\n  - VerifySSHIPv6Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/security.py
class VerifySSHIPv6Acl(AntaTest):\n    \"\"\"Verifies if the SSHD agent has the right number IPv6 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SSHD agent has the provided number of IPv6 ACL(s) in the specified VRF.\n    * Failure: The test will fail if the SSHD agent has not the right number of IPv6 ACL(s) in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifySSHIPv6Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifySSHIPv6Acl\"\n    description = \"Verifies if the SSHD agent has IPv6 ACL(s) configured.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management ssh ipv6 access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySSHIPv6Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv6 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for the SSHD agent. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySSHIPv6Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv6_acl_list = command_output[\"ipv6AclList\"][\"aclList\"]\n        ipv6_acl_number = len(ipv6_acl_list)\n        if ipv6_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} SSH IPv6 ACL(s) in vrf {self.inputs.vrf} but got {ipv6_acl_number}\")\n            return\n\n        not_configured_acl = [acl[\"name\"] for acl in ipv6_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if not_configured_acl:\n            self.result.is_failure(f\"SSH IPv6 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.security/#anta.tests.security.VerifySSHIPv6Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv6 ACL(s). - vrf str The name of the VRF in which to check for the SSHD agent. Defaults to `default` VRF. 'default'"},{"location":"api/tests.security/#anta.tests.security.VerifySSHStatus","title":"VerifySSHStatus","text":"

Verifies if the SSHD agent is disabled in the default VRF.

Expected Results
  • Success: The test will pass if the SSHD agent is disabled in the default VRF.
  • Failure: The test will fail if the SSHD agent is NOT disabled in the default VRF.
Examples
anta.tests.security:\n  - VerifySSHStatus:\n
Source code in anta/tests/security.py
class VerifySSHStatus(AntaTest):\n    \"\"\"Verifies if the SSHD agent is disabled in the default VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SSHD agent is disabled in the default VRF.\n    * Failure: The test will fail if the SSHD agent is NOT disabled in the default VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifySSHStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifySSHStatus\"\n    description = \"Verifies if the SSHD agent is disabled in the default VRF.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management ssh\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySSHStatus.\"\"\"\n        command_output = self.instance_commands[0].text_output\n\n        line = next(line for line in command_output.split(\"\\n\") if line.startswith(\"SSHD status\"))\n        status = line.split(\"is \")[1]\n\n        if status == \"disabled\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(line)\n
"},{"location":"api/tests.security/#anta.tests.security.VerifyTelnetStatus","title":"VerifyTelnetStatus","text":"

Verifies if Telnet is disabled in the default VRF.

Expected Results
  • Success: The test will pass if Telnet is disabled in the default VRF.
  • Failure: The test will fail if Telnet is NOT disabled in the default VRF.
Examples
anta.tests.security:\n  - VerifyTelnetStatus:\n
Source code in anta/tests/security.py
class VerifyTelnetStatus(AntaTest):\n    \"\"\"Verifies if Telnet is disabled in the default VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if Telnet is disabled in the default VRF.\n    * Failure: The test will fail if Telnet is NOT disabled in the default VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.security:\n      - VerifyTelnetStatus:\n    ```\n    \"\"\"\n\n    name = \"VerifyTelnetStatus\"\n    description = \"Verifies if Telnet is disabled in the default VRF.\"\n    categories: ClassVar[list[str]] = [\"security\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management telnet\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTelnetStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"serverState\"] == \"disabled\":\n            self.result.is_success()\n        else:\n            self.result.is_failure(\"Telnet status for Default VRF is enabled\")\n
"},{"location":"api/tests.services/","title":"Services","text":""},{"location":"api/tests.services/#anta.tests.services.VerifyDNSLookup","title":"VerifyDNSLookup","text":"

Verifies the DNS (Domain Name Service) name to IP address resolution.

Expected Results
  • Success: The test will pass if a domain name is resolved to an IP address.
  • Failure: The test will fail if a domain name does not resolve to an IP address.
  • Error: This test will error out if a domain name is invalid.
Examples
anta.tests.services:\n  - VerifyDNSLookup:\n      domain_names:\n        - arista.com\n        - www.google.com\n        - arista.ca\n
Source code in anta/tests/services.py
class VerifyDNSLookup(AntaTest):\n    \"\"\"Verifies the DNS (Domain Name Service) name to IP address resolution.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if a domain name is resolved to an IP address.\n    * Failure: The test will fail if a domain name does not resolve to an IP address.\n    * Error: This test will error out if a domain name is invalid.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.services:\n      - VerifyDNSLookup:\n          domain_names:\n            - arista.com\n            - www.google.com\n            - arista.ca\n    ```\n    \"\"\"\n\n    name = \"VerifyDNSLookup\"\n    description = \"Verifies the DNS name to IP address resolution.\"\n    categories: ClassVar[list[str]] = [\"services\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"bash timeout 10 nslookup {domain}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyDNSLookup test.\"\"\"\n\n        domain_names: list[str]\n        \"\"\"List of domain names.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each domain name in the input list.\"\"\"\n        return [template.render(domain=domain_name) for domain_name in self.inputs.domain_names]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyDNSLookup.\"\"\"\n        self.result.is_success()\n        failed_domains = []\n        for command in self.instance_commands:\n            domain = command.params[\"domain\"]\n            output = command.json_output[\"messages\"][0]\n            if f\"Can't find {domain}: No answer\" in output:\n                failed_domains.append(domain)\n        if failed_domains:\n            self.result.is_failure(f\"The following domain(s) are not resolved to an IP address: {', '.join(failed_domains)}\")\n
"},{"location":"api/tests.services/#anta.tests.services.VerifyDNSLookup-attributes","title":"Inputs","text":"Name Type Description Default domain_names list[str] List of domain names. -"},{"location":"api/tests.services/#anta.tests.services.VerifyDNSServers","title":"VerifyDNSServers","text":"

Verifies if the DNS (Domain Name Service) servers are correctly configured.

Expected Results
  • Success: The test will pass if the DNS server specified in the input is configured with the correct VRF and priority.
  • Failure: The test will fail if the DNS server is not configured or if the VRF and priority of the DNS server do not match the input.
Examples
anta.tests.services:\n  - VerifyDNSServers:\n      dns_servers:\n        - server_address: 10.14.0.1\n          vrf: default\n          priority: 1\n        - server_address: 10.14.0.11\n          vrf: MGMT\n          priority: 0\n
Source code in anta/tests/services.py
class VerifyDNSServers(AntaTest):\n    \"\"\"Verifies if the DNS (Domain Name Service) servers are correctly configured.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the DNS server specified in the input is configured with the correct VRF and priority.\n    * Failure: The test will fail if the DNS server is not configured or if the VRF and priority of the DNS server do not match the input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.services:\n      - VerifyDNSServers:\n          dns_servers:\n            - server_address: 10.14.0.1\n              vrf: default\n              priority: 1\n            - server_address: 10.14.0.11\n              vrf: MGMT\n              priority: 0\n    ```\n    \"\"\"\n\n    name = \"VerifyDNSServers\"\n    description = \"Verifies if the DNS servers are correctly configured.\"\n    categories: ClassVar[list[str]] = [\"services\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip name-server\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyDNSServers test.\"\"\"\n\n        dns_servers: list[DnsServer]\n        \"\"\"List of DNS servers to verify.\"\"\"\n\n        class DnsServer(BaseModel):\n            \"\"\"Model for a DNS server.\"\"\"\n\n            server_address: IPv4Address | IPv6Address\n            \"\"\"The IPv4/IPv6 address of the DNS server.\"\"\"\n            vrf: str = \"default\"\n            \"\"\"The VRF for the DNS server. Defaults to 'default' if not provided.\"\"\"\n            priority: int = Field(ge=0, le=4)\n            \"\"\"The priority of the DNS server from 0 to 4, lower is first.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyDNSServers.\"\"\"\n        command_output = self.instance_commands[0].json_output[\"nameServerConfigs\"]\n        self.result.is_success()\n        for server in self.inputs.dns_servers:\n            address = str(server.server_address)\n            vrf = server.vrf\n            priority = server.priority\n            input_dict = {\"ipAddr\": address, \"vrf\": vrf}\n\n            if get_item(command_output, \"ipAddr\", address) is None:\n                self.result.is_failure(f\"DNS server `{address}` is not configured with any VRF.\")\n                continue\n\n            if (output := get_dict_superset(command_output, input_dict)) is None:\n                self.result.is_failure(f\"DNS server `{address}` is not configured with VRF `{vrf}`.\")\n                continue\n\n            if output[\"priority\"] != priority:\n                self.result.is_failure(f\"For DNS server `{address}`, the expected priority is `{priority}`, but `{output['priority']}` was found instead.\")\n
"},{"location":"api/tests.services/#anta.tests.services.VerifyDNSServers-attributes","title":"Inputs","text":"Name Type Description Default dns_servers list[DnsServer] List of DNS servers to verify. -"},{"location":"api/tests.services/#anta.tests.services.VerifyDNSServers-attributes","title":"DnsServer","text":"Name Type Description Default server_address IPv4Address | IPv6Address The IPv4/IPv6 address of the DNS server. - vrf str The VRF for the DNS server. Defaults to 'default' if not provided. 'default' priority int The priority of the DNS server from 0 to 4, lower is first. Field(ge=0, le=4)"},{"location":"api/tests.services/#anta.tests.services.VerifyErrdisableRecovery","title":"VerifyErrdisableRecovery","text":"

Verifies the errdisable recovery reason, status, and interval.

Expected Results
  • Success: The test will pass if the errdisable recovery reason status is enabled and the interval matches the input.
  • Failure: The test will fail if the errdisable recovery reason is not found, the status is not enabled, or the interval does not match the input.
Examples
anta.tests.services:\n  - VerifyErrdisableRecovery:\n      reasons:\n        - reason: acl\n          interval: 30\n        - reason: bpduguard\n          interval: 30\n
Source code in anta/tests/services.py
class VerifyErrdisableRecovery(AntaTest):\n    \"\"\"Verifies the errdisable recovery reason, status, and interval.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the errdisable recovery reason status is enabled and the interval matches the input.\n    * Failure: The test will fail if the errdisable recovery reason is not found, the status is not enabled, or the interval does not match the input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.services:\n      - VerifyErrdisableRecovery:\n          reasons:\n            - reason: acl\n              interval: 30\n            - reason: bpduguard\n              interval: 30\n    ```\n    \"\"\"\n\n    name = \"VerifyErrdisableRecovery\"\n    description = \"Verifies the errdisable recovery reason, status, and interval.\"\n    categories: ClassVar[list[str]] = [\"services\"]\n    # NOTE: Only `text` output format is supported for this command\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show errdisable recovery\", ofmt=\"text\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyErrdisableRecovery test.\"\"\"\n\n        reasons: list[ErrDisableReason]\n        \"\"\"List of errdisable reasons.\"\"\"\n\n        class ErrDisableReason(BaseModel):\n            \"\"\"Model for an errdisable reason.\"\"\"\n\n            reason: ErrDisableReasons\n            \"\"\"Type or name of the errdisable reason.\"\"\"\n            interval: ErrDisableInterval\n            \"\"\"Interval of the reason in seconds.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyErrdisableRecovery.\"\"\"\n        command_output = self.instance_commands[0].text_output\n        self.result.is_success()\n        for error_reason in self.inputs.reasons:\n            input_reason = error_reason.reason\n            input_interval = error_reason.interval\n            reason_found = False\n\n            # Skip header and last empty line\n            lines = command_output.split(\"\\n\")[2:-1]\n            for line in lines:\n                # Skip empty lines\n                if not line.strip():\n                    continue\n                # Split by first two whitespaces\n                reason, status, interval = line.split(None, 2)\n                if reason != input_reason:\n                    continue\n                reason_found = True\n                actual_reason_data = {\"interval\": interval, \"status\": status}\n                expected_reason_data = {\"interval\": str(input_interval), \"status\": \"Enabled\"}\n                if actual_reason_data != expected_reason_data:\n                    failed_log = get_failed_logs(expected_reason_data, actual_reason_data)\n                    self.result.is_failure(f\"`{input_reason}`:{failed_log}\\n\")\n                break\n\n            if not reason_found:\n                self.result.is_failure(f\"`{input_reason}`: Not found.\\n\")\n
"},{"location":"api/tests.services/#anta.tests.services.VerifyErrdisableRecovery-attributes","title":"Inputs","text":"Name Type Description Default reasons list[ErrDisableReason] List of errdisable reasons. -"},{"location":"api/tests.services/#anta.tests.services.VerifyErrdisableRecovery-attributes","title":"ErrDisableReason","text":"Name Type Description Default reason ErrDisableReasons Type or name of the errdisable reason. - interval ErrDisableInterval Interval of the reason in seconds. -"},{"location":"api/tests.services/#anta.tests.services.VerifyHostname","title":"VerifyHostname","text":"

Verifies the hostname of a device.

Expected Results
  • Success: The test will pass if the hostname matches the provided input.
  • Failure: The test will fail if the hostname does not match the provided input.
Examples
anta.tests.services:\n  - VerifyHostname:\n      hostname: s1-spine1\n
Source code in anta/tests/services.py
class VerifyHostname(AntaTest):\n    \"\"\"Verifies the hostname of a device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the hostname matches the provided input.\n    * Failure: The test will fail if the hostname does not match the provided input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.services:\n      - VerifyHostname:\n          hostname: s1-spine1\n    ```\n    \"\"\"\n\n    name = \"VerifyHostname\"\n    description = \"Verifies the hostname of a device.\"\n    categories: ClassVar[list[str]] = [\"services\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hostname\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyHostname test.\"\"\"\n\n        hostname: str\n        \"\"\"Expected hostname of the device.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyHostname.\"\"\"\n        hostname = self.instance_commands[0].json_output[\"hostname\"]\n\n        if hostname != self.inputs.hostname:\n            self.result.is_failure(f\"Expected `{self.inputs.hostname}` as the hostname, but found `{hostname}` instead.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.services/#anta.tests.services.VerifyHostname-attributes","title":"Inputs","text":"Name Type Description Default hostname str Expected hostname of the device. -"},{"location":"api/tests.snmp/","title":"SNMP","text":""},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpContact","title":"VerifySnmpContact","text":"

Verifies the SNMP contact of a device.

Expected Results
  • Success: The test will pass if the SNMP contact matches the provided input.
  • Failure: The test will fail if the SNMP contact does not match the provided input.
Examples
anta.tests.snmp:\n  - VerifySnmpContact:\n      contact: Jon@example.com\n
Source code in anta/tests/snmp.py
class VerifySnmpContact(AntaTest):\n    \"\"\"Verifies the SNMP contact of a device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SNMP contact matches the provided input.\n    * Failure: The test will fail if the SNMP contact does not match the provided input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.snmp:\n      - VerifySnmpContact:\n          contact: Jon@example.com\n    ```\n    \"\"\"\n\n    name = \"VerifySnmpContact\"\n    description = \"Verifies the SNMP contact of a device.\"\n    categories: ClassVar[list[str]] = [\"snmp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySnmpContact test.\"\"\"\n\n        contact: str\n        \"\"\"Expected SNMP contact details of the device.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySnmpContact.\"\"\"\n        contact = self.instance_commands[0].json_output[\"contact\"][\"contact\"]\n\n        if contact != self.inputs.contact:\n            self.result.is_failure(f\"Expected `{self.inputs.contact}` as the contact, but found `{contact}` instead.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpContact-attributes","title":"Inputs","text":"Name Type Description Default contact str Expected SNMP contact details of the device. -"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpIPv4Acl","title":"VerifySnmpIPv4Acl","text":"

Verifies if the SNMP agent has the right number IPv4 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if the SNMP agent has the provided number of IPv4 ACL(s) in the specified VRF.
  • Failure: The test will fail if the SNMP agent has not the right number of IPv4 ACL(s) in the specified VRF.
Examples
anta.tests.snmp:\n  - VerifySnmpIPv4Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/snmp.py
class VerifySnmpIPv4Acl(AntaTest):\n    \"\"\"Verifies if the SNMP agent has the right number IPv4 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SNMP agent has the provided number of IPv4 ACL(s) in the specified VRF.\n    * Failure: The test will fail if the SNMP agent has not the right number of IPv4 ACL(s) in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.snmp:\n      - VerifySnmpIPv4Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifySnmpIPv4Acl\"\n    description = \"Verifies if the SNMP agent has IPv4 ACL(s) configured.\"\n    categories: ClassVar[list[str]] = [\"snmp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp ipv4 access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySnmpIPv4Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv4 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySnmpIPv4Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv4_acl_list = command_output[\"ipAclList\"][\"aclList\"]\n        ipv4_acl_number = len(ipv4_acl_list)\n        if ipv4_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} SNMP IPv4 ACL(s) in vrf {self.inputs.vrf} but got {ipv4_acl_number}\")\n            return\n\n        not_configured_acl = [acl[\"name\"] for acl in ipv4_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if not_configured_acl:\n            self.result.is_failure(f\"SNMP IPv4 ACL(s) not configured or active in vrf {self.inputs.vrf}: {not_configured_acl}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpIPv4Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv4 ACL(s). - vrf str The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF. 'default'"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpIPv6Acl","title":"VerifySnmpIPv6Acl","text":"

Verifies if the SNMP agent has the right number IPv6 ACL(s) configured for a specified VRF.

Expected Results
  • Success: The test will pass if the SNMP agent has the provided number of IPv6 ACL(s) in the specified VRF.
  • Failure: The test will fail if the SNMP agent has not the right number of IPv6 ACL(s) in the specified VRF.
Examples
anta.tests.snmp:\n  - VerifySnmpIPv6Acl:\n      number: 3\n      vrf: default\n
Source code in anta/tests/snmp.py
class VerifySnmpIPv6Acl(AntaTest):\n    \"\"\"Verifies if the SNMP agent has the right number IPv6 ACL(s) configured for a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SNMP agent has the provided number of IPv6 ACL(s) in the specified VRF.\n    * Failure: The test will fail if the SNMP agent has not the right number of IPv6 ACL(s) in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.snmp:\n      - VerifySnmpIPv6Acl:\n          number: 3\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifySnmpIPv6Acl\"\n    description = \"Verifies if the SNMP agent has IPv6 ACL(s) configured.\"\n    categories: ClassVar[list[str]] = [\"snmp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp ipv6 access-list summary\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySnmpIPv6Acl test.\"\"\"\n\n        number: PositiveInteger\n        \"\"\"The number of expected IPv6 ACL(s).\"\"\"\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySnmpIPv6Acl.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        ipv6_acl_list = command_output[\"ipv6AclList\"][\"aclList\"]\n        ipv6_acl_number = len(ipv6_acl_list)\n        if ipv6_acl_number != self.inputs.number:\n            self.result.is_failure(f\"Expected {self.inputs.number} SNMP IPv6 ACL(s) in vrf {self.inputs.vrf} but got {ipv6_acl_number}\")\n            return\n\n        acl_not_configured = [acl[\"name\"] for acl in ipv6_acl_list if self.inputs.vrf not in acl[\"configuredVrfs\"] or self.inputs.vrf not in acl[\"activeVrfs\"]]\n\n        if acl_not_configured:\n            self.result.is_failure(f\"SNMP IPv6 ACL(s) not configured or active in vrf {self.inputs.vrf}: {acl_not_configured}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpIPv6Acl-attributes","title":"Inputs","text":"Name Type Description Default number PositiveInteger The number of expected IPv6 ACL(s). - vrf str The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF. 'default'"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpLocation","title":"VerifySnmpLocation","text":"

Verifies the SNMP location of a device.

Expected Results
  • Success: The test will pass if the SNMP location matches the provided input.
  • Failure: The test will fail if the SNMP location does not match the provided input.
Examples
anta.tests.snmp:\n  - VerifySnmpLocation:\n      location: New York\n
Source code in anta/tests/snmp.py
class VerifySnmpLocation(AntaTest):\n    \"\"\"Verifies the SNMP location of a device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SNMP location matches the provided input.\n    * Failure: The test will fail if the SNMP location does not match the provided input.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.snmp:\n      - VerifySnmpLocation:\n          location: New York\n    ```\n    \"\"\"\n\n    name = \"VerifySnmpLocation\"\n    description = \"Verifies the SNMP location of a device.\"\n    categories: ClassVar[list[str]] = [\"snmp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySnmpLocation test.\"\"\"\n\n        location: str\n        \"\"\"Expected SNMP location of the device.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySnmpLocation.\"\"\"\n        location = self.instance_commands[0].json_output[\"location\"][\"location\"]\n\n        if location != self.inputs.location:\n            self.result.is_failure(f\"Expected `{self.inputs.location}` as the location, but found `{location}` instead.\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpLocation-attributes","title":"Inputs","text":"Name Type Description Default location str Expected SNMP location of the device. -"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpStatus","title":"VerifySnmpStatus","text":"

Verifies whether the SNMP agent is enabled in a specified VRF.

Expected Results
  • Success: The test will pass if the SNMP agent is enabled in the specified VRF.
  • Failure: The test will fail if the SNMP agent is disabled in the specified VRF.
Examples
anta.tests.snmp:\n  - VerifySnmpStatus:\n      vrf: default\n
Source code in anta/tests/snmp.py
class VerifySnmpStatus(AntaTest):\n    \"\"\"Verifies whether the SNMP agent is enabled in a specified VRF.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the SNMP agent is enabled in the specified VRF.\n    * Failure: The test will fail if the SNMP agent is disabled in the specified VRF.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.snmp:\n      - VerifySnmpStatus:\n          vrf: default\n    ```\n    \"\"\"\n\n    name = \"VerifySnmpStatus\"\n    description = \"Verifies if the SNMP agent is enabled.\"\n    categories: ClassVar[list[str]] = [\"snmp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySnmpStatus test.\"\"\"\n\n        vrf: str = \"default\"\n        \"\"\"The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySnmpStatus.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"enabled\"] and self.inputs.vrf in command_output[\"vrfs\"][\"snmpVrfs\"]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"SNMP agent disabled in vrf {self.inputs.vrf}\")\n
"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpStatus-attributes","title":"Inputs","text":"Name Type Description Default vrf str The name of the VRF in which to check for the SNMP agent. Defaults to `default` VRF. 'default'"},{"location":"api/tests.software/","title":"Software","text":""},{"location":"api/tests.software/#anta.tests.software.VerifyEOSExtensions","title":"VerifyEOSExtensions","text":"

Verifies that all EOS extensions installed on the device are enabled for boot persistence.

Expected Results
  • Success: The test will pass if all EOS extensions installed on the device are enabled for boot persistence.
  • Failure: The test will fail if some EOS extensions installed on the device are not enabled for boot persistence.
Examples
anta.tests.software:\n  - VerifyEOSExtensions:\n
Source code in anta/tests/software.py
class VerifyEOSExtensions(AntaTest):\n    \"\"\"Verifies that all EOS extensions installed on the device are enabled for boot persistence.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all EOS extensions installed on the device are enabled for boot persistence.\n    * Failure: The test will fail if some EOS extensions installed on the device are not enabled for boot persistence.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.software:\n      - VerifyEOSExtensions:\n    ```\n    \"\"\"\n\n    name = \"VerifyEOSExtensions\"\n    description = \"Verifies that all EOS extensions installed on the device are enabled for boot persistence.\"\n    categories: ClassVar[list[str]] = [\"software\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show extensions\"), AntaCommand(command=\"show boot-extensions\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEOSExtensions.\"\"\"\n        boot_extensions = []\n        show_extensions_command_output = self.instance_commands[0].json_output\n        show_boot_extensions_command_output = self.instance_commands[1].json_output\n        installed_extensions = [\n            extension for extension, extension_data in show_extensions_command_output[\"extensions\"].items() if extension_data[\"status\"] == \"installed\"\n        ]\n        for extension in show_boot_extensions_command_output[\"extensions\"]:\n            formatted_extension = extension.strip(\"\\n\")\n            if formatted_extension != \"\":\n                boot_extensions.append(formatted_extension)\n        installed_extensions.sort()\n        boot_extensions.sort()\n        if installed_extensions == boot_extensions:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Missing EOS extensions: installed {installed_extensions} / configured: {boot_extensions}\")\n
"},{"location":"api/tests.software/#anta.tests.software.VerifyEOSVersion","title":"VerifyEOSVersion","text":"

Verifies that the device is running one of the allowed EOS version.

Expected Results
  • Success: The test will pass if the device is running one of the allowed EOS version.
  • Failure: The test will fail if the device is not running one of the allowed EOS version.
Examples
anta.tests.software:\n  - VerifyEOSVersion:\n      versions:\n        - 4.25.4M\n        - 4.26.1F\n
Source code in anta/tests/software.py
class VerifyEOSVersion(AntaTest):\n    \"\"\"Verifies that the device is running one of the allowed EOS version.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is running one of the allowed EOS version.\n    * Failure: The test will fail if the device is not running one of the allowed EOS version.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.software:\n      - VerifyEOSVersion:\n          versions:\n            - 4.25.4M\n            - 4.26.1F\n    ```\n    \"\"\"\n\n    name = \"VerifyEOSVersion\"\n    description = \"Verifies the EOS version of the device.\"\n    categories: ClassVar[list[str]] = [\"software\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyEOSVersion test.\"\"\"\n\n        versions: list[str]\n        \"\"\"List of allowed EOS versions.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyEOSVersion.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"version\"] in self.inputs.versions:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f'device is running version \"{command_output[\"version\"]}\" not in expected versions: {self.inputs.versions}')\n
"},{"location":"api/tests.software/#anta.tests.software.VerifyEOSVersion-attributes","title":"Inputs","text":"Name Type Description Default versions list[str] List of allowed EOS versions. -"},{"location":"api/tests.software/#anta.tests.software.VerifyTerminAttrVersion","title":"VerifyTerminAttrVersion","text":"

Verifies that he device is running one of the allowed TerminAttr version.

Expected Results
  • Success: The test will pass if the device is running one of the allowed TerminAttr version.
  • Failure: The test will fail if the device is not running one of the allowed TerminAttr version.
Examples
anta.tests.software:\n  - VerifyTerminAttrVersion:\n      versions:\n        - v1.13.6\n        - v1.8.0\n
Source code in anta/tests/software.py
class VerifyTerminAttrVersion(AntaTest):\n    \"\"\"Verifies that he device is running one of the allowed TerminAttr version.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device is running one of the allowed TerminAttr version.\n    * Failure: The test will fail if the device is not running one of the allowed TerminAttr version.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.software:\n      - VerifyTerminAttrVersion:\n          versions:\n            - v1.13.6\n            - v1.8.0\n    ```\n    \"\"\"\n\n    name = \"VerifyTerminAttrVersion\"\n    description = \"Verifies the TerminAttr version of the device.\"\n    categories: ClassVar[list[str]] = [\"software\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version detail\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyTerminAttrVersion test.\"\"\"\n\n        versions: list[str]\n        \"\"\"List of allowed TerminAttr versions.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyTerminAttrVersion.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        command_output_data = command_output[\"details\"][\"packages\"][\"TerminAttr-core\"][\"version\"]\n        if command_output_data in self.inputs.versions:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"device is running TerminAttr version {command_output_data} and is not in the allowed list: {self.inputs.versions}\")\n
"},{"location":"api/tests.software/#anta.tests.software.VerifyTerminAttrVersion-attributes","title":"Inputs","text":"Name Type Description Default versions list[str] List of allowed TerminAttr versions. -"},{"location":"api/tests.stp/","title":"STP","text":""},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPBlockedPorts","title":"VerifySTPBlockedPorts","text":"

Verifies there is no STP blocked ports.

Expected Results
  • Success: The test will pass if there are NO ports blocked by STP.
  • Failure: The test will fail if there are ports blocked by STP.
Examples
anta.tests.stp:\n  - VerifySTPBlockedPorts:\n
Source code in anta/tests/stp.py
class VerifySTPBlockedPorts(AntaTest):\n    \"\"\"Verifies there is no STP blocked ports.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO ports blocked by STP.\n    * Failure: The test will fail if there are ports blocked by STP.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.stp:\n      - VerifySTPBlockedPorts:\n    ```\n    \"\"\"\n\n    name = \"VerifySTPBlockedPorts\"\n    description = \"Verifies there is no STP blocked ports.\"\n    categories: ClassVar[list[str]] = [\"stp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree blockedports\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySTPBlockedPorts.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if not (stp_instances := command_output[\"spanningTreeInstances\"]):\n            self.result.is_success()\n        else:\n            for key, value in stp_instances.items():\n                stp_instances[key] = value.pop(\"spanningTreeBlockedPorts\")\n            self.result.is_failure(f\"The following ports are blocked by STP: {stp_instances}\")\n
"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPCounters","title":"VerifySTPCounters","text":"

Verifies there is no errors in STP BPDU packets.

Expected Results
  • Success: The test will pass if there are NO STP BPDU packet errors under all interfaces participating in STP.
  • Failure: The test will fail if there are STP BPDU packet errors on one or many interface(s).
Examples
anta.tests.stp:\n  - VerifySTPCounters:\n
Source code in anta/tests/stp.py
class VerifySTPCounters(AntaTest):\n    \"\"\"Verifies there is no errors in STP BPDU packets.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO STP BPDU packet errors under all interfaces participating in STP.\n    * Failure: The test will fail if there are STP BPDU packet errors on one or many interface(s).\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.stp:\n      - VerifySTPCounters:\n    ```\n    \"\"\"\n\n    name = \"VerifySTPCounters\"\n    description = \"Verifies there is no errors in STP BPDU packets.\"\n    categories: ClassVar[list[str]] = [\"stp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree counters\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySTPCounters.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        interfaces_with_errors = [\n            interface for interface, counters in command_output[\"interfaces\"].items() if counters[\"bpduTaggedError\"] or counters[\"bpduOtherError\"] != 0\n        ]\n        if interfaces_with_errors:\n            self.result.is_failure(f\"The following interfaces have STP BPDU packet errors: {interfaces_with_errors}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPForwardingPorts","title":"VerifySTPForwardingPorts","text":"

Verifies that all interfaces are in a forwarding state for a provided list of VLAN(s).

Expected Results
  • Success: The test will pass if all interfaces are in a forwarding state for the specified VLAN(s).
  • Failure: The test will fail if one or many interfaces are NOT in a forwarding state in the specified VLAN(s).
Examples
anta.tests.stp:\n  - VerifySTPForwardingPorts:\n      vlans:\n        - 10\n        - 20\n
Source code in anta/tests/stp.py
class VerifySTPForwardingPorts(AntaTest):\n    \"\"\"Verifies that all interfaces are in a forwarding state for a provided list of VLAN(s).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all interfaces are in a forwarding state for the specified VLAN(s).\n    * Failure: The test will fail if one or many interfaces are NOT in a forwarding state in the specified VLAN(s).\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.stp:\n      - VerifySTPForwardingPorts:\n          vlans:\n            - 10\n            - 20\n    ```\n    \"\"\"\n\n    name = \"VerifySTPForwardingPorts\"\n    description = \"Verifies that all interfaces are forwarding for a provided list of VLAN(s).\"\n    categories: ClassVar[list[str]] = [\"stp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show spanning-tree topology vlan {vlan} status\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySTPForwardingPorts test.\"\"\"\n\n        vlans: list[Vlan]\n        \"\"\"List of VLAN on which to verify forwarding states.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each VLAN in the input list.\"\"\"\n        return [template.render(vlan=vlan) for vlan in self.inputs.vlans]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySTPForwardingPorts.\"\"\"\n        not_configured = []\n        not_forwarding = []\n        for command in self.instance_commands:\n            if \"vlan\" in command.params:\n                vlan_id = command.params[\"vlan\"]\n            if not (topologies := get_value(command.json_output, \"topologies\")):\n                not_configured.append(vlan_id)\n            else:\n                for value in topologies.values():\n                    if int(vlan_id) in value[\"vlans\"]:\n                        interfaces_not_forwarding = [interface for interface, state in value[\"interfaces\"].items() if state[\"state\"] != \"forwarding\"]\n                if interfaces_not_forwarding:\n                    not_forwarding.append({f\"VLAN {vlan_id}\": interfaces_not_forwarding})\n        if not_configured:\n            self.result.is_failure(f\"STP instance is not configured for the following VLAN(s): {not_configured}\")\n        if not_forwarding:\n            self.result.is_failure(f\"The following VLAN(s) have interface(s) that are not in a fowarding state: {not_forwarding}\")\n        if not not_configured and not interfaces_not_forwarding:\n            self.result.is_success()\n
"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPForwardingPorts-attributes","title":"Inputs","text":"Name Type Description Default vlans list[Vlan] List of VLAN on which to verify forwarding states. -"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPMode","title":"VerifySTPMode","text":"

Verifies the configured STP mode for a provided list of VLAN(s).

Expected Results
  • Success: The test will pass if the STP mode is configured properly in the specified VLAN(s).
  • Failure: The test will fail if the STP mode is NOT configured properly for one or more specified VLAN(s).
Examples
anta.tests.stp:\n  - VerifySTPMode:\n      mode: rapidPvst\n      vlans:\n        - 10\n        - 20\n
Source code in anta/tests/stp.py
class VerifySTPMode(AntaTest):\n    \"\"\"Verifies the configured STP mode for a provided list of VLAN(s).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the STP mode is configured properly in the specified VLAN(s).\n    * Failure: The test will fail if the STP mode is NOT configured properly for one or more specified VLAN(s).\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.stp:\n      - VerifySTPMode:\n          mode: rapidPvst\n          vlans:\n            - 10\n            - 20\n    ```\n    \"\"\"\n\n    name = \"VerifySTPMode\"\n    description = \"Verifies the configured STP mode for a provided list of VLAN(s).\"\n    categories: ClassVar[list[str]] = [\"stp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show spanning-tree vlan {vlan}\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySTPMode test.\"\"\"\n\n        mode: Literal[\"mstp\", \"rstp\", \"rapidPvst\"] = \"mstp\"\n        \"\"\"STP mode to verify. Supported values: mstp, rstp, rapidPvst. Defaults to mstp.\"\"\"\n        vlans: list[Vlan]\n        \"\"\"List of VLAN on which to verify STP mode.\"\"\"\n\n    def render(self, template: AntaTemplate) -> list[AntaCommand]:\n        \"\"\"Render the template for each VLAN in the input list.\"\"\"\n        return [template.render(vlan=vlan) for vlan in self.inputs.vlans]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySTPMode.\"\"\"\n        not_configured = []\n        wrong_stp_mode = []\n        for command in self.instance_commands:\n            if \"vlan\" in command.params:\n                vlan_id = command.params[\"vlan\"]\n            if not (\n                stp_mode := get_value(\n                    command.json_output,\n                    f\"spanningTreeVlanInstances.{vlan_id}.spanningTreeVlanInstance.protocol\",\n                )\n            ):\n                not_configured.append(vlan_id)\n            elif stp_mode != self.inputs.mode:\n                wrong_stp_mode.append(vlan_id)\n        if not_configured:\n            self.result.is_failure(f\"STP mode '{self.inputs.mode}' not configured for the following VLAN(s): {not_configured}\")\n        if wrong_stp_mode:\n            self.result.is_failure(f\"Wrong STP mode configured for the following VLAN(s): {wrong_stp_mode}\")\n        if not not_configured and not wrong_stp_mode:\n            self.result.is_success()\n
"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPMode-attributes","title":"Inputs","text":"Name Type Description Default mode Literal['mstp', 'rstp', 'rapidPvst'] STP mode to verify. Supported values: mstp, rstp, rapidPvst. Defaults to mstp. 'mstp' vlans list[Vlan] List of VLAN on which to verify STP mode. -"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPRootPriority","title":"VerifySTPRootPriority","text":"

Verifies the STP root priority for a provided list of VLAN or MST instance ID(s).

Expected Results
  • Success: The test will pass if the STP root priority is configured properly for the specified VLAN or MST instance ID(s).
  • Failure: The test will fail if the STP root priority is NOT configured properly for the specified VLAN or MST instance ID(s).
Examples
anta.tests.stp:\n  - VerifySTPRootPriority:\n      priority: 32768\n      instances:\n        - 10\n        - 20\n
Source code in anta/tests/stp.py
class VerifySTPRootPriority(AntaTest):\n    \"\"\"Verifies the STP root priority for a provided list of VLAN or MST instance ID(s).\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the STP root priority is configured properly for the specified VLAN or MST instance ID(s).\n    * Failure: The test will fail if the STP root priority is NOT configured properly for the specified VLAN or MST instance ID(s).\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.stp:\n      - VerifySTPRootPriority:\n          priority: 32768\n          instances:\n            - 10\n            - 20\n    ```\n    \"\"\"\n\n    name = \"VerifySTPRootPriority\"\n    description = \"Verifies the STP root priority for a provided list of VLAN or MST instance ID(s).\"\n    categories: ClassVar[list[str]] = [\"stp\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree root detail\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifySTPRootPriority test.\"\"\"\n\n        priority: int\n        \"\"\"STP root priority to verify.\"\"\"\n        instances: list[Vlan] = Field(default=[])\n        \"\"\"List of VLAN or MST instance ID(s). If empty, ALL VLAN or MST instance ID(s) will be verified.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifySTPRootPriority.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if not (stp_instances := command_output[\"instances\"]):\n            self.result.is_failure(\"No STP instances configured\")\n            return\n        # Checking the type of instances based on first instance\n        first_name = next(iter(stp_instances))\n        if first_name.startswith(\"MST\"):\n            prefix = \"MST\"\n        elif first_name.startswith(\"VL\"):\n            prefix = \"VL\"\n        else:\n            self.result.is_failure(f\"Unsupported STP instance type: {first_name}\")\n            return\n        check_instances = [f\"{prefix}{instance_id}\" for instance_id in self.inputs.instances] if self.inputs.instances else command_output[\"instances\"].keys()\n        wrong_priority_instances = [\n            instance for instance in check_instances if get_value(command_output, f\"instances.{instance}.rootBridge.priority\") != self.inputs.priority\n        ]\n        if wrong_priority_instances:\n            self.result.is_failure(f\"The following instance(s) have the wrong STP root priority configured: {wrong_priority_instances}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.stp/#anta.tests.stp.VerifySTPRootPriority-attributes","title":"Inputs","text":"Name Type Description Default priority int STP root priority to verify. - instances list[Vlan] List of VLAN or MST instance ID(s). If empty, ALL VLAN or MST instance ID(s) will be verified. Field(default=[])"},{"location":"api/tests.system/","title":"System","text":""},{"location":"api/tests.system/#anta.tests.system.VerifyAgentLogs","title":"VerifyAgentLogs","text":"

Verifies that no agent crash reports are present on the device.

Expected Results
  • Success: The test will pass if there is NO agent crash reported.
  • Failure: The test will fail if any agent crashes are reported.
Examples
anta.tests.system:\n  - VerifyAgentLogs:\n
Source code in anta/tests/system.py
class VerifyAgentLogs(AntaTest):\n    \"\"\"Verifies that no agent crash reports are present on the device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there is NO agent crash reported.\n    * Failure: The test will fail if any agent crashes are reported.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyAgentLogs:\n    ```\n    \"\"\"\n\n    name = \"VerifyAgentLogs\"\n    description = \"Verifies there are no agent crash reports.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show agent logs crash\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyAgentLogs.\"\"\"\n        command_output = self.instance_commands[0].text_output\n        if len(command_output) == 0:\n            self.result.is_success()\n        else:\n            pattern = re.compile(r\"^===> (.*?) <===$\", re.MULTILINE)\n            agents = \"\\n * \".join(pattern.findall(command_output))\n            self.result.is_failure(f\"Device has reported agent crashes:\\n * {agents}\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyCPUUtilization","title":"VerifyCPUUtilization","text":"

Verifies whether the CPU utilization is below 75%.

Expected Results
  • Success: The test will pass if the CPU utilization is below 75%.
  • Failure: The test will fail if the CPU utilization is over 75%.
Examples
anta.tests.system:\n  - VerifyCPUUtilization:\n
Source code in anta/tests/system.py
class VerifyCPUUtilization(AntaTest):\n    \"\"\"Verifies whether the CPU utilization is below 75%.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the CPU utilization is below 75%.\n    * Failure: The test will fail if the CPU utilization is over 75%.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyCPUUtilization:\n    ```\n    \"\"\"\n\n    name = \"VerifyCPUUtilization\"\n    description = \"Verifies whether the CPU utilization is below 75%.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show processes top once\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyCPUUtilization.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        command_output_data = command_output[\"cpuInfo\"][\"%Cpu(s)\"][\"idle\"]\n        if command_output_data > CPU_IDLE_THRESHOLD:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device has reported a high CPU utilization: {100 - command_output_data}%\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyCoredump","title":"VerifyCoredump","text":"

Verifies if there are core dump files in the /var/core directory.

Expected Results
  • Success: The test will pass if there are NO core dump(s) in /var/core.
  • Failure: The test will fail if there are core dump(s) in /var/core.
Info
  • This test will NOT check for minidump(s) generated by certain agents in /var/core/minidump.
Examples
anta.tests.system:\n  - VerifyCoreDump:\n
Source code in anta/tests/system.py
class VerifyCoredump(AntaTest):\n    \"\"\"Verifies if there are core dump files in the /var/core directory.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO core dump(s) in /var/core.\n    * Failure: The test will fail if there are core dump(s) in /var/core.\n\n    Info\n    ----\n    * This test will NOT check for minidump(s) generated by certain agents in /var/core/minidump.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyCoreDump:\n    ```\n    \"\"\"\n\n    name = \"VerifyCoredump\"\n    description = \"Verifies there are no core dump files.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system coredump\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyCoredump.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        core_files = command_output[\"coreFiles\"]\n        if \"minidump\" in core_files:\n            core_files.remove(\"minidump\")\n        if not core_files:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Core dump(s) have been found: {core_files}\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyFileSystemUtilization","title":"VerifyFileSystemUtilization","text":"

Verifies that no partition is utilizing more than 75% of its disk space.

Expected Results
  • Success: The test will pass if all partitions are using less than 75% of its disk space.
  • Failure: The test will fail if any partitions are using more than 75% of its disk space.
Examples
anta.tests.system:\n  - VerifyFileSystemUtilization:\n
Source code in anta/tests/system.py
class VerifyFileSystemUtilization(AntaTest):\n    \"\"\"Verifies that no partition is utilizing more than 75% of its disk space.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all partitions are using less than 75% of its disk space.\n    * Failure: The test will fail if any partitions are using more than 75% of its disk space.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyFileSystemUtilization:\n    ```\n    \"\"\"\n\n    name = \"VerifyFileSystemUtilization\"\n    description = \"Verifies that no partition is utilizing more than 75% of its disk space.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"bash timeout 10 df -h\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyFileSystemUtilization.\"\"\"\n        command_output = self.instance_commands[0].text_output\n        self.result.is_success()\n        for line in command_output.split(\"\\n\")[1:]:\n            if \"loop\" not in line and len(line) > 0 and (percentage := int(line.split()[4].replace(\"%\", \"\"))) > DISK_SPACE_THRESHOLD:\n                self.result.is_failure(f\"Mount point {line} is higher than 75%: reported {percentage}%\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyMemoryUtilization","title":"VerifyMemoryUtilization","text":"

Verifies whether the memory utilization is below 75%.

Expected Results
  • Success: The test will pass if the memory utilization is below 75%.
  • Failure: The test will fail if the memory utilization is over 75%.
Examples
anta.tests.system:\n  - VerifyMemoryUtilization:\n
Source code in anta/tests/system.py
class VerifyMemoryUtilization(AntaTest):\n    \"\"\"Verifies whether the memory utilization is below 75%.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the memory utilization is below 75%.\n    * Failure: The test will fail if the memory utilization is over 75%.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyMemoryUtilization:\n    ```\n    \"\"\"\n\n    name = \"VerifyMemoryUtilization\"\n    description = \"Verifies whether the memory utilization is below 75%.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyMemoryUtilization.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        memory_usage = command_output[\"memFree\"] / command_output[\"memTotal\"]\n        if memory_usage > MEMORY_THRESHOLD:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device has reported a high memory usage: {(1 - memory_usage)*100:.2f}%\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyNTP","title":"VerifyNTP","text":"

Verifies that the Network Time Protocol (NTP) is synchronized.

Expected Results
  • Success: The test will pass if the NTP is synchronised.
  • Failure: The test will fail if the NTP is NOT synchronised.
Examples
anta.tests.system:\n  - VerifyNTP:\n
Source code in anta/tests/system.py
class VerifyNTP(AntaTest):\n    \"\"\"Verifies that the Network Time Protocol (NTP) is synchronized.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the NTP is synchronised.\n    * Failure: The test will fail if the NTP is NOT synchronised.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyNTP:\n    ```\n    \"\"\"\n\n    name = \"VerifyNTP\"\n    description = \"Verifies if NTP is synchronised.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ntp status\", ofmt=\"text\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyNTP.\"\"\"\n        command_output = self.instance_commands[0].text_output\n        if command_output.split(\"\\n\")[0].split(\" \")[0] == \"synchronised\":\n            self.result.is_success()\n        else:\n            data = command_output.split(\"\\n\")[0]\n            self.result.is_failure(f\"The device is not synchronized with the configured NTP server(s): '{data}'\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyReloadCause","title":"VerifyReloadCause","text":"

Verifies the last reload cause of the device.

Expected Results
  • Success: The test will pass if there are NO reload causes or if the last reload was caused by the user or after an FPGA upgrade.
  • Failure: The test will fail if the last reload was NOT caused by the user or after an FPGA upgrade.
  • Error: The test will report an error if the reload cause is NOT available.
Examples
anta.tests.system:\n  - VerifyReloadCause:\n
Source code in anta/tests/system.py
class VerifyReloadCause(AntaTest):\n    \"\"\"Verifies the last reload cause of the device.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if there are NO reload causes or if the last reload was caused by the user or after an FPGA upgrade.\n    * Failure: The test will fail if the last reload was NOT caused by the user or after an FPGA upgrade.\n    * Error: The test will report an error if the reload cause is NOT available.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyReloadCause:\n    ```\n    \"\"\"\n\n    name = \"VerifyReloadCause\"\n    description = \"Verifies the last reload cause of the device.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show reload cause\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyReloadCause.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if \"resetCauses\" not in command_output:\n            self.result.is_error(message=\"No reload causes available\")\n            return\n        if len(command_output[\"resetCauses\"]) == 0:\n            # No reload causes\n            self.result.is_success()\n            return\n        reset_causes = command_output[\"resetCauses\"]\n        command_output_data = reset_causes[0].get(\"description\")\n        if command_output_data in [\n            \"Reload requested by the user.\",\n            \"Reload requested after FPGA upgrade\",\n        ]:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Reload cause is: '{command_output_data}'\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyUptime","title":"VerifyUptime","text":"

Verifies if the device uptime is higher than the provided minimum uptime value.

Expected Results
  • Success: The test will pass if the device uptime is higher than the provided value.
  • Failure: The test will fail if the device uptime is lower than the provided value.
Examples
anta.tests.system:\n  - VerifyUptime:\n      minimum: 86400\n
Source code in anta/tests/system.py
class VerifyUptime(AntaTest):\n    \"\"\"Verifies if the device uptime is higher than the provided minimum uptime value.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the device uptime is higher than the provided value.\n    * Failure: The test will fail if the device uptime is lower than the provided value.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.system:\n      - VerifyUptime:\n          minimum: 86400\n    ```\n    \"\"\"\n\n    name = \"VerifyUptime\"\n    description = \"Verifies the device uptime.\"\n    categories: ClassVar[list[str]] = [\"system\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show uptime\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyUptime test.\"\"\"\n\n        minimum: PositiveInteger\n        \"\"\"Minimum uptime in seconds.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyUptime.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if command_output[\"upTime\"] > self.inputs.minimum:\n            self.result.is_success()\n        else:\n            self.result.is_failure(f\"Device uptime is {command_output['upTime']} seconds\")\n
"},{"location":"api/tests.system/#anta.tests.system.VerifyUptime-attributes","title":"Inputs","text":"Name Type Description Default minimum PositiveInteger Minimum uptime in seconds. -"},{"location":"api/tests.vlan/","title":"VLAN","text":""},{"location":"api/tests.vlan/#anta.tests.vlan.VerifyVlanInternalPolicy","title":"VerifyVlanInternalPolicy","text":"

Verifies if the VLAN internal allocation policy is ascending or descending and if the VLANs are within the specified range.

Expected Results
  • Success: The test will pass if the VLAN internal allocation policy is either ascending or descending and the VLANs are within the specified range.
  • Failure: The test will fail if the VLAN internal allocation policy is neither ascending nor descending or the VLANs are outside the specified range.
Examples
anta.tests.vlan:\n  - VerifyVlanInternalPolicy:\n      policy: ascending\n      start_vlan_id: 1006\n      end_vlan_id: 4094\n
Source code in anta/tests/vlan.py
class VerifyVlanInternalPolicy(AntaTest):\n    \"\"\"Verifies if the VLAN internal allocation policy is ascending or descending and if the VLANs are within the specified range.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the VLAN internal allocation policy is either ascending or descending\n                 and the VLANs are within the specified range.\n    * Failure: The test will fail if the VLAN internal allocation policy is neither ascending nor descending\n                 or the VLANs are outside the specified range.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vlan:\n      - VerifyVlanInternalPolicy:\n          policy: ascending\n          start_vlan_id: 1006\n          end_vlan_id: 4094\n    ```\n    \"\"\"\n\n    name = \"VerifyVlanInternalPolicy\"\n    description = \"Verifies the VLAN internal allocation policy and the range of VLANs.\"\n    categories: ClassVar[list[str]] = [\"vlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vlan internal allocation policy\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyVlanInternalPolicy test.\"\"\"\n\n        policy: Literal[\"ascending\", \"descending\"]\n        \"\"\"The VLAN internal allocation policy. Supported values: ascending, descending.\"\"\"\n        start_vlan_id: Vlan\n        \"\"\"The starting VLAN ID in the range.\"\"\"\n        end_vlan_id: Vlan\n        \"\"\"The ending VLAN ID in the range.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVlanInternalPolicy.\"\"\"\n        command_output = self.instance_commands[0].json_output\n\n        keys_to_verify = [\"policy\", \"startVlanId\", \"endVlanId\"]\n        actual_policy_output = {key: get_value(command_output, key) for key in keys_to_verify}\n        expected_policy_output = {\"policy\": self.inputs.policy, \"startVlanId\": self.inputs.start_vlan_id, \"endVlanId\": self.inputs.end_vlan_id}\n\n        # Check if the actual output matches the expected output\n        if actual_policy_output != expected_policy_output:\n            failed_log = \"The VLAN internal allocation policy is not configured properly:\"\n            failed_log += get_failed_logs(expected_policy_output, actual_policy_output)\n            self.result.is_failure(failed_log)\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.vlan/#anta.tests.vlan.VerifyVlanInternalPolicy-attributes","title":"Inputs","text":"Name Type Description Default policy Literal['ascending', 'descending'] The VLAN internal allocation policy. Supported values: ascending, descending. - start_vlan_id Vlan The starting VLAN ID in the range. - end_vlan_id Vlan The ending VLAN ID in the range. -"},{"location":"api/tests.vxlan/","title":"VXLAN","text":""},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlan1ConnSettings","title":"VerifyVxlan1ConnSettings","text":"

Verifies the interface vxlan1 source interface and UDP port.

Expected Results
  • Success: Passes if the interface vxlan1 source interface and UDP port are correct.
  • Failure: Fails if the interface vxlan1 source interface or UDP port are incorrect.
  • Skipped: Skips if the Vxlan1 interface is not configured.
Examples
anta.tests.vxlan:\n  - VerifyVxlan1ConnSettings:\n      source_interface: Loopback1\n      udp_port: 4789\n
Source code in anta/tests/vxlan.py
class VerifyVxlan1ConnSettings(AntaTest):\n    \"\"\"Verifies the interface vxlan1 source interface and UDP port.\n\n    Expected Results\n    ----------------\n    * Success: Passes if the interface vxlan1 source interface and UDP port are correct.\n    * Failure: Fails if the interface vxlan1 source interface or UDP port are incorrect.\n    * Skipped: Skips if the Vxlan1 interface is not configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vxlan:\n      - VerifyVxlan1ConnSettings:\n          source_interface: Loopback1\n          udp_port: 4789\n    ```\n    \"\"\"\n\n    name = \"VerifyVxlan1ConnSettings\"\n    description = \"Verifies the interface vxlan1 source interface and UDP port.\"\n    categories: ClassVar[list[str]] = [\"vxlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyVxlan1ConnSettings test.\"\"\"\n\n        source_interface: VxlanSrcIntf\n        \"\"\"Source loopback interface of vxlan1 interface.\"\"\"\n        udp_port: int = Field(ge=1024, le=65335)\n        \"\"\"UDP port used for vxlan1 interface.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVxlan1ConnSettings.\"\"\"\n        self.result.is_success()\n        command_output = self.instance_commands[0].json_output\n\n        # Skip the test case if vxlan1 interface is not configured\n        vxlan_output = get_value(command_output, \"interfaces.Vxlan1\")\n        if not vxlan_output:\n            self.result.is_skipped(\"Vxlan1 interface is not configured.\")\n            return\n\n        src_intf = vxlan_output.get(\"srcIpIntf\")\n        port = vxlan_output.get(\"udpPort\")\n\n        # Check vxlan1 source interface and udp port\n        if src_intf != self.inputs.source_interface:\n            self.result.is_failure(f\"Source interface is not correct. Expected `{self.inputs.source_interface}` as source interface but found `{src_intf}` instead.\")\n        if port != self.inputs.udp_port:\n            self.result.is_failure(f\"UDP port is not correct. Expected `{self.inputs.udp_port}` as UDP port but found `{port}` instead.\")\n
"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlan1ConnSettings-attributes","title":"Inputs","text":"Name Type Description Default source_interface VxlanSrcIntf Source loopback interface of vxlan1 interface. - udp_port int UDP port used for vxlan1 interface. Field(ge=1024, le=65335)"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlan1Interface","title":"VerifyVxlan1Interface","text":"

Verifies if the Vxlan1 interface is configured and \u2018up/up\u2019.

Warning

The name of this test has been updated from \u2018VerifyVxlan\u2019 for better representation.

Expected Results
  • Success: The test will pass if the Vxlan1 interface is configured with line protocol status and interface status \u2018up\u2019.
  • Failure: The test will fail if the Vxlan1 interface line protocol status or interface status are not \u2018up\u2019.
  • Skipped: The test will be skipped if the Vxlan1 interface is not configured.
Examples
anta.tests.vxlan:\n  - VerifyVxlan1Interface:\n
Source code in anta/tests/vxlan.py
class VerifyVxlan1Interface(AntaTest):\n    \"\"\"Verifies if the Vxlan1 interface is configured and 'up/up'.\n\n    Warning\n    -------\n    The name of this test has been updated from 'VerifyVxlan' for better representation.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the Vxlan1 interface is configured with line protocol status and interface status 'up'.\n    * Failure: The test will fail if the Vxlan1 interface line protocol status or interface status are not 'up'.\n    * Skipped: The test will be skipped if the Vxlan1 interface is not configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vxlan:\n      - VerifyVxlan1Interface:\n    ```\n    \"\"\"\n\n    name = \"VerifyVxlan1Interface\"\n    description = \"Verifies the Vxlan1 interface status.\"\n    categories: ClassVar[list[str]] = [\"vxlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVxlan1Interface.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if \"Vxlan1\" not in command_output[\"interfaceDescriptions\"]:\n            self.result.is_skipped(\"Vxlan1 interface is not configured\")\n        elif (\n            command_output[\"interfaceDescriptions\"][\"Vxlan1\"][\"lineProtocolStatus\"] == \"up\"\n            and command_output[\"interfaceDescriptions\"][\"Vxlan1\"][\"interfaceStatus\"] == \"up\"\n        ):\n            self.result.is_success()\n        else:\n            self.result.is_failure(\n                f\"Vxlan1 interface is {command_output['interfaceDescriptions']['Vxlan1']['lineProtocolStatus']}\"\n                f\"/{command_output['interfaceDescriptions']['Vxlan1']['interfaceStatus']}\",\n            )\n
"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlanConfigSanity","title":"VerifyVxlanConfigSanity","text":"

Verifies that no issues are detected with the VXLAN configuration.

Expected Results
  • Success: The test will pass if no issues are detected with the VXLAN configuration.
  • Failure: The test will fail if issues are detected with the VXLAN configuration.
  • Skipped: The test will be skipped if VXLAN is not configured on the device.
Examples
anta.tests.vxlan:\n  - VerifyVxlanConfigSanity:\n
Source code in anta/tests/vxlan.py
class VerifyVxlanConfigSanity(AntaTest):\n    \"\"\"Verifies that no issues are detected with the VXLAN configuration.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if no issues are detected with the VXLAN configuration.\n    * Failure: The test will fail if issues are detected with the VXLAN configuration.\n    * Skipped: The test will be skipped if VXLAN is not configured on the device.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vxlan:\n      - VerifyVxlanConfigSanity:\n    ```\n    \"\"\"\n\n    name = \"VerifyVxlanConfigSanity\"\n    description = \"Verifies there are no VXLAN config-sanity inconsistencies.\"\n    categories: ClassVar[list[str]] = [\"vxlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan config-sanity\", ofmt=\"json\")]\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVxlanConfigSanity.\"\"\"\n        command_output = self.instance_commands[0].json_output\n        if \"categories\" not in command_output or len(command_output[\"categories\"]) == 0:\n            self.result.is_skipped(\"VXLAN is not configured\")\n            return\n        failed_categories = {\n            category: content\n            for category, content in command_output[\"categories\"].items()\n            if category in [\"localVtep\", \"mlag\", \"pd\"] and content[\"allCheckPass\"] is not True\n        }\n        if len(failed_categories) > 0:\n            self.result.is_failure(f\"VXLAN config sanity check is not passing: {failed_categories}\")\n        else:\n            self.result.is_success()\n
"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlanVniBinding","title":"VerifyVxlanVniBinding","text":"

Verifies the VNI-VLAN bindings of the Vxlan1 interface.

Expected Results
  • Success: The test will pass if the VNI-VLAN bindings provided are properly configured.
  • Failure: The test will fail if any VNI lacks bindings or if any bindings are incorrect.
  • Skipped: The test will be skipped if the Vxlan1 interface is not configured.
Examples
anta.tests.vxlan:\n  - VerifyVxlanVniBinding:\n      bindings:\n        10010: 10\n        10020: 20\n
Source code in anta/tests/vxlan.py
class VerifyVxlanVniBinding(AntaTest):\n    \"\"\"Verifies the VNI-VLAN bindings of the Vxlan1 interface.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if the VNI-VLAN bindings provided are properly configured.\n    * Failure: The test will fail if any VNI lacks bindings or if any bindings are incorrect.\n    * Skipped: The test will be skipped if the Vxlan1 interface is not configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vxlan:\n      - VerifyVxlanVniBinding:\n          bindings:\n            10010: 10\n            10020: 20\n    ```\n    \"\"\"\n\n    name = \"VerifyVxlanVniBinding\"\n    description = \"Verifies the VNI-VLAN bindings of the Vxlan1 interface.\"\n    categories: ClassVar[list[str]] = [\"vxlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vni\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyVxlanVniBinding test.\"\"\"\n\n        bindings: dict[Vni, Vlan]\n        \"\"\"VNI to VLAN bindings to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVxlanVniBinding.\"\"\"\n        self.result.is_success()\n\n        no_binding = []\n        wrong_binding = []\n\n        if (vxlan1 := get_value(self.instance_commands[0].json_output, \"vxlanIntfs.Vxlan1\")) is None:\n            self.result.is_skipped(\"Vxlan1 interface is not configured\")\n            return\n\n        for vni, vlan in self.inputs.bindings.items():\n            str_vni = str(vni)\n            if str_vni in vxlan1[\"vniBindings\"]:\n                retrieved_vlan = vxlan1[\"vniBindings\"][str_vni][\"vlan\"]\n            elif str_vni in vxlan1[\"vniBindingsToVrf\"]:\n                retrieved_vlan = vxlan1[\"vniBindingsToVrf\"][str_vni][\"vlan\"]\n            else:\n                no_binding.append(str_vni)\n                retrieved_vlan = None\n\n            if retrieved_vlan and vlan != retrieved_vlan:\n                wrong_binding.append({str_vni: retrieved_vlan})\n\n        if no_binding:\n            self.result.is_failure(f\"The following VNI(s) have no binding: {no_binding}\")\n\n        if wrong_binding:\n            self.result.is_failure(f\"The following VNI(s) have the wrong VLAN binding: {wrong_binding}\")\n
"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlanVniBinding-attributes","title":"Inputs","text":"Name Type Description Default bindings dict[Vni, Vlan] VNI to VLAN bindings to verify. -"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlanVtep","title":"VerifyVxlanVtep","text":"

Verifies the VTEP peers of the Vxlan1 interface.

Expected Results
  • Success: The test will pass if all provided VTEP peers are identified and matching.
  • Failure: The test will fail if any VTEP peer is missing or there are unexpected VTEP peers.
  • Skipped: The test will be skipped if the Vxlan1 interface is not configured.
Examples
anta.tests.vxlan:\n  - VerifyVxlanVtep:\n      vteps:\n        - 10.1.1.5\n        - 10.1.1.6\n
Source code in anta/tests/vxlan.py
class VerifyVxlanVtep(AntaTest):\n    \"\"\"Verifies the VTEP peers of the Vxlan1 interface.\n\n    Expected Results\n    ----------------\n    * Success: The test will pass if all provided VTEP peers are identified and matching.\n    * Failure: The test will fail if any VTEP peer is missing or there are unexpected VTEP peers.\n    * Skipped: The test will be skipped if the Vxlan1 interface is not configured.\n\n    Examples\n    --------\n    ```yaml\n    anta.tests.vxlan:\n      - VerifyVxlanVtep:\n          vteps:\n            - 10.1.1.5\n            - 10.1.1.6\n    ```\n    \"\"\"\n\n    name = \"VerifyVxlanVtep\"\n    description = \"Verifies the VTEP peers of the Vxlan1 interface\"\n    categories: ClassVar[list[str]] = [\"vxlan\"]\n    commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vtep\", ofmt=\"json\")]\n\n    class Input(AntaTest.Input):\n        \"\"\"Input model for the VerifyVxlanVtep test.\"\"\"\n\n        vteps: list[IPv4Address]\n        \"\"\"List of VTEP peers to verify.\"\"\"\n\n    @AntaTest.anta_test\n    def test(self) -> None:\n        \"\"\"Main test function for VerifyVxlanVtep.\"\"\"\n        self.result.is_success()\n\n        inputs_vteps = [str(input_vtep) for input_vtep in self.inputs.vteps]\n\n        if (vxlan1 := get_value(self.instance_commands[0].json_output, \"interfaces.Vxlan1\")) is None:\n            self.result.is_skipped(\"Vxlan1 interface is not configured\")\n            return\n\n        difference1 = set(inputs_vteps).difference(set(vxlan1[\"vteps\"]))\n        difference2 = set(vxlan1[\"vteps\"]).difference(set(inputs_vteps))\n\n        if difference1:\n            self.result.is_failure(f\"The following VTEP peer(s) are missing from the Vxlan1 interface: {sorted(difference1)}\")\n\n        if difference2:\n            self.result.is_failure(f\"Unexpected VTEP peer(s) on Vxlan1 interface: {sorted(difference2)}\")\n
"},{"location":"api/tests.vxlan/#anta.tests.vxlan.VerifyVxlanVtep-attributes","title":"Inputs","text":"Name Type Description Default vteps list[IPv4Address] List of VTEP peers to verify. -"},{"location":"api/types/","title":"Input Types","text":""},{"location":"api/types/#anta.custom_types","title":"anta.custom_types","text":"

Module that provides predefined types for AntaTest.Input instances.

"},{"location":"api/types/#anta.custom_types.AAAAuthMethod","title":"AAAAuthMethod module-attribute","text":"
AAAAuthMethod = Annotated[str, AfterValidator(aaa_group_prefix)]\n
"},{"location":"api/types/#anta.custom_types.Afi","title":"Afi module-attribute","text":"
Afi = Literal['ipv4', 'ipv6', 'vpn-ipv4', 'vpn-ipv6', 'evpn', 'rt-membership', 'path-selection', 'link-state']\n
"},{"location":"api/types/#anta.custom_types.BfdInterval","title":"BfdInterval module-attribute","text":"
BfdInterval = Annotated[int, Field(ge=50, le=60000)]\n
"},{"location":"api/types/#anta.custom_types.BfdMultiplier","title":"BfdMultiplier module-attribute","text":"
BfdMultiplier = Annotated[int, Field(ge=3, le=50)]\n
"},{"location":"api/types/#anta.custom_types.EcdsaKeySize","title":"EcdsaKeySize module-attribute","text":"
EcdsaKeySize = Literal[256, 384, 521]\n
"},{"location":"api/types/#anta.custom_types.EncryptionAlgorithm","title":"EncryptionAlgorithm module-attribute","text":"
EncryptionAlgorithm = Literal['RSA', 'ECDSA']\n
"},{"location":"api/types/#anta.custom_types.ErrDisableInterval","title":"ErrDisableInterval module-attribute","text":"
ErrDisableInterval = Annotated[int, Field(ge=30, le=86400)]\n
"},{"location":"api/types/#anta.custom_types.ErrDisableReasons","title":"ErrDisableReasons module-attribute","text":"
ErrDisableReasons = Literal['acl', 'arp-inspection', 'bpduguard', 'dot1x-session-replace', 'hitless-reload-down', 'lacp-rate-limit', 'link-flap', 'no-internal-vlan', 'portchannelguard', 'portsec', 'tapagg', 'uplink-failure-detection']\n
"},{"location":"api/types/#anta.custom_types.Hostname","title":"Hostname module-attribute","text":"
Hostname = Annotated[str, Field(pattern='^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\\\\-]*[a-zA-Z0-9])\\\\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\\\\-]*[A-Za-z0-9])$')]\n
"},{"location":"api/types/#anta.custom_types.Interface","title":"Interface module-attribute","text":"
Interface = Annotated[str, Field(pattern='^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\\\\/[0-9]+)*(\\\\.[0-9]+)?$'), BeforeValidator(interface_autocomplete), BeforeValidator(interface_case_sensitivity)]\n
"},{"location":"api/types/#anta.custom_types.MlagPriority","title":"MlagPriority module-attribute","text":"
MlagPriority = Annotated[int, Field(ge=1, le=32767)]\n
"},{"location":"api/types/#anta.custom_types.MultiProtocolCaps","title":"MultiProtocolCaps module-attribute","text":"
MultiProtocolCaps = Annotated[str, BeforeValidator(bgp_multiprotocol_capabilities_abbreviations)]\n
"},{"location":"api/types/#anta.custom_types.Percent","title":"Percent module-attribute","text":"
Percent = Annotated[float, Field(ge=0.0, le=100.0)]\n
"},{"location":"api/types/#anta.custom_types.Port","title":"Port module-attribute","text":"
Port = Annotated[int, Field(ge=1, le=65535)]\n
"},{"location":"api/types/#anta.custom_types.PositiveInteger","title":"PositiveInteger module-attribute","text":"
PositiveInteger = Annotated[int, Field(ge=0)]\n
"},{"location":"api/types/#anta.custom_types.Revision","title":"Revision module-attribute","text":"
Revision = Annotated[int, Field(ge=1, le=99)]\n
"},{"location":"api/types/#anta.custom_types.RsaKeySize","title":"RsaKeySize module-attribute","text":"
RsaKeySize = Literal[2048, 3072, 4096]\n
"},{"location":"api/types/#anta.custom_types.Safi","title":"Safi module-attribute","text":"
Safi = Literal['unicast', 'multicast', 'labeled-unicast', 'sr-te']\n
"},{"location":"api/types/#anta.custom_types.TestStatus","title":"TestStatus module-attribute","text":"
TestStatus = Literal['unset', 'success', 'failure', 'error', 'skipped']\n
"},{"location":"api/types/#anta.custom_types.Vlan","title":"Vlan module-attribute","text":"
Vlan = Annotated[int, Field(ge=0, le=4094)]\n
"},{"location":"api/types/#anta.custom_types.Vni","title":"Vni module-attribute","text":"
Vni = Annotated[int, Field(ge=1, le=16777215)]\n
"},{"location":"api/types/#anta.custom_types.VxlanSrcIntf","title":"VxlanSrcIntf module-attribute","text":"
VxlanSrcIntf = Annotated[str, Field(pattern='^(Loopback)([0-9]|[1-9][0-9]{1,2}|[1-7][0-9]{3}|8[01][0-9]{2}|819[01])$'), BeforeValidator(interface_autocomplete), BeforeValidator(interface_case_sensitivity)]\n
"},{"location":"api/types/#anta.custom_types.aaa_group_prefix","title":"aaa_group_prefix","text":"
aaa_group_prefix(v: str) -> str\n

Prefix the AAA method with \u2018group\u2019 if it is known.

Source code in anta/custom_types.py
def aaa_group_prefix(v: str) -> str:\n    \"\"\"Prefix the AAA method with 'group' if it is known.\"\"\"\n    built_in_methods = [\"local\", \"none\", \"logging\"]\n    return f\"group {v}\" if v not in built_in_methods and not v.startswith(\"group \") else v\n
"},{"location":"api/types/#anta.custom_types.bgp_multiprotocol_capabilities_abbreviations","title":"bgp_multiprotocol_capabilities_abbreviations","text":"
bgp_multiprotocol_capabilities_abbreviations(value: str) -> str\n

Abbreviations for different BGP multiprotocol capabilities.

Examples
- IPv4 Unicast\n- L2vpnEVPN\n- ipv4 MPLS Labels\n- ipv4Mplsvpn\n
Source code in anta/custom_types.py
def bgp_multiprotocol_capabilities_abbreviations(value: str) -> str:\n    \"\"\"Abbreviations for different BGP multiprotocol capabilities.\n\n    Examples\n    --------\n        - IPv4 Unicast\n        - L2vpnEVPN\n        - ipv4 MPLS Labels\n        - ipv4Mplsvpn\n\n    \"\"\"\n    patterns = {\n        r\"\\b(l2[\\s\\-]?vpn[\\s\\-]?evpn)\\b\": \"l2VpnEvpn\",\n        r\"\\bipv4[\\s_-]?mpls[\\s_-]?label(s)?\\b\": \"ipv4MplsLabels\",\n        r\"\\bipv4[\\s_-]?mpls[\\s_-]?vpn\\b\": \"ipv4MplsVpn\",\n        r\"\\bipv4[\\s_-]?uni[\\s_-]?cast\\b\": \"ipv4Unicast\",\n    }\n\n    for pattern, replacement in patterns.items():\n        match = re.search(pattern, value, re.IGNORECASE)\n        if match:\n            return replacement\n\n    return value\n
"},{"location":"api/types/#anta.custom_types.interface_autocomplete","title":"interface_autocomplete","text":"
interface_autocomplete(v: str) -> str\n

Allow the user to only provide the beginning of an interface name.

Supported alias: - et, eth will be changed to Ethernet - po will be changed to Port-Channel - lo will be changed to Loopback

Source code in anta/custom_types.py
def interface_autocomplete(v: str) -> str:\n    \"\"\"Allow the user to only provide the beginning of an interface name.\n\n    Supported alias:\n         - `et`, `eth` will be changed to `Ethernet`\n         - `po` will be changed to `Port-Channel`\n    - `lo` will be changed to `Loopback`\n    \"\"\"\n    intf_id_re = re.compile(r\"[0-9]+(\\/[0-9]+)*(\\.[0-9]+)?\")\n    m = intf_id_re.search(v)\n    if m is None:\n        msg = f\"Could not parse interface ID in interface '{v}'\"\n        raise ValueError(msg)\n    intf_id = m[0]\n\n    alias_map = {\"et\": \"Ethernet\", \"eth\": \"Ethernet\", \"po\": \"Port-Channel\", \"lo\": \"Loopback\"}\n\n    for alias, full_name in alias_map.items():\n        if v.lower().startswith(alias):\n            return f\"{full_name}{intf_id}\"\n\n    return v\n
"},{"location":"api/types/#anta.custom_types.interface_case_sensitivity","title":"interface_case_sensitivity","text":"
interface_case_sensitivity(v: str) -> str\n

Reformat interface name to match expected case sensitivity.

Examples
 - ethernet -> Ethernet\n - vlan -> Vlan\n - loopback -> Loopback\n
Source code in anta/custom_types.py
def interface_case_sensitivity(v: str) -> str:\n    \"\"\"Reformat interface name to match expected case sensitivity.\n\n    Examples\n    --------\n         - ethernet -> Ethernet\n         - vlan -> Vlan\n         - loopback -> Loopback\n\n    \"\"\"\n    if isinstance(v, str) and len(v) > 0 and not v[0].isupper():\n        return f\"{v[0].upper()}{v[1:]}\"\n    return v\n
"},{"location":"cli/check/","title":"Check","text":""},{"location":"cli/check/#anta-check-commands","title":"ANTA check commands","text":"

The ANTA check command allow to execute some checks on the ANTA input files. Only checking the catalog is currently supported.

anta check --help\nUsage: anta check [OPTIONS] COMMAND [ARGS]...\n\n  Check commands for building ANTA\n\nOptions:\n  --help  Show this message and exit.\n\nCommands:\n  catalog  Check that the catalog is valid\n
"},{"location":"cli/check/#checking-the-catalog","title":"Checking the catalog","text":"
Usage: anta check catalog [OPTIONS]\n\n  Check that the catalog is valid\n\nOptions:\n  -c, --catalog FILE  Path to the test catalog YAML file  [env var:\n                      ANTA_CATALOG; required]\n  --help              Show this message and exit.\n
"},{"location":"cli/debug/","title":"Helpers","text":""},{"location":"cli/debug/#anta-debug-commands","title":"ANTA debug commands","text":"

The ANTA CLI includes a set of debugging tools, making it easier to build and test ANTA content. This functionality is accessed via the debug subcommand and offers the following options:

  • Executing a command on a device from your inventory and retrieving the result.
  • Running a templated command on a device from your inventory and retrieving the result.

These tools are especially helpful in building the tests, as they give a visual access to the output received from the eAPI. They also facilitate the extraction of output content for use in unit tests, as described in our contribution guide.

Warning

The debug tools require a device from your inventory. Thus, you MUST use a valid ANTA Inventory.

"},{"location":"cli/debug/#executing-an-eos-command","title":"Executing an EOS command","text":"

You can use the run-cmd entrypoint to run a command, which includes the following options:

"},{"location":"cli/debug/#command-overview","title":"Command overview","text":"
$ anta debug run-cmd --help\nUsage: anta debug run-cmd [OPTIONS]\n\n  Run arbitrary command to an ANTA device\n\nOptions:\n  -u, --username TEXT       Username to connect to EOS  [env var:\n                            ANTA_USERNAME; required]\n  -p, --password TEXT       Password to connect to EOS that must be provided.\n                            It can be prompted using '--prompt' option.  [env\n                            var: ANTA_PASSWORD]\n  --enable-password TEXT    Password to access EOS Privileged EXEC mode. It\n                            can be prompted using '--prompt' option. Requires\n                            '--enable' option.  [env var:\n                            ANTA_ENABLE_PASSWORD]\n  --enable                  Some commands may require EOS Privileged EXEC\n                            mode. This option tries to access this mode before\n                            sending a command to the device.  [env var:\n                            ANTA_ENABLE]\n  -P, --prompt              Prompt for passwords if they are not provided.\n                            [env var: ANTA_PROMPT]\n  --timeout INTEGER         Global connection timeout  [env var: ANTA_TIMEOUT;\n                            default: 30]\n  --insecure                Disable SSH Host Key validation  [env var:\n                            ANTA_INSECURE]\n  --disable-cache           Disable cache globally  [env var:\n                            ANTA_DISABLE_CACHE]\n  -i, --inventory FILE      Path to the inventory YAML file  [env var:\n                            ANTA_INVENTORY; required]\n  -t, --tags TEXT           List of tags using comma as separator:\n                            tag1,tag2,tag3  [env var: ANTA_TAGS]\n  --ofmt [json|text]        EOS eAPI format to use. can be text or json\n  -v, --version [1|latest]  EOS eAPI version\n  -r, --revision INTEGER    eAPI command revision\n  -d, --device TEXT         Device from inventory to use  [required]\n  -c, --command TEXT        Command to run  [required]\n  --help                    Show this message and exit.\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

"},{"location":"cli/debug/#example","title":"Example","text":"

This example illustrates how to run the show interfaces description command with a JSON format (default):

anta debug run-cmd --command \"show interfaces description\" --device DC1-SPINE1\nRun command show interfaces description on DC1-SPINE1\n{\n    'interfaceDescriptions': {\n        'Ethernet1': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-LEAF1A_Ethernet1', 'interfaceStatus': 'up'},\n        'Ethernet2': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-LEAF1B_Ethernet1', 'interfaceStatus': 'up'},\n        'Ethernet3': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-BL1_Ethernet1', 'interfaceStatus': 'up'},\n        'Ethernet4': {'lineProtocolStatus': 'up', 'description': 'P2P_LINK_TO_DC1-BL2_Ethernet1', 'interfaceStatus': 'up'},\n        'Loopback0': {'lineProtocolStatus': 'up', 'description': 'EVPN_Overlay_Peering', 'interfaceStatus': 'up'},\n        'Management0': {'lineProtocolStatus': 'up', 'description': 'oob_management', 'interfaceStatus': 'up'}\n    }\n}\n
"},{"location":"cli/debug/#executing-an-eos-command-using-templates","title":"Executing an EOS command using templates","text":"

The run-template entrypoint allows the user to provide an f-string templated command. It is followed by a list of arguments (key-value pairs) that build a dictionary used as template parameters.

"},{"location":"cli/debug/#command-overview_1","title":"Command overview","text":"
$ anta debug run-template --help\nUsage: anta debug run-template [OPTIONS] PARAMS...\n\n  Run arbitrary templated command to an ANTA device.\n\n  Takes a list of arguments (keys followed by a value) to build a dictionary\n  used as template parameters. Example:\n\n  anta debug run-template -d leaf1a -t 'show vlan {vlan_id}' vlan_id 1\n\nOptions:\n  -u, --username TEXT       Username to connect to EOS  [env var:\n                            ANTA_USERNAME; required]\n  -p, --password TEXT       Password to connect to EOS that must be provided.\n                            It can be prompted using '--prompt' option.  [env\n                            var: ANTA_PASSWORD]\n  --enable-password TEXT    Password to access EOS Privileged EXEC mode. It\n                            can be prompted using '--prompt' option. Requires\n                            '--enable' option.  [env var:\n                            ANTA_ENABLE_PASSWORD]\n  --enable                  Some commands may require EOS Privileged EXEC\n                            mode. This option tries to access this mode before\n                            sending a command to the device.  [env var:\n                            ANTA_ENABLE]\n  -P, --prompt              Prompt for passwords if they are not provided.\n                            [env var: ANTA_PROMPT]\n  --timeout INTEGER         Global connection timeout  [env var: ANTA_TIMEOUT;\n                            default: 30]\n  --insecure                Disable SSH Host Key validation  [env var:\n                            ANTA_INSECURE]\n  --disable-cache           Disable cache globally  [env var:\n                            ANTA_DISABLE_CACHE]\n  -i, --inventory FILE      Path to the inventory YAML file  [env var:\n                            ANTA_INVENTORY; required]\n  -t, --tags TEXT           List of tags using comma as separator:\n                            tag1,tag2,tag3  [env var: ANTA_TAGS]\n  --ofmt [json|text]        EOS eAPI format to use. can be text or json\n  -v, --version [1|latest]  EOS eAPI version\n  -r, --revision INTEGER    eAPI command revision\n  -d, --device TEXT         Device from inventory to use  [required]\n  -t, --template TEXT       Command template to run. E.g. 'show vlan\n                            {vlan_id}'  [required]\n  --help                    Show this message and exit.\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

"},{"location":"cli/debug/#example_1","title":"Example","text":"

This example uses the show vlan {vlan_id} command in a JSON format:

anta debug run-template --template \"show vlan {vlan_id}\" vlan_id 10 --device DC1-LEAF1A\nRun templated command 'show vlan {vlan_id}' with {'vlan_id': '10'} on DC1-LEAF1A\n{\n    'vlans': {\n        '10': {\n            'name': 'VRFPROD_VLAN10',\n            'dynamic': False,\n            'status': 'active',\n            'interfaces': {\n                'Cpu': {'privatePromoted': False, 'blocked': None},\n                'Port-Channel11': {'privatePromoted': False, 'blocked': None},\n                'Vxlan1': {'privatePromoted': False, 'blocked': None}\n            }\n        }\n    },\n    'sourceDetail': ''\n}\n

Warning

If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters.

"},{"location":"cli/debug/#example-of-multiple-arguments","title":"Example of multiple arguments","text":"
anta -log DEBUG debug run-template --template \"ping {dst} source {src}\" dst \"8.8.8.8\" src Loopback0 --device DC1-SPINE1 \u00a0 \u00a0\n> {'dst': '8.8.8.8', 'src': 'Loopback0'}\n\nanta -log DEBUG debug run-template --template \"ping {dst} source {src}\" dst \"8.8.8.8\" src Loopback0 dst \"1.1.1.1\" src Loopback1 --device DC1-SPINE1 \u00a0 \u00a0 \u00a0 \u00a0 \u00a0\n> {'dst': '1.1.1.1', 'src': 'Loopback1'}\n# Notice how `src` and `dst` keep only the latest value\n
"},{"location":"cli/exec/","title":"Execute commands","text":""},{"location":"cli/exec/#executing-commands-on-devices","title":"Executing Commands on Devices","text":"

ANTA CLI provides a set of entrypoints to facilitate remote command execution on EOS devices.

"},{"location":"cli/exec/#exec-command-overview","title":"EXEC Command overview","text":"
anta exec --help\nUsage: anta exec [OPTIONS] COMMAND [ARGS]...\n\n  Execute commands to inventory devices\n\nOptions:\n  --help  Show this message and exit.\n\nCommands:\n  clear-counters        Clear counter statistics on EOS devices\n  collect-tech-support  Collect scheduled tech-support from EOS devices\n  snapshot              Collect commands output from devices in inventory\n
"},{"location":"cli/exec/#clear-interfaces-counters","title":"Clear interfaces counters","text":"

This command clears interface counters on EOS devices specified in your inventory.

"},{"location":"cli/exec/#command-overview","title":"Command overview","text":"
anta exec clear-counters --help\nUsage: anta exec clear-counters [OPTIONS]\n\n  Clear counter statistics on EOS devices\n\nOptions:\n  -u, --username TEXT     Username to connect to EOS  [env var: ANTA_USERNAME;\n                          required]\n  -p, --password TEXT     Password to connect to EOS that must be provided. It\n                          can be prompted using '--prompt' option.  [env var:\n                          ANTA_PASSWORD]\n  --enable-password TEXT  Password to access EOS Privileged EXEC mode. It can\n                          be prompted using '--prompt' option. Requires '--\n                          enable' option.  [env var: ANTA_ENABLE_PASSWORD]\n  --enable                Some commands may require EOS Privileged EXEC mode.\n                          This option tries to access this mode before sending\n                          a command to the device.  [env var: ANTA_ENABLE]\n  -P, --prompt            Prompt for passwords if they are not provided.  [env\n                          var: ANTA_PROMPT]\n  --timeout INTEGER       Global connection timeout  [env var: ANTA_TIMEOUT;\n                          default: 30]\n  --insecure              Disable SSH Host Key validation  [env var:\n                          ANTA_INSECURE]\n  --disable-cache         Disable cache globally  [env var:\n                          ANTA_DISABLE_CACHE]\n  -i, --inventory FILE    Path to the inventory YAML file  [env var:\n                          ANTA_INVENTORY; required]\n  -t, --tags TEXT         List of tags using comma as separator:\n                          tag1,tag2,tag3  [env var: ANTA_TAGS]\n  --help                  Show this message and exit.\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

"},{"location":"cli/exec/#example","title":"Example","text":"
anta exec clear-counters --tags SPINE\n[20:19:13] INFO     Connecting to devices...                                                                                                                         utils.py:43\n           INFO     Clearing counters on remote devices...                                                                                                           utils.py:46\n           INFO     Cleared counters on DC1-SPINE2 (cEOSLab)                                                                                                         utils.py:41\n           INFO     Cleared counters on DC2-SPINE1 (cEOSLab)                                                                                                         utils.py:41\n           INFO     Cleared counters on DC1-SPINE1 (cEOSLab)                                                                                                         utils.py:41\n           INFO     Cleared counters on DC2-SPINE2 (cEOSLab)\n
"},{"location":"cli/exec/#collect-a-set-of-commands","title":"Collect a set of commands","text":"

This command collects all the commands specified in a commands-list file, which can be in either json or text format.

"},{"location":"cli/exec/#command-overview_1","title":"Command overview","text":"
anta exec snapshot --help\nUsage: anta exec snapshot [OPTIONS]\n\n  Collect commands output from devices in inventory\n\nOptions:\n  -u, --username TEXT       Username to connect to EOS  [env var:\n                            ANTA_USERNAME; required]\n  -p, --password TEXT       Password to connect to EOS that must be provided.\n                            It can be prompted using '--prompt' option.  [env\n                            var: ANTA_PASSWORD]\n  --enable-password TEXT    Password to access EOS Privileged EXEC mode. It\n                            can be prompted using '--prompt' option. Requires\n                            '--enable' option.  [env var:\n                            ANTA_ENABLE_PASSWORD]\n  --enable                  Some commands may require EOS Privileged EXEC\n                            mode. This option tries to access this mode before\n                            sending a command to the device.  [env var:\n                            ANTA_ENABLE]\n  -P, --prompt              Prompt for passwords if they are not provided.\n                            [env var: ANTA_PROMPT]\n  --timeout INTEGER         Global connection timeout  [env var: ANTA_TIMEOUT;\n                            default: 30]\n  --insecure                Disable SSH Host Key validation  [env var:\n                            ANTA_INSECURE]\n  --disable-cache           Disable cache globally  [env var:\n                            ANTA_DISABLE_CACHE]\n  -i, --inventory FILE      Path to the inventory YAML file  [env var:\n                            ANTA_INVENTORY; required]\n  -t, --tags TEXT           List of tags using comma as separator:\n                            tag1,tag2,tag3  [env var: ANTA_TAGS]\n  -c, --commands-list FILE  File with list of commands to collect  [env var:\n                            ANTA_EXEC_SNAPSHOT_COMMANDS_LIST; required]\n  -o, --output DIRECTORY    Directory to save commands output.  [env var:\n                            ANTA_EXEC_SNAPSHOT_OUTPUT; default:\n                            anta_snapshot_2023-12-06_09_22_11]\n  --help                    Show this message and exit.\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

The commands-list file should follow this structure:

---\njson_format:\n  - show version\ntext_format:\n  - show bfd peers\n
"},{"location":"cli/exec/#example_1","title":"Example","text":"
anta exec snapshot --tags SPINE --commands-list ./commands.yaml --output ./\n[20:25:15] INFO     Connecting to devices...                                                                                                                         utils.py:78\n           INFO     Collecting commands from remote devices                                                                                                          utils.py:81\n           INFO     Collected command 'show version' from device DC2-SPINE1 (cEOSLab)                                                                                utils.py:76\n           INFO     Collected command 'show version' from device DC2-SPINE2 (cEOSLab)                                                                                utils.py:76\n           INFO     Collected command 'show version' from device DC1-SPINE1 (cEOSLab)                                                                                utils.py:76\n           INFO     Collected command 'show version' from device DC1-SPINE2 (cEOSLab)                                                                                utils.py:76\n[20:25:16] INFO     Collected command 'show bfd peers' from device DC2-SPINE2 (cEOSLab)                                                                              utils.py:76\n           INFO     Collected command 'show bfd peers' from device DC2-SPINE1 (cEOSLab)                                                                              utils.py:76\n           INFO     Collected command 'show bfd peers' from device DC1-SPINE1 (cEOSLab)                                                                              utils.py:76\n           INFO     Collected command 'show bfd peers' from device DC1-SPINE2 (cEOSLab)\n

The results of the executed commands will be stored in the output directory specified during command execution:

tree _2023-07-14_20_25_15\n_2023-07-14_20_25_15\n\u251c\u2500\u2500 DC1-SPINE1\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 json\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 show version.json\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 text\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 show bfd peers.log\n\u251c\u2500\u2500 DC1-SPINE2\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 json\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 show version.json\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 text\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 show bfd peers.log\n\u251c\u2500\u2500 DC2-SPINE1\n\u2502\u00a0\u00a0 \u251c\u2500\u2500 json\n\u2502\u00a0\u00a0 \u2502\u00a0\u00a0 \u2514\u2500\u2500 show version.json\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 text\n\u2502\u00a0\u00a0     \u2514\u2500\u2500 show bfd peers.log\n\u2514\u2500\u2500 DC2-SPINE2\n    \u251c\u2500\u2500 json\n    \u2502\u00a0\u00a0 \u2514\u2500\u2500 show version.json\n    \u2514\u2500\u2500 text\n        \u2514\u2500\u2500 show bfd peers.log\n\n12 directories, 8 files\n
"},{"location":"cli/exec/#get-scheduled-tech-support","title":"Get Scheduled tech-support","text":"

EOS offers a feature that automatically creates a tech-support archive every hour by default. These archives are stored under /mnt/flash/schedule/tech-support.

leaf1#show schedule summary\nMaximum concurrent jobs  1\nPrepend host name to logfile: Yes\nName                 At Time       Last        Interval       Timeout        Max        Max     Logfile Location                  Status\n                                   Time         (mins)        (mins)         Log        Logs\n                                                                            Files       Size\n----------------- ------------- ----------- -------------- ------------- ----------- ---------- --------------------------------- ------\ntech-support           now         08:37          60            30           100         -      flash:schedule/tech-support/      Success\n\n\nleaf1#bash ls /mnt/flash/schedule/tech-support\nleaf1_tech-support_2023-03-09.1337.log.gz  leaf1_tech-support_2023-03-10.0837.log.gz  leaf1_tech-support_2023-03-11.0337.log.gz\n

For Network Readiness for Use (NRFU) tests and to keep a comprehensive report of the system state before going live, ANTA provides a command-line interface that efficiently retrieves these files.

"},{"location":"cli/exec/#command-overview_2","title":"Command overview","text":"
anta exec collect-tech-support --help\nUsage: anta exec collect-tech-support [OPTIONS]\n\n  Collect scheduled tech-support from EOS devices\n\nOptions:\n  -u, --username TEXT     Username to connect to EOS  [env var: ANTA_USERNAME;\n                          required]\n  -p, --password TEXT     Password to connect to EOS that must be provided. It\n                          can be prompted using '--prompt' option.  [env var:\n                          ANTA_PASSWORD]\n  --enable-password TEXT  Password to access EOS Privileged EXEC mode. It can\n                          be prompted using '--prompt' option. Requires '--\n                          enable' option.  [env var: ANTA_ENABLE_PASSWORD]\n  --enable                Some commands may require EOS Privileged EXEC mode.\n                          This option tries to access this mode before sending\n                          a command to the device.  [env var: ANTA_ENABLE]\n  -P, --prompt            Prompt for passwords if they are not provided.  [env\n                          var: ANTA_PROMPT]\n  --timeout INTEGER       Global connection timeout  [env var: ANTA_TIMEOUT;\n                          default: 30]\n  --insecure              Disable SSH Host Key validation  [env var:\n                          ANTA_INSECURE]\n  --disable-cache         Disable cache globally  [env var:\n                          ANTA_DISABLE_CACHE]\n  -i, --inventory FILE    Path to the inventory YAML file  [env var:\n                          ANTA_INVENTORY; required]\n  -t, --tags TEXT         List of tags using comma as separator:\n                          tag1,tag2,tag3  [env var: ANTA_TAGS]\n  -o, --output PATH       Path for test catalog  [default: ./tech-support]\n  --latest INTEGER        Number of scheduled show-tech to retrieve\n  --configure             Ensure devices have 'aaa authorization exec default\n                          local' configured (required for SCP on EOS). THIS\n                          WILL CHANGE THE CONFIGURATION OF YOUR NETWORK.\n  --help                  Show this message and exit.\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

When executed, this command fetches tech-support files and downloads them locally into a device-specific subfolder within the designated folder. You can specify the output folder with the --output option.

ANTA uses SCP to download files from devices and will not trust unknown SSH hosts by default. Add the SSH public keys of your devices to your known_hosts file or use the anta --insecure option to ignore SSH host keys validation.

The configuration aaa authorization exec default must be present on devices to be able to use SCP. ANTA can automatically configure aaa authorization exec default local using the anta exec collect-tech-support --configure option. If you require specific AAA configuration for aaa authorization exec default, like aaa authorization exec default none or aaa authorization exec default group tacacs+, you will need to configure it manually.

The --latest option allows retrieval of a specific number of the most recent tech-support files.

Warning

By default all the tech-support files present on the devices are retrieved.

"},{"location":"cli/exec/#example_2","title":"Example","text":"
anta --insecure exec collect-tech-support\n[15:27:19] INFO     Connecting to devices...\nINFO     Copying '/mnt/flash/schedule/tech-support/spine1_tech-support_2023-06-09.1315.log.gz' from device spine1 to 'tech-support/spine1' locally\nINFO     Copying '/mnt/flash/schedule/tech-support/leaf3_tech-support_2023-06-09.1315.log.gz' from device leaf3 to 'tech-support/leaf3' locally\nINFO     Copying '/mnt/flash/schedule/tech-support/leaf1_tech-support_2023-06-09.1315.log.gz' from device leaf1 to 'tech-support/leaf1' locally\nINFO     Copying '/mnt/flash/schedule/tech-support/leaf2_tech-support_2023-06-09.1315.log.gz' from device leaf2 to 'tech-support/leaf2' locally\nINFO     Copying '/mnt/flash/schedule/tech-support/spine2_tech-support_2023-06-09.1315.log.gz' from device spine2 to 'tech-support/spine2' locally\nINFO     Copying '/mnt/flash/schedule/tech-support/leaf4_tech-support_2023-06-09.1315.log.gz' from device leaf4 to 'tech-support/leaf4' locally\nINFO     Collected 1 scheduled tech-support from leaf2\nINFO     Collected 1 scheduled tech-support from spine2\nINFO     Collected 1 scheduled tech-support from leaf3\nINFO     Collected 1 scheduled tech-support from spine1\nINFO     Collected 1 scheduled tech-support from leaf1\nINFO     Collected 1 scheduled tech-support from leaf4\n

The output folder structure is as follows:

tree tech-support/\ntech-support/\n\u251c\u2500\u2500 leaf1\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 leaf1_tech-support_2023-06-09.1315.log.gz\n\u251c\u2500\u2500 leaf2\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 leaf2_tech-support_2023-06-09.1315.log.gz\n\u251c\u2500\u2500 leaf3\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 leaf3_tech-support_2023-06-09.1315.log.gz\n\u251c\u2500\u2500 leaf4\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 leaf4_tech-support_2023-06-09.1315.log.gz\n\u251c\u2500\u2500 spine1\n\u2502\u00a0\u00a0 \u2514\u2500\u2500 spine1_tech-support_2023-06-09.1315.log.gz\n\u2514\u2500\u2500 spine2\n    \u2514\u2500\u2500 spine2_tech-support_2023-06-09.1315.log.gz\n\n6 directories, 6 files\n

Each device has its own subdirectory containing the collected tech-support files.

"},{"location":"cli/get-inventory-information/","title":"Get Inventory Information","text":""},{"location":"cli/get-inventory-information/#retrieving-inventory-information","title":"Retrieving Inventory Information","text":"

The ANTA CLI offers multiple entrypoints to access data from your local inventory.

"},{"location":"cli/get-inventory-information/#inventory-used-of-examples","title":"Inventory used of examples","text":"

Let\u2019s consider the following inventory:

---\nanta_inventory:\n  hosts:\n    - host: 172.20.20.101\n      name: DC1-SPINE1\n      tags: [\"SPINE\", \"DC1\"]\n\n    - host: 172.20.20.102\n      name: DC1-SPINE2\n      tags: [\"SPINE\", \"DC1\"]\n\n    - host: 172.20.20.111\n      name: DC1-LEAF1A\n      tags: [\"LEAF\", \"DC1\"]\n\n    - host: 172.20.20.112\n      name: DC1-LEAF1B\n      tags: [\"LEAF\", \"DC1\"]\n\n    - host: 172.20.20.121\n      name: DC1-BL1\n      tags: [\"BL\", \"DC1\"]\n\n    - host: 172.20.20.122\n      name: DC1-BL2\n      tags: [\"BL\", \"DC1\"]\n\n    - host: 172.20.20.201\n      name: DC2-SPINE1\n      tags: [\"SPINE\", \"DC2\"]\n\n    - host: 172.20.20.202\n      name: DC2-SPINE2\n      tags: [\"SPINE\", \"DC2\"]\n\n    - host: 172.20.20.211\n      name: DC2-LEAF1A\n      tags: [\"LEAF\", \"DC2\"]\n\n    - host: 172.20.20.212\n      name: DC2-LEAF1B\n      tags: [\"LEAF\", \"DC2\"]\n\n    - host: 172.20.20.221\n      name: DC2-BL1\n      tags: [\"BL\", \"DC2\"]\n\n    - host: 172.20.20.222\n      name: DC2-BL2\n      tags: [\"BL\", \"DC2\"]\n
"},{"location":"cli/get-inventory-information/#obtaining-all-configured-tags","title":"Obtaining all configured tags","text":"

As most of ANTA\u2019s commands accommodate tag filtering, this particular command is useful for enumerating all tags configured in the inventory. Running the anta get tags command will return a list of all tags that have been configured in the inventory.

"},{"location":"cli/get-inventory-information/#command-overview","title":"Command overview","text":"
anta get tags --help\nUsage: anta get tags [OPTIONS]\n\n  Get list of configured tags in user inventory.\n\nOptions:\n  -u, --username TEXT     Username to connect to EOS  [env var: ANTA_USERNAME;\n                          required]\n  -p, --password TEXT     Password to connect to EOS that must be provided. It\n                          can be prompted using '--prompt' option.  [env var:\n                          ANTA_PASSWORD]\n  --enable-password TEXT  Password to access EOS Privileged EXEC mode. It can\n                          be prompted using '--prompt' option. Requires '--\n                          enable' option.  [env var: ANTA_ENABLE_PASSWORD]\n  --enable                Some commands may require EOS Privileged EXEC mode.\n                          This option tries to access this mode before sending\n                          a command to the device.  [env var: ANTA_ENABLE]\n  -P, --prompt            Prompt for passwords if they are not provided.  [env\n                          var: ANTA_PROMPT]\n  --timeout INTEGER       Global connection timeout  [env var: ANTA_TIMEOUT;\n                          default: 30]\n  --insecure              Disable SSH Host Key validation  [env var:\n                          ANTA_INSECURE]\n  --disable-cache         Disable cache globally  [env var:\n                          ANTA_DISABLE_CACHE]\n  -i, --inventory FILE    Path to the inventory YAML file  [env var:\n                          ANTA_INVENTORY; required]\n  -t, --tags TEXT         List of tags using comma as separator:\n                          tag1,tag2,tag3  [env var: ANTA_TAGS]\n  --help                  Show this message and exit.\n
"},{"location":"cli/get-inventory-information/#example","title":"Example","text":"

To get the list of all configured tags in the inventory, run the following command:

anta get tags\nTags found:\n[\n  \"BL\",\n  \"DC1\",\n  \"DC2\",\n  \"LEAF\",\n  \"SPINE\"\n]\n\n* note that tag all has been added by anta\n

Note

Even if you haven\u2019t explicitly configured the all tag in the inventory, it is automatically added. This default tag allows to execute commands on all devices in the inventory when no tag is specified.

"},{"location":"cli/get-inventory-information/#list-devices-in-inventory","title":"List devices in inventory","text":"

This command will list all devices available in the inventory. Using the --tags option, you can filter this list to only include devices with specific tags. The --connected option allows to display only the devices where a connection has been established.

"},{"location":"cli/get-inventory-information/#command-overview_1","title":"Command overview","text":"
anta get inventory --help\nUsage: anta get inventory [OPTIONS]\n\n  Show inventory loaded in ANTA.\n\nOptions:\n  -u, --username TEXT            Username to connect to EOS  [env var:\n                                 ANTA_USERNAME; required]\n  -p, --password TEXT            Password to connect to EOS that must be\n                                 provided. It can be prompted using '--prompt'\n                                 option.  [env var: ANTA_PASSWORD]\n  --enable-password TEXT         Password to access EOS Privileged EXEC mode.\n                                 It can be prompted using '--prompt' option.\n                                 Requires '--enable' option.  [env var:\n                                 ANTA_ENABLE_PASSWORD]\n  --enable                       Some commands may require EOS Privileged EXEC\n                                 mode. This option tries to access this mode\n                                 before sending a command to the device.  [env\n                                 var: ANTA_ENABLE]\n  -P, --prompt                   Prompt for passwords if they are not\n                                 provided.  [env var: ANTA_PROMPT]\n  --timeout INTEGER              Global connection timeout  [env var:\n                                 ANTA_TIMEOUT; default: 30]\n  --insecure                     Disable SSH Host Key validation  [env var:\n                                 ANTA_INSECURE]\n  --disable-cache                Disable cache globally  [env var:\n                                 ANTA_DISABLE_CACHE]\n  -i, --inventory FILE           Path to the inventory YAML file  [env var:\n                                 ANTA_INVENTORY; required]\n  -t, --tags TEXT                List of tags using comma as separator:\n                                 tag1,tag2,tag3  [env var: ANTA_TAGS]\n  --connected / --not-connected  Display inventory after connection has been\n                                 created\n  --help                         Show this message and exit.\n

Tip

In its default mode, anta get inventory provides only information that doesn\u2019t rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, please use the --connected option.

"},{"location":"cli/get-inventory-information/#example_1","title":"Example","text":"

To retrieve a comprehensive list of all devices along with their details, execute the following command. It will provide all the data loaded into the ANTA inventory from your inventory file.

anta get inventory --tags SPINE\nCurrent inventory content is:\n{\n    'DC1-SPINE1': AsyncEOSDevice(\n        name='DC1-SPINE1',\n        tags=['SPINE', 'DC1'],\n        hw_model=None,\n        is_online=False,\n        established=False,\n        disable_cache=False,\n        host='172.20.20.101',\n        eapi_port=443,\n        username='arista',\n        enable=True,\n        enable_password='arista',\n        insecure=False\n    ),\n    'DC1-SPINE2': AsyncEOSDevice(\n        name='DC1-SPINE2',\n        tags=['SPINE', 'DC1'],\n        hw_model=None,\n        is_online=False,\n        established=False,\n        disable_cache=False,\n        host='172.20.20.102',\n        eapi_port=443,\n        username='arista',\n        enable=True,\n        insecure=False\n    ),\n    'DC2-SPINE1': AsyncEOSDevice(\n        name='DC2-SPINE1',\n        tags=['SPINE', 'DC2'],\n        hw_model=None,\n        is_online=False,\n        established=False,\n        disable_cache=False,\n        host='172.20.20.201',\n        eapi_port=443,\n        username='arista',\n        enable=True,\n        insecure=False\n    ),\n    'DC2-SPINE2': AsyncEOSDevice(\n        name='DC2-SPINE2',\n        tags=['SPINE', 'DC2'],\n        hw_model=None,\n        is_online=False,\n        established=False,\n        disable_cache=False,\n        host='172.20.20.202',\n        eapi_port=443,\n        username='arista',\n        enable=True,\n        insecure=False\n    )\n}\n
"},{"location":"cli/inv-from-ansible/","title":"Inventory from Ansible","text":""},{"location":"cli/inv-from-ansible/#create-an-inventory-from-ansible-inventory","title":"Create an Inventory from Ansible inventory","text":"

In large setups, it might be beneficial to construct your inventory based on your Ansible inventory. The from-ansible entrypoint of the get command enables the user to create an ANTA inventory from Ansible.

"},{"location":"cli/inv-from-ansible/#command-overview","title":"Command overview","text":"
$ anta get from-ansible --help\nUsage: anta get from-ansible [OPTIONS]\n\n  Build ANTA inventory from an ansible inventory YAML file\n\nOptions:\n  -g, --ansible-group TEXT        Ansible group to filter\n  --ansible-inventory FILENAME\n                                  Path to your ansible inventory file to read\n  -o, --output FILENAME           Path to save inventory file\n  -d, --inventory-directory PATH  Directory to save inventory file\n  --help                          Show this message and exit.\n

The output is an inventory where the name of the container is added as a tag for each host:

anta_inventory:\n  hosts:\n  - host: 10.73.252.41\n    name: srv-pod01\n  - host: 10.73.252.42\n    name: srv-pod02\n  - host: 10.73.252.43\n    name: srv-pod03\n

Warning

The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritence when using the --ansible-group option.

By default, if user does not provide --output file, anta will save output to configured anta inventory (anta --inventory). If the output file has content, anta will ask user to overwrite when running in interactive console. This mechanism can be controlled by triggers in case of CI usage: --overwrite to force anta to overwrite file. If not set, anta will exit

"},{"location":"cli/inv-from-ansible/#command-output","title":"Command output","text":"

host value is coming from the ansible_host key in your inventory while name is the name you defined for your host. Below is an ansible inventory example used to generate previous inventory:

---\ntooling:\n  children:\n    endpoints:\n      hosts:\n        srv-pod01:\n          ansible_httpapi_port: 9023\n          ansible_port: 9023\n          ansible_host: 10.73.252.41\n          type: endpoint\n        srv-pod02:\n          ansible_httpapi_port: 9024\n          ansible_port: 9024\n          ansible_host: 10.73.252.42\n          type: endpoint\n        srv-pod03:\n          ansible_httpapi_port: 9025\n          ansible_port: 9025\n          ansible_host: 10.73.252.43\n          type: endpoint\n
"},{"location":"cli/inv-from-cvp/","title":"Inventory from CVP","text":""},{"location":"cli/inv-from-cvp/#create-an-inventory-from-cloudvision","title":"Create an Inventory from CloudVision","text":"

In large setups, it might be beneficial to construct your inventory based on CloudVision. The from-cvp entrypoint of the get command enables the user to create an ANTA inventory from CloudVision.

"},{"location":"cli/inv-from-cvp/#command-overview","title":"Command overview","text":"
anta get from-cvp --help\nUsage: anta get from-cvp [OPTIONS]\n\n  Build ANTA inventory from Cloudvision\n\nOptions:\n  -ip, --cvp-ip TEXT              CVP IP Address  [required]\n  -u, --cvp-username TEXT         CVP Username  [required]\n  -p, --cvp-password TEXT         CVP Password / token  [required]\n  -c, --cvp-container TEXT        Container where devices are configured\n  -d, --inventory-directory PATH  Path to save inventory file\n  --help                          Show this message and exit.\n

The output is an inventory where the name of the container is added as a tag for each host:

anta_inventory:\n  hosts:\n  - host: 192.168.0.13\n    name: leaf2\n    tags:\n    - pod1\n  - host: 192.168.0.15\n    name: leaf4\n    tags:\n    - pod2\n

Warning

The current implementation only considers devices directly attached to a specific container when using the --cvp-container option.

"},{"location":"cli/inv-from-cvp/#creating-an-inventory-from-multiple-containers","title":"Creating an inventory from multiple containers","text":"

If you need to create an inventory from multiple containers, you can use a bash command and then manually concatenate files to create a single inventory file:

$ for container in pod01 pod02 spines; do anta get from-cvp -ip <cvp-ip> -u cvpadmin -p cvpadmin -c $container -d test-inventory; done\n\n[12:25:35] INFO     Getting auth token from cvp.as73.inetsix.net for user tom\n[12:25:36] INFO     Creating inventory folder /home/tom/Projects/arista/network-test-automation/test-inventory\n           WARNING  Using the new api_token parameter. This will override usage of the cvaas_token parameter if both are provided. This is because api_token and cvaas_token parameters\n                    are for the same use case and api_token is more generic\n           INFO     Connected to CVP cvp.as73.inetsix.net\n\n\n[12:25:37] INFO     Getting auth token from cvp.as73.inetsix.net for user tom\n[12:25:38] WARNING  Using the new api_token parameter. This will override usage of the cvaas_token parameter if both are provided. This is because api_token and cvaas_token parameters\n                    are for the same use case and api_token is more generic\n           INFO     Connected to CVP cvp.as73.inetsix.net\n\n\n[12:25:38] INFO     Getting auth token from cvp.as73.inetsix.net for user tom\n[12:25:39] WARNING  Using the new api_token parameter. This will override usage of the cvaas_token parameter if both are provided. This is because api_token and cvaas_token parameters\n                    are for the same use case and api_token is more generic\n           INFO     Connected to CVP cvp.as73.inetsix.net\n\n           INFO     Inventory file has been created in /home/tom/Projects/arista/network-test-automation/test-inventory/inventory-spines.yml\n
"},{"location":"cli/nrfu/","title":"NRFU","text":""},{"location":"cli/nrfu/#execute-network-readiness-for-use-nrfu-testing","title":"Execute Network Readiness For Use (NRFU) Testing","text":"

ANTA provides a set of commands for performing NRFU tests on devices. These commands are under the anta nrfu namespace and offer multiple output format options:

  • Text view
  • Table view
  • JSON view
  • Custom template view
"},{"location":"cli/nrfu/#nrfu-command-overview","title":"NRFU Command overview","text":"
anta nrfu --help\nUsage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n  Run NRFU against inventory devices\n\nOptions:\n  -u, --username TEXT     Username to connect to EOS  [env var: ANTA_USERNAME;\n                          required]\n  -p, --password TEXT     Password to connect to EOS that must be provided. It\n                          can be prompted using '--prompt' option.  [env var:\n                          ANTA_PASSWORD]\n  --enable-password TEXT  Password to access EOS Privileged EXEC mode. It can\n                          be prompted using '--prompt' option. Requires '--\n                          enable' option.  [env var: ANTA_ENABLE_PASSWORD]\n  --enable                Some commands may require EOS Privileged EXEC mode.\n                          This option tries to access this mode before sending\n                          a command to the device.  [env var: ANTA_ENABLE]\n  -P, --prompt            Prompt for passwords if they are not provided.  [env\n                          var: ANTA_PROMPT]\n  --timeout INTEGER       Global connection timeout  [env var: ANTA_TIMEOUT;\n                          default: 30]\n  --insecure              Disable SSH Host Key validation  [env var:\n                          ANTA_INSECURE]\n  --disable-cache         Disable cache globally  [env var:\n                          ANTA_DISABLE_CACHE]\n  -i, --inventory FILE    Path to the inventory YAML file  [env var:\n                          ANTA_INVENTORY; required]\n  -t, --tags TEXT         List of tags using comma as separator:\n                          tag1,tag2,tag3  [env var: ANTA_TAGS]\n  -c, --catalog FILE      Path to the test catalog YAML file  [env var:\n                          ANTA_CATALOG; required]\n  --ignore-status         Always exit with success  [env var:\n                          ANTA_NRFU_IGNORE_STATUS]\n  --ignore-error          Only report failures and not errors  [env var:\n                          ANTA_NRFU_IGNORE_ERROR]\n  --help                  Show this message and exit.\n\nCommands:\n  json        ANTA command to check network state with JSON result\n  table       ANTA command to check network states with table result\n  text        ANTA command to check network states with text result\n  tpl-report  ANTA command to check network state with templated report\n

username, password, enable-password, enable, timeout and insecure values are the same for all devices

All commands under the anta nrfu namespace require a catalog yaml file specified with the --catalog option and a device inventory file specified with the --inventory option.

Info

Issuing the command anta nrfu will run anta nrfu table without any option.

"},{"location":"cli/nrfu/#tag-management","title":"Tag management","text":"

The --tags option can be used to target specific devices in your inventory and run only tests configured with this specific tags from your catalog. The default tag is set to all and is implicit. Expected behaviour is provided below:

Command Description none Run all tests on all devices according tag definition in your inventory and test catalog. And tests with no tag are executed on all devices --tags leaf Run all tests marked with leaf tag on all devices configured with leaf tag. All other tags are ignored --tags leaf,spine Run all tests marked with leaf tag on all devices configured with leaf tag.Run all tests marked with spine tag on all devices configured with spine tag. All other tags are ignored

Info

More examples available on this dedicated page.

"},{"location":"cli/nrfu/#performing-nrfu-with-text-rendering","title":"Performing NRFU with text rendering","text":"

The text subcommand provides a straightforward text report for each test executed on all devices in your inventory.

"},{"location":"cli/nrfu/#command-overview","title":"Command overview","text":"
anta nrfu text --help\nUsage: anta nrfu text [OPTIONS]\n\n  ANTA command to check network states with text result\n\nOptions:\n  -s, --search TEXT  Regular expression to search in both name and test\n  --skip-error       Hide tests in errors due to connectivity issue\n  --help             Show this message and exit.\n

The --search option permits filtering based on a regular expression pattern in both the hostname and the test name.

The --skip-error option can be used to exclude tests that failed due to connectivity issues or unsupported commands.

"},{"location":"cli/nrfu/#example","title":"Example","text":"

anta nrfu text --tags LEAF --search DC1-LEAF1A\n

"},{"location":"cli/nrfu/#performing-nrfu-with-table-rendering","title":"Performing NRFU with table rendering","text":"

The table command under the anta nrfu namespace offers a clear and organized table view of the test results, suitable for filtering. It also has its own set of options for better control over the output.

"},{"location":"cli/nrfu/#command-overview_1","title":"Command overview","text":"
anta nrfu table --help\nUsage: anta nrfu table [OPTIONS]\n\n  ANTA command to check network states with table result\n\nOptions:\n  -d, --device TEXT         Show a summary for this device\n  -t, --test TEXT           Show a summary for this test\n  --group-by [device|test]  Group result by test or host. default none\n  --help                    Show this message and exit.\n

The --device and --test options show a summarized view of the test results for a specific host or test case, respectively.

The --group-by option show a summarized view of the test results per host or per test.

"},{"location":"cli/nrfu/#examples","title":"Examples","text":"

anta nrfu --tags LEAF table\n

For larger setups, you can also group the results by host or test to get a summarized view:

anta nrfu table --group-by device\n

anta nrfu table --group-by test\n

To get more specific information, it is possible to filter on a single device or a single test:

anta nrfu table --device spine1\n

anta nrfu table --test VerifyZeroTouch\n

"},{"location":"cli/nrfu/#performing-nrfu-with-json-rendering","title":"Performing NRFU with JSON rendering","text":"

The JSON rendering command in NRFU testing is useful in generating a JSON output that can subsequently be passed on to another tool for reporting purposes.

"},{"location":"cli/nrfu/#command-overview_2","title":"Command overview","text":"
anta nrfu json --help\nUsage: anta nrfu json [OPTIONS]\n\n  ANTA command to check network state with JSON result\n\nOptions:\n  -o, --output FILE  Path to save report as a file  [env var:\n                     ANTA_NRFU_JSON_OUTPUT]\n  --help             Show this message and exit.\n

The --output option allows you to save the JSON report as a file.

"},{"location":"cli/nrfu/#example_1","title":"Example","text":"

anta nrfu --tags LEAF json\n

"},{"location":"cli/nrfu/#performing-nrfu-with-custom-reports","title":"Performing NRFU with custom reports","text":"

ANTA offers a CLI option for creating custom reports. This leverages the Jinja2 template system, allowing you to tailor reports to your specific needs.

"},{"location":"cli/nrfu/#command-overview_3","title":"Command overview","text":"

anta nrfu tpl-report --help\nUsage: anta nrfu tpl-report [OPTIONS]\n\n  ANTA command to check network state with templated report\n\nOptions:\n  -tpl, --template FILE  Path to the template to use for the report  [env var:\n                         ANTA_NRFU_TPL_REPORT_TEMPLATE; required]\n  -o, --output FILE      Path to save report as a file  [env var:\n                         ANTA_NRFU_TPL_REPORT_OUTPUT]\n  --help                 Show this message and exit.\n
The --template option is used to specify the Jinja2 template file for generating the custom report.

The --output option allows you to choose the path where the final report will be saved.

"},{"location":"cli/nrfu/#example_2","title":"Example","text":"

anta nrfu --tags LEAF tpl-report --template ./custom_template.j2\n

The template ./custom_template.j2 is a simple Jinja2 template:

{% for d in data %}\n* {{ d.test }} is [green]{{ d.result | upper}}[/green] for {{ d.name }}\n{% endfor %}\n

The Jinja2 template has access to all TestResult elements and their values, as described in this documentation.

You can also save the report result to a file using the --output option:

anta nrfu --tags LEAF tpl-report --template ./custom_template.j2 --output nrfu-tpl-report.txt\n

The resulting output might look like this:

cat nrfu-tpl-report.txt\n* VerifyMlagStatus is [green]SUCCESS[/green] for DC1-LEAF1A\n* VerifyMlagInterfaces is [green]SUCCESS[/green] for DC1-LEAF1A\n* VerifyMlagConfigSanity is [green]SUCCESS[/green] for DC1-LEAF1A\n* VerifyMlagReloadDelay is [green]SUCCESS[/green] for DC1-LEAF1A\n
"},{"location":"cli/overview/","title":"Overview","text":""},{"location":"cli/overview/#overview-of-antas-command-line-interface-cli","title":"Overview of ANTA\u2019s Command-Line Interface (CLI)","text":"

ANTA provides a powerful Command-Line Interface (CLI) to perform a wide range of operations. This document provides a comprehensive overview of ANTA CLI usage and its commands.

ANTA can also be used as a Python library, allowing you to build your own tools based on it. Visit this page for more details.

To start using the ANTA CLI, open your terminal and type anta.

Warning

The ANTA CLI options have changed after version 0.11 and have moved away from the top level anta and are now required at their respective commands (e.g. anta nrfu). This breaking change occurs after users feedback on making the CLI more intuitive. This change should not affect user experience when using environment variables.

"},{"location":"cli/overview/#invoking-anta-cli","title":"Invoking ANTA CLI","text":"
$ anta --help\nUsage: anta [OPTIONS] COMMAND [ARGS]...\n\n  Arista Network Test Automation (ANTA) CLI\n\nOptions:\n  --version                       Show the version and exit.\n  --log-file FILE                 Send the logs to a file. If logging level is\n                                  DEBUG, only INFO or higher will be sent to\n                                  stdout.  [env var: ANTA_LOG_FILE]\n  -l, --log-level [CRITICAL|ERROR|WARNING|INFO|DEBUG]\n                                  ANTA logging level  [env var:\n                                  ANTA_LOG_LEVEL; default: INFO]\n  --help                          Show this message and exit.\n\nCommands:\n  check  Commands to validate configuration files\n  debug  Commands to execute EOS commands on remote devices\n  exec   Commands to execute various scripts on EOS devices\n  get    Commands to get information from or generate inventories\n  nrfu   Run ANTA tests on devices\n
"},{"location":"cli/overview/#anta-environement-variables","title":"ANTA environement variables","text":"

Certain parameters are required and can be either passed to the ANTA CLI or set as an environment variable (ENV VAR).

To pass the parameters via the CLI:

anta nrfu -u admin -p arista123 -i inventory.yaml -c tests.yaml\n

To set them as environment variables:

export ANTA_USERNAME=admin\nexport ANTA_PASSWORD=arista123\nexport ANTA_INVENTORY=inventory.yml\nexport ANTA_INVENTORY=tests.yml\n

Then, run the CLI without options:

anta nrfu\n

Note

All environement variables may not be needed for every commands. Refer to <command> --help for the comprehensive environment varibles names.

Below are the environement variables usable with the anta nrfu command:

Variable Name Purpose Required ANTA_USERNAME The username to use in the inventory to connect to devices. Yes ANTA_PASSWORD The password to use in the inventory to connect to devices. Yes ANTA_INVENTORY The path to the inventory file. Yes ANTA_CATALOG The path to the catalog file. Yes ANTA_PROMPT The value to pass to the prompt for password is password is not provided No ANTA_INSECURE Whether or not using insecure mode when connecting to the EOS devices HTTP API. No ANTA_DISABLE_CACHE A variable to disable caching for all ANTA tests (enabled by default). No ANTA_ENABLE Whether it is necessary to go to enable mode on devices. No ANTA_ENABLE_PASSWORD The optional enable password, when this variable is set, ANTA_ENABLE or --enable is required. No

Info

Caching can be disabled with the global parameter --disable-cache. For more details about how caching is implemented in ANTA, please refer to Caching in ANTA.

"},{"location":"cli/overview/#anta-exit-codes","title":"ANTA Exit Codes","text":"

ANTA CLI utilizes the following exit codes:

  • Exit code 0 - All tests passed successfully.
  • Exit code 1 - An internal error occurred while executing ANTA.
  • Exit code 2 - A usage error was raised.
  • Exit code 3 - Tests were run, but at least one test returned an error.
  • Exit code 4 - Tests were run, but at least one test returned a failure.

To ignore the test status, use anta nrfu --ignore-status, and the exit code will always be 0.

To ignore errors, use anta nrfu --ignore-error, and the exit code will be 0 if all tests succeeded or 1 if any test failed.

"},{"location":"cli/overview/#shell-completion","title":"Shell Completion","text":"

You can enable shell completion for the ANTA CLI:

ZSHBASH

If you use ZSH shell, add the following line in your ~/.zshrc:

eval \"$(_ANTA_COMPLETE=zsh_source anta)\" > /dev/null\n

With bash, add the following line in your ~/.bashrc:

eval \"$(_ANTA_COMPLETE=bash_source anta)\" > /dev/null\n
"},{"location":"cli/tag-management/","title":"Tag Management","text":""},{"location":"cli/tag-management/#tag-management","title":"Tag management","text":""},{"location":"cli/tag-management/#overview","title":"Overview","text":"

Some of the ANTA commands like anta nrfu command come with a --tags option.

For nrfu, this allows users to specify a set of tests, marked with a given tag, to be run on devices marked with the same tag. For instance, you can run tests dedicated to leaf devices on your leaf devices only and not on other devices.

Tags are string defined by the user and can be anything considered as a string by Python. A default one is present for all tests and devices.

The next table provides a short summary of the scope of tags using CLI

Command Description none Run all tests on all devices according tag definition in your inventory and test catalog. And tests with no tag are executed on all devices --tags leaf Run all tests marked with leaf tag on all devices configured with leaf tag. All other tags are ignored --tags leaf,spine Run all tests marked with leaf tag on all devices configured with leaf tag.Run all tests marked with spine tag on all devices configured with spine tag. All other tags are ignored"},{"location":"cli/tag-management/#inventory-and-catalog-for-tests","title":"Inventory and Catalog for tests","text":"

All commands in this page are based on the following inventory and test catalog.

InventoryTest Catalog
---\nanta_inventory:\n  hosts:\n  - host: 192.168.0.10\n    name: spine01\n    tags: ['fabric', 'spine']\n  - host: 192.168.0.11\n    name: spine02\n    tags: ['fabric', 'spine']\n  - host: 192.168.0.12\n    name: leaf01\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.13\n    name: leaf02\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.14\n    name: leaf03\n    tags: ['fabric', 'leaf']\n  - host: 192.168.0.15\n    name: leaf04\n    tags: ['fabric', 'leaf'\n
anta.tests.system:\n  - VerifyUptime:\n      minimum: 10\n      filters:\n        tags: ['fabric']\n  - VerifyReloadCause:\n      tags: ['leaf', spine']\n  - VerifyCoredump:\n  - VerifyAgentLogs:\n  - VerifyCPUUtilization:\n      filters:\n        tags: ['spine', 'leaf']\n  - VerifyMemoryUtilization:\n  - VerifyFileSystemUtilization:\n  - VerifyNTP:\n\nanta.tests.mlag:\n  - VerifyMlagStatus:\n\n\nanta.tests.interfaces:\n  - VerifyL3MTU:\n      mtu: 1500\n      filters:\n        tags: ['demo']\n
"},{"location":"cli/tag-management/#default-tags","title":"Default tags","text":"

By default, ANTA uses a default tag for both devices and tests. This default tag is all and it can be explicit if you want to make it visible in your inventory and also implicit since the framework injects this tag if it is not defined.

So this command will run all tests from your catalog on all devices. With a mapping for tags defined in your inventory and catalog. If no tags configured, then tests are executed against all devices.

$ anta nrfu -c .personal/catalog-class.yml table --group-by device\n\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 Running ANTA tests:                                  \u2502\n\u2502 - ANTA Inventory contains 6 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 10 tests                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\n\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\n\u2503 Device  \u2503 # of success \u2503 # of skipped \u2503 # of failure \u2503 # of errors \u2503 List of failed or error test cases \u2503\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\n\u2502 spine01 \u2502 5            \u2502 1            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2502 spine02 \u2502 5            \u2502 1            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2502 leaf01  \u2502 6            \u2502 0            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2502 leaf02  \u2502 6            \u2502 0            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2502 leaf03  \u2502 6            \u2502 0            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2502 leaf04  \u2502 6            \u2502 0            \u2502 1            \u2502 0           \u2502 ['VerifyCPUUtilization']           \u2502\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n
"},{"location":"cli/tag-management/#use-a-single-tag-in-cli","title":"Use a single tag in CLI","text":"

The most used approach is to use a single tag in your CLI to filter tests & devices configured with this one.

In such scenario, ANTA will run tests marked with $tag only on devices marked with $tag. All other tests and devices will be ignored

$ anta nrfu -c .personal/catalog-class.yml --tags leaf text\n\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 Settings \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\n\u2502 Running ANTA tests:                                  \u2502\n\u2502 - ANTA Inventory contains 6 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 10 tests                    \u2502\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\n\nleaf01 :: VerifyUptime :: SUCCESS\nleaf01 :: VerifyReloadCause :: SUCCESS\nleaf01 :: VerifyCPUUtilization :: SUCCESS\nleaf02 :: VerifyUptime :: SUCCESS\nleaf02 :: VerifyReloadCause :: SUCCESS\nleaf02 :: VerifyCPUUtilization :: SUCCESS\nleaf03 :: VerifyUptime :: SUCCESS\nleaf03 :: VerifyReloadCause :: SUCCESS\nleaf03 :: VerifyCPUUtilization :: SUCCESS\nleaf04 :: VerifyUptime :: SUCCESS\nleaf04 :: VerifyReloadCause :: SUCCESS\nleaf04 :: VerifyCPUUtilization :: SUCCESS\n

In this case, only leaf devices defined in your inventory are used to run tests marked with leaf in your test catalog

"},{"location":"cli/tag-management/#use-multiple-tags-in-cli","title":"Use multiple tags in CLI","text":"

A more advanced usage of the tag feature is to list multiple tags in your CLI using --tags $tag1,$tag2 syntax.

In such scenario, all devices marked with $tag1 will be selected and ANTA will run tests with $tag1, then devices with $tag2 will be selected and will be tested with tests marked with $tag2

anta nrfu -c .personal/catalog-class.yml --tags leaf,fabric text\n\nspine01 :: VerifyUptime :: SUCCESS\nspine02 :: VerifyUptime :: SUCCESS\nleaf01 :: VerifyUptime :: SUCCESS\nleaf01 :: VerifyReloadCause :: SUCCESS\nleaf01 :: VerifyCPUUtilization :: SUCCESS\nleaf02 :: VerifyUptime :: SUCCESS\nleaf02 :: VerifyReloadCause :: SUCCESS\nleaf02 :: VerifyCPUUtilization :: SUCCESS\nleaf03 :: VerifyUptime :: SUCCESS\nleaf03 :: VerifyReloadCause :: SUCCESS\nleaf03 :: VerifyCPUUtilization :: SUCCESS\nleaf04 :: VerifyUptime :: SUCCESS\nleaf04 :: VerifyReloadCause :: SUCCESS\nleaf04 :: VerifyCPUUtilization :: SUCCESS\n
"}]} \ No newline at end of file diff --git a/main/sitemap.xml.gz b/main/sitemap.xml.gz index cdc83a233b4227875865ed6d7198bd185ab77cdc..49272b1cc184420dc1970dd92db6eb51de101acc 100644 GIT binary patch delta 14 Vcmb=g=aBE_;K*}eOq1x^3} diff --git a/main/usage-inventory-catalog/index.html b/main/usage-inventory-catalog/index.html index 5dede33ba..ce974f312 100644 --- a/main/usage-inventory-catalog/index.html +++ b/main/usage-inventory-catalog/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - +