diff --git a/main/404.html b/main/404.html index 7b8d97be7..722141588 100644 --- a/main/404.html +++ b/main/404.html @@ -16,7 +16,7 @@ - + @@ -24,7 +24,7 @@ - + @@ -115,7 +115,7 @@ - + @@ -212,7 +212,7 @@ - + ANTA on Github @@ -248,7 +248,7 @@ - + Arista Network Test Automation - ANTA @@ -258,7 +258,7 @@ - + ANTA on Github @@ -2159,7 +2159,7 @@ 404 - Not found - + @@ -2170,7 +2170,7 @@ 404 - Not found - + @@ -2181,7 +2181,7 @@ 404 - Not found - + diff --git a/main/advanced_usages/as-python-lib/index.html b/main/advanced_usages/as-python-lib/index.html index 59e1b9271..ba73d6f44 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2583,7 +2583,7 @@ Run EOS commands - + @@ -2594,7 +2594,7 @@ Run EOS commands - + @@ -2605,7 +2605,7 @@ Run EOS commands - + diff --git a/main/advanced_usages/caching/index.html b/main/advanced_usages/caching/index.html index 8615506f3..a35e12263 100644 --- a/main/advanced_usages/caching/index.html +++ b/main/advanced_usages/caching/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2472,7 +2472,7 @@ Disable caching in a chi - + @@ -2483,7 +2483,7 @@ Disable caching in a chi - + @@ -2494,7 +2494,7 @@ Disable caching in a chi - + diff --git a/main/advanced_usages/custom-tests/index.html b/main/advanced_usages/custom-tests/index.html index 36ca9a5bd..e8f70beb9 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3259,7 +3259,7 @@ Access your custom tests i - + @@ -3270,7 +3270,7 @@ Access your custom tests i - + @@ -3281,7 +3281,7 @@ Access your custom tests i - + diff --git a/main/api/catalog/index.html b/main/api/catalog/index.html index 20dba8ac5..1e7be8784 100644 --- a/main/api/catalog/index.html +++ b/main/api/catalog/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4837,7 +4837,7 @@ - + @@ -4848,7 +4848,7 @@ - + @@ -4859,7 +4859,7 @@ - + diff --git a/main/api/csv_reporter/index.html b/main/api/csv_reporter/index.html index 5e6d4be23..905b5aed7 100644 --- a/main/api/csv_reporter/index.html +++ b/main/api/csv_reporter/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2990,7 +2990,7 @@ - + @@ -3001,7 +3001,7 @@ - + @@ -3012,7 +3012,7 @@ - + diff --git a/main/api/device/index.html b/main/api/device/index.html index 8e72c634e..e8fe8422e 100644 --- a/main/api/device/index.html +++ b/main/api/device/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4526,7 +4526,7 @@ - + @@ -4537,7 +4537,7 @@ - + @@ -4548,7 +4548,7 @@ - + diff --git a/main/api/inventory.models.input/index.html b/main/api/inventory.models.input/index.html index c1739579e..046202c82 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2835,7 +2835,7 @@ - + @@ -2846,7 +2846,7 @@ - + @@ -2857,7 +2857,7 @@ - + diff --git a/main/api/inventory/index.html b/main/api/inventory/index.html index b1e451993..e9ebce694 100644 --- a/main/api/inventory/index.html +++ b/main/api/inventory/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3454,7 +3454,7 @@ - + @@ -3465,7 +3465,7 @@ - + @@ -3476,7 +3476,7 @@ - + diff --git a/main/api/md_reporter/index.html b/main/api/md_reporter/index.html index affdaeb64..d42be9697 100644 --- a/main/api/md_reporter/index.html +++ b/main/api/md_reporter/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4749,7 +4749,7 @@ - + @@ -4760,7 +4760,7 @@ - + @@ -4771,7 +4771,7 @@ - + diff --git a/main/api/models/index.html b/main/api/models/index.html index c3257af53..70a952c86 100644 --- a/main/api/models/index.html +++ b/main/api/models/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4845,7 +4845,7 @@ - + @@ -4856,7 +4856,7 @@ - + @@ -4867,7 +4867,7 @@ - + diff --git a/main/api/reporters/index.html b/main/api/reporters/index.html index aa9f88b5a..8103ff609 100644 --- a/main/api/reporters/index.html +++ b/main/api/reporters/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3441,7 +3441,7 @@ - + @@ -3452,7 +3452,7 @@ - + @@ -3463,7 +3463,7 @@ - + diff --git a/main/api/result_manager/index.html b/main/api/result_manager/index.html index 7668aef2d..9c39498a3 100644 --- a/main/api/result_manager/index.html +++ b/main/api/result_manager/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3857,7 +3857,7 @@ - + @@ -3868,7 +3868,7 @@ - + @@ -3879,7 +3879,7 @@ - + diff --git a/main/api/result_manager_models/index.html b/main/api/result_manager_models/index.html index 28a36e179..8c1a01836 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2892,7 +2892,7 @@ - + @@ -2903,7 +2903,7 @@ - + @@ -2914,7 +2914,7 @@ - + diff --git a/main/api/runner/index.html b/main/api/runner/index.html index fb64a6c29..2a2c4c63d 100644 --- a/main/api/runner/index.html +++ b/main/api/runner/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3670,7 +3670,7 @@ - + @@ -3681,7 +3681,7 @@ - + @@ -3692,7 +3692,7 @@ - + diff --git a/main/api/test.cvx/index.html b/main/api/test.cvx/index.html index ee2f3bffe..7cfff040b 100644 --- a/main/api/test.cvx/index.html +++ b/main/api/test.cvx/index.html @@ -16,7 +16,7 @@ - + @@ -24,7 +24,7 @@ - + @@ -129,7 +129,7 @@ - + @@ -226,7 +226,7 @@ - + ANTA on Github @@ -262,7 +262,7 @@ - + Arista Network Test Automation - ANTA @@ -272,7 +272,7 @@ - + ANTA on Github @@ -2598,7 +2598,7 @@ - + @@ -2609,7 +2609,7 @@ - + @@ -2620,7 +2620,7 @@ - + diff --git a/main/api/tests.aaa/index.html b/main/api/tests.aaa/index.html index 0d99e457d..0368140c0 100644 --- a/main/api/tests.aaa/index.html +++ b/main/api/tests.aaa/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4223,7 +4223,7 @@ Inputs - + @@ -4234,7 +4234,7 @@ Inputs - + @@ -4245,7 +4245,7 @@ Inputs - + diff --git a/main/api/tests.avt/index.html b/main/api/tests.avt/index.html index 9e360a87d..872791a68 100644 --- a/main/api/tests.avt/index.html +++ b/main/api/tests.avt/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3244,7 +3244,7 @@ AVTPaths - + @@ -3255,7 +3255,7 @@ AVTPaths - + @@ -3266,7 +3266,7 @@ AVTPaths - + diff --git a/main/api/tests.bfd/index.html b/main/api/tests.bfd/index.html index e79418acf..83ebe8b71 100644 --- a/main/api/tests.bfd/index.html +++ b/main/api/tests.bfd/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3915,7 +3915,7 @@ BFDPeer - + @@ -3926,7 +3926,7 @@ BFDPeer - + @@ -3937,7 +3937,7 @@ BFDPeer - + diff --git a/main/api/tests.configuration/index.html b/main/api/tests.configuration/index.html index e7bcd2cc1..0de0bddd1 100644 --- a/main/api/tests.configuration/index.html +++ b/main/api/tests.configuration/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2846,7 +2846,7 @@ - + @@ -2857,7 +2857,7 @@ - + @@ -2868,7 +2868,7 @@ - + diff --git a/main/api/tests.connectivity/index.html b/main/api/tests.connectivity/index.html index bdd2eed45..e542f2126 100644 --- a/main/api/tests.connectivity/index.html +++ b/main/api/tests.connectivity/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3580,7 +3580,7 @@ - + @@ -3591,7 +3591,7 @@ - + @@ -3602,7 +3602,7 @@ - + diff --git a/main/api/tests.field_notices/index.html b/main/api/tests.field_notices/index.html index 3d7d3489b..035a99ef3 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2426,11 +2426,7 @@ 117 118 119 -120 -121 -122 -123 -124class VerifyFieldNotice44Resolution(AntaTest): +120class VerifyFieldNotice44Resolution(AntaTest): """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. @@ -2525,15 +2521,11 @@ self.result.is_success() incorrect_aboot_version = ( - aboot_version.startswith("4.0.") - and int(aboot_version.split(".")[2]) < 7 - or aboot_version.startswith("4.1.") - and int(aboot_version.split(".")[2]) < 1 + (aboot_version.startswith("4.0.") and int(aboot_version.split(".")[2]) < 7) + or (aboot_version.startswith("4.1.") and int(aboot_version.split(".")[2]) < 1) or ( - aboot_version.startswith("6.0.") - and int(aboot_version.split(".")[2]) < 9 - or aboot_version.startswith("6.1.") - and int(aboot_version.split(".")[2]) < 7 + (aboot_version.startswith("6.0.") and int(aboot_version.split(".")[2]) < 9) + or (aboot_version.startswith("6.1.") and int(aboot_version.split(".")[2]) < 7) ) ) if incorrect_aboot_version: @@ -2600,7 +2592,11 @@ Source code in anta/tests/field_notices.py - 127 + 123 +124 +125 +126 +127 128 129 130 @@ -2666,11 +2662,7 @@ 190 191 192 -193 -194 -195 -196 -197class VerifyFieldNotice72Resolution(AntaTest): +193class VerifyFieldNotice72Resolution(AntaTest): """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 @@ -2834,7 +2826,7 @@ - + @@ -2845,7 +2837,7 @@ - + @@ -2856,7 +2848,7 @@ - + diff --git a/main/api/tests.flow_tracking/index.html b/main/api/tests.flow_tracking/index.html index 0a196333b..97726c936 100644 --- a/main/api/tests.flow_tracking/index.html +++ b/main/api/tests.flow_tracking/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3322,7 +3322,7 @@ - + @@ -3333,7 +3333,7 @@ - + @@ -3344,7 +3344,7 @@ - + diff --git a/main/api/tests.greent/index.html b/main/api/tests.greent/index.html index b1ea4a557..6b86338ef 100644 --- a/main/api/tests.greent/index.html +++ b/main/api/tests.greent/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2593,7 +2593,7 @@ - + @@ -2604,7 +2604,7 @@ - + @@ -2615,7 +2615,7 @@ - + diff --git a/main/api/tests.hardware/index.html b/main/api/tests.hardware/index.html index 3762debb9..1ce9ab109 100644 --- a/main/api/tests.hardware/index.html +++ b/main/api/tests.hardware/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3611,7 +3611,7 @@ - + @@ -3622,7 +3622,7 @@ - + @@ -3633,7 +3633,7 @@ - + diff --git a/main/api/tests.interfaces/index.html b/main/api/tests.interfaces/index.html index 2d767ea9f..d2162dfa9 100644 --- a/main/api/tests.interfaces/index.html +++ b/main/api/tests.interfaces/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -6967,7 +6967,7 @@ - + @@ -6978,7 +6978,7 @@ - + @@ -6989,7 +6989,7 @@ - + diff --git a/main/api/tests.lanz/index.html b/main/api/tests.lanz/index.html index 63ed4a717..bd63b81b9 100644 --- a/main/api/tests.lanz/index.html +++ b/main/api/tests.lanz/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2449,7 +2449,7 @@ - + @@ -2460,7 +2460,7 @@ - + @@ -2471,7 +2471,7 @@ - + diff --git a/main/api/tests.logging/index.html b/main/api/tests.logging/index.html index 307bf9315..5efda247a 100644 --- a/main/api/tests.logging/index.html +++ b/main/api/tests.logging/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4005,7 +4005,7 @@ - + @@ -4016,7 +4016,7 @@ - + @@ -4027,7 +4027,7 @@ - + diff --git a/main/api/tests.mlag/index.html b/main/api/tests.mlag/index.html index d8987f555..f2a748e95 100644 --- a/main/api/tests.mlag/index.html +++ b/main/api/tests.mlag/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3607,7 +3607,7 @@ - + @@ -3618,7 +3618,7 @@ - + @@ -3629,7 +3629,7 @@ - + diff --git a/main/api/tests.multicast/index.html b/main/api/tests.multicast/index.html index 84d512a14..ec75770d8 100644 --- a/main/api/tests.multicast/index.html +++ b/main/api/tests.multicast/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2767,7 +2767,7 @@ Inputs - + @@ -2778,7 +2778,7 @@ Inputs - + @@ -2789,7 +2789,7 @@ Inputs - + diff --git a/main/api/tests.path_selection/index.html b/main/api/tests.path_selection/index.html index 10781ddf6..1e996847e 100644 --- a/main/api/tests.path_selection/index.html +++ b/main/api/tests.path_selection/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2944,7 +2944,7 @@ RouterPath - + @@ -2955,7 +2955,7 @@ RouterPath - + @@ -2966,7 +2966,7 @@ RouterPath - + diff --git a/main/api/tests.profiles/index.html b/main/api/tests.profiles/index.html index ad7134d4f..d2e832279 100644 --- a/main/api/tests.profiles/index.html +++ b/main/api/tests.profiles/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2759,7 +2759,7 @@ Inputs< - + @@ -2770,7 +2770,7 @@ Inputs< - + @@ -2781,7 +2781,7 @@ Inputs< - + diff --git a/main/api/tests.ptp/index.html b/main/api/tests.ptp/index.html index 85ef5a5ff..73c2d28af 100644 --- a/main/api/tests.ptp/index.html +++ b/main/api/tests.ptp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3210,7 +3210,7 @@ - + @@ -3221,7 +3221,7 @@ - + @@ -3232,7 +3232,7 @@ - + diff --git a/main/api/tests.routing.bgp/index.html b/main/api/tests.routing.bgp/index.html index dca68a1fc..64de1e0b6 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -9227,7 +9227,7 @@ - + @@ -9238,7 +9238,7 @@ - + @@ -9249,7 +9249,7 @@ - + diff --git a/main/api/tests.routing.generic/index.html b/main/api/tests.routing.generic/index.html index a97dfb4fb..d80640fbf 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3190,7 +3190,7 @@ Inputs - + @@ -3201,7 +3201,7 @@ Inputs - + @@ -3212,7 +3212,7 @@ Inputs - + diff --git a/main/api/tests.routing.isis/index.html b/main/api/tests.routing.isis/index.html index 817e5f85b..1e2e5cf07 100644 --- a/main/api/tests.routing.isis/index.html +++ b/main/api/tests.routing.isis/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -5077,7 +5077,7 @@ Vias - + @@ -5088,7 +5088,7 @@ Vias - + @@ -5099,7 +5099,7 @@ Vias - + diff --git a/main/api/tests.routing.ospf/index.html b/main/api/tests.routing.ospf/index.html index e29e71dc0..db70676e7 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2858,7 +2858,7 @@ - + @@ -2869,7 +2869,7 @@ - + @@ -2880,7 +2880,7 @@ - + diff --git a/main/api/tests.security/index.html b/main/api/tests.security/index.html index a51964fbb..747288d15 100644 --- a/main/api/tests.security/index.html +++ b/main/api/tests.security/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -6422,7 +6422,7 @@ - + @@ -6433,7 +6433,7 @@ - + @@ -6444,7 +6444,7 @@ - + diff --git a/main/api/tests.services/index.html b/main/api/tests.services/index.html index 7b20d6acd..cc27eb5bb 100644 --- a/main/api/tests.services/index.html +++ b/main/api/tests.services/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3749,7 +3749,7 @@ - + @@ -3760,7 +3760,7 @@ - + @@ -3771,7 +3771,7 @@ - + diff --git a/main/api/tests.snmp/index.html b/main/api/tests.snmp/index.html index 3e6e32650..dc4d855b3 100644 --- a/main/api/tests.snmp/index.html +++ b/main/api/tests.snmp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4048,7 +4048,7 @@ Inputs - + @@ -4059,7 +4059,7 @@ Inputs - + @@ -4070,7 +4070,7 @@ Inputs - + diff --git a/main/api/tests.software/index.html b/main/api/tests.software/index.html index 549116e1b..8f7935376 100644 --- a/main/api/tests.software/index.html +++ b/main/api/tests.software/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2935,7 +2935,7 @@ Inputs - + @@ -2946,7 +2946,7 @@ Inputs - + @@ -2957,7 +2957,7 @@ Inputs - + diff --git a/main/api/tests.stp/index.html b/main/api/tests.stp/index.html index e5d902b6f..1024cd4a6 100644 --- a/main/api/tests.stp/index.html +++ b/main/api/tests.stp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3696,7 +3696,7 @@ Inputs - + @@ -3707,7 +3707,7 @@ Inputs - + @@ -3718,7 +3718,7 @@ Inputs - + diff --git a/main/api/tests.stun/index.html b/main/api/tests.stun/index.html index 747f2d3a3..2ea88167b 100644 --- a/main/api/tests.stun/index.html +++ b/main/api/tests.stun/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2906,7 +2906,7 @@ - + @@ -2917,7 +2917,7 @@ - + @@ -2928,7 +2928,7 @@ - + diff --git a/main/api/tests.system/index.html b/main/api/tests.system/index.html index 406a5497b..6bfaa38b6 100644 --- a/main/api/tests.system/index.html +++ b/main/api/tests.system/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4154,7 +4154,7 @@ - + @@ -4165,7 +4165,7 @@ - + @@ -4176,7 +4176,7 @@ - + diff --git a/main/api/tests.vlan/index.html b/main/api/tests.vlan/index.html index 00feb26d7..0fd71d0dd 100644 --- a/main/api/tests.vlan/index.html +++ b/main/api/tests.vlan/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2594,7 +2594,7 @@ Inputs - + @@ -2605,7 +2605,7 @@ Inputs - + @@ -2616,7 +2616,7 @@ Inputs - + diff --git a/main/api/tests.vxlan/index.html b/main/api/tests.vxlan/index.html index b55df5d03..eccd46cb2 100644 --- a/main/api/tests.vxlan/index.html +++ b/main/api/tests.vxlan/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3435,7 +3435,7 @@ Inputs - + @@ -3446,7 +3446,7 @@ Inputs - + @@ -3457,7 +3457,7 @@ Inputs - + diff --git a/main/api/tests/index.html b/main/api/tests/index.html index 837937a96..486addd84 100644 --- a/main/api/tests/index.html +++ b/main/api/tests/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2356,7 +2356,7 @@ Using the Tests - + @@ -2367,7 +2367,7 @@ Using the Tests - + @@ -2378,7 +2378,7 @@ Using the Tests - + diff --git a/main/api/types/index.html b/main/api/types/index.html index 3fa39ceb0..ec45d8303 100644 --- a/main/api/types/index.html +++ b/main/api/types/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4867,7 +4867,7 @@ - + @@ -4878,7 +4878,7 @@ - + @@ -4889,7 +4889,7 @@ - + diff --git a/main/assets/stylesheets/main.0253249f.min.css b/main/assets/stylesheets/main.0253249f.min.css deleted file mode 100644 index 9a7a6982d..000000000 --- a/main/assets/stylesheets/main.0253249f.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}.md-typeset ul[type]{list-style-type:revert-layer}[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}.md-typeset ol ol ol ol,.md-typeset ul ol ol ol{list-style-type:upper-alpha}.md-typeset ol ol ol ol ol,.md-typeset ul ol ol ol ol{list-style-type:upper-roman}.md-typeset ol[type],.md-typeset ul[type]{list-style-type:revert-layer}[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:-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{border-radius:100%;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)}.md-typeset .md-code__content{display:grid}@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)}@media print{.md-feedback{display:none}}.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;position:relative}.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{-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-style:normal;font-weight:400;outline:none;text-align:initial;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}:root{--md-tooltip-width:20rem;--md-tooltip-tail:0.3rem}.md-tooltip2{-webkit-backface-visibility:hidden;backface-visibility:hidden;color:var(--md-default-fg-color);font-family:var(--md-text-font-family);opacity:0;pointer-events:none;position:absolute;top:calc(var(--md-tooltip-host-y) + var(--md-tooltip-y));transform:translateY(-.4rem);transform-origin:calc(var(--md-tooltip-host-x) + var(--md-tooltip-x)) 0;transition:transform 0ms .25s,opacity .25s,z-index .25s;width:100%;z-index:0}.md-tooltip2:before{border-left:var(--md-tooltip-tail) solid #0000;border-right:var(--md-tooltip-tail) solid #0000;content:"";display:block;left:clamp(1.5 * .8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-tail),100vw - 2 * var(--md-tooltip-tail) - 1.5 * .8rem);position:absolute;z-index:1}.md-tooltip2--top:before{border-top:var(--md-tooltip-tail) solid var(--md-default-bg-color);bottom:calc(var(--md-tooltip-tail)*-1 + .025rem);filter:drop-shadow(0 1px 0 hsla(0,0%,0%,.05))}.md-tooltip2--bottom:before{border-bottom:var(--md-tooltip-tail) solid var(--md-default-bg-color);filter:drop-shadow(0 -1px 0 hsla(0,0%,0%,.05));top:calc(var(--md-tooltip-tail)*-1 + .025rem)}.md-tooltip2--active{opacity:1;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.5,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip2__inner{scrollbar-gutter:stable;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);max-height:40vh;max-width:calc(100vw - 1.6rem);position:relative;scrollbar-width:thin}.md-tooltip2__inner::-webkit-scrollbar{height:.2rem;width:.2rem}.md-tooltip2__inner::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-tooltip2__inner::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}[role=tooltip]>.md-tooltip2__inner{font-size:.5rem;font-weight:700;left:clamp(.8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-width)/2,100vw - var(--md-tooltip-width) - .8rem);max-width:min(100vw - 2 * .8rem,400px);padding:.2rem .4rem;-webkit-user-select:none;user-select:none;width:-moz-fit-content;width:fit-content}.md-tooltip2__inner.md-typeset>:first-child{margin-top:0}.md-tooltip2__inner.md-typeset>:last-child{margin-bottom:0}[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__alias{margin-left:.3rem;opacity:.7}.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: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]{display:initial}.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)}@media print{.giscus,[id=__comments]{display:none}}: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.0253249f.min.css.map b/main/assets/stylesheets/main.0253249f.min.css.map deleted file mode 100644 index 748194709..000000000 --- a/main/assets/stylesheets/main.0253249f.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/_code.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/_tooltip2.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/_giscus.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,gBCyyCF,CCvzCA,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,wNAAA,CACA,gNAAA,CACA,iNJgIF,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,qBAWE,kCAAA,CAAA,0BAAA,CADA,eAAA,CATA,aAAA,CAEA,QAAA,CAMA,uCAAA,CALA,aAAA,CAFA,oCAAA,CAKA,yDAAA,CACA,oBAAA,CAFA,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,CIxDI,qBACE,4BJ0DN,CIrDE,kDAGE,kBJuDJ,CI1DE,kDAGE,mBJuDJ,CI1DE,8BAEE,SJwDJ,CIpDI,0DACE,iBJuDN,CInDI,oCACE,2BJsDN,CInDM,0CACE,2BJsDR,CInDQ,gDACE,2BJsDV,CInDU,sDACE,2BJsDZ,CI9CI,0CACE,4BJiDN,CI7CI,wDACE,kBJiDN,CIlDI,wDACE,mBJiDN,CIlDI,oCAEE,kBJgDN,CI7CM,kGAEE,aJiDR,CI7CM,0DACE,eJgDR,CI5CM,4HAEE,kBJ+CR,CIjDM,4HAEE,mBJ+CR,CIjDM,oFACE,kBAAA,CAAA,eJgDR,CIzCE,yBAEE,mBJ2CJ,CI7CE,yBAEE,oBJ2CJ,CI7CE,eACE,mBAAA,CAAA,cJ4CJ,CIvCE,kDAIE,WAAA,CADA,cJ0CJ,CIlCI,4BAEE,oBJoCN,CIhCI,6BAEE,oBJkCN,CI9BI,kCACE,YJgCN,CI3BE,mBACE,iBAAA,CAGA,eAAA,CADA,cAAA,CAEA,iBAAA,CAHA,sBAAA,CAAA,iBJgCJ,CI1BI,uBACE,aAAA,CACA,aJ4BN,CIvBE,uBAGE,iBAAA,CADA,eAAA,CADA,eJ2BJ,CIrBE,mBACE,cJuBJ,CInBE,+BAME,2CAAA,CACA,iDAAA,CACA,mBAAA,CAPA,oBAAA,CAGA,gBAAA,CAFA,cAAA,CACA,aAAA,CAEA,iBJwBJ,CIlBI,aAXF,+BAYI,aJqBJ,CACF,CIhBI,iCACE,gBJkBN,CIXM,8FACE,YJaR,CITM,4FACE,eJWR,CINI,8FACE,eJQN,CILM,kHACE,gBJOR,CIFI,kCAGE,eAAA,CAFA,cAAA,CACA,sBAAA,CAEA,kBJIN,CIAI,kCAGE,qDAAA,CAFA,sBAAA,CACA,kBJGN,CIEI,wCACE,iCJAN,CIGM,8CACE,qDAAA,CACA,sDJDR,CIMI,iCACE,iBJJN,CISE,wCACE,cJPJ,CIUI,wDAIE,gBJFN,CIFI,wDAIE,iBJFN,CIFI,8CAME,UAAA,CALA,oBAAA,CAEA,YAAA,CAIA,oDAAA,CAAA,4CAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CALA,0BAAA,CAHA,WJAN,CIYI,oDACE,oDJVN,CIcI,mEACE,kDAAA,CACA,yDAAA,CAAA,iDJZN,CIgBI,oEACE,kDAAA,CACA,0DAAA,CAAA,kDJdN,CImBE,wBACE,iBAAA,CACA,eAAA,CACA,iBJjBJ,CIqBE,mBACE,oBAAA,CAEA,kBAAA,CADA,eJlBJ,CIsBI,aANF,mBAOI,aJnBJ,CACF,CIsBI,8BACE,aAAA,CAEA,QAAA,CACA,eAAA,CAFA,UJlBN,CKlWI,0CDmYF,uBACE,iBJ7BF,CIgCE,4BACE,eJ9BJ,CACF,CMjiBE,uBAOE,kBAAA,CALA,aAAA,CACA,aAAA,CAEA,aAAA,CACA,eAAA,CALA,iBAAA,CAOA,sCACE,CALF,YNuiBJ,CM9hBI,2BACE,aNgiBN,CM5hBI,6BAME,+CAAA,CAFA,yCAAA,CAHA,eAAA,CACA,eAAA,CACA,kBAAA,CAEA,iBN+hBN,CM1hBI,6BAEE,aAAA,CADA,YN6hBN,CMvhBE,wBACE,kBNyhBJ,CMthBI,4BAIE,kBAAA,CAHA,mCAAA,CAIA,uBNshBN,CMlhBI,4DAEE,oBAAA,CADA,SNqhBN,CMjhBM,oEACE,mBNmhBR,CO5kBA,WAGE,0CAAA,CADA,+BAAA,CADA,aPilBF,CO5kBE,aANF,WAOI,YP+kBF,CACF,CO5kBE,oBAEE,2CAAA,CADA,gCP+kBJ,CO1kBE,kBAGE,eAAA,CADA,iBAAA,CADA,eP8kBJ,COxkBE,6BACE,WP6kBJ,CO9kBE,6BACE,UP6kBJ,CO9kBE,mBAEE,aAAA,CACA,cAAA,CACA,uBP0kBJ,COvkBI,0BACE,YPykBN,COrkBI,yBACE,UPukBN,CQ5mBA,KASE,cAAA,CARA,WAAA,CACA,iBRgnBF,CK5cI,oCGtKJ,KAaI,gBRymBF,CACF,CKjdI,oCGtKJ,KAkBI,cRymBF,CACF,CQpmBA,KASE,2CAAA,CAPA,YAAA,CACA,qBAAA,CAKA,eAAA,CAHA,eAAA,CAJA,iBAAA,CAGA,UR0mBF,CQlmBE,aAZF,KAaI,aRqmBF,CACF,CKldI,0CGhJF,yBAII,cRkmBJ,CACF,CQzlBA,SAEE,gBAAA,CAAA,iBAAA,CADA,eR6lBF,CQxlBA,cACE,YAAA,CAEA,qBAAA,CADA,WR4lBF,CQxlBE,aANF,cAOI,aR2lBF,CACF,CQvlBA,SACE,WR0lBF,CQvlBE,gBACE,YAAA,CACA,WAAA,CACA,iBRylBJ,CQplBA,aACE,eAAA,CACA,sBRulBF,CQ9kBA,WACE,YRilBF,CQ5kBA,WAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,ORilBF,CQ5kBE,uCACE,aR8kBJ,CQ1kBE,+BAEE,uCAAA,CADA,kBR6kBJ,CQvkBA,SASE,2CAAA,CACA,mBAAA,CAFA,gCAAA,CADA,gBAAA,CADA,YAAA,CAMA,SAAA,CADA,uCAAA,CANA,mBAAA,CAJA,cAAA,CAYA,2BAAA,CATA,URilBF,CQrkBE,eAEE,SAAA,CAIA,uBAAA,CAHA,oEACE,CAHF,UR0kBJ,CQ5jBA,MACE,WR+jBF,CSxtBA,MACE,6PT0tBF,CSptBA,cASE,mBAAA,CAFA,0CAAA,CACA,cAAA,CAFA,YAAA,CAIA,uCAAA,CACA,oBAAA,CAVA,iBAAA,CAEA,UAAA,CADA,QAAA,CAUA,qBAAA,CAPA,WAAA,CADA,ST+tBF,CSptBE,aAfF,cAgBI,YTutBF,CACF,CSptBE,kCAEE,uCAAA,CADA,YTutBJ,CSltBE,qBACE,uCTotBJ,CShtBE,wCACE,+BTktBJ,CS7sBE,oBAME,6BAAA,CADA,UAAA,CAJA,aAAA,CAEA,cAAA,CACA,aAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,aTutBJ,CS3sBE,sBACE,cT6sBJ,CS1sBI,2BACE,2CT4sBN,CStsBI,kEAEE,uDAAA,CADA,+BTysBN,CU3wBE,8BACE,YV8wBJ,CWnxBA,mBACE,GACE,SAAA,CACA,0BXsxBF,CWnxBA,GACE,SAAA,CACA,uBXqxBF,CACF,CWjxBA,mBACE,GACE,SXmxBF,CWhxBA,GACE,SXkxBF,CACF,CWvwBE,qBASE,2BAAA,CAFA,mCAAA,CAAA,2BAAA,CADA,0BAAA,CADA,WAAA,CAGA,SAAA,CAPA,cAAA,CACA,KAAA,CAEA,UAAA,CADA,SX+wBJ,CWrwBE,mBAcE,mDAAA,CANA,2CAAA,CACA,QAAA,CACA,mBAAA,CARA,QAAA,CASA,kDACE,CAPF,eAAA,CAEA,aAAA,CADA,SAAA,CALA,cAAA,CAGA,UAAA,CADA,SXgxBJ,CWjwBE,kBACE,aXmwBJ,CW/vBE,sBACE,YAAA,CACA,YXiwBJ,CW9vBI,oCACE,aXgwBN,CW3vBE,sBACE,mBX6vBJ,CW1vBI,6CACE,cX4vBN,CKtpBI,0CMvGA,6CAKI,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,UX8vBN,CACF,CWvvBE,kBACE,cXyvBJ,CY11BA,YACE,WAAA,CAIA,WZ01BF,CYv1BE,mBAEE,qBAAA,CADA,iBZ01BJ,CK7rBI,sCOtJE,4EACE,kBZs1BN,CYl1BI,0JACE,mBZo1BN,CYr1BI,8EACE,kBZo1BN,CACF,CY/0BI,0BAGE,UAAA,CAFA,aAAA,CACA,YZk1BN,CY70BI,+BACE,eZ+0BN,CYz0BE,8BACE,WZ80BJ,CY/0BE,8BACE,UZ80BJ,CY/0BE,8BAIE,iBZ20BJ,CY/0BE,8BAIE,kBZ20BJ,CY/0BE,oBAGE,cAAA,CADA,SZ60BJ,CYx0BI,aAPF,oBAQI,YZ20BJ,CACF,CYx0BI,gCACE,yCZ00BN,CYt0BI,wBACE,cAAA,CACA,kBZw0BN,CYr0BM,kCACE,oBZu0BR,Cax4BA,qBAEE,Wbs5BF,Cax5BA,qBAEE,Ubs5BF,Cax5BA,WAQE,2CAAA,CACA,mBAAA,CANA,YAAA,CAOA,8BAAA,CALA,iBAAA,CAMA,SAAA,CALA,mBAAA,CACA,mBAAA,CANA,cAAA,CAcA,0BAAA,CAHA,wCACE,CATF,Sbo5BF,Cat4BE,aAlBF,WAmBI,Yby4BF,CACF,Cat4BE,mBAEE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,kEby4BJ,Cal4BE,kBAEE,gCAAA,CADA,ebq4BJ,Ccv6BA,aACE,gBAAA,CACA,iBd06BF,Ccv6BE,sBAGE,WAAA,CADA,QAAA,CADA,Sd26BJ,Ccr6BE,oBAEE,eAAA,CADA,edw6BJ,Ccn6BE,oBACE,iBdq6BJ,Ccj6BE,mBAEE,YAAA,CACA,cAAA,CACA,6BAAA,CAHA,iBds6BJ,Cch6BI,iDACE,yCdk6BN,Cc95BI,6BACE,iBdg6BN,Cc35BE,mBAGE,uCAAA,CACA,cAAA,CAHA,aAAA,CACA,cAAA,CAGA,sBd65BJ,Cc15BI,gDACE,+Bd45BN,Ccx5BI,4BACE,0CAAA,CACA,mBd05BN,Ccr5BE,mBAEE,SAAA,CADA,iBAAA,CAKA,2BAAA,CAHA,8Ddw5BJ,Ccl5BI,qBAEE,aAAA,CADA,edq5BN,Cch5BI,6BACE,SAAA,CACA,uBdk5BN,Cc74BE,aAnFF,aAoFI,Ydg5BF,CACF,Cer+BA,WAEE,0CAAA,CADA,+Bfy+BF,Cer+BE,aALF,WAMI,Yfw+BF,CACF,Cer+BE,kBACE,6BAAA,CAEA,aAAA,CADA,afw+BJ,Cep+BI,gCACE,Yfs+BN,Cej+BE,iBAOE,eAAA,CANA,YAAA,CAKA,cAAA,CAGA,mBAAA,CAAA,eAAA,CADA,cAAA,CAGA,uCAAA,CADA,eAAA,CAEA,uBf+9BJ,Ce59BI,8CACE,Uf89BN,Ce19BI,+BACE,oBf49BN,CK90BI,0CUvIE,uBACE,afw9BN,Cer9BM,yCACE,Yfu9BR,CACF,Cel9BI,iCACE,gBfq9BN,Cet9BI,iCACE,iBfq9BN,Cet9BI,uBAEE,gBfo9BN,Cej9BM,iCACE,efm9BR,Ce78BE,kBACE,WAAA,CAIA,eAAA,CADA,mBAAA,CAFA,6BAAA,CACA,cAAA,CAGA,kBf+8BJ,Ce38BE,mBAEE,YAAA,CADA,af88BJ,Cez8BE,sBACE,gBAAA,CACA,Uf28BJ,Cet8BA,gBACE,gDfy8BF,Cet8BE,uBACE,YAAA,CACA,cAAA,CACA,6BAAA,CACA,afw8BJ,Cep8BE,kCACE,sCfs8BJ,Cen8BI,gFACE,+Bfq8BN,Ce77BA,cAKE,wCAAA,CADA,gBAAA,CADA,iBAAA,CADA,eAAA,CADA,Ufo8BF,CKx5BI,mCU7CJ,cASI,Ufg8BF,CACF,Ce57BE,yBACE,sCf87BJ,Cev7BA,WACE,mBAAA,CACA,SAAA,CAEA,cAAA,CADA,qBf27BF,CKv6BI,mCUvBJ,WAQI,ef07BF,CACF,Cev7BE,iBACE,oBAAA,CAEA,aAAA,CACA,iBAAA,CAFA,Yf27BJ,Cet7BI,wBACE,efw7BN,Cep7BI,qBAGE,iBAAA,CAFA,gBAAA,CACA,mBfu7BN,CgB7lCE,uBAME,kBAAA,CACA,mBAAA,CAHA,gCAAA,CACA,cAAA,CAJA,oBAAA,CAEA,eAAA,CADA,kBAAA,CAMA,gEhBgmCJ,CgB1lCI,gCAEE,2CAAA,CACA,uCAAA,CAFA,gChB8lCN,CgBxlCI,0DAEE,0CAAA,CACA,sCAAA,CAFA,+BhB4lCN,CgBrlCE,gCAKE,4BhB0lCJ,CgB/lCE,gEAME,6BhBylCJ,CgB/lCE,gCAME,4BhBylCJ,CgB/lCE,sBAIE,6DAAA,CAGA,8BAAA,CAJA,eAAA,CAFA,aAAA,CACA,eAAA,CAMA,sChBulCJ,CgBllCI,wDACE,6CAAA,CACA,8BhBolCN,CgBhlCI,+BACE,UhBklCN,CiBroCA,WAOE,2CAAA,CAGA,8CACE,CALF,gCAAA,CADA,aAAA,CAHA,MAAA,CADA,eAAA,CACA,OAAA,CACA,KAAA,CACA,SjB4oCF,CiBjoCE,aAfF,WAgBI,YjBooCF,CACF,CiBjoCE,mBAIE,2BAAA,CAHA,iEjBooCJ,CiB7nCE,mBACE,kDACE,CAEF,kEjB6nCJ,CiBvnCE,kBAEE,kBAAA,CADA,YAAA,CAEA,ejBynCJ,CiBrnCE,mBAKE,kBAAA,CAEA,cAAA,CAHA,YAAA,CAIA,uCAAA,CALA,aAAA,CAFA,iBAAA,CAQA,uBAAA,CAHA,qBAAA,CAJA,SjB8nCJ,CiBpnCI,yBACE,UjBsnCN,CiBlnCI,iCACE,oBjBonCN,CiBhnCI,uCAEE,uCAAA,CADA,YjBmnCN,CiB9mCI,2BAEE,YAAA,CADA,ajBinCN,CKngCI,0CY/GA,2BAMI,YjBgnCN,CACF,CiB7mCM,8DAIE,iBAAA,CAHA,aAAA,CAEA,aAAA,CADA,UjBinCR,CKjiCI,mCYzEA,iCAII,YjB0mCN,CACF,CiBvmCM,wCACE,YjBymCR,CiBrmCM,+CACE,oBjBumCR,CK5iCI,sCYtDA,iCAII,YjBkmCN,CACF,CiB7lCE,kBAEE,YAAA,CACA,cAAA,CAFA,iBAAA,CAIA,8DACE,CAFF,kBjBgmCJ,CiB1lCI,oCAGE,SAAA,CADA,mBAAA,CAKA,6BAAA,CAHA,8DACE,CAJF,UjBgmCN,CiBvlCM,8CACE,8BjBylCR,CiBplCI,8BACE,ejBslCN,CiBjlCE,4BAGE,gBAAA,CAAA,kBjBqlCJ,CiBxlCE,4BAGE,iBAAA,CAAA,iBjBqlCJ,CiBxlCE,kBACE,WAAA,CAGA,eAAA,CAFA,aAAA,CAGA,kBjBmlCJ,CiBhlCI,4CAGE,SAAA,CADA,mBAAA,CAKA,8BAAA,CAHA,8DACE,CAJF,UjBslCN,CiB7kCM,sDACE,6BjB+kCR,CiB3kCM,8DAGE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,8DACE,CAJF,SjBilCR,CiBtkCI,uCAGE,WAAA,CAFA,iBAAA,CACA,UjBykCN,CiBnkCE,mBACE,YAAA,CACA,aAAA,CACA,cAAA,CAEA,+CACE,CAFF,kBjBskCJ,CiBhkCI,8DACE,WAAA,CACA,SAAA,CACA,oCjBkkCN,CiBzjCI,yBACE,QjB2jCN,CiBtjCE,mBACE,YjBwjCJ,CKpnCI,mCY2DF,6BAQI,gBjBwjCJ,CiBhkCA,6BAQI,iBjBwjCJ,CiBhkCA,mBAKI,aAAA,CAEA,iBAAA,CADA,ajB0jCJ,CACF,CK5nCI,sCY2DF,6BAaI,kBjBwjCJ,CiBrkCA,6BAaI,mBjBwjCJ,CACF,CDvyCA,SAGE,uCAAA,CAFA,eAAA,CACA,eC2yCF,CDvyCE,eACE,mBAAA,CACA,cAAA,CAGA,eAAA,CADA,QAAA,CADA,SC2yCJ,CDryCE,sCAEE,WAAA,CADA,iBAAA,CAAA,kBCwyCJ,CDnyCE,eACE,+BCqyCJ,CDlyCI,0CACE,+BCoyCN,CD9xCA,UAKE,wBmBaa,CnBZb,oBAAA,CAFA,UAAA,CAHA,oBAAA,CAEA,eAAA,CADA,0BAAA,CAAA,2BCqyCF,CmBv0CA,MACE,uMAAA,CACA,sLAAA,CACA,iNnB00CF,CmBp0CA,QACE,eAAA,CACA,enBu0CF,CmBp0CE,eAKE,uCAAA,CAJA,aAAA,CAGA,eAAA,CADA,eAAA,CADA,eAAA,CAIA,sBnBs0CJ,CmBn0CI,+BACE,YnBq0CN,CmBl0CM,mCAEE,WAAA,CADA,UnBq0CR,CmB7zCQ,sFAME,iBAAA,CALA,aAAA,CAGA,aAAA,CADA,cAAA,CAEA,kBAAA,CAHA,UnBm0CV,CmBxzCE,cAGE,eAAA,CADA,QAAA,CADA,SnB4zCJ,CmBtzCE,cAGE,sBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBAAA,CACA,uBAAA,CACA,sBnBwzCJ,CmBrzCI,sBACE,uCnBuzCN,CmBhzCM,6EAEE,+BnBkzCR,CmB7yCI,2BAIE,iBnB4yCN,CmBxyCI,4CACE,gBnB0yCN,CmB3yCI,4CACE,iBnB0yCN,CmBtyCI,kBAME,iBAAA,CAFA,aAAA,CACA,YAAA,CAFA,iBnByyCN,CmBlyCI,sGACE,+BAAA,CACA,cnBoyCN,CmBhyCI,4BACE,uCAAA,CACA,oBnBkyCN,CmB9xCI,0CACE,YnBgyCN,CmB7xCM,yDAIE,6BAAA,CAHA,aAAA,CAEA,WAAA,CAEA,qCAAA,CAAA,6BAAA,CAHA,UnBkyCR,CmB3xCM,kDACE,YnB6xCR,CmBvxCE,iCACE,YnByxCJ,CmBtxCI,6CACE,WAAA,CAGA,WnBsxCN,CmBjxCE,cACE,anBmxCJ,CmB/wCE,gBACE,YnBixCJ,CKlvCI,0CcxBA,0CASE,2CAAA,CAHA,YAAA,CACA,qBAAA,CACA,WAAA,CALA,MAAA,CADA,iBAAA,CACA,OAAA,CACA,KAAA,CACA,SnBgxCJ,CmBrwCI,+DACE,eAAA,CACA,enBuwCN,CmBnwCI,gCAQE,qDAAA,CAHA,uCAAA,CAEA,cAAA,CALA,aAAA,CAEA,kBAAA,CADA,wBAAA,CAFA,iBAAA,CAKA,kBnBuwCN,CmBlwCM,wDAEE,UnBywCR,CmB3wCM,wDAEE,WnBywCR,CmB3wCM,8CAIE,aAAA,CAEA,aAAA,CACA,YAAA,CANA,iBAAA,CAEA,SAAA,CAEA,YnBswCR,CmBjwCQ,oDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnB0wCV,CmB9vCM,8CAIE,2CAAA,CACA,gEACE,CALF,eAAA,CAEA,4BAAA,CADA,kBnBmwCR,CmB5vCQ,2DACE,YnB8vCV,CmBzvCM,8CAGE,2CAAA,CADA,gCAAA,CADA,enB6vCR,CmBvvCM,yCAIE,aAAA,CAFA,UAAA,CAIA,YAAA,CADA,aAAA,CAJA,iBAAA,CACA,WAAA,CACA,SnB4vCR,CmBpvCI,+BACE,MnBsvCN,CmBlvCI,+BACE,4DnBovCN,CmBjvCM,qDACE,+BnBmvCR,CmBhvCQ,sHACE,+BnBkvCV,CmB5uCI,+BAEE,YAAA,CADA,mBnB+uCN,CmB3uCM,mCACE,enB6uCR,CmBzuCM,6CACE,SnB2uCR,CmBvuCM,uDAGE,mBnB0uCR,CmB7uCM,uDAGE,kBnB0uCR,CmB7uCM,6CAIE,gBAAA,CAFA,aAAA,CADA,YnB4uCR,CmBtuCQ,mDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnB+uCV,CmB/tCM,+CACE,mBnBiuCR,CmBztCM,4CAEE,wBAAA,CADA,enB4tCR,CmBxtCQ,oEACE,mBnB0tCV,CmB3tCQ,oEACE,oBnB0tCV,CmBttCQ,4EACE,iBnBwtCV,CmBztCQ,4EACE,kBnBwtCV,CmBptCQ,oFACE,mBnBstCV,CmBvtCQ,oFACE,oBnBstCV,CmBltCQ,4FACE,mBnBotCV,CmBrtCQ,4FACE,oBnBotCV,CmB7sCE,mBACE,wBnB+sCJ,CmB3sCE,wBACE,YAAA,CACA,SAAA,CAIA,0BAAA,CAHA,oEnB8sCJ,CmBxsCI,kCACE,2BnB0sCN,CmBrsCE,gCACE,SAAA,CAIA,uBAAA,CAHA,qEnBwsCJ,CmBlsCI,8CAEE,kCAAA,CAAA,0BnBmsCN,CACF,CKr4CI,0Cc0MA,0CACE,YnB8rCJ,CmB3rCI,yDACE,UnB6rCN,CmBzrCI,wDACE,YnB2rCN,CmBvrCI,kDACE,YnByrCN,CmBprCE,gBAIE,iDAAA,CADA,gCAAA,CAFA,aAAA,CACA,enBwrCJ,CACF,CKl8CM,+DcmRF,6CACE,YnBkrCJ,CmB/qCI,4DACE,UnBirCN,CmB7qCI,2DACE,YnB+qCN,CmB3qCI,qDACE,YnB6qCN,CACF,CK17CI,mCc7JJ,QAgbI,oBnB2qCF,CmBrqCI,kCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnBuqCN,CmBlqCM,6CACE,uBnBoqCR,CmBhqCM,gDACE,YnBkqCR,CmB7pCI,2CACE,kBnBgqCN,CmBjqCI,2CACE,mBnBgqCN,CmBjqCI,iCAEE,oBnB+pCN,CmBxpCI,yDACE,kBnB0pCN,CmB3pCI,yDACE,iBnB0pCN,CACF,CKn9CI,sCc7JJ,QA4dI,oBAAA,CACA,oDnBwpCF,CmBlpCI,gCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnBopCN,CmB/oCM,8CACE,uBnBipCR,CmB7oCM,8CACE,YnB+oCR,CmB1oCI,yCACE,kBnB6oCN,CmB9oCI,yCACE,mBnB6oCN,CmB9oCI,+BAEE,oBnB4oCN,CmBroCI,uDACE,kBnBuoCN,CmBxoCI,uDACE,iBnBuoCN,CmBloCE,wBACE,YAAA,CACA,sBAAA,CAEA,SAAA,CACA,6FACE,CAHF,mBnBsoCJ,CmB9nCI,sCACE,enBgoCN,CmB3nCE,iFACE,sBAAA,CAEA,SAAA,CACA,4FACE,CAHF,kBnB+nCJ,CmBtnCE,iDACE,enBwnCJ,CmBpnCE,6CACE,YnBsnCJ,CmBlnCE,uBACE,aAAA,CACA,enBonCJ,CmBjnCI,kCACE,enBmnCN,CmB/mCI,qCACE,enBinCN,CmB9mCM,0CACE,uCnBgnCR,CmB5mCM,6DACE,mBnB8mCR,CmB1mCM,yFAEE,YnB4mCR,CmBvmCI,yCAEE,kBnB2mCN,CmB7mCI,yCAEE,mBnB2mCN,CmB7mCI,+BACE,aAAA,CAGA,SAAA,CADA,kBnB0mCN,CmBtmCM,2DACE,SnBwmCR,CmBlmCE,cAGE,kBAAA,CADA,YAAA,CAEA,gCAAA,CAHA,WnBumCJ,CmBjmCI,oBACE,uDnBmmCN,CmB/lCI,oBAME,6BAAA,CACA,kBAAA,CAFA,UAAA,CAJA,oBAAA,CAEA,WAAA,CAKA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,yBAAA,CARA,qBAAA,CAFA,UnB2mCN,CmB9lCM,8BACE,wBnBgmCR,CmB5lCM,kKAEE,uBnB6lCR,CmB/kCI,2EACE,YnBolCN,CmBjlCM,oDACE,anBmlCR,CmBhlCQ,kEAKE,qCAAA,CACA,qDAAA,CAFA,YAAA,CAHA,eAAA,CACA,KAAA,CACA,SnBqlCV,CmB/kCU,0FACE,mBnBilCZ,CmB5kCQ,0EACE,QnB8kCV,CmBzkCM,sFACE,kBnB2kCR,CmB5kCM,sFACE,mBnB2kCR,CmBvkCM,kDACE,uCnBykCR,CmBnkCI,2CACE,sBAAA,CAEA,SAAA,CADA,kBnBskCN,CmB7jCI,qFAIE,mDnBgkCN,CmBpkCI,qFAIE,oDnBgkCN,CmBpkCI,2EACE,aAAA,CACA,oBAAA,CAGA,SAAA,CAFA,kBnBikCN,CmB5jCM,yFAEE,gBAAA,CADA,gBnB+jCR,CmB1jCM,0FACE,YnB4jCR,CACF,CoBnxDA,eAKE,eAAA,CACA,eAAA,CAJA,SpB0xDF,CoBnxDE,gCANA,kBAAA,CAFA,YAAA,CAGA,sBpBiyDF,CoB5xDE,iBAOE,mBAAA,CAFA,aAAA,CADA,gBAAA,CAEA,iBpBsxDJ,CoBjxDE,wBAEE,qDAAA,CADA,uCpBoxDJ,CoB/wDE,qBACE,6CpBixDJ,CoB5wDI,sDAEE,uDAAA,CADA,+BpB+wDN,CoB3wDM,8DACE,+BpB6wDR,CoBxwDI,mCACE,uCAAA,CACA,oBpB0wDN,CoBtwDI,yBAKE,iBAAA,CADA,yCAAA,CAHA,aAAA,CAEA,eAAA,CADA,YpB2wDN,CqB3zDE,eAGE,+DAAA,CADA,oBAAA,CADA,qBrBg0DJ,CK3oDI,0CgBtLF,eAOI,YrB8zDJ,CACF,CqBxzDM,6BACE,oBrB0zDR,CqBpzDE,kBACE,YAAA,CACA,qBAAA,CACA,SAAA,CACA,qBrBszDJ,CqB/yDI,0BACE,sBrBizDN,CqB9yDM,gEACE,+BrBgzDR,CqB1yDE,gBAEE,uCAAA,CADA,erB6yDJ,CqBxyDE,kBACE,oBrB0yDJ,CqBvyDI,mCAGE,kBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBrByyDN,CqBryDI,oCAIE,kBAAA,CAHA,mBAAA,CACA,kBAAA,CACA,SAAA,CAGA,QAAA,CADA,iBrBwyDN,CqBnyDI,0DACE,kBrBqyDN,CqBtyDI,0DACE,iBrBqyDN,CqBjyDI,iDACE,uBAAA,CAEA,YrBkyDN,CqB7xDE,4BACE,YrB+xDJ,CqBxxDA,YAGE,kBAAA,CAFA,YAAA,CAIA,eAAA,CAHA,SAAA,CAIA,eAAA,CAFA,UrB6xDF,CqBxxDE,yBACE,WrB0xDJ,CqBnxDA,kBACE,YrBsxDF,CK9sDI,0CgBzEJ,kBAKI,wBrBsxDF,CACF,CqBnxDE,qCACE,WrBqxDJ,CKzuDI,sCgB7CF,+CAKI,kBrBqxDJ,CqB1xDA,+CAKI,mBrBqxDJ,CACF,CK3tDI,0CgBrDJ,6BAMI,SAAA,CAFA,eAAA,CACA,UrBkxDF,CqB/wDE,qDACE,gBrBixDJ,CqB9wDE,gDACE,SrBgxDJ,CqB7wDE,4CACE,iBAAA,CAAA,kBrB+wDJ,CqB5wDE,2CAEE,WAAA,CADA,crB+wDJ,CqB3wDE,2CACE,mBAAA,CACA,cAAA,CACA,SAAA,CACA,oBAAA,CAAA,iBrB6wDJ,CqB1wDE,2CACE,SrB4wDJ,CqBzwDE,qCAEE,WAAA,CACA,eAAA,CAFA,erB6wDJ,CACF,CsBv7DA,MACE,qBAAA,CACA,yBtB07DF,CsBp7DA,aAME,qCAAA,CADA,cAAA,CAEA,0FACE,CAPF,cAAA,CACA,KAAA,CAaA,mDAAA,CACA,qBAAA,CAJA,wFACE,CATF,UAAA,CADA,StB87DF,CuBz8DA,MACE,mfvB48DF,CuBt8DA,WACE,iBvBy8DF,CK3yDI,mCkB/JJ,WAKI,evBy8DF,CACF,CuBt8DE,kBACE,YvBw8DJ,CuBp8DE,oBAEE,SAAA,CADA,SvBu8DJ,CKpyDI,0CkBpKF,8BAOI,YvB+8DJ,CuBt9DA,8BAOI,avB+8DJ,CuBt9DA,oBAaI,2CAAA,CACA,kBAAA,CAJA,WAAA,CACA,eAAA,CACA,mBAAA,CANA,iBAAA,CAEA,SAAA,CAUA,uBAAA,CAHA,4CACE,CAPF,UvB68DJ,CuBj8DI,+DACE,SAAA,CACA,oCvBm8DN,CACF,CK10DI,mCkBjJF,8BAgCI,MvBs8DJ,CuBt+DA,8BAgCI,OvBs8DJ,CuBt+DA,oBAqCI,0BAAA,CADA,cAAA,CADA,QAAA,CAJA,cAAA,CAEA,KAAA,CAKA,sDACE,CALF,OvBo8DJ,CuB17DI,+DAME,YAAA,CACA,SAAA,CACA,4CACE,CARF,UvB+7DN,CACF,CKz0DI,0CkBxGA,+DAII,mBvBi7DN,CACF,CKv3DM,+DkB/DF,+DASI,mBvBi7DN,CACF,CK53DM,+DkB/DF,+DAcI,mBvBi7DN,CACF,CuB56DE,kBAEE,kCAAA,CAAA,0BvB66DJ,CK31DI,0CkBpFF,4BAOI,MvBq7DJ,CuB57DA,4BAOI,OvBq7DJ,CuB57DA,kBAWI,QAAA,CAEA,SAAA,CADA,eAAA,CANA,cAAA,CAEA,KAAA,CAWA,wBAAA,CALA,qGACE,CALF,OAAA,CADA,SvBm7DJ,CuBt6DI,4BACE,yBvBw6DN,CuBp6DI,6DAEE,WAAA,CACA,SAAA,CAMA,uBAAA,CALA,sGACE,CAJF,UvB06DN,CACF,CKt4DI,mCkBjEF,4BA2CI,WvBo6DJ,CuB/8DA,4BA2CI,UvBo6DJ,CuB/8DA,kBA6CI,eAAA,CAHA,iBAAA,CAIA,8CAAA,CAFA,avBm6DJ,CACF,CKr6DM,+DkBOF,6DAII,avB85DN,CACF,CKp5DI,sCkBfA,6DASI,avB85DN,CACF,CuBz5DE,iBAIE,2CAAA,CACA,0BAAA,CAFA,aAAA,CAFA,iBAAA,CAKA,2CACE,CALF,SvB+5DJ,CKj6DI,mCkBAF,iBAaI,0BAAA,CACA,mBAAA,CAFA,avB25DJ,CuBt5DI,uBACE,0BvBw5DN,CACF,CuBp5DI,4DAEE,2CAAA,CACA,6BAAA,CACA,8BAAA,CAHA,gCvBy5DN,CuBj5DE,4BAKE,mBAAA,CAAA,oBvBs5DJ,CuB35DE,4BAKE,mBAAA,CAAA,oBvBs5DJ,CuB35DE,kBAQE,gBAAA,CAFA,eAAA,CAFA,WAAA,CAHA,iBAAA,CAMA,sBAAA,CAJA,UAAA,CADA,SvBy5DJ,CuBh5DI,+BACE,qBvBk5DN,CuB94DI,kEAEE,uCvB+4DN,CuB34DI,6BACE,YvB64DN,CKj7DI,0CkBaF,kBA8BI,eAAA,CADA,aAAA,CADA,UvB84DJ,CACF,CK38DI,mCkBgCF,4BAmCI,mBvB84DJ,CuBj7DA,4BAmCI,oBvB84DJ,CuBj7DA,kBAqCI,aAAA,CADA,evB64DJ,CuBz4DI,+BACE,uCvB24DN,CuBv4DI,mCACE,gCvBy4DN,CuBr4DI,6DACE,kBvBu4DN,CuBp4DM,8EACE,uCvBs4DR,CuBl4DM,0EACE,WvBo4DR,CACF,CuB93DE,iBAIE,cAAA,CAHA,oBAAA,CAEA,aAAA,CAEA,kCACE,CAJF,YvBm4DJ,CuB33DI,uBACE,UvB63DN,CuBz3DI,yCAEE,UvB63DN,CuB/3DI,yCAEE,WvB63DN,CuB/3DI,+BACE,iBAAA,CAEA,SAAA,CACA,SvB23DN,CuBx3DM,6CACE,oBvB03DR,CKj+DI,0CkB+FA,yCAaI,UvB03DN,CuBv4DE,yCAaI,WvB03DN,CuBv4DE,+BAcI,SvBy3DN,CuBt3DM,+CACE,YvBw3DR,CACF,CK7/DI,mCkBkHA,+BAwBI,mBvBu3DN,CuBp3DM,8CACE,YvBs3DR,CACF,CuBh3DE,8BAEE,WvBq3DJ,CuBv3DE,8BAEE,UvBq3DJ,CuBv3DE,oBAKE,mBAAA,CAJA,iBAAA,CAEA,SAAA,CACA,SvBm3DJ,CKz/DI,0CkBkIF,8BASI,WvBm3DJ,CuB53DA,8BASI,UvBm3DJ,CuB53DA,oBAUI,SvBk3DJ,CACF,CuB/2DI,uCACE,iBvBq3DN,CuBt3DI,uCACE,kBvBq3DN,CuBt3DI,6BAEE,uCAAA,CACA,SAAA,CAIA,oBAAA,CAHA,+DvBk3DN,CuB52DM,iDAEE,uCAAA,CADA,YvB+2DR,CuB12DM,gGAGE,SAAA,CADA,mBAAA,CAEA,kBvB22DR,CuBx2DQ,sGACE,UvB02DV,CuBn2DE,8BAOE,mBAAA,CAAA,oBvB02DJ,CuBj3DE,8BAOE,mBAAA,CAAA,oBvB02DJ,CuBj3DE,oBAIE,kBAAA,CAKA,yCAAA,CANA,YAAA,CAKA,eAAA,CAFA,WAAA,CAKA,SAAA,CAVA,iBAAA,CACA,KAAA,CAUA,uBAAA,CAFA,kBAAA,CALA,UvB42DJ,CKnjEI,mCkBkMF,8BAgBI,mBvBs2DJ,CuBt3DA,8BAgBI,oBvBs2DJ,CuBt3DA,oBAiBI,evBq2DJ,CACF,CuBl2DI,+DACE,SAAA,CACA,0BvBo2DN,CuB/1DE,6BAKE,+BvBk2DJ,CuBv2DE,0DAME,gCvBi2DJ,CuBv2DE,6BAME,+BvBi2DJ,CuBv2DE,mBAIE,eAAA,CAHA,iBAAA,CAEA,UAAA,CADA,SvBq2DJ,CKljEI,0CkB2MF,mBAWI,QAAA,CADA,UvBk2DJ,CACF,CK3kEI,mCkB8NF,mBAiBI,SAAA,CADA,UAAA,CAEA,sBvBi2DJ,CuB91DI,8DACE,8BAAA,CACA,SvBg2DN,CACF,CuB31DE,uBASE,kCAAA,CAAA,0BAAA,CAFA,2CAAA,CANA,WAAA,CACA,eAAA,CAIA,kBvB41DJ,CuBt1DI,iEAZF,uBAaI,uBvBy1DJ,CACF,CKxnEM,+DkBiRJ,uBAkBI,avBy1DJ,CACF,CKvmEI,sCkB2PF,uBAuBI,avBy1DJ,CACF,CK5mEI,mCkB2PF,uBA4BI,YAAA,CACA,yDAAA,CACA,oBvBy1DJ,CuBt1DI,kEACE,evBw1DN,CuBp1DI,6BACE,+CvBs1DN,CuBl1DI,0CAEE,YAAA,CADA,WvBq1DN,CuBh1DI,gDACE,oDvBk1DN,CuB/0DM,sDACE,0CvBi1DR,CACF,CuB10DA,kBACE,gCAAA,CACA,qBvB60DF,CuB10DE,wBAME,qDAAA,CAFA,uCAAA,CAFA,gBAAA,CACA,kBAAA,CAFA,eAAA,CAIA,uBvB60DJ,CKhpEI,mCkB8TF,kCAUI,mBvB40DJ,CuBt1DA,kCAUI,oBvB40DJ,CACF,CuBx0DE,wBAGE,eAAA,CADA,QAAA,CADA,SAAA,CAIA,wBAAA,CAAA,gBvBy0DJ,CuBr0DE,wBACE,yDvBu0DJ,CuBp0DI,oCACE,evBs0DN,CuBj0DE,wBACE,aAAA,CAEA,YAAA,CADA,uBAAA,CAEA,gCvBm0DJ,CuBh0DI,4DACE,uDvBk0DN,CuB9zDI,gDACE,mBvBg0DN,CuB3zDE,gCAKE,cAAA,CADA,aAAA,CAGA,YAAA,CANA,eAAA,CAKA,uBAAA,CAJA,KAAA,CACA,SvBi0DJ,CuB1zDI,wCACE,YvB4zDN,CuBvzDI,wDACE,YvByzDN,CuBrzDI,oCAGE,+BAAA,CADA,gBAAA,CADA,mBAAA,CAGA,2CvBuzDN,CKlsEI,mCkBuYA,8CAUI,mBvBqzDN,CuB/zDE,8CAUI,oBvBqzDN,CACF,CuBjzDI,oFAEE,uDAAA,CADA,+BvBozDN,CuB9yDE,sCACE,2CvBgzDJ,CuB3yDE,2BAGE,eAAA,CADA,eAAA,CADA,iBvB+yDJ,CKntEI,mCkBmaF,qCAOI,mBvB6yDJ,CuBpzDA,qCAOI,oBvB6yDJ,CACF,CuBzyDE,kCAEE,MvB+yDJ,CuBjzDE,kCAEE,OvB+yDJ,CuBjzDE,wBAME,uCAAA,CAFA,aAAA,CACA,YAAA,CAJA,iBAAA,CAEA,YvB8yDJ,CK7sEI,0CkB4ZF,wBAUI,YvB2yDJ,CACF,CuBxyDI,8BAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,+CAAA,CAAA,uCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UvBizDN,CuBvyDM,wCACE,oBvByyDR,CuBnyDE,8BAGE,uCAAA,CAFA,gBAAA,CACA,evBsyDJ,CuBlyDI,iCAKE,gCAAA,CAHA,eAAA,CACA,eAAA,CACA,eAAA,CAHA,evBwyDN,CuBjyDM,sCACE,oBvBmyDR,CuB9xDI,iCAKE,gCAAA,CAHA,gBAAA,CACA,eAAA,CACA,eAAA,CAHA,avBoyDN,CuB7xDM,sCACE,oBvB+xDR,CuBzxDE,yBAKE,gCAAA,CAJA,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,avB8xDJ,CuBvxDE,uBAGE,wBAAA,CAFA,+BAAA,CACA,yBvB0xDJ,CwB97EA,WACE,iBAAA,CACA,SxBi8EF,CwB97EE,kBAOE,2CAAA,CACA,mBAAA,CACA,8BAAA,CAHA,gCAAA,CAHA,QAAA,CAEA,gBAAA,CADA,YAAA,CAMA,SAAA,CATA,iBAAA,CACA,sBAAA,CAaA,mCAAA,CAJA,oExBi8EJ,CwB17EI,6EACE,gBAAA,CACA,SAAA,CAKA,+BAAA,CAJA,8ExB67EN,CwBr7EI,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,OxB87EN,CwBl7EE,iBAOE,mBAAA,CAFA,eAAA,CACA,oBAAA,CAHA,QAAA,CAFA,kBAAA,CAGA,aAAA,CAFA,SxBy7EJ,CwBh7EE,iBACE,kBxBk7EJ,CwB96EE,2BAGE,kBAAA,CAAA,oBxBo7EJ,CwBv7EE,2BAGE,mBAAA,CAAA,mBxBo7EJ,CwBv7EE,iBAIE,cAAA,CAHA,aAAA,CAKA,YAAA,CADA,uBAAA,CAEA,2CACE,CANF,UxBq7EJ,CwB36EI,8CACE,+BxB66EN,CwBz6EI,uBACE,qDxB26EN,CyB//EA,YAIE,qBAAA,CADA,aAAA,CAGA,gBAAA,CALA,eAAA,CACA,UAAA,CAGA,azBmgFF,CyB//EE,aATF,YAUI,YzBkgFF,CACF,CKp1EI,0CoB3KF,+BAKI,azBugFJ,CyB5gFA,+BAKI,czBugFJ,CyB5gFA,qBAWI,2CAAA,CAHA,aAAA,CAEA,WAAA,CANA,cAAA,CAEA,KAAA,CASA,uBAAA,CAHA,iEACE,CAJF,aAAA,CAFA,SzBqgFJ,CyB1/EI,mEACE,8BAAA,CACA,6BzB4/EN,CyBz/EM,6EACE,8BzB2/ER,CyBt/EI,6CAEE,QAAA,CAAA,MAAA,CACA,QAAA,CACA,eAAA,CAHA,iBAAA,CACA,OAAA,CAGA,qBAAA,CAHA,KzB2/EN,CACF,CKn4EI,sCoBtKJ,YAuDI,QzBs/EF,CyBn/EE,mBACE,WzBq/EJ,CyBj/EE,6CACE,UzBm/EJ,CACF,CyB/+EE,uBACE,YAAA,CACA,OzBi/EJ,CKl5EI,mCoBjGF,uBAMI,QzBi/EJ,CyB9+EI,8BACE,WzBg/EN,CyB5+EI,qCACE,azB8+EN,CyB1+EI,+CACE,kBzB4+EN,CACF,CyBv+EE,wBAIE,uBAAA,CAOA,kCAAA,CAAA,0BAAA,CAVA,cAAA,CACA,eAAA,CACA,yDAAA,CAMA,oBzBs+EJ,CyBj+EI,2CAEE,YAAA,CADA,WzBo+EN,CyB/9EI,mEACE,+CzBi+EN,CyB99EM,qHACE,oDzBg+ER,CyB79EQ,iIACE,0CzB+9EV,CyBh9EE,wCAGE,wBACE,qBzBg9EJ,CyB58EE,6BACE,kCzB88EJ,CyB/8EE,6BACE,iCzB88EJ,CACF,CK16EI,0CoB5BF,YAME,0BAAA,CADA,QAAA,CAEA,SAAA,CANA,cAAA,CACA,KAAA,CAMA,sDACE,CALF,OAAA,CADA,SzB+8EF,CyBp8EE,4CAEE,WAAA,CACA,SAAA,CACA,4CACE,CAJF,UzBy8EJ,CACF,C0BtnFA,iBACE,GACE,Q1BwnFF,C0BrnFA,GACE,a1BunFF,CACF,C0BnnFA,gBACE,GACE,SAAA,CACA,0B1BqnFF,C0BlnFA,IACE,S1BonFF,C0BjnFA,GACE,SAAA,CACA,uB1BmnFF,CACF,C0B3mFA,MACE,2eAAA,CACA,+fAAA,CACA,0lBAAA,CACA,kf1B6mFF,C0BvmFA,WAOE,kCAAA,CAAA,0BAAA,CANA,aAAA,CACA,gBAAA,CACA,eAAA,CAEA,uCAAA,CAGA,uBAAA,CAJA,kB1B6mFF,C0BtmFE,iBACE,U1BwmFJ,C0BpmFE,iBACE,oBAAA,CAEA,aAAA,CACA,qBAAA,CAFA,U1BwmFJ,C0BnmFI,+BACE,iB1BsmFN,C0BvmFI,+BACE,kB1BsmFN,C0BvmFI,qBAEE,gB1BqmFN,C0BjmFI,kDACE,iB1BomFN,C0BrmFI,kDACE,kB1BomFN,C0BrmFI,kDAEE,iB1BmmFN,C0BrmFI,kDAEE,kB1BmmFN,C0B9lFE,iCAGE,iB1BmmFJ,C0BtmFE,iCAGE,kB1BmmFJ,C0BtmFE,uBACE,oBAAA,CACA,6BAAA,CAEA,eAAA,CACA,sBAAA,CACA,qB1BgmFJ,C0B5lFE,kBACE,YAAA,CAMA,gBAAA,CALA,SAAA,CAMA,oBAAA,CAHA,gBAAA,CAIA,WAAA,CAHA,eAAA,CAFA,SAAA,CADA,U1BomFJ,C0B3lFI,iDACE,4B1B6lFN,C0BxlFE,iBACE,eAAA,CACA,sB1B0lFJ,C0BvlFI,gDACE,2B1BylFN,C0BrlFI,kCAIE,kB1B6lFN,C0BjmFI,kCAIE,iB1B6lFN,C0BjmFI,wBAOE,6BAAA,CADA,UAAA,CALA,oBAAA,CAEA,YAAA,CAMA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CALA,uBAAA,CAHA,W1B+lFN,C0BnlFI,iCACE,a1BqlFN,C0BjlFI,iCACE,gDAAA,CAAA,wC1BmlFN,C0B/kFI,+BACE,8CAAA,CAAA,sC1BilFN,C0B7kFI,+BACE,8CAAA,CAAA,sC1B+kFN,C0B3kFI,sCACE,qDAAA,CAAA,6C1B6kFN,C0BvkFA,gBACE,Y1B0kFF,C0BvkFE,gCAIE,kB1B2kFJ,C0B/kFE,gCAIE,iB1B2kFJ,C0B/kFE,sBAGE,kBAAA,CAGA,uCAAA,CALA,mBAAA,CAIA,gBAAA,CAHA,S1B6kFJ,C0BtkFI,+BACE,aAAA,CACA,oB1BwkFN,C0BpkFI,2CACE,U1BukFN,C0BxkFI,2CACE,W1BukFN,C0BxkFI,iCAEE,kB1BskFN,C0BlkFI,0BACE,W1BokFN,C2B3vFA,MACE,iSAAA,CACA,4UAAA,CACA,+NAAA,CACA,gZ3B8vFF,C2BrvFE,iBAME,kDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,cAAA,CAIA,mCAAA,CAAA,2BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CANA,0BAAA,CAFA,a3BgwFJ,C2BpvFE,uBACE,6B3BsvFJ,C2BlvFE,sBACE,wCAAA,CAAA,gC3BovFJ,C2BhvFE,6BACE,+CAAA,CAAA,uC3BkvFJ,C2B9uFE,4BACE,8CAAA,CAAA,sC3BgvFJ,C4B3xFA,SASE,2CAAA,CADA,gCAAA,CAJA,aAAA,CAGA,eAAA,CADA,aAAA,CADA,UAAA,CAFA,S5BkyFF,C4BzxFE,aAZF,SAaI,Y5B4xFF,CACF,CKjnFI,0CuBzLJ,SAkBI,Y5B4xFF,CACF,C4BzxFE,iBACE,mB5B2xFJ,C4BvxFE,yBAIE,iB5B8xFJ,C4BlyFE,yBAIE,kB5B8xFJ,C4BlyFE,eAQE,eAAA,CAPA,YAAA,CAMA,eAAA,CAJA,QAAA,CAEA,aAAA,CAHA,SAAA,CAWA,oBAAA,CAPA,kB5B4xFJ,C4BlxFI,kCACE,Y5BoxFN,C4B/wFE,eACE,aAAA,CACA,kBAAA,CAAA,mB5BixFJ,C4B9wFI,sCACE,aAAA,CACA,S5BgxFN,C4B1wFE,eAOE,kCAAA,CAAA,0BAAA,CANA,YAAA,CAEA,eAAA,CADA,gBAAA,CAMA,UAAA,CAJA,uCAAA,CACA,oBAAA,CAIA,8D5B2wFJ,C4BtwFI,0CACE,aAAA,CACA,S5BwwFN,C4BpwFI,6BAEE,kB5BuwFN,C4BzwFI,6BAEE,iB5BuwFN,C4BzwFI,mBAGE,iBAAA,CAFA,Y5BwwFN,C4BjwFM,2CACE,qB5BmwFR,C4BpwFM,2CACE,qB5BswFR,C4BvwFM,2CACE,qB5BywFR,C4B1wFM,2CACE,qB5B4wFR,C4B7wFM,2CACE,oB5B+wFR,C4BhxFM,2CACE,qB5BkxFR,C4BnxFM,2CACE,qB5BqxFR,C4BtxFM,2CACE,qB5BwxFR,C4BzxFM,4CACE,qB5B2xFR,C4B5xFM,4CACE,oB5B8xFR,C4B/xFM,4CACE,qB5BiyFR,C4BlyFM,4CACE,qB5BoyFR,C4BryFM,4CACE,qB5BuyFR,C4BxyFM,4CACE,qB5B0yFR,C4B3yFM,4CACE,oB5B6yFR,C4BvyFI,gCACE,SAAA,CAIA,yBAAA,CAHA,wC5B0yFN,C6B74FA,MACE,mS7Bg5FF,C6Bv4FE,mCACE,mBAAA,CACA,cAAA,CACA,QAAA,CAEA,mBAAA,CADA,kB7B24FJ,C6Bt4FE,oBAGE,kBAAA,CAOA,+CAAA,CACA,oBAAA,CAVA,mBAAA,CAIA,gBAAA,CACA,0BAAA,CACA,eAAA,CALA,QAAA,CAOA,qBAAA,CADA,eAAA,CAJA,wB7B+4FJ,C6Br4FI,0BAGE,uCAAA,CAFA,aAAA,CACA,YAAA,CAEA,6C7Bu4FN,C6Bl4FM,gEAEE,0CAAA,CADA,+B7Bq4FR,C6B/3FI,yBACE,uB7Bi4FN,C6Bz3FI,gCAME,oDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,qCAAA,CAAA,6BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CAPA,0BAAA,CAFA,W7Bo4FN,C6Bv3FI,wFACE,0C7By3FN,C8Bn8FA,iBACE,GACE,oB9Bs8FF,C8Bn8FA,IACE,kB9Bq8FF,C8Bl8FA,GACE,oB9Bo8FF,CACF,C8B57FA,MACE,yNAAA,CACA,sP9B+7FF,C8Bx7FA,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,S9B47FF,C8B16FE,oBAEE,SAAA,CAKA,uBAAA,CAJA,2EACE,CAHF,S9B+6FJ,C8Br6FE,oBAEE,eAAA,CACA,wBAAA,CAAA,gBAAA,CAFA,U9By6FJ,C8Bp6FI,6CACE,qC9Bs6FN,C8Bl6FI,uCAEE,eAAA,CADA,mB9Bq6FN,C8B/5FI,6BACE,Y9Bi6FN,C8B55FE,8CACE,sC9B85FJ,C8B15FE,mBAEE,gBAAA,CADA,a9B65FJ,C8Bz5FI,2CACE,Y9B25FN,C8Bv5FI,0CACE,e9By5FN,C8Bj5FA,eACE,iBAAA,CACA,eAAA,CAIA,YAAA,CAHA,kBAAA,CAEA,0BAAA,CADA,kB9Bs5FF,C8Bj5FE,yBACE,a9Bm5FJ,C8B/4FE,oBACE,sCAAA,CACA,iB9Bi5FJ,C8B74FE,6BACE,oBAAA,CAGA,gB9B64FJ,C8Bz4FE,sBAYE,mBAAA,CANA,cAAA,CAHA,oBAAA,CACA,gBAAA,CAAA,iBAAA,CAIA,YAAA,CAGA,eAAA,CAVA,iBAAA,CAMA,wBAAA,CAAA,gBAAA,CAFA,uBAAA,CAHA,S9Bm5FJ,C8Br4FI,qCACE,uB9Bu4FN,C8Bn4FI,cArBF,sBAsBI,W9Bs4FJ,C8Bn4FI,wCACE,2B9Bq4FN,C8Bj4FI,6BAOE,qCAAA,CACA,+CAAA,CAAA,uC9Bs4FN,C8B53FI,yDAZE,UAAA,CADA,YAAA,CAKA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CACA,SAAA,CAEA,WAAA,CADA,U9B05FN,C8B34FI,4BAOE,oDAAA,CACA,4CAAA,CAAA,oCAAA,CAQA,uBAAA,CAJA,+C9B+3FN,C8Bx3FM,gDACE,uB9B03FR,C8Bt3FM,mFACE,0C9Bw3FR,CACF,C8Bn3FI,0CAGE,2BAAA,CADA,uBAAA,CADA,S9Bu3FN,C8Bj3FI,8CACE,oB9Bm3FN,C8Bh3FM,aAJF,8CASI,8CAAA,CACA,iBAAA,CAHA,gCAAA,CADA,eAAA,CADA,cAAA,CAGA,kB9Bq3FN,C8Bh3FM,oDACE,mC9Bk3FR,CACF,C8Bt2FE,gCAEE,iBAAA,CADA,e9B02FJ,C8Bt2FI,mCACE,iB9Bw2FN,C8Br2FM,oDAEE,a9Bo3FR,C8Bt3FM,oDAEE,c9Bo3FR,C8Bt3FM,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,CARA,S9Bm3FR,C+BnoGA,MACE,wBAAA,CACA,wB/BsoGF,C+BhoGA,aA+BE,kCAAA,CAAA,0BAAA,CAjBA,gCAAA,CADA,sCAAA,CAGA,SAAA,CADA,mBAAA,CAdA,iBAAA,CAGA,wDACE,CAgBF,4BAAA,CAGA,uEACE,CARF,uDACE,CANF,UAAA,CADA,S/BooGF,C+B7mGE,oBAuBE,8CAAA,CAAA,+CAAA,CADA,UAAA,CADA,aAAA,CAfA,gJACE,CANF,iBAAA,CAmBA,S/BimGJ,C+B1lGE,yBAGE,kEAAA,CAFA,gDAAA,CACA,6C/B6lGJ,C+BxlGE,4BAGE,qEAAA,CADA,8CAAA,CADA,6C/B4lGJ,C+BtlGE,qBAEE,SAAA,CAKA,uBAAA,CAJA,wEACE,CAHF,S/B2lGJ,C+BjlGE,oBAqBE,uBAAA,CAEA,2CAAA,CACA,mBAAA,CACA,8BAAA,CAnBA,0FACE,CAaF,eAAA,CADA,8BAAA,CAlBA,iBAAA,CAqBA,oB/BskGJ,C+BhkGI,uCAEE,YAAA,CADA,W/BmkGN,C+B9jGI,6CACE,oD/BgkGN,C+B7jGM,mDACE,0C/B+jGR,C+BvjGI,mCAwBE,eAAA,CACA,eAAA,CAxBA,oIACE,CAgBF,sCACE,CAIF,mBAAA,CAKA,wBAAA,CAAA,gBAAA,CAbA,sBAAA,CAAA,iB/BijGN,C+BhiGI,4CACE,Y/BkiGN,C+B9hGI,2CACE,e/BgiGN,CgCntGA,kBAME,ehC+tGF,CgCruGA,kBAME,gBhC+tGF,CgCruGA,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,ShCkuGF,CgC/sGE,aAtBF,QAuBI,YhCktGF,CACF,CgC/sGE,kBACE,wBhCitGJ,CgC7sGE,gBAEE,SAAA,CADA,mBAAA,CAGA,+BAAA,CADA,uBhCgtGJ,CgC5sGI,0BACE,8BhC8sGN,CgCzsGE,4BAEE,0CAAA,CADA,+BhC4sGJ,CgCvsGE,YACE,oBAAA,CACA,oBhCysGJ,CiC9vGA,oBACE,GACE,mBjCiwGF,CACF,CiCzvGA,MACE,wfjC2vGF,CiCrvGA,YACE,aAAA,CAEA,eAAA,CADA,ajCyvGF,CiCrvGE,+BAOE,kBAAA,CAAA,kBjCsvGJ,CiC7vGE,+BAOE,iBAAA,CAAA,mBjCsvGJ,CiC7vGE,qBAQE,aAAA,CACA,cAAA,CACA,YAAA,CATA,iBAAA,CAKA,UjCuvGJ,CiChvGI,qCAIE,iBjCwvGN,CiC5vGI,qCAIE,kBjCwvGN,CiC5vGI,2BAME,6BAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,WjC0vGN,CiC7uGE,mBACE,iBAAA,CACA,UjC+uGJ,CiC3uGE,kBAWE,2CAAA,CACA,mBAAA,CACA,8BAAA,CALA,gCAAA,CACA,oBAAA,CAHA,kBAAA,CAFA,YAAA,CAUA,SAAA,CAPA,aAAA,CAFA,SAAA,CAJA,iBAAA,CASA,4BAAA,CARA,UAAA,CAaA,+CACE,CAbF,SjCyvGJ,CiCxuGI,+EACE,gBAAA,CACA,SAAA,CACA,sCjC0uGN,CiCpuGI,qCAEE,oCACE,gCjCquGN,CiCjuGI,2CACE,cjCmuGN,CACF,CiC9tGE,kBACE,kBjCguGJ,CiC5tGE,4BAGE,kBAAA,CAAA,oBjCmuGJ,CiCtuGE,4BAGE,mBAAA,CAAA,mBjCmuGJ,CiCtuGE,kBAKE,cAAA,CAJA,aAAA,CAMA,YAAA,CADA,uBAAA,CAEA,2CACE,CALF,kBAAA,CAFA,UjCouGJ,CiCztGI,gDACE,+BjC2tGN,CiCvtGI,wBACE,qDjCytGN,CkC/zGA,MAEI,6VAAA,CAAA,uWAAA,CAAA,qPAAA,CAAA,2xBAAA,CAAA,qMAAA,CAAA,+aAAA,CAAA,2LAAA,CAAA,yPAAA,CAAA,2TAAA,CAAA,oaAAA,CAAA,2SAAA,CAAA,2LlCw1GJ,CkC50GE,4CAME,8CAAA,CACA,4BAAA,CACA,mBAAA,CACA,8BAAA,CAJA,mCAAA,CAJA,iBAAA,CAGA,gBAAA,CADA,iBAAA,CADA,eAAA,CASA,uBAAA,CADA,2BlCg1GJ,CkC50GI,aAdF,4CAeI,elC+0GJ,CACF,CkC50GI,sEACE,gClC80GN,CkCz0GI,gDACE,qBlC20GN,CkCv0GI,gIAEE,iBAAA,CADA,clC00GN,CkCr0GI,4FACE,iBlCu0GN,CkCn0GI,kFACE,elCq0GN,CkCj0GI,0FACE,YlCm0GN,CkC/zGI,8EACE,mBlCi0GN,CkC5zGE,sEAGE,iBAAA,CAAA,mBlCs0GJ,CkCz0GE,sEAGE,kBAAA,CAAA,kBlCs0GJ,CkCz0GE,sEASE,uBlCg0GJ,CkCz0GE,sEASE,wBlCg0GJ,CkCz0GE,sEAUE,4BlC+zGJ,CkCz0GE,4IAWE,6BlC8zGJ,CkCz0GE,sEAWE,4BlC8zGJ,CkCz0GE,kDAOE,0BAAA,CACA,WAAA,CAFA,eAAA,CADA,eAAA,CAHA,oBAAA,CAAA,iBAAA,CADA,iBlCw0GJ,CkC3zGI,kFACE,elC6zGN,CkCzzGI,oFAEE,UlCo0GN,CkCt0GI,oFAEE,WlCo0GN,CkCt0GI,gEAOE,wBhBiIU,CgBlIV,UAAA,CADA,WAAA,CAGA,kDAAA,CAAA,0CAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CACA,UlCk0GN,CkCvzGI,4DACE,4DlCyzGN,CkC3yGE,sDACE,oBlC8yGJ,CkC3yGI,gFACE,gClC6yGN,CkCxyGE,8DACE,0BlC2yGJ,CkCxyGI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClC0yGN,CkCtyGI,0EACE,alCwyGN,CkC7zGE,8DACE,oBlCg0GJ,CkC7zGI,wFACE,gClC+zGN,CkC1zGE,sEACE,0BlC6zGJ,CkC1zGI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClC4zGN,CkCxzGI,kFACE,alC0zGN,CkC/0GE,sDACE,oBlCk1GJ,CkC/0GI,gFACE,gClCi1GN,CkC50GE,8DACE,0BlC+0GJ,CkC50GI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClC80GN,CkC10GI,0EACE,alC40GN,CkCj2GE,oDACE,oBlCo2GJ,CkCj2GI,8EACE,gClCm2GN,CkC91GE,4DACE,0BlCi2GJ,CkC91GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClCg2GN,CkC51GI,wEACE,alC81GN,CkCn3GE,4DACE,oBlCs3GJ,CkCn3GI,sFACE,gClCq3GN,CkCh3GE,oEACE,0BlCm3GJ,CkCh3GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCk3GN,CkC92GI,gFACE,alCg3GN,CkCr4GE,8DACE,oBlCw4GJ,CkCr4GI,wFACE,gClCu4GN,CkCl4GE,sEACE,0BlCq4GJ,CkCl4GI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClCo4GN,CkCh4GI,kFACE,alCk4GN,CkCv5GE,4DACE,oBlC05GJ,CkCv5GI,sFACE,gClCy5GN,CkCp5GE,oEACE,0BlCu5GJ,CkCp5GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCs5GN,CkCl5GI,gFACE,alCo5GN,CkCz6GE,4DACE,oBlC46GJ,CkCz6GI,sFACE,gClC26GN,CkCt6GE,oEACE,0BlCy6GJ,CkCt6GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCw6GN,CkCp6GI,gFACE,alCs6GN,CkC37GE,0DACE,oBlC87GJ,CkC37GI,oFACE,gClC67GN,CkCx7GE,kEACE,0BlC27GJ,CkCx7GI,gFACE,wBAlBG,CAmBH,oDAAA,CAAA,4ClC07GN,CkCt7GI,8EACE,alCw7GN,CkC78GE,oDACE,oBlCg9GJ,CkC78GI,8EACE,gClC+8GN,CkC18GE,4DACE,0BlC68GJ,CkC18GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClC48GN,CkCx8GI,wEACE,alC08GN,CkC/9GE,4DACE,oBlCk+GJ,CkC/9GI,sFACE,gClCi+GN,CkC59GE,oEACE,0BlC+9GJ,CkC59GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClC89GN,CkC19GI,gFACE,alC49GN,CkCj/GE,wDACE,oBlCo/GJ,CkCj/GI,kFACE,gClCm/GN,CkC9+GE,gEACE,0BlCi/GJ,CkC9+GI,8EACE,wBAlBG,CAmBH,mDAAA,CAAA,2ClCg/GN,CkC5+GI,4EACE,alC8+GN,CmClpHA,MACE,qMnCqpHF,CmC5oHE,sBAEE,uCAAA,CADA,gBnCgpHJ,CmC5oHI,mCACE,anC8oHN,CmC/oHI,mCACE,cnC8oHN,CmC1oHM,4BACE,sBnC4oHR,CmCzoHQ,mCACE,gCnC2oHV,CmCvoHQ,2DACE,SAAA,CAEA,uBAAA,CADA,enC0oHV,CmCroHQ,yGACE,SAAA,CACA,uBnCuoHV,CmCnoHQ,yCACE,YnCqoHV,CmC9nHE,0BACE,eAAA,CACA,enCgoHJ,CmC7nHI,+BACE,oBnC+nHN,CmC1nHE,gDACE,YnC4nHJ,CmCxnHE,8BAIE,+BAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,SAAA,CAKA,4BAAA,CAJA,4DACE,CAHF,0BnC4nHJ,CmCnnHI,aAdF,8BAeI,+BAAA,CACA,SAAA,CACA,uBnCsnHJ,CACF,CmCnnHI,wCACE,6BnCqnHN,CmCjnHI,oCACE,+BnCmnHN,CmC/mHI,qCAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,YAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,WnCwnHN,CmC3mHQ,mDACE,oBnC6mHV,CoC3tHE,kCAEE,iBpCiuHJ,CoCnuHE,kCAEE,kBpCiuHJ,CoCnuHE,wBAGE,yCAAA,CAFA,oBAAA,CAGA,SAAA,CACA,mCpC8tHJ,CoCztHI,aAVF,wBAWI,YpC4tHJ,CACF,CoCxtHE,6FAEE,SAAA,CACA,mCpC0tHJ,CoCptHE,4FAEE,+BpCstHJ,CoCltHE,oBACE,yBAAA,CACA,uBAAA,CAGA,yEpCktHJ,CKnlHI,sC+BrHE,qDACE,uBpC2sHN,CACF,CoCtsHE,kEACE,yBpCwsHJ,CoCpsHE,sBACE,0BpCssHJ,CqCjwHE,2BACE,arCowHJ,CK/kHI,0CgCtLF,2BAKI,erCowHJ,CqCjwHI,6BACE,iBrCmwHN,CACF,CqC/vHI,6BAEE,0BAAA,CAAA,2BAAA,CADA,eAAA,CAEA,iBrCiwHN,CqC9vHM,2CACE,kBrCgwHR,CqC1vHI,6CACE,QrC4vHN,CsCxxHE,uBACE,4CtC4xHJ,CsCvxHE,8CAJE,kCAAA,CAAA,0BtC+xHJ,CsC3xHE,uBACE,4CtC0xHJ,CsCrxHE,4BAEE,kCAAA,CAAA,0BAAA,CADA,qCtCwxHJ,CsCpxHI,mCACE,atCsxHN,CsClxHI,kCACE,atCoxHN,CsC/wHE,0BAKE,eAAA,CAJA,aAAA,CAEA,YAAA,CACA,aAAA,CAFA,kBAAA,CAAA,mBtCoxHJ,CsC9wHI,uCACE,etCgxHN,CsC5wHI,sCACE,kBtC8wHN,CuC3zHA,MACE,oLvC8zHF,CuCrzHE,oBAGE,iBAAA,CAEA,gBAAA,CADA,avCuzHJ,CuCnzHI,wCACE,uBvCqzHN,CuCjzHI,gCAEE,eAAA,CADA,gBvCozHN,CuC7yHM,wCACE,mBvC+yHR,CuCzyHE,8BAKE,oBvC6yHJ,CuClzHE,8BAKE,mBvC6yHJ,CuClzHE,8BAUE,4BvCwyHJ,CuClzHE,4DAWE,6BvCuyHJ,CuClzHE,8BAWE,4BvCuyHJ,CuClzHE,oBASE,cAAA,CANA,aAAA,CACA,eAAA,CAIA,evC0yHJ,CuCpyHI,kCACE,uCAAA,CACA,oBvCsyHN,CuClyHI,wCAEE,uCAAA,CADA,YvCqyHN,CuChyHI,oCAEE,WvC6yHN,CuC/yHI,oCAEE,UvC6yHN,CuC/yHI,0BAOE,6BAAA,CADA,UAAA,CADA,WAAA,CAGA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CAUA,sBAAA,CADA,yBAAA,CARA,UvC2yHN,CuC/xHM,oCACE,wBvCiyHR,CuC5xHI,4BACE,YvC8xHN,CuCzxHI,4CACE,YvC2xHN,CwCr3HE,+DACE,sBAAA,CAEA,mBAAA,CACA,0BAAA,CACA,uBxCu3HJ,CwCp3HI,2EAGE,iBAAA,CADA,eAAA,CADA,yBxCw3HN,CwCj3HE,mEACE,0BxCm3HJ,CwC/2HE,oBACE,qBxCi3HJ,CwC72HE,gBACE,oBxC+2HJ,CwC32HE,gBACE,qBxC62HJ,CwCz2HE,iBACE,kBxC22HJ,CwCv2HE,kBACE,kBxCy2HJ,CyCl5HE,6BACE,sCzCq5HJ,CyCl5HE,cACE,yCzCo5HJ,CyCx4HE,sIACE,oCzC04HJ,CyCl4HE,2EACE,qCzCo4HJ,CyC13HE,wGACE,oCzC43HJ,CyCn3HE,yFACE,qCzCq3HJ,CyCh3HE,6BACE,kCzCk3HJ,CyC52HE,6CACE,sCzC82HJ,CyCv2HE,4DACE,sCzCy2HJ,CyCl2HE,4DACE,qCzCo2HJ,CyC31HE,yFACE,qCzC61HJ,CyCr1HE,2EACE,sCzCu1HJ,CyC50HE,wHACE,qCzC80HJ,CyCz0HE,8BAGE,mBAAA,CADA,gBAAA,CADA,gBzC60HJ,CyCx0HE,eACE,4CzC00HJ,CyCv0HE,eACE,4CzCy0HJ,CyCr0HE,gBAIE,+CAAA,CACA,kDAAA,CAJA,aAAA,CAEA,wBAAA,CADA,wBzC00HJ,CyCn0HE,yBAOE,wCAAA,CACA,+DAAA,CACA,4BAAA,CACA,6BAAA,CARA,iBAAA,CAGA,eAAA,CACA,eAAA,CAFA,cAAA,CADA,oCAAA,CAFA,iBzC80HJ,CyCl0HI,6BACE,YzCo0HN,CyCj0HM,kCACE,wBAAA,CACA,yBzCm0HR,CyC7zHE,iCAaE,wCAAA,CACA,+DAAA,CAJA,uCAAA,CACA,0BAAA,CALA,UAAA,CAJA,oBAAA,CAOA,2BAAA,CADA,2BAAA,CADA,2BAAA,CANA,eAAA,CAWA,wBAAA,CAAA,gBAAA,CAPA,SzCs0HJ,CyCpzHE,sBACE,iBAAA,CACA,iBzCszHJ,CyCjzHE,iCAKE,ezC+yHJ,CyC5yHI,sCACE,gBzC8yHN,CyC1yHI,gDACE,YzC4yHN,CyClyHA,gBACE,iBzCqyHF,CyCjyHE,yCACE,aAAA,CACA,SzCmyHJ,CyC9xHE,mBACE,YzCgyHJ,CyC3xHE,oBACE,QzC6xHJ,CyCzxHE,4BACE,WAAA,CACA,SAAA,CACA,ezC2xHJ,CyCxxHI,0CACE,YzC0xHN,CyCpxHE,yBAKE,wCAAA,CAEA,+BAAA,CADA,4BAAA,CAHA,eAAA,CADA,oDAAA,CAEA,wBAAA,CAAA,gBzCyxHJ,CyClxHE,2BAEE,+DAAA,CADA,2BzCqxHJ,CyCjxHI,+BACE,uCAAA,CACA,gBzCmxHN,CyC9wHE,sBACE,MAAA,CACA,WzCgxHJ,CyC3wHA,aACE,azC8wHF,CyCpwHE,4BAEE,aAAA,CADA,YzCwwHJ,CyCpwHI,wDAEE,2BAAA,CADA,wBzCuwHN,CyCjwHE,+BAKE,2CAAA,CAEA,+BAAA,CADA,gCAAA,CADA,sBAAA,CAHA,mBAAA,CACA,gBAAA,CAFA,azCywHJ,CyChwHI,qCAEE,UAAA,CACA,UAAA,CAFA,azCowHN,CK34HI,0CoCsJF,8BACE,iBzCyvHF,CyC/uHE,wSAGE,ezCqvHJ,CyCjvHE,sCAEE,mBAAA,CACA,eAAA,CADA,oBAAA,CADA,kBAAA,CAAA,mBzCqvHJ,CACF,C0CllII,yDAIE,+BAAA,CACA,8BAAA,CAFA,aAAA,CADA,QAAA,CADA,iB1CwlIN,C0ChlII,uBAEE,uCAAA,CADA,c1CmlIN,C0C9hIM,iHAEE,WAlDkB,CAiDlB,kB1CyiIR,C0C1iIM,6HAEE,WAlDkB,CAiDlB,kB1CqjIR,C0CtjIM,6HAEE,WAlDkB,CAiDlB,kB1CikIR,C0ClkIM,oHAEE,WAlDkB,CAiDlB,kB1C6kIR,C0C9kIM,0HAEE,WAlDkB,CAiDlB,kB1CylIR,C0C1lIM,uHAEE,WAlDkB,CAiDlB,kB1CqmIR,C0CtmIM,uHAEE,WAlDkB,CAiDlB,kB1CinIR,C0ClnIM,6HAEE,WAlDkB,CAiDlB,kB1C6nIR,C0C9nIM,yCAEE,WAlDkB,CAiDlB,kB1CioIR,C0CloIM,yCAEE,WAlDkB,CAiDlB,kB1CqoIR,C0CtoIM,0CAEE,WAlDkB,CAiDlB,kB1CyoIR,C0C1oIM,uCAEE,WAlDkB,CAiDlB,kB1C6oIR,C0C9oIM,wCAEE,WAlDkB,CAiDlB,kB1CipIR,C0ClpIM,sCAEE,WAlDkB,CAiDlB,kB1CqpIR,C0CtpIM,wCAEE,WAlDkB,CAiDlB,kB1CypIR,C0C1pIM,oCAEE,WAlDkB,CAiDlB,kB1C6pIR,C0C9pIM,2CAEE,WAlDkB,CAiDlB,kB1CiqIR,C0ClqIM,qCAEE,WAlDkB,CAiDlB,kB1CqqIR,C0CtqIM,oCAEE,WAlDkB,CAiDlB,kB1CyqIR,C0C1qIM,kCAEE,WAlDkB,CAiDlB,kB1C6qIR,C0C9qIM,qCAEE,WAlDkB,CAiDlB,kB1CirIR,C0ClrIM,mCAEE,WAlDkB,CAiDlB,kB1CqrIR,C0CtrIM,qCAEE,WAlDkB,CAiDlB,kB1CyrIR,C0C1rIM,wCAEE,WAlDkB,CAiDlB,kB1C6rIR,C0C9rIM,sCAEE,WAlDkB,CAiDlB,kB1CisIR,C0ClsIM,2CAEE,WAlDkB,CAiDlB,kB1CqsIR,C0C1rIM,iCAEE,WAPkB,CAMlB,iB1C6rIR,C0C9rIM,uCAEE,WAPkB,CAMlB,iB1CisIR,C0ClsIM,mCAEE,WAPkB,CAMlB,iB1CqsIR,C2CvxIA,MACE,2LAAA,CACA,yL3C0xIF,C2CjxIE,wBAKE,mBAAA,CAHA,YAAA,CACA,qBAAA,CACA,YAAA,CAHA,iB3CwxIJ,C2C9wII,8BAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,O3CkxIN,C2C7wIM,qCACE,0B3C+wIR,C2ClvIM,kEACE,0C3CovIR,C2C9uIE,2BAME,uBAAA,CADA,+DAAA,CAJA,YAAA,CACA,cAAA,CACA,aAAA,CACA,oB3CkvIJ,C2C7uII,aATF,2BAUI,gB3CgvIJ,CACF,C2C7uII,cAGE,+BACE,iB3C6uIN,C2C1uIM,sCAQE,qCAAA,CANA,QAAA,CAKA,UAAA,CAHA,aAAA,CAEA,UAAA,CAHA,MAAA,CAFA,iBAAA,CAaA,2CAAA,CALA,2DACE,CAGF,kDAAA,CARA,+B3CkvIR,CACF,C2CpuII,8CACE,Y3CsuIN,C2CluII,iCAUE,+BAAA,CACA,6BAAA,CALA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,gBAAA,CACA,eAAA,CAFA,8BAAA,CAMA,+BAAA,CAGA,2CACE,CANF,kBAAA,CALA,U3C8uIN,C2C/tIM,aAII,6CACE,O3C8tIV,C2C/tIQ,8CACE,O3CiuIV,C2CluIQ,8CACE,O3CouIV,C2CruIQ,8CACE,O3CuuIV,C2CxuIQ,8CACE,O3C0uIV,C2C3uIQ,8CACE,O3C6uIV,C2C9uIQ,8CACE,O3CgvIV,C2CjvIQ,8CACE,O3CmvIV,C2CpvIQ,8CACE,O3CsvIV,C2CvvIQ,+CACE,Q3CyvIV,C2C1vIQ,+CACE,Q3C4vIV,C2C7vIQ,+CACE,Q3C+vIV,C2ChwIQ,+CACE,Q3CkwIV,C2CnwIQ,+CACE,Q3CqwIV,C2CtwIQ,+CACE,Q3CwwIV,C2CzwIQ,+CACE,Q3C2wIV,C2C5wIQ,+CACE,Q3C8wIV,C2C/wIQ,+CACE,Q3CixIV,C2ClxIQ,+CACE,Q3CoxIV,C2CrxIQ,+CACE,Q3CuxIV,CACF,C2ClxIM,uCACE,gC3CoxIR,C2ChxIM,oDACE,a3CkxIR,C2C7wII,yCACE,S3C+wIN,C2C3wIM,2CACE,aAAA,CACA,8B3C6wIR,C2CvwIE,4BACE,U3CywIJ,C2CtwII,aAJF,4BAKI,gB3CywIJ,CACF,C2CrwIE,0BACE,Y3CuwIJ,C2CpwII,aAJF,0BAKI,a3CuwIJ,C2CnwIM,sCACE,O3CqwIR,C2CtwIM,uCACE,O3CwwIR,C2CzwIM,uCACE,O3C2wIR,C2C5wIM,uCACE,O3C8wIR,C2C/wIM,uCACE,O3CixIR,C2ClxIM,uCACE,O3CoxIR,C2CrxIM,uCACE,O3CuxIR,C2CxxIM,uCACE,O3C0xIR,C2C3xIM,uCACE,O3C6xIR,C2C9xIM,wCACE,Q3CgyIR,C2CjyIM,wCACE,Q3CmyIR,C2CpyIM,wCACE,Q3CsyIR,C2CvyIM,wCACE,Q3CyyIR,C2C1yIM,wCACE,Q3C4yIR,C2C7yIM,wCACE,Q3C+yIR,C2ChzIM,wCACE,Q3CkzIR,C2CnzIM,wCACE,Q3CqzIR,C2CtzIM,wCACE,Q3CwzIR,C2CzzIM,wCACE,Q3C2zIR,C2C5zIM,wCACE,Q3C8zIR,CACF,C2CxzII,+FAEE,Q3C0zIN,C2CvzIM,yGACE,wBAAA,CACA,yB3C0zIR,C2CjzIM,2DAEE,wBAAA,CACA,yBAAA,CAFA,Q3CqzIR,C2C9yIM,iEACE,Q3CgzIR,C2C7yIQ,qLAGE,wBAAA,CACA,yBAAA,CAFA,Q3CizIV,C2C3yIQ,6FACE,wBAAA,CACA,yB3C6yIV,C2CxyIM,yDACE,kB3C0yIR,C2CryII,sCACE,Q3CuyIN,C2ClyIE,2BAEE,iBAAA,CAOA,kBAAA,CAHA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,YAAA,CACA,gBAAA,CAEA,mBAAA,CAGA,gCAAA,CAPA,W3C2yIJ,C2CjyII,iCAEE,uDAAA,CADA,+B3CoyIN,C2C/xII,iCAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,8CAAA,CAAA,sCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,+CACE,CATF,U3CyyIN,C2C1xIE,4BAOE,yEACE,CANF,YAAA,CAGA,aAAA,CAFA,qBAAA,CAGA,mBAAA,CALA,iBAAA,CAYA,wBAAA,CATA,Y3CgyIJ,C2CpxII,sCACE,wB3CsxIN,C2ClxII,oCACE,S3CoxIN,C2ChxII,kCAGE,wEACE,CAFF,mBAAA,CADA,O3CoxIN,C2C1wIM,uDACE,8CAAA,CAAA,sC3C4wIR,CKn5II,0CsCqJF,wDAEE,kB3CowIF,C2CtwIA,wDAEE,mB3CowIF,C2CtwIA,8CAGE,eAAA,CAFA,eAAA,CAGA,iC3CkwIF,C2C9vIE,8DACE,mB3CiwIJ,C2ClwIE,8DACE,kB3CiwIJ,C2ClwIE,oDAEE,U3CgwIJ,C2C5vIE,8EAEE,kB3C+vIJ,C2CjwIE,8EAEE,mB3C+vIJ,C2CjwIE,8EAGE,kB3C8vIJ,C2CjwIE,8EAGE,mB3C8vIJ,C2CjwIE,oEACE,U3CgwIJ,C2C1vIE,8EAEE,mB3C6vIJ,C2C/vIE,8EAEE,kB3C6vIJ,C2C/vIE,8EAGE,mB3C4vIJ,C2C/vIE,8EAGE,kB3C4vIJ,C2C/vIE,oEACE,U3C8vIJ,CACF,C2ChvIE,cAHF,olDAII,gC3CmvIF,C2ChvIE,g8GACE,uC3CkvIJ,CACF,C2C7uIA,4sDACE,+B3CgvIF,C2C5uIA,wmDACE,a3C+uIF,C4CnnJA,MACE,qWAAA,CACA,8W5CsnJF,C4C7mJE,4BAEE,oBAAA,CADA,iB5CinJJ,C4C5mJI,sDAEE,S5C+mJN,C4CjnJI,sDAEE,U5C+mJN,C4CjnJI,4CACE,iBAAA,CAEA,S5C8mJN,C4CzmJE,+CAEE,SAAA,CADA,U5C4mJJ,C4CvmJE,kDAEE,W5CknJJ,C4CpnJE,kDAEE,Y5CknJJ,C4CpnJE,wCAOE,qDAAA,CADA,UAAA,CADA,aAAA,CAGA,0CAAA,CAAA,kCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,SAAA,CACA,Y5CgnJJ,C4CrmJE,gEACE,wB1B2Wa,C0B1Wb,mDAAA,CAAA,2C5CumJJ,C6CvpJA,aAQE,wBACE,Y7CspJF,CACF,C8ChqJA,QACE,8DAAA,CAGA,+CAAA,CACA,iEAAA,CACA,oDAAA,CACA,sDAAA,CACA,mDAAA,CAGA,qEAAA,CACA,qEAAA,CACA,wEAAA,CACA,0EAAA,CACA,wEAAA,CACA,yEAAA,CACA,kEAAA,CACA,+DAAA,CACA,oEAAA,CACA,oEAAA,CACA,mEAAA,CACA,gEAAA,CACA,uEAAA,CACA,mEAAA,CACA,qEAAA,CACA,oEAAA,CACA,gEAAA,CACA,wEAAA,CACA,qEAAA,CACA,+D9C8pJF,C8CxpJA,SAEE,kBAAA,CADA,Y9C4pJF,C+C9rJE,kBAUE,cAAA,CATA,YAAA,CACA,kEACE,CAQF,Y/C0rJJ,C+CtrJI,sDACE,gB/CwrJN,C+ClrJI,oFAKE,wDAAA,CACA,mBAAA,CAJA,aAAA,CAEA,QAAA,CADA,aAAA,CAIA,sC/CorJN,C+C/qJM,iOACE,kBAAA,CACA,8B/CkrJR,C+C9qJM,6FACE,iBAAA,CAAA,c/CirJR,C+C7qJM,2HACE,Y/CgrJR,C+C5qJM,wHACE,e/C+qJR,C+ChqJI,yMAGE,eAAA,CAAA,Y/CwqJN,C+C1pJI,ybAOE,W/CgqJN,C+C5pJI,8BACE,eAAA,CAAA,Y/C8pJN,CK1lJI,mC2ChKA,8BACE,UhDkwJJ,CgDnwJE,8BACE,WhDkwJJ,CgDnwJE,8BAGE,kBhDgwJJ,CgDnwJE,8BAGE,iBhDgwJJ,CgDnwJE,oBAKE,mBAAA,CADA,YAAA,CAFA,ahDiwJJ,CgD3vJI,kCACE,WhD8vJN,CgD/vJI,kCACE,UhD8vJN,CgD/vJI,kCAEE,iBAAA,CAAA,chD6vJN,CgD/vJI,kCAEE,aAAA,CAAA,kBhD6vJN,CACF","file":"main.css"} \ No newline at end of file diff --git a/main/assets/stylesheets/main.6f8fc17f.min.css b/main/assets/stylesheets/main.6f8fc17f.min.css new file mode 100644 index 000000000..a0d06b0eb --- /dev/null +++ b/main/assets/stylesheets/main.6f8fc17f.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 h5 code{text-transform:none}.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}.md-typeset ul[type]{list-style-type:revert-layer}[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}.md-typeset ol ol ol ol,.md-typeset ul ol ol ol{list-style-type:upper-alpha}.md-typeset ol ol ol ol ol,.md-typeset ul ol ol ol ol{list-style-type:upper-roman}.md-typeset ol[type],.md-typeset ul[type]{list-style-type:revert-layer}[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:-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{border-radius:100%;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)}.md-typeset .md-code__content{display:grid}@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)}@media print{.md-feedback{display:none}}.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;position:relative}.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{-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-style:normal;font-weight:400;outline:none;text-align:initial;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}:root{--md-tooltip-width:20rem;--md-tooltip-tail:0.3rem}.md-tooltip2{-webkit-backface-visibility:hidden;backface-visibility:hidden;color:var(--md-default-fg-color);font-family:var(--md-text-font-family);opacity:0;pointer-events:none;position:absolute;top:calc(var(--md-tooltip-host-y) + var(--md-tooltip-y));transform:translateY(-.4rem);transform-origin:calc(var(--md-tooltip-host-x) + var(--md-tooltip-x)) 0;transition:transform 0ms .25s,opacity .25s,z-index .25s;width:100%;z-index:0}.md-tooltip2:before{border-left:var(--md-tooltip-tail) solid #0000;border-right:var(--md-tooltip-tail) solid #0000;content:"";display:block;left:clamp(1.5 * .8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-tail),100vw - 2 * var(--md-tooltip-tail) - 1.5 * .8rem);position:absolute;z-index:1}.md-tooltip2--top:before{border-top:var(--md-tooltip-tail) solid var(--md-default-bg-color);bottom:calc(var(--md-tooltip-tail)*-1 + .025rem);filter:drop-shadow(0 1px 0 hsla(0,0%,0%,.05))}.md-tooltip2--bottom:before{border-bottom:var(--md-tooltip-tail) solid var(--md-default-bg-color);filter:drop-shadow(0 -1px 0 hsla(0,0%,0%,.05));top:calc(var(--md-tooltip-tail)*-1 + .025rem)}.md-tooltip2--active{opacity:1;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.5,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip2__inner{scrollbar-gutter:stable;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);max-height:40vh;max-width:calc(100vw - 1.6rem);position:relative;scrollbar-width:thin}.md-tooltip2__inner::-webkit-scrollbar{height:.2rem;width:.2rem}.md-tooltip2__inner::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-tooltip2__inner::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}[role=tooltip]>.md-tooltip2__inner{font-size:.5rem;font-weight:700;left:clamp(.8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-width)/2,100vw - var(--md-tooltip-width) - .8rem);max-width:min(100vw - 2 * .8rem,400px);padding:.2rem .4rem;-webkit-user-select:none;user-select:none;width:-moz-fit-content;width:fit-content}.md-tooltip2__inner.md-typeset>:first-child{margin-top:0}.md-tooltip2__inner.md-typeset>:last-child{margin-bottom:0}[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__alias{margin-left:.3rem;opacity:.7}.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: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]{display:initial}.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)}@media print{.giscus,[id=__comments]{display:none}}: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.6f8fc17f.min.css.map b/main/assets/stylesheets/main.6f8fc17f.min.css.map new file mode 100644 index 000000000..8ba5ce39e --- /dev/null +++ b/main/assets/stylesheets/main.6f8fc17f.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/_code.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/_tooltip2.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/_giscus.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,gBC4yCF,CC1zCA,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,wNAAA,CACA,gNAAA,CACA,iNJgIF,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,CIzGI,oBACE,mBJ2GN,CItGE,eAGE,+DAAA,CAFA,iBAAA,CACA,cJyGJ,CIpGE,cACE,+BAAA,CACA,qBJsGJ,CInGI,mCAEE,sBJoGN,CIhGI,wCACE,+BJkGN,CI/FM,kDACE,uDJiGR,CI5FI,mBACE,kBAAA,CACA,iCJ8FN,CI1FI,4BACE,uCAAA,CACA,oBJ4FN,CIvFE,iDAIE,6BAAA,CACA,aAAA,CAFA,2BJ2FJ,CItFI,aARF,iDASI,oBJ2FJ,CACF,CIvFE,iBAIE,wCAAA,CACA,mBAAA,CACA,kCAAA,CAAA,0BAAA,CAJA,eAAA,CADA,uBAAA,CAEA,qBJ4FJ,CItFI,qCAEE,uCAAA,CADA,YJyFN,CInFE,gBAEE,iBAAA,CACA,eAAA,CAFA,iBJuFJ,CIlFI,qBAWE,kCAAA,CAAA,0BAAA,CADA,eAAA,CATA,aAAA,CAEA,QAAA,CAMA,uCAAA,CALA,aAAA,CAFA,oCAAA,CAKA,yDAAA,CACA,oBAAA,CAFA,iBAAA,CADA,iBJ0FN,CIjFM,2BACE,+CJmFR,CI/EM,wCAEE,YAAA,CADA,WJkFR,CI7EM,8CACE,oDJ+ER,CI5EQ,oDACE,0CJ8EV,CIvEE,gBAOE,4CAAA,CACA,mBAAA,CACA,mKACE,CANF,gCAAA,CAHA,oBAAA,CAEA,eAAA,CADA,uBAAA,CAIA,uBAAA,CADA,qBJ6EJ,CIlEE,iBAGE,6CAAA,CACA,kCAAA,CAAA,0BAAA,CAHA,aAAA,CACA,qBJsEJ,CIhEE,iBAGE,6DAAA,CADA,WAAA,CADA,oBJoEJ,CI9DE,kBACE,WJgEJ,CI5DE,oDAEE,qBJ8DJ,CIhEE,oDAEE,sBJ8DJ,CI1DE,iCACE,kBJ+DJ,CIhEE,iCACE,mBJ+DJ,CIhEE,iCAIE,2DJ4DJ,CIhEE,iCAIE,4DJ4DJ,CIhEE,uBAGE,uCAAA,CADA,aAAA,CAAA,cJ8DJ,CIxDE,eACE,oBJ0DJ,CItDI,qBACE,4BJwDN,CInDE,kDAGE,kBJqDJ,CIxDE,kDAGE,mBJqDJ,CIxDE,8BAEE,SJsDJ,CIlDI,0DACE,iBJqDN,CIjDI,oCACE,2BJoDN,CIjDM,0CACE,2BJoDR,CIjDQ,gDACE,2BJoDV,CIjDU,sDACE,2BJoDZ,CI5CI,0CACE,4BJ+CN,CI3CI,wDACE,kBJ+CN,CIhDI,wDACE,mBJ+CN,CIhDI,oCAEE,kBJ8CN,CI3CM,kGAEE,aJ+CR,CI3CM,0DACE,eJ8CR,CI1CM,4HAEE,kBJ6CR,CI/CM,4HAEE,mBJ6CR,CI/CM,oFACE,kBAAA,CAAA,eJ8CR,CIvCE,yBAEE,mBJyCJ,CI3CE,yBAEE,oBJyCJ,CI3CE,eACE,mBAAA,CAAA,cJ0CJ,CIrCE,kDAIE,WAAA,CADA,cJwCJ,CIhCI,4BAEE,oBJkCN,CI9BI,6BAEE,oBJgCN,CI5BI,kCACE,YJ8BN,CIzBE,mBACE,iBAAA,CAGA,eAAA,CADA,cAAA,CAEA,iBAAA,CAHA,sBAAA,CAAA,iBJ8BJ,CIxBI,uBACE,aAAA,CACA,aJ0BN,CIrBE,uBAGE,iBAAA,CADA,eAAA,CADA,eJyBJ,CInBE,mBACE,cJqBJ,CIjBE,+BAME,2CAAA,CACA,iDAAA,CACA,mBAAA,CAPA,oBAAA,CAGA,gBAAA,CAFA,cAAA,CACA,aAAA,CAEA,iBJsBJ,CIhBI,aAXF,+BAYI,aJmBJ,CACF,CIdI,iCACE,gBJgBN,CITM,8FACE,YJWR,CIPM,4FACE,eJSR,CIJI,8FACE,eJMN,CIHM,kHACE,gBJKR,CIAI,kCAGE,eAAA,CAFA,cAAA,CACA,sBAAA,CAEA,kBJEN,CIEI,kCAGE,qDAAA,CAFA,sBAAA,CACA,kBJCN,CIII,wCACE,iCJFN,CIKM,8CACE,qDAAA,CACA,sDJHR,CIQI,iCACE,iBJNN,CIWE,wCACE,cJTJ,CIYI,wDAIE,gBJJN,CIAI,wDAIE,iBJJN,CIAI,8CAME,UAAA,CALA,oBAAA,CAEA,YAAA,CAIA,oDAAA,CAAA,4CAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CALA,0BAAA,CAHA,WJFN,CIcI,oDACE,oDJZN,CIgBI,mEACE,kDAAA,CACA,yDAAA,CAAA,iDJdN,CIkBI,oEACE,kDAAA,CACA,0DAAA,CAAA,kDJhBN,CIqBE,wBACE,iBAAA,CACA,eAAA,CACA,iBJnBJ,CIuBE,mBACE,oBAAA,CAEA,kBAAA,CADA,eJpBJ,CIwBI,aANF,mBAOI,aJrBJ,CACF,CIwBI,8BACE,aAAA,CAEA,QAAA,CACA,eAAA,CAFA,UJpBN,CKrWI,0CDwYF,uBACE,iBJ/BF,CIkCE,4BACE,eJhCJ,CACF,CMpiBE,uBAOE,kBAAA,CALA,aAAA,CACA,aAAA,CAEA,aAAA,CACA,eAAA,CALA,iBAAA,CAOA,sCACE,CALF,YN0iBJ,CMjiBI,2BACE,aNmiBN,CM/hBI,6BAME,+CAAA,CAFA,yCAAA,CAHA,eAAA,CACA,eAAA,CACA,kBAAA,CAEA,iBNkiBN,CM7hBI,6BAEE,aAAA,CADA,YNgiBN,CM1hBE,wBACE,kBN4hBJ,CMzhBI,4BAIE,kBAAA,CAHA,mCAAA,CAIA,uBNyhBN,CMrhBI,4DAEE,oBAAA,CADA,SNwhBN,CMphBM,oEACE,mBNshBR,CO/kBA,WAGE,0CAAA,CADA,+BAAA,CADA,aPolBF,CO/kBE,aANF,WAOI,YPklBF,CACF,CO/kBE,oBAEE,2CAAA,CADA,gCPklBJ,CO7kBE,kBAGE,eAAA,CADA,iBAAA,CADA,ePilBJ,CO3kBE,6BACE,WPglBJ,COjlBE,6BACE,UPglBJ,COjlBE,mBAEE,aAAA,CACA,cAAA,CACA,uBP6kBJ,CO1kBI,0BACE,YP4kBN,COxkBI,yBACE,UP0kBN,CQ/mBA,KASE,cAAA,CARA,WAAA,CACA,iBRmnBF,CK/cI,oCGtKJ,KAaI,gBR4mBF,CACF,CKpdI,oCGtKJ,KAkBI,cR4mBF,CACF,CQvmBA,KASE,2CAAA,CAPA,YAAA,CACA,qBAAA,CAKA,eAAA,CAHA,eAAA,CAJA,iBAAA,CAGA,UR6mBF,CQrmBE,aAZF,KAaI,aRwmBF,CACF,CKrdI,0CGhJF,yBAII,cRqmBJ,CACF,CQ5lBA,SAEE,gBAAA,CAAA,iBAAA,CADA,eRgmBF,CQ3lBA,cACE,YAAA,CAEA,qBAAA,CADA,WR+lBF,CQ3lBE,aANF,cAOI,aR8lBF,CACF,CQ1lBA,SACE,WR6lBF,CQ1lBE,gBACE,YAAA,CACA,WAAA,CACA,iBR4lBJ,CQvlBA,aACE,eAAA,CACA,sBR0lBF,CQjlBA,WACE,YRolBF,CQ/kBA,WAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,ORolBF,CQ/kBE,uCACE,aRilBJ,CQ7kBE,+BAEE,uCAAA,CADA,kBRglBJ,CQ1kBA,SASE,2CAAA,CACA,mBAAA,CAFA,gCAAA,CADA,gBAAA,CADA,YAAA,CAMA,SAAA,CADA,uCAAA,CANA,mBAAA,CAJA,cAAA,CAYA,2BAAA,CATA,URolBF,CQxkBE,eAEE,SAAA,CAIA,uBAAA,CAHA,oEACE,CAHF,UR6kBJ,CQ/jBA,MACE,WRkkBF,CS3tBA,MACE,6PT6tBF,CSvtBA,cASE,mBAAA,CAFA,0CAAA,CACA,cAAA,CAFA,YAAA,CAIA,uCAAA,CACA,oBAAA,CAVA,iBAAA,CAEA,UAAA,CADA,QAAA,CAUA,qBAAA,CAPA,WAAA,CADA,STkuBF,CSvtBE,aAfF,cAgBI,YT0tBF,CACF,CSvtBE,kCAEE,uCAAA,CADA,YT0tBJ,CSrtBE,qBACE,uCTutBJ,CSntBE,wCACE,+BTqtBJ,CShtBE,oBAME,6BAAA,CADA,UAAA,CAJA,aAAA,CAEA,cAAA,CACA,aAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,aT0tBJ,CS9sBE,sBACE,cTgtBJ,CS7sBI,2BACE,2CT+sBN,CSzsBI,kEAEE,uDAAA,CADA,+BT4sBN,CU9wBE,8BACE,YVixBJ,CWtxBA,mBACE,GACE,SAAA,CACA,0BXyxBF,CWtxBA,GACE,SAAA,CACA,uBXwxBF,CACF,CWpxBA,mBACE,GACE,SXsxBF,CWnxBA,GACE,SXqxBF,CACF,CW1wBE,qBASE,2BAAA,CAFA,mCAAA,CAAA,2BAAA,CADA,0BAAA,CADA,WAAA,CAGA,SAAA,CAPA,cAAA,CACA,KAAA,CAEA,UAAA,CADA,SXkxBJ,CWxwBE,mBAcE,mDAAA,CANA,2CAAA,CACA,QAAA,CACA,mBAAA,CARA,QAAA,CASA,kDACE,CAPF,eAAA,CAEA,aAAA,CADA,SAAA,CALA,cAAA,CAGA,UAAA,CADA,SXmxBJ,CWpwBE,kBACE,aXswBJ,CWlwBE,sBACE,YAAA,CACA,YXowBJ,CWjwBI,oCACE,aXmwBN,CW9vBE,sBACE,mBXgwBJ,CW7vBI,6CACE,cX+vBN,CKzpBI,0CMvGA,6CAKI,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,UXiwBN,CACF,CW1vBE,kBACE,cX4vBJ,CY71BA,YACE,WAAA,CAIA,WZ61BF,CY11BE,mBAEE,qBAAA,CADA,iBZ61BJ,CKhsBI,sCOtJE,4EACE,kBZy1BN,CYr1BI,0JACE,mBZu1BN,CYx1BI,8EACE,kBZu1BN,CACF,CYl1BI,0BAGE,UAAA,CAFA,aAAA,CACA,YZq1BN,CYh1BI,+BACE,eZk1BN,CY50BE,8BACE,WZi1BJ,CYl1BE,8BACE,UZi1BJ,CYl1BE,8BAIE,iBZ80BJ,CYl1BE,8BAIE,kBZ80BJ,CYl1BE,oBAGE,cAAA,CADA,SZg1BJ,CY30BI,aAPF,oBAQI,YZ80BJ,CACF,CY30BI,gCACE,yCZ60BN,CYz0BI,wBACE,cAAA,CACA,kBZ20BN,CYx0BM,kCACE,oBZ00BR,Ca34BA,qBAEE,Wby5BF,Ca35BA,qBAEE,Uby5BF,Ca35BA,WAQE,2CAAA,CACA,mBAAA,CANA,YAAA,CAOA,8BAAA,CALA,iBAAA,CAMA,SAAA,CALA,mBAAA,CACA,mBAAA,CANA,cAAA,CAcA,0BAAA,CAHA,wCACE,CATF,Sbu5BF,Caz4BE,aAlBF,WAmBI,Yb44BF,CACF,Caz4BE,mBAEE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,kEb44BJ,Car4BE,kBAEE,gCAAA,CADA,ebw4BJ,Cc16BA,aACE,gBAAA,CACA,iBd66BF,Cc16BE,sBAGE,WAAA,CADA,QAAA,CADA,Sd86BJ,Ccx6BE,oBAEE,eAAA,CADA,ed26BJ,Cct6BE,oBACE,iBdw6BJ,Ccp6BE,mBAEE,YAAA,CACA,cAAA,CACA,6BAAA,CAHA,iBdy6BJ,Ccn6BI,iDACE,yCdq6BN,Ccj6BI,6BACE,iBdm6BN,Cc95BE,mBAGE,uCAAA,CACA,cAAA,CAHA,aAAA,CACA,cAAA,CAGA,sBdg6BJ,Cc75BI,gDACE,+Bd+5BN,Cc35BI,4BACE,0CAAA,CACA,mBd65BN,Ccx5BE,mBAEE,SAAA,CADA,iBAAA,CAKA,2BAAA,CAHA,8Dd25BJ,Ccr5BI,qBAEE,aAAA,CADA,edw5BN,Ccn5BI,6BACE,SAAA,CACA,uBdq5BN,Cch5BE,aAnFF,aAoFI,Ydm5BF,CACF,Cex+BA,WAEE,0CAAA,CADA,+Bf4+BF,Cex+BE,aALF,WAMI,Yf2+BF,CACF,Cex+BE,kBACE,6BAAA,CAEA,aAAA,CADA,af2+BJ,Cev+BI,gCACE,Yfy+BN,Cep+BE,iBAOE,eAAA,CANA,YAAA,CAKA,cAAA,CAGA,mBAAA,CAAA,eAAA,CADA,cAAA,CAGA,uCAAA,CADA,eAAA,CAEA,uBfk+BJ,Ce/9BI,8CACE,Ufi+BN,Ce79BI,+BACE,oBf+9BN,CKj1BI,0CUvIE,uBACE,af29BN,Cex9BM,yCACE,Yf09BR,CACF,Cer9BI,iCACE,gBfw9BN,Cez9BI,iCACE,iBfw9BN,Cez9BI,uBAEE,gBfu9BN,Cep9BM,iCACE,efs9BR,Ceh9BE,kBACE,WAAA,CAIA,eAAA,CADA,mBAAA,CAFA,6BAAA,CACA,cAAA,CAGA,kBfk9BJ,Ce98BE,mBAEE,YAAA,CADA,afi9BJ,Ce58BE,sBACE,gBAAA,CACA,Uf88BJ,Cez8BA,gBACE,gDf48BF,Cez8BE,uBACE,YAAA,CACA,cAAA,CACA,6BAAA,CACA,af28BJ,Cev8BE,kCACE,sCfy8BJ,Cet8BI,gFACE,+Bfw8BN,Ceh8BA,cAKE,wCAAA,CADA,gBAAA,CADA,iBAAA,CADA,eAAA,CADA,Ufu8BF,CK35BI,mCU7CJ,cASI,Ufm8BF,CACF,Ce/7BE,yBACE,sCfi8BJ,Ce17BA,WACE,mBAAA,CACA,SAAA,CAEA,cAAA,CADA,qBf87BF,CK16BI,mCUvBJ,WAQI,ef67BF,CACF,Ce17BE,iBACE,oBAAA,CAEA,aAAA,CACA,iBAAA,CAFA,Yf87BJ,Cez7BI,wBACE,ef27BN,Cev7BI,qBAGE,iBAAA,CAFA,gBAAA,CACA,mBf07BN,CgBhmCE,uBAME,kBAAA,CACA,mBAAA,CAHA,gCAAA,CACA,cAAA,CAJA,oBAAA,CAEA,eAAA,CADA,kBAAA,CAMA,gEhBmmCJ,CgB7lCI,gCAEE,2CAAA,CACA,uCAAA,CAFA,gChBimCN,CgB3lCI,0DAEE,0CAAA,CACA,sCAAA,CAFA,+BhB+lCN,CgBxlCE,gCAKE,4BhB6lCJ,CgBlmCE,gEAME,6BhB4lCJ,CgBlmCE,gCAME,4BhB4lCJ,CgBlmCE,sBAIE,6DAAA,CAGA,8BAAA,CAJA,eAAA,CAFA,aAAA,CACA,eAAA,CAMA,sChB0lCJ,CgBrlCI,wDACE,6CAAA,CACA,8BhBulCN,CgBnlCI,+BACE,UhBqlCN,CiBxoCA,WAOE,2CAAA,CAGA,8CACE,CALF,gCAAA,CADA,aAAA,CAHA,MAAA,CADA,eAAA,CACA,OAAA,CACA,KAAA,CACA,SjB+oCF,CiBpoCE,aAfF,WAgBI,YjBuoCF,CACF,CiBpoCE,mBAIE,2BAAA,CAHA,iEjBuoCJ,CiBhoCE,mBACE,kDACE,CAEF,kEjBgoCJ,CiB1nCE,kBAEE,kBAAA,CADA,YAAA,CAEA,ejB4nCJ,CiBxnCE,mBAKE,kBAAA,CAEA,cAAA,CAHA,YAAA,CAIA,uCAAA,CALA,aAAA,CAFA,iBAAA,CAQA,uBAAA,CAHA,qBAAA,CAJA,SjBioCJ,CiBvnCI,yBACE,UjBynCN,CiBrnCI,iCACE,oBjBunCN,CiBnnCI,uCAEE,uCAAA,CADA,YjBsnCN,CiBjnCI,2BAEE,YAAA,CADA,ajBonCN,CKtgCI,0CY/GA,2BAMI,YjBmnCN,CACF,CiBhnCM,8DAIE,iBAAA,CAHA,aAAA,CAEA,aAAA,CADA,UjBonCR,CKpiCI,mCYzEA,iCAII,YjB6mCN,CACF,CiB1mCM,wCACE,YjB4mCR,CiBxmCM,+CACE,oBjB0mCR,CK/iCI,sCYtDA,iCAII,YjBqmCN,CACF,CiBhmCE,kBAEE,YAAA,CACA,cAAA,CAFA,iBAAA,CAIA,8DACE,CAFF,kBjBmmCJ,CiB7lCI,oCAGE,SAAA,CADA,mBAAA,CAKA,6BAAA,CAHA,8DACE,CAJF,UjBmmCN,CiB1lCM,8CACE,8BjB4lCR,CiBvlCI,8BACE,ejBylCN,CiBplCE,4BAGE,gBAAA,CAAA,kBjBwlCJ,CiB3lCE,4BAGE,iBAAA,CAAA,iBjBwlCJ,CiB3lCE,kBACE,WAAA,CAGA,eAAA,CAFA,aAAA,CAGA,kBjBslCJ,CiBnlCI,4CAGE,SAAA,CADA,mBAAA,CAKA,8BAAA,CAHA,8DACE,CAJF,UjBylCN,CiBhlCM,sDACE,6BjBklCR,CiB9kCM,8DAGE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,8DACE,CAJF,SjBolCR,CiBzkCI,uCAGE,WAAA,CAFA,iBAAA,CACA,UjB4kCN,CiBtkCE,mBACE,YAAA,CACA,aAAA,CACA,cAAA,CAEA,+CACE,CAFF,kBjBykCJ,CiBnkCI,8DACE,WAAA,CACA,SAAA,CACA,oCjBqkCN,CiB5jCI,yBACE,QjB8jCN,CiBzjCE,mBACE,YjB2jCJ,CKvnCI,mCY2DF,6BAQI,gBjB2jCJ,CiBnkCA,6BAQI,iBjB2jCJ,CiBnkCA,mBAKI,aAAA,CAEA,iBAAA,CADA,ajB6jCJ,CACF,CK/nCI,sCY2DF,6BAaI,kBjB2jCJ,CiBxkCA,6BAaI,mBjB2jCJ,CACF,CD1yCA,SAGE,uCAAA,CAFA,eAAA,CACA,eC8yCF,CD1yCE,eACE,mBAAA,CACA,cAAA,CAGA,eAAA,CADA,QAAA,CADA,SC8yCJ,CDxyCE,sCAEE,WAAA,CADA,iBAAA,CAAA,kBC2yCJ,CDtyCE,eACE,+BCwyCJ,CDryCI,0CACE,+BCuyCN,CDjyCA,UAKE,wBmBaa,CnBZb,oBAAA,CAFA,UAAA,CAHA,oBAAA,CAEA,eAAA,CADA,0BAAA,CAAA,2BCwyCF,CmB10CA,MACE,uMAAA,CACA,sLAAA,CACA,iNnB60CF,CmBv0CA,QACE,eAAA,CACA,enB00CF,CmBv0CE,eAKE,uCAAA,CAJA,aAAA,CAGA,eAAA,CADA,eAAA,CADA,eAAA,CAIA,sBnBy0CJ,CmBt0CI,+BACE,YnBw0CN,CmBr0CM,mCAEE,WAAA,CADA,UnBw0CR,CmBh0CQ,sFAME,iBAAA,CALA,aAAA,CAGA,aAAA,CADA,cAAA,CAEA,kBAAA,CAHA,UnBs0CV,CmB3zCE,cAGE,eAAA,CADA,QAAA,CADA,SnB+zCJ,CmBzzCE,cAGE,sBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBAAA,CACA,uBAAA,CACA,sBnB2zCJ,CmBxzCI,sBACE,uCnB0zCN,CmBnzCM,6EAEE,+BnBqzCR,CmBhzCI,2BAIE,iBnB+yCN,CmB3yCI,4CACE,gBnB6yCN,CmB9yCI,4CACE,iBnB6yCN,CmBzyCI,kBAME,iBAAA,CAFA,aAAA,CACA,YAAA,CAFA,iBnB4yCN,CmBryCI,sGACE,+BAAA,CACA,cnBuyCN,CmBnyCI,4BACE,uCAAA,CACA,oBnBqyCN,CmBjyCI,0CACE,YnBmyCN,CmBhyCM,yDAIE,6BAAA,CAHA,aAAA,CAEA,WAAA,CAEA,qCAAA,CAAA,6BAAA,CAHA,UnBqyCR,CmB9xCM,kDACE,YnBgyCR,CmB1xCE,iCACE,YnB4xCJ,CmBzxCI,6CACE,WAAA,CAGA,WnByxCN,CmBpxCE,cACE,anBsxCJ,CmBlxCE,gBACE,YnBoxCJ,CKrvCI,0CcxBA,0CASE,2CAAA,CAHA,YAAA,CACA,qBAAA,CACA,WAAA,CALA,MAAA,CADA,iBAAA,CACA,OAAA,CACA,KAAA,CACA,SnBmxCJ,CmBxwCI,+DACE,eAAA,CACA,enB0wCN,CmBtwCI,gCAQE,qDAAA,CAHA,uCAAA,CAEA,cAAA,CALA,aAAA,CAEA,kBAAA,CADA,wBAAA,CAFA,iBAAA,CAKA,kBnB0wCN,CmBrwCM,wDAEE,UnB4wCR,CmB9wCM,wDAEE,WnB4wCR,CmB9wCM,8CAIE,aAAA,CAEA,aAAA,CACA,YAAA,CANA,iBAAA,CAEA,SAAA,CAEA,YnBywCR,CmBpwCQ,oDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnB6wCV,CmBjwCM,8CAIE,2CAAA,CACA,gEACE,CALF,eAAA,CAEA,4BAAA,CADA,kBnBswCR,CmB/vCQ,2DACE,YnBiwCV,CmB5vCM,8CAGE,2CAAA,CADA,gCAAA,CADA,enBgwCR,CmB1vCM,yCAIE,aAAA,CAFA,UAAA,CAIA,YAAA,CADA,aAAA,CAJA,iBAAA,CACA,WAAA,CACA,SnB+vCR,CmBvvCI,+BACE,MnByvCN,CmBrvCI,+BACE,4DnBuvCN,CmBpvCM,qDACE,+BnBsvCR,CmBnvCQ,sHACE,+BnBqvCV,CmB/uCI,+BAEE,YAAA,CADA,mBnBkvCN,CmB9uCM,mCACE,enBgvCR,CmB5uCM,6CACE,SnB8uCR,CmB1uCM,uDAGE,mBnB6uCR,CmBhvCM,uDAGE,kBnB6uCR,CmBhvCM,6CAIE,gBAAA,CAFA,aAAA,CADA,YnB+uCR,CmBzuCQ,mDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnBkvCV,CmBluCM,+CACE,mBnBouCR,CmB5tCM,4CAEE,wBAAA,CADA,enB+tCR,CmB3tCQ,oEACE,mBnB6tCV,CmB9tCQ,oEACE,oBnB6tCV,CmBztCQ,4EACE,iBnB2tCV,CmB5tCQ,4EACE,kBnB2tCV,CmBvtCQ,oFACE,mBnBytCV,CmB1tCQ,oFACE,oBnBytCV,CmBrtCQ,4FACE,mBnButCV,CmBxtCQ,4FACE,oBnButCV,CmBhtCE,mBACE,wBnBktCJ,CmB9sCE,wBACE,YAAA,CACA,SAAA,CAIA,0BAAA,CAHA,oEnBitCJ,CmB3sCI,kCACE,2BnB6sCN,CmBxsCE,gCACE,SAAA,CAIA,uBAAA,CAHA,qEnB2sCJ,CmBrsCI,8CAEE,kCAAA,CAAA,0BnBssCN,CACF,CKx4CI,0Cc0MA,0CACE,YnBisCJ,CmB9rCI,yDACE,UnBgsCN,CmB5rCI,wDACE,YnB8rCN,CmB1rCI,kDACE,YnB4rCN,CmBvrCE,gBAIE,iDAAA,CADA,gCAAA,CAFA,aAAA,CACA,enB2rCJ,CACF,CKr8CM,+DcmRF,6CACE,YnBqrCJ,CmBlrCI,4DACE,UnBorCN,CmBhrCI,2DACE,YnBkrCN,CmB9qCI,qDACE,YnBgrCN,CACF,CK77CI,mCc7JJ,QAgbI,oBnB8qCF,CmBxqCI,kCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnB0qCN,CmBrqCM,6CACE,uBnBuqCR,CmBnqCM,gDACE,YnBqqCR,CmBhqCI,2CACE,kBnBmqCN,CmBpqCI,2CACE,mBnBmqCN,CmBpqCI,iCAEE,oBnBkqCN,CmB3pCI,yDACE,kBnB6pCN,CmB9pCI,yDACE,iBnB6pCN,CACF,CKt9CI,sCc7JJ,QA4dI,oBAAA,CACA,oDnB2pCF,CmBrpCI,gCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnBupCN,CmBlpCM,8CACE,uBnBopCR,CmBhpCM,8CACE,YnBkpCR,CmB7oCI,yCACE,kBnBgpCN,CmBjpCI,yCACE,mBnBgpCN,CmBjpCI,+BAEE,oBnB+oCN,CmBxoCI,uDACE,kBnB0oCN,CmB3oCI,uDACE,iBnB0oCN,CmBroCE,wBACE,YAAA,CACA,sBAAA,CAEA,SAAA,CACA,6FACE,CAHF,mBnByoCJ,CmBjoCI,sCACE,enBmoCN,CmB9nCE,iFACE,sBAAA,CAEA,SAAA,CACA,4FACE,CAHF,kBnBkoCJ,CmBznCE,iDACE,enB2nCJ,CmBvnCE,6CACE,YnBynCJ,CmBrnCE,uBACE,aAAA,CACA,enBunCJ,CmBpnCI,kCACE,enBsnCN,CmBlnCI,qCACE,enBonCN,CmBjnCM,0CACE,uCnBmnCR,CmB/mCM,6DACE,mBnBinCR,CmB7mCM,yFAEE,YnB+mCR,CmB1mCI,yCAEE,kBnB8mCN,CmBhnCI,yCAEE,mBnB8mCN,CmBhnCI,+BACE,aAAA,CAGA,SAAA,CADA,kBnB6mCN,CmBzmCM,2DACE,SnB2mCR,CmBrmCE,cAGE,kBAAA,CADA,YAAA,CAEA,gCAAA,CAHA,WnB0mCJ,CmBpmCI,oBACE,uDnBsmCN,CmBlmCI,oBAME,6BAAA,CACA,kBAAA,CAFA,UAAA,CAJA,oBAAA,CAEA,WAAA,CAKA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,yBAAA,CARA,qBAAA,CAFA,UnB8mCN,CmBjmCM,8BACE,wBnBmmCR,CmB/lCM,kKAEE,uBnBgmCR,CmBllCI,2EACE,YnBulCN,CmBplCM,oDACE,anBslCR,CmBnlCQ,kEAKE,qCAAA,CACA,qDAAA,CAFA,YAAA,CAHA,eAAA,CACA,KAAA,CACA,SnBwlCV,CmBllCU,0FACE,mBnBolCZ,CmB/kCQ,0EACE,QnBilCV,CmB5kCM,sFACE,kBnB8kCR,CmB/kCM,sFACE,mBnB8kCR,CmB1kCM,kDACE,uCnB4kCR,CmBtkCI,2CACE,sBAAA,CAEA,SAAA,CADA,kBnBykCN,CmBhkCI,qFAIE,mDnBmkCN,CmBvkCI,qFAIE,oDnBmkCN,CmBvkCI,2EACE,aAAA,CACA,oBAAA,CAGA,SAAA,CAFA,kBnBokCN,CmB/jCM,yFAEE,gBAAA,CADA,gBnBkkCR,CmB7jCM,0FACE,YnB+jCR,CACF,CoBtxDA,eAKE,eAAA,CACA,eAAA,CAJA,SpB6xDF,CoBtxDE,gCANA,kBAAA,CAFA,YAAA,CAGA,sBpBoyDF,CoB/xDE,iBAOE,mBAAA,CAFA,aAAA,CADA,gBAAA,CAEA,iBpByxDJ,CoBpxDE,wBAEE,qDAAA,CADA,uCpBuxDJ,CoBlxDE,qBACE,6CpBoxDJ,CoB/wDI,sDAEE,uDAAA,CADA,+BpBkxDN,CoB9wDM,8DACE,+BpBgxDR,CoB3wDI,mCACE,uCAAA,CACA,oBpB6wDN,CoBzwDI,yBAKE,iBAAA,CADA,yCAAA,CAHA,aAAA,CAEA,eAAA,CADA,YpB8wDN,CqB9zDE,eAGE,+DAAA,CADA,oBAAA,CADA,qBrBm0DJ,CK9oDI,0CgBtLF,eAOI,YrBi0DJ,CACF,CqB3zDM,6BACE,oBrB6zDR,CqBvzDE,kBACE,YAAA,CACA,qBAAA,CACA,SAAA,CACA,qBrByzDJ,CqBlzDI,0BACE,sBrBozDN,CqBjzDM,gEACE,+BrBmzDR,CqB7yDE,gBAEE,uCAAA,CADA,erBgzDJ,CqB3yDE,kBACE,oBrB6yDJ,CqB1yDI,mCAGE,kBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBrB4yDN,CqBxyDI,oCAIE,kBAAA,CAHA,mBAAA,CACA,kBAAA,CACA,SAAA,CAGA,QAAA,CADA,iBrB2yDN,CqBtyDI,0DACE,kBrBwyDN,CqBzyDI,0DACE,iBrBwyDN,CqBpyDI,iDACE,uBAAA,CAEA,YrBqyDN,CqBhyDE,4BACE,YrBkyDJ,CqB3xDA,YAGE,kBAAA,CAFA,YAAA,CAIA,eAAA,CAHA,SAAA,CAIA,eAAA,CAFA,UrBgyDF,CqB3xDE,yBACE,WrB6xDJ,CqBtxDA,kBACE,YrByxDF,CKjtDI,0CgBzEJ,kBAKI,wBrByxDF,CACF,CqBtxDE,qCACE,WrBwxDJ,CK5uDI,sCgB7CF,+CAKI,kBrBwxDJ,CqB7xDA,+CAKI,mBrBwxDJ,CACF,CK9tDI,0CgBrDJ,6BAMI,SAAA,CAFA,eAAA,CACA,UrBqxDF,CqBlxDE,qDACE,gBrBoxDJ,CqBjxDE,gDACE,SrBmxDJ,CqBhxDE,4CACE,iBAAA,CAAA,kBrBkxDJ,CqB/wDE,2CAEE,WAAA,CADA,crBkxDJ,CqB9wDE,2CACE,mBAAA,CACA,cAAA,CACA,SAAA,CACA,oBAAA,CAAA,iBrBgxDJ,CqB7wDE,2CACE,SrB+wDJ,CqB5wDE,qCAEE,WAAA,CACA,eAAA,CAFA,erBgxDJ,CACF,CsB17DA,MACE,qBAAA,CACA,yBtB67DF,CsBv7DA,aAME,qCAAA,CADA,cAAA,CAEA,0FACE,CAPF,cAAA,CACA,KAAA,CAaA,mDAAA,CACA,qBAAA,CAJA,wFACE,CATF,UAAA,CADA,StBi8DF,CuB58DA,MACE,mfvB+8DF,CuBz8DA,WACE,iBvB48DF,CK9yDI,mCkB/JJ,WAKI,evB48DF,CACF,CuBz8DE,kBACE,YvB28DJ,CuBv8DE,oBAEE,SAAA,CADA,SvB08DJ,CKvyDI,0CkBpKF,8BAOI,YvBk9DJ,CuBz9DA,8BAOI,avBk9DJ,CuBz9DA,oBAaI,2CAAA,CACA,kBAAA,CAJA,WAAA,CACA,eAAA,CACA,mBAAA,CANA,iBAAA,CAEA,SAAA,CAUA,uBAAA,CAHA,4CACE,CAPF,UvBg9DJ,CuBp8DI,+DACE,SAAA,CACA,oCvBs8DN,CACF,CK70DI,mCkBjJF,8BAgCI,MvBy8DJ,CuBz+DA,8BAgCI,OvBy8DJ,CuBz+DA,oBAqCI,0BAAA,CADA,cAAA,CADA,QAAA,CAJA,cAAA,CAEA,KAAA,CAKA,sDACE,CALF,OvBu8DJ,CuB77DI,+DAME,YAAA,CACA,SAAA,CACA,4CACE,CARF,UvBk8DN,CACF,CK50DI,0CkBxGA,+DAII,mBvBo7DN,CACF,CK13DM,+DkB/DF,+DASI,mBvBo7DN,CACF,CK/3DM,+DkB/DF,+DAcI,mBvBo7DN,CACF,CuB/6DE,kBAEE,kCAAA,CAAA,0BvBg7DJ,CK91DI,0CkBpFF,4BAOI,MvBw7DJ,CuB/7DA,4BAOI,OvBw7DJ,CuB/7DA,kBAWI,QAAA,CAEA,SAAA,CADA,eAAA,CANA,cAAA,CAEA,KAAA,CAWA,wBAAA,CALA,qGACE,CALF,OAAA,CADA,SvBs7DJ,CuBz6DI,4BACE,yBvB26DN,CuBv6DI,6DAEE,WAAA,CACA,SAAA,CAMA,uBAAA,CALA,sGACE,CAJF,UvB66DN,CACF,CKz4DI,mCkBjEF,4BA2CI,WvBu6DJ,CuBl9DA,4BA2CI,UvBu6DJ,CuBl9DA,kBA6CI,eAAA,CAHA,iBAAA,CAIA,8CAAA,CAFA,avBs6DJ,CACF,CKx6DM,+DkBOF,6DAII,avBi6DN,CACF,CKv5DI,sCkBfA,6DASI,avBi6DN,CACF,CuB55DE,iBAIE,2CAAA,CACA,0BAAA,CAFA,aAAA,CAFA,iBAAA,CAKA,2CACE,CALF,SvBk6DJ,CKp6DI,mCkBAF,iBAaI,0BAAA,CACA,mBAAA,CAFA,avB85DJ,CuBz5DI,uBACE,0BvB25DN,CACF,CuBv5DI,4DAEE,2CAAA,CACA,6BAAA,CACA,8BAAA,CAHA,gCvB45DN,CuBp5DE,4BAKE,mBAAA,CAAA,oBvBy5DJ,CuB95DE,4BAKE,mBAAA,CAAA,oBvBy5DJ,CuB95DE,kBAQE,gBAAA,CAFA,eAAA,CAFA,WAAA,CAHA,iBAAA,CAMA,sBAAA,CAJA,UAAA,CADA,SvB45DJ,CuBn5DI,+BACE,qBvBq5DN,CuBj5DI,kEAEE,uCvBk5DN,CuB94DI,6BACE,YvBg5DN,CKp7DI,0CkBaF,kBA8BI,eAAA,CADA,aAAA,CADA,UvBi5DJ,CACF,CK98DI,mCkBgCF,4BAmCI,mBvBi5DJ,CuBp7DA,4BAmCI,oBvBi5DJ,CuBp7DA,kBAqCI,aAAA,CADA,evBg5DJ,CuB54DI,+BACE,uCvB84DN,CuB14DI,mCACE,gCvB44DN,CuBx4DI,6DACE,kBvB04DN,CuBv4DM,8EACE,uCvBy4DR,CuBr4DM,0EACE,WvBu4DR,CACF,CuBj4DE,iBAIE,cAAA,CAHA,oBAAA,CAEA,aAAA,CAEA,kCACE,CAJF,YvBs4DJ,CuB93DI,uBACE,UvBg4DN,CuB53DI,yCAEE,UvBg4DN,CuBl4DI,yCAEE,WvBg4DN,CuBl4DI,+BACE,iBAAA,CAEA,SAAA,CACA,SvB83DN,CuB33DM,6CACE,oBvB63DR,CKp+DI,0CkB+FA,yCAaI,UvB63DN,CuB14DE,yCAaI,WvB63DN,CuB14DE,+BAcI,SvB43DN,CuBz3DM,+CACE,YvB23DR,CACF,CKhgEI,mCkBkHA,+BAwBI,mBvB03DN,CuBv3DM,8CACE,YvBy3DR,CACF,CuBn3DE,8BAEE,WvBw3DJ,CuB13DE,8BAEE,UvBw3DJ,CuB13DE,oBAKE,mBAAA,CAJA,iBAAA,CAEA,SAAA,CACA,SvBs3DJ,CK5/DI,0CkBkIF,8BASI,WvBs3DJ,CuB/3DA,8BASI,UvBs3DJ,CuB/3DA,oBAUI,SvBq3DJ,CACF,CuBl3DI,uCACE,iBvBw3DN,CuBz3DI,uCACE,kBvBw3DN,CuBz3DI,6BAEE,uCAAA,CACA,SAAA,CAIA,oBAAA,CAHA,+DvBq3DN,CuB/2DM,iDAEE,uCAAA,CADA,YvBk3DR,CuB72DM,gGAGE,SAAA,CADA,mBAAA,CAEA,kBvB82DR,CuB32DQ,sGACE,UvB62DV,CuBt2DE,8BAOE,mBAAA,CAAA,oBvB62DJ,CuBp3DE,8BAOE,mBAAA,CAAA,oBvB62DJ,CuBp3DE,oBAIE,kBAAA,CAKA,yCAAA,CANA,YAAA,CAKA,eAAA,CAFA,WAAA,CAKA,SAAA,CAVA,iBAAA,CACA,KAAA,CAUA,uBAAA,CAFA,kBAAA,CALA,UvB+2DJ,CKtjEI,mCkBkMF,8BAgBI,mBvBy2DJ,CuBz3DA,8BAgBI,oBvBy2DJ,CuBz3DA,oBAiBI,evBw2DJ,CACF,CuBr2DI,+DACE,SAAA,CACA,0BvBu2DN,CuBl2DE,6BAKE,+BvBq2DJ,CuB12DE,0DAME,gCvBo2DJ,CuB12DE,6BAME,+BvBo2DJ,CuB12DE,mBAIE,eAAA,CAHA,iBAAA,CAEA,UAAA,CADA,SvBw2DJ,CKrjEI,0CkB2MF,mBAWI,QAAA,CADA,UvBq2DJ,CACF,CK9kEI,mCkB8NF,mBAiBI,SAAA,CADA,UAAA,CAEA,sBvBo2DJ,CuBj2DI,8DACE,8BAAA,CACA,SvBm2DN,CACF,CuB91DE,uBASE,kCAAA,CAAA,0BAAA,CAFA,2CAAA,CANA,WAAA,CACA,eAAA,CAIA,kBvB+1DJ,CuBz1DI,iEAZF,uBAaI,uBvB41DJ,CACF,CK3nEM,+DkBiRJ,uBAkBI,avB41DJ,CACF,CK1mEI,sCkB2PF,uBAuBI,avB41DJ,CACF,CK/mEI,mCkB2PF,uBA4BI,YAAA,CACA,yDAAA,CACA,oBvB41DJ,CuBz1DI,kEACE,evB21DN,CuBv1DI,6BACE,+CvBy1DN,CuBr1DI,0CAEE,YAAA,CADA,WvBw1DN,CuBn1DI,gDACE,oDvBq1DN,CuBl1DM,sDACE,0CvBo1DR,CACF,CuB70DA,kBACE,gCAAA,CACA,qBvBg1DF,CuB70DE,wBAME,qDAAA,CAFA,uCAAA,CAFA,gBAAA,CACA,kBAAA,CAFA,eAAA,CAIA,uBvBg1DJ,CKnpEI,mCkB8TF,kCAUI,mBvB+0DJ,CuBz1DA,kCAUI,oBvB+0DJ,CACF,CuB30DE,wBAGE,eAAA,CADA,QAAA,CADA,SAAA,CAIA,wBAAA,CAAA,gBvB40DJ,CuBx0DE,wBACE,yDvB00DJ,CuBv0DI,oCACE,evBy0DN,CuBp0DE,wBACE,aAAA,CAEA,YAAA,CADA,uBAAA,CAEA,gCvBs0DJ,CuBn0DI,4DACE,uDvBq0DN,CuBj0DI,gDACE,mBvBm0DN,CuB9zDE,gCAKE,cAAA,CADA,aAAA,CAGA,YAAA,CANA,eAAA,CAKA,uBAAA,CAJA,KAAA,CACA,SvBo0DJ,CuB7zDI,wCACE,YvB+zDN,CuB1zDI,wDACE,YvB4zDN,CuBxzDI,oCAGE,+BAAA,CADA,gBAAA,CADA,mBAAA,CAGA,2CvB0zDN,CKrsEI,mCkBuYA,8CAUI,mBvBwzDN,CuBl0DE,8CAUI,oBvBwzDN,CACF,CuBpzDI,oFAEE,uDAAA,CADA,+BvBuzDN,CuBjzDE,sCACE,2CvBmzDJ,CuB9yDE,2BAGE,eAAA,CADA,eAAA,CADA,iBvBkzDJ,CKttEI,mCkBmaF,qCAOI,mBvBgzDJ,CuBvzDA,qCAOI,oBvBgzDJ,CACF,CuB5yDE,kCAEE,MvBkzDJ,CuBpzDE,kCAEE,OvBkzDJ,CuBpzDE,wBAME,uCAAA,CAFA,aAAA,CACA,YAAA,CAJA,iBAAA,CAEA,YvBizDJ,CKhtEI,0CkB4ZF,wBAUI,YvB8yDJ,CACF,CuB3yDI,8BAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,+CAAA,CAAA,uCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UvBozDN,CuB1yDM,wCACE,oBvB4yDR,CuBtyDE,8BAGE,uCAAA,CAFA,gBAAA,CACA,evByyDJ,CuBryDI,iCAKE,gCAAA,CAHA,eAAA,CACA,eAAA,CACA,eAAA,CAHA,evB2yDN,CuBpyDM,sCACE,oBvBsyDR,CuBjyDI,iCAKE,gCAAA,CAHA,gBAAA,CACA,eAAA,CACA,eAAA,CAHA,avBuyDN,CuBhyDM,sCACE,oBvBkyDR,CuB5xDE,yBAKE,gCAAA,CAJA,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,avBiyDJ,CuB1xDE,uBAGE,wBAAA,CAFA,+BAAA,CACA,yBvB6xDJ,CwBj8EA,WACE,iBAAA,CACA,SxBo8EF,CwBj8EE,kBAOE,2CAAA,CACA,mBAAA,CACA,8BAAA,CAHA,gCAAA,CAHA,QAAA,CAEA,gBAAA,CADA,YAAA,CAMA,SAAA,CATA,iBAAA,CACA,sBAAA,CAaA,mCAAA,CAJA,oExBo8EJ,CwB77EI,6EACE,gBAAA,CACA,SAAA,CAKA,+BAAA,CAJA,8ExBg8EN,CwBx7EI,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,OxBi8EN,CwBr7EE,iBAOE,mBAAA,CAFA,eAAA,CACA,oBAAA,CAHA,QAAA,CAFA,kBAAA,CAGA,aAAA,CAFA,SxB47EJ,CwBn7EE,iBACE,kBxBq7EJ,CwBj7EE,2BAGE,kBAAA,CAAA,oBxBu7EJ,CwB17EE,2BAGE,mBAAA,CAAA,mBxBu7EJ,CwB17EE,iBAIE,cAAA,CAHA,aAAA,CAKA,YAAA,CADA,uBAAA,CAEA,2CACE,CANF,UxBw7EJ,CwB96EI,8CACE,+BxBg7EN,CwB56EI,uBACE,qDxB86EN,CyBlgFA,YAIE,qBAAA,CADA,aAAA,CAGA,gBAAA,CALA,eAAA,CACA,UAAA,CAGA,azBsgFF,CyBlgFE,aATF,YAUI,YzBqgFF,CACF,CKv1EI,0CoB3KF,+BAKI,azB0gFJ,CyB/gFA,+BAKI,czB0gFJ,CyB/gFA,qBAWI,2CAAA,CAHA,aAAA,CAEA,WAAA,CANA,cAAA,CAEA,KAAA,CASA,uBAAA,CAHA,iEACE,CAJF,aAAA,CAFA,SzBwgFJ,CyB7/EI,mEACE,8BAAA,CACA,6BzB+/EN,CyB5/EM,6EACE,8BzB8/ER,CyBz/EI,6CAEE,QAAA,CAAA,MAAA,CACA,QAAA,CACA,eAAA,CAHA,iBAAA,CACA,OAAA,CAGA,qBAAA,CAHA,KzB8/EN,CACF,CKt4EI,sCoBtKJ,YAuDI,QzBy/EF,CyBt/EE,mBACE,WzBw/EJ,CyBp/EE,6CACE,UzBs/EJ,CACF,CyBl/EE,uBACE,YAAA,CACA,OzBo/EJ,CKr5EI,mCoBjGF,uBAMI,QzBo/EJ,CyBj/EI,8BACE,WzBm/EN,CyB/+EI,qCACE,azBi/EN,CyB7+EI,+CACE,kBzB++EN,CACF,CyB1+EE,wBAIE,uBAAA,CAOA,kCAAA,CAAA,0BAAA,CAVA,cAAA,CACA,eAAA,CACA,yDAAA,CAMA,oBzBy+EJ,CyBp+EI,2CAEE,YAAA,CADA,WzBu+EN,CyBl+EI,mEACE,+CzBo+EN,CyBj+EM,qHACE,oDzBm+ER,CyBh+EQ,iIACE,0CzBk+EV,CyBn9EE,wCAGE,wBACE,qBzBm9EJ,CyB/8EE,6BACE,kCzBi9EJ,CyBl9EE,6BACE,iCzBi9EJ,CACF,CK76EI,0CoB5BF,YAME,0BAAA,CADA,QAAA,CAEA,SAAA,CANA,cAAA,CACA,KAAA,CAMA,sDACE,CALF,OAAA,CADA,SzBk9EF,CyBv8EE,4CAEE,WAAA,CACA,SAAA,CACA,4CACE,CAJF,UzB48EJ,CACF,C0BznFA,iBACE,GACE,Q1B2nFF,C0BxnFA,GACE,a1B0nFF,CACF,C0BtnFA,gBACE,GACE,SAAA,CACA,0B1BwnFF,C0BrnFA,IACE,S1BunFF,C0BpnFA,GACE,SAAA,CACA,uB1BsnFF,CACF,C0B9mFA,MACE,2eAAA,CACA,+fAAA,CACA,0lBAAA,CACA,kf1BgnFF,C0B1mFA,WAOE,kCAAA,CAAA,0BAAA,CANA,aAAA,CACA,gBAAA,CACA,eAAA,CAEA,uCAAA,CAGA,uBAAA,CAJA,kB1BgnFF,C0BzmFE,iBACE,U1B2mFJ,C0BvmFE,iBACE,oBAAA,CAEA,aAAA,CACA,qBAAA,CAFA,U1B2mFJ,C0BtmFI,+BACE,iB1BymFN,C0B1mFI,+BACE,kB1BymFN,C0B1mFI,qBAEE,gB1BwmFN,C0BpmFI,kDACE,iB1BumFN,C0BxmFI,kDACE,kB1BumFN,C0BxmFI,kDAEE,iB1BsmFN,C0BxmFI,kDAEE,kB1BsmFN,C0BjmFE,iCAGE,iB1BsmFJ,C0BzmFE,iCAGE,kB1BsmFJ,C0BzmFE,uBACE,oBAAA,CACA,6BAAA,CAEA,eAAA,CACA,sBAAA,CACA,qB1BmmFJ,C0B/lFE,kBACE,YAAA,CAMA,gBAAA,CALA,SAAA,CAMA,oBAAA,CAHA,gBAAA,CAIA,WAAA,CAHA,eAAA,CAFA,SAAA,CADA,U1BumFJ,C0B9lFI,iDACE,4B1BgmFN,C0B3lFE,iBACE,eAAA,CACA,sB1B6lFJ,C0B1lFI,gDACE,2B1B4lFN,C0BxlFI,kCAIE,kB1BgmFN,C0BpmFI,kCAIE,iB1BgmFN,C0BpmFI,wBAOE,6BAAA,CADA,UAAA,CALA,oBAAA,CAEA,YAAA,CAMA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CALA,uBAAA,CAHA,W1BkmFN,C0BtlFI,iCACE,a1BwlFN,C0BplFI,iCACE,gDAAA,CAAA,wC1BslFN,C0BllFI,+BACE,8CAAA,CAAA,sC1BolFN,C0BhlFI,+BACE,8CAAA,CAAA,sC1BklFN,C0B9kFI,sCACE,qDAAA,CAAA,6C1BglFN,C0B1kFA,gBACE,Y1B6kFF,C0B1kFE,gCAIE,kB1B8kFJ,C0BllFE,gCAIE,iB1B8kFJ,C0BllFE,sBAGE,kBAAA,CAGA,uCAAA,CALA,mBAAA,CAIA,gBAAA,CAHA,S1BglFJ,C0BzkFI,+BACE,aAAA,CACA,oB1B2kFN,C0BvkFI,2CACE,U1B0kFN,C0B3kFI,2CACE,W1B0kFN,C0B3kFI,iCAEE,kB1BykFN,C0BrkFI,0BACE,W1BukFN,C2B9vFA,MACE,iSAAA,CACA,4UAAA,CACA,+NAAA,CACA,gZ3BiwFF,C2BxvFE,iBAME,kDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,cAAA,CAIA,mCAAA,CAAA,2BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CANA,0BAAA,CAFA,a3BmwFJ,C2BvvFE,uBACE,6B3ByvFJ,C2BrvFE,sBACE,wCAAA,CAAA,gC3BuvFJ,C2BnvFE,6BACE,+CAAA,CAAA,uC3BqvFJ,C2BjvFE,4BACE,8CAAA,CAAA,sC3BmvFJ,C4B9xFA,SASE,2CAAA,CADA,gCAAA,CAJA,aAAA,CAGA,eAAA,CADA,aAAA,CADA,UAAA,CAFA,S5BqyFF,C4B5xFE,aAZF,SAaI,Y5B+xFF,CACF,CKpnFI,0CuBzLJ,SAkBI,Y5B+xFF,CACF,C4B5xFE,iBACE,mB5B8xFJ,C4B1xFE,yBAIE,iB5BiyFJ,C4BryFE,yBAIE,kB5BiyFJ,C4BryFE,eAQE,eAAA,CAPA,YAAA,CAMA,eAAA,CAJA,QAAA,CAEA,aAAA,CAHA,SAAA,CAWA,oBAAA,CAPA,kB5B+xFJ,C4BrxFI,kCACE,Y5BuxFN,C4BlxFE,eACE,aAAA,CACA,kBAAA,CAAA,mB5BoxFJ,C4BjxFI,sCACE,aAAA,CACA,S5BmxFN,C4B7wFE,eAOE,kCAAA,CAAA,0BAAA,CANA,YAAA,CAEA,eAAA,CADA,gBAAA,CAMA,UAAA,CAJA,uCAAA,CACA,oBAAA,CAIA,8D5B8wFJ,C4BzwFI,0CACE,aAAA,CACA,S5B2wFN,C4BvwFI,6BAEE,kB5B0wFN,C4B5wFI,6BAEE,iB5B0wFN,C4B5wFI,mBAGE,iBAAA,CAFA,Y5B2wFN,C4BpwFM,2CACE,qB5BswFR,C4BvwFM,2CACE,qB5BywFR,C4B1wFM,2CACE,qB5B4wFR,C4B7wFM,2CACE,qB5B+wFR,C4BhxFM,2CACE,oB5BkxFR,C4BnxFM,2CACE,qB5BqxFR,C4BtxFM,2CACE,qB5BwxFR,C4BzxFM,2CACE,qB5B2xFR,C4B5xFM,4CACE,qB5B8xFR,C4B/xFM,4CACE,oB5BiyFR,C4BlyFM,4CACE,qB5BoyFR,C4BryFM,4CACE,qB5BuyFR,C4BxyFM,4CACE,qB5B0yFR,C4B3yFM,4CACE,qB5B6yFR,C4B9yFM,4CACE,oB5BgzFR,C4B1yFI,gCACE,SAAA,CAIA,yBAAA,CAHA,wC5B6yFN,C6Bh5FA,MACE,mS7Bm5FF,C6B14FE,mCACE,mBAAA,CACA,cAAA,CACA,QAAA,CAEA,mBAAA,CADA,kB7B84FJ,C6Bz4FE,oBAGE,kBAAA,CAOA,+CAAA,CACA,oBAAA,CAVA,mBAAA,CAIA,gBAAA,CACA,0BAAA,CACA,eAAA,CALA,QAAA,CAOA,qBAAA,CADA,eAAA,CAJA,wB7Bk5FJ,C6Bx4FI,0BAGE,uCAAA,CAFA,aAAA,CACA,YAAA,CAEA,6C7B04FN,C6Br4FM,gEAEE,0CAAA,CADA,+B7Bw4FR,C6Bl4FI,yBACE,uB7Bo4FN,C6B53FI,gCAME,oDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,qCAAA,CAAA,6BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CAPA,0BAAA,CAFA,W7Bu4FN,C6B13FI,wFACE,0C7B43FN,C8Bt8FA,iBACE,GACE,oB9By8FF,C8Bt8FA,IACE,kB9Bw8FF,C8Br8FA,GACE,oB9Bu8FF,CACF,C8B/7FA,MACE,yNAAA,CACA,sP9Bk8FF,C8B37FA,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,S9B+7FF,C8B76FE,oBAEE,SAAA,CAKA,uBAAA,CAJA,2EACE,CAHF,S9Bk7FJ,C8Bx6FE,oBAEE,eAAA,CACA,wBAAA,CAAA,gBAAA,CAFA,U9B46FJ,C8Bv6FI,6CACE,qC9By6FN,C8Br6FI,uCAEE,eAAA,CADA,mB9Bw6FN,C8Bl6FI,6BACE,Y9Bo6FN,C8B/5FE,8CACE,sC9Bi6FJ,C8B75FE,mBAEE,gBAAA,CADA,a9Bg6FJ,C8B55FI,2CACE,Y9B85FN,C8B15FI,0CACE,e9B45FN,C8Bp5FA,eACE,iBAAA,CACA,eAAA,CAIA,YAAA,CAHA,kBAAA,CAEA,0BAAA,CADA,kB9By5FF,C8Bp5FE,yBACE,a9Bs5FJ,C8Bl5FE,oBACE,sCAAA,CACA,iB9Bo5FJ,C8Bh5FE,6BACE,oBAAA,CAGA,gB9Bg5FJ,C8B54FE,sBAYE,mBAAA,CANA,cAAA,CAHA,oBAAA,CACA,gBAAA,CAAA,iBAAA,CAIA,YAAA,CAGA,eAAA,CAVA,iBAAA,CAMA,wBAAA,CAAA,gBAAA,CAFA,uBAAA,CAHA,S9Bs5FJ,C8Bx4FI,qCACE,uB9B04FN,C8Bt4FI,cArBF,sBAsBI,W9By4FJ,C8Bt4FI,wCACE,2B9Bw4FN,C8Bp4FI,6BAOE,qCAAA,CACA,+CAAA,CAAA,uC9By4FN,C8B/3FI,yDAZE,UAAA,CADA,YAAA,CAKA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CACA,SAAA,CAEA,WAAA,CADA,U9B65FN,C8B94FI,4BAOE,oDAAA,CACA,4CAAA,CAAA,oCAAA,CAQA,uBAAA,CAJA,+C9Bk4FN,C8B33FM,gDACE,uB9B63FR,C8Bz3FM,mFACE,0C9B23FR,CACF,C8Bt3FI,0CAGE,2BAAA,CADA,uBAAA,CADA,S9B03FN,C8Bp3FI,8CACE,oB9Bs3FN,C8Bn3FM,aAJF,8CASI,8CAAA,CACA,iBAAA,CAHA,gCAAA,CADA,eAAA,CADA,cAAA,CAGA,kB9Bw3FN,C8Bn3FM,oDACE,mC9Bq3FR,CACF,C8Bz2FE,gCAEE,iBAAA,CADA,e9B62FJ,C8Bz2FI,mCACE,iB9B22FN,C8Bx2FM,oDAEE,a9Bu3FR,C8Bz3FM,oDAEE,c9Bu3FR,C8Bz3FM,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,CARA,S9Bs3FR,C+BtoGA,MACE,wBAAA,CACA,wB/ByoGF,C+BnoGA,aA+BE,kCAAA,CAAA,0BAAA,CAjBA,gCAAA,CADA,sCAAA,CAGA,SAAA,CADA,mBAAA,CAdA,iBAAA,CAGA,wDACE,CAgBF,4BAAA,CAGA,uEACE,CARF,uDACE,CANF,UAAA,CADA,S/BuoGF,C+BhnGE,oBAuBE,8CAAA,CAAA,+CAAA,CADA,UAAA,CADA,aAAA,CAfA,gJACE,CANF,iBAAA,CAmBA,S/BomGJ,C+B7lGE,yBAGE,kEAAA,CAFA,gDAAA,CACA,6C/BgmGJ,C+B3lGE,4BAGE,qEAAA,CADA,8CAAA,CADA,6C/B+lGJ,C+BzlGE,qBAEE,SAAA,CAKA,uBAAA,CAJA,wEACE,CAHF,S/B8lGJ,C+BplGE,oBAqBE,uBAAA,CAEA,2CAAA,CACA,mBAAA,CACA,8BAAA,CAnBA,0FACE,CAaF,eAAA,CADA,8BAAA,CAlBA,iBAAA,CAqBA,oB/BykGJ,C+BnkGI,uCAEE,YAAA,CADA,W/BskGN,C+BjkGI,6CACE,oD/BmkGN,C+BhkGM,mDACE,0C/BkkGR,C+B1jGI,mCAwBE,eAAA,CACA,eAAA,CAxBA,oIACE,CAgBF,sCACE,CAIF,mBAAA,CAKA,wBAAA,CAAA,gBAAA,CAbA,sBAAA,CAAA,iB/BojGN,C+BniGI,4CACE,Y/BqiGN,C+BjiGI,2CACE,e/BmiGN,CgCttGA,kBAME,ehCkuGF,CgCxuGA,kBAME,gBhCkuGF,CgCxuGA,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,ShCquGF,CgCltGE,aAtBF,QAuBI,YhCqtGF,CACF,CgCltGE,kBACE,wBhCotGJ,CgChtGE,gBAEE,SAAA,CADA,mBAAA,CAGA,+BAAA,CADA,uBhCmtGJ,CgC/sGI,0BACE,8BhCitGN,CgC5sGE,4BAEE,0CAAA,CADA,+BhC+sGJ,CgC1sGE,YACE,oBAAA,CACA,oBhC4sGJ,CiCjwGA,oBACE,GACE,mBjCowGF,CACF,CiC5vGA,MACE,wfjC8vGF,CiCxvGA,YACE,aAAA,CAEA,eAAA,CADA,ajC4vGF,CiCxvGE,+BAOE,kBAAA,CAAA,kBjCyvGJ,CiChwGE,+BAOE,iBAAA,CAAA,mBjCyvGJ,CiChwGE,qBAQE,aAAA,CACA,cAAA,CACA,YAAA,CATA,iBAAA,CAKA,UjC0vGJ,CiCnvGI,qCAIE,iBjC2vGN,CiC/vGI,qCAIE,kBjC2vGN,CiC/vGI,2BAME,6BAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,WjC6vGN,CiChvGE,mBACE,iBAAA,CACA,UjCkvGJ,CiC9uGE,kBAWE,2CAAA,CACA,mBAAA,CACA,8BAAA,CALA,gCAAA,CACA,oBAAA,CAHA,kBAAA,CAFA,YAAA,CAUA,SAAA,CAPA,aAAA,CAFA,SAAA,CAJA,iBAAA,CASA,4BAAA,CARA,UAAA,CAaA,+CACE,CAbF,SjC4vGJ,CiC3uGI,+EACE,gBAAA,CACA,SAAA,CACA,sCjC6uGN,CiCvuGI,qCAEE,oCACE,gCjCwuGN,CiCpuGI,2CACE,cjCsuGN,CACF,CiCjuGE,kBACE,kBjCmuGJ,CiC/tGE,4BAGE,kBAAA,CAAA,oBjCsuGJ,CiCzuGE,4BAGE,mBAAA,CAAA,mBjCsuGJ,CiCzuGE,kBAKE,cAAA,CAJA,aAAA,CAMA,YAAA,CADA,uBAAA,CAEA,2CACE,CALF,kBAAA,CAFA,UjCuuGJ,CiC5tGI,gDACE,+BjC8tGN,CiC1tGI,wBACE,qDjC4tGN,CkCl0GA,MAEI,6VAAA,CAAA,uWAAA,CAAA,qPAAA,CAAA,2xBAAA,CAAA,qMAAA,CAAA,+aAAA,CAAA,2LAAA,CAAA,yPAAA,CAAA,2TAAA,CAAA,oaAAA,CAAA,2SAAA,CAAA,2LlC21GJ,CkC/0GE,4CAME,8CAAA,CACA,4BAAA,CACA,mBAAA,CACA,8BAAA,CAJA,mCAAA,CAJA,iBAAA,CAGA,gBAAA,CADA,iBAAA,CADA,eAAA,CASA,uBAAA,CADA,2BlCm1GJ,CkC/0GI,aAdF,4CAeI,elCk1GJ,CACF,CkC/0GI,sEACE,gClCi1GN,CkC50GI,gDACE,qBlC80GN,CkC10GI,gIAEE,iBAAA,CADA,clC60GN,CkCx0GI,4FACE,iBlC00GN,CkCt0GI,kFACE,elCw0GN,CkCp0GI,0FACE,YlCs0GN,CkCl0GI,8EACE,mBlCo0GN,CkC/zGE,sEAGE,iBAAA,CAAA,mBlCy0GJ,CkC50GE,sEAGE,kBAAA,CAAA,kBlCy0GJ,CkC50GE,sEASE,uBlCm0GJ,CkC50GE,sEASE,wBlCm0GJ,CkC50GE,sEAUE,4BlCk0GJ,CkC50GE,4IAWE,6BlCi0GJ,CkC50GE,sEAWE,4BlCi0GJ,CkC50GE,kDAOE,0BAAA,CACA,WAAA,CAFA,eAAA,CADA,eAAA,CAHA,oBAAA,CAAA,iBAAA,CADA,iBlC20GJ,CkC9zGI,kFACE,elCg0GN,CkC5zGI,oFAEE,UlCu0GN,CkCz0GI,oFAEE,WlCu0GN,CkCz0GI,gEAOE,wBhBiIU,CgBlIV,UAAA,CADA,WAAA,CAGA,kDAAA,CAAA,0CAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CACA,UlCq0GN,CkC1zGI,4DACE,4DlC4zGN,CkC9yGE,sDACE,oBlCizGJ,CkC9yGI,gFACE,gClCgzGN,CkC3yGE,8DACE,0BlC8yGJ,CkC3yGI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClC6yGN,CkCzyGI,0EACE,alC2yGN,CkCh0GE,8DACE,oBlCm0GJ,CkCh0GI,wFACE,gClCk0GN,CkC7zGE,sEACE,0BlCg0GJ,CkC7zGI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClC+zGN,CkC3zGI,kFACE,alC6zGN,CkCl1GE,sDACE,oBlCq1GJ,CkCl1GI,gFACE,gClCo1GN,CkC/0GE,8DACE,0BlCk1GJ,CkC/0GI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClCi1GN,CkC70GI,0EACE,alC+0GN,CkCp2GE,oDACE,oBlCu2GJ,CkCp2GI,8EACE,gClCs2GN,CkCj2GE,4DACE,0BlCo2GJ,CkCj2GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClCm2GN,CkC/1GI,wEACE,alCi2GN,CkCt3GE,4DACE,oBlCy3GJ,CkCt3GI,sFACE,gClCw3GN,CkCn3GE,oEACE,0BlCs3GJ,CkCn3GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCq3GN,CkCj3GI,gFACE,alCm3GN,CkCx4GE,8DACE,oBlC24GJ,CkCx4GI,wFACE,gClC04GN,CkCr4GE,sEACE,0BlCw4GJ,CkCr4GI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClCu4GN,CkCn4GI,kFACE,alCq4GN,CkC15GE,4DACE,oBlC65GJ,CkC15GI,sFACE,gClC45GN,CkCv5GE,oEACE,0BlC05GJ,CkCv5GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCy5GN,CkCr5GI,gFACE,alCu5GN,CkC56GE,4DACE,oBlC+6GJ,CkC56GI,sFACE,gClC86GN,CkCz6GE,oEACE,0BlC46GJ,CkCz6GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClC26GN,CkCv6GI,gFACE,alCy6GN,CkC97GE,0DACE,oBlCi8GJ,CkC97GI,oFACE,gClCg8GN,CkC37GE,kEACE,0BlC87GJ,CkC37GI,gFACE,wBAlBG,CAmBH,oDAAA,CAAA,4ClC67GN,CkCz7GI,8EACE,alC27GN,CkCh9GE,oDACE,oBlCm9GJ,CkCh9GI,8EACE,gClCk9GN,CkC78GE,4DACE,0BlCg9GJ,CkC78GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClC+8GN,CkC38GI,wEACE,alC68GN,CkCl+GE,4DACE,oBlCq+GJ,CkCl+GI,sFACE,gClCo+GN,CkC/9GE,oEACE,0BlCk+GJ,CkC/9GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCi+GN,CkC79GI,gFACE,alC+9GN,CkCp/GE,wDACE,oBlCu/GJ,CkCp/GI,kFACE,gClCs/GN,CkCj/GE,gEACE,0BlCo/GJ,CkCj/GI,8EACE,wBAlBG,CAmBH,mDAAA,CAAA,2ClCm/GN,CkC/+GI,4EACE,alCi/GN,CmCrpHA,MACE,qMnCwpHF,CmC/oHE,sBAEE,uCAAA,CADA,gBnCmpHJ,CmC/oHI,mCACE,anCipHN,CmClpHI,mCACE,cnCipHN,CmC7oHM,4BACE,sBnC+oHR,CmC5oHQ,mCACE,gCnC8oHV,CmC1oHQ,2DACE,SAAA,CAEA,uBAAA,CADA,enC6oHV,CmCxoHQ,yGACE,SAAA,CACA,uBnC0oHV,CmCtoHQ,yCACE,YnCwoHV,CmCjoHE,0BACE,eAAA,CACA,enCmoHJ,CmChoHI,+BACE,oBnCkoHN,CmC7nHE,gDACE,YnC+nHJ,CmC3nHE,8BAIE,+BAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,SAAA,CAKA,4BAAA,CAJA,4DACE,CAHF,0BnC+nHJ,CmCtnHI,aAdF,8BAeI,+BAAA,CACA,SAAA,CACA,uBnCynHJ,CACF,CmCtnHI,wCACE,6BnCwnHN,CmCpnHI,oCACE,+BnCsnHN,CmClnHI,qCAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,YAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,WnC2nHN,CmC9mHQ,mDACE,oBnCgnHV,CoC9tHE,kCAEE,iBpCouHJ,CoCtuHE,kCAEE,kBpCouHJ,CoCtuHE,wBAGE,yCAAA,CAFA,oBAAA,CAGA,SAAA,CACA,mCpCiuHJ,CoC5tHI,aAVF,wBAWI,YpC+tHJ,CACF,CoC3tHE,6FAEE,SAAA,CACA,mCpC6tHJ,CoCvtHE,4FAEE,+BpCytHJ,CoCrtHE,oBACE,yBAAA,CACA,uBAAA,CAGA,yEpCqtHJ,CKtlHI,sC+BrHE,qDACE,uBpC8sHN,CACF,CoCzsHE,kEACE,yBpC2sHJ,CoCvsHE,sBACE,0BpCysHJ,CqCpwHE,2BACE,arCuwHJ,CKllHI,0CgCtLF,2BAKI,erCuwHJ,CqCpwHI,6BACE,iBrCswHN,CACF,CqClwHI,6BAEE,0BAAA,CAAA,2BAAA,CADA,eAAA,CAEA,iBrCowHN,CqCjwHM,2CACE,kBrCmwHR,CqC7vHI,6CACE,QrC+vHN,CsC3xHE,uBACE,4CtC+xHJ,CsC1xHE,8CAJE,kCAAA,CAAA,0BtCkyHJ,CsC9xHE,uBACE,4CtC6xHJ,CsCxxHE,4BAEE,kCAAA,CAAA,0BAAA,CADA,qCtC2xHJ,CsCvxHI,mCACE,atCyxHN,CsCrxHI,kCACE,atCuxHN,CsClxHE,0BAKE,eAAA,CAJA,aAAA,CAEA,YAAA,CACA,aAAA,CAFA,kBAAA,CAAA,mBtCuxHJ,CsCjxHI,uCACE,etCmxHN,CsC/wHI,sCACE,kBtCixHN,CuC9zHA,MACE,oLvCi0HF,CuCxzHE,oBAGE,iBAAA,CAEA,gBAAA,CADA,avC0zHJ,CuCtzHI,wCACE,uBvCwzHN,CuCpzHI,gCAEE,eAAA,CADA,gBvCuzHN,CuChzHM,wCACE,mBvCkzHR,CuC5yHE,8BAKE,oBvCgzHJ,CuCrzHE,8BAKE,mBvCgzHJ,CuCrzHE,8BAUE,4BvC2yHJ,CuCrzHE,4DAWE,6BvC0yHJ,CuCrzHE,8BAWE,4BvC0yHJ,CuCrzHE,oBASE,cAAA,CANA,aAAA,CACA,eAAA,CAIA,evC6yHJ,CuCvyHI,kCACE,uCAAA,CACA,oBvCyyHN,CuCryHI,wCAEE,uCAAA,CADA,YvCwyHN,CuCnyHI,oCAEE,WvCgzHN,CuClzHI,oCAEE,UvCgzHN,CuClzHI,0BAOE,6BAAA,CADA,UAAA,CADA,WAAA,CAGA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CAUA,sBAAA,CADA,yBAAA,CARA,UvC8yHN,CuClyHM,oCACE,wBvCoyHR,CuC/xHI,4BACE,YvCiyHN,CuC5xHI,4CACE,YvC8xHN,CwCx3HE,+DACE,sBAAA,CAEA,mBAAA,CACA,0BAAA,CACA,uBxC03HJ,CwCv3HI,2EAGE,iBAAA,CADA,eAAA,CADA,yBxC23HN,CwCp3HE,mEACE,0BxCs3HJ,CwCl3HE,oBACE,qBxCo3HJ,CwCh3HE,gBACE,oBxCk3HJ,CwC92HE,gBACE,qBxCg3HJ,CwC52HE,iBACE,kBxC82HJ,CwC12HE,kBACE,kBxC42HJ,CyCr5HE,6BACE,sCzCw5HJ,CyCr5HE,cACE,yCzCu5HJ,CyC34HE,sIACE,oCzC64HJ,CyCr4HE,2EACE,qCzCu4HJ,CyC73HE,wGACE,oCzC+3HJ,CyCt3HE,yFACE,qCzCw3HJ,CyCn3HE,6BACE,kCzCq3HJ,CyC/2HE,6CACE,sCzCi3HJ,CyC12HE,4DACE,sCzC42HJ,CyCr2HE,4DACE,qCzCu2HJ,CyC91HE,yFACE,qCzCg2HJ,CyCx1HE,2EACE,sCzC01HJ,CyC/0HE,wHACE,qCzCi1HJ,CyC50HE,8BAGE,mBAAA,CADA,gBAAA,CADA,gBzCg1HJ,CyC30HE,eACE,4CzC60HJ,CyC10HE,eACE,4CzC40HJ,CyCx0HE,gBAIE,+CAAA,CACA,kDAAA,CAJA,aAAA,CAEA,wBAAA,CADA,wBzC60HJ,CyCt0HE,yBAOE,wCAAA,CACA,+DAAA,CACA,4BAAA,CACA,6BAAA,CARA,iBAAA,CAGA,eAAA,CACA,eAAA,CAFA,cAAA,CADA,oCAAA,CAFA,iBzCi1HJ,CyCr0HI,6BACE,YzCu0HN,CyCp0HM,kCACE,wBAAA,CACA,yBzCs0HR,CyCh0HE,iCAaE,wCAAA,CACA,+DAAA,CAJA,uCAAA,CACA,0BAAA,CALA,UAAA,CAJA,oBAAA,CAOA,2BAAA,CADA,2BAAA,CADA,2BAAA,CANA,eAAA,CAWA,wBAAA,CAAA,gBAAA,CAPA,SzCy0HJ,CyCvzHE,sBACE,iBAAA,CACA,iBzCyzHJ,CyCpzHE,iCAKE,ezCkzHJ,CyC/yHI,sCACE,gBzCizHN,CyC7yHI,gDACE,YzC+yHN,CyCryHA,gBACE,iBzCwyHF,CyCpyHE,yCACE,aAAA,CACA,SzCsyHJ,CyCjyHE,mBACE,YzCmyHJ,CyC9xHE,oBACE,QzCgyHJ,CyC5xHE,4BACE,WAAA,CACA,SAAA,CACA,ezC8xHJ,CyC3xHI,0CACE,YzC6xHN,CyCvxHE,yBAKE,wCAAA,CAEA,+BAAA,CADA,4BAAA,CAHA,eAAA,CADA,oDAAA,CAEA,wBAAA,CAAA,gBzC4xHJ,CyCrxHE,2BAEE,+DAAA,CADA,2BzCwxHJ,CyCpxHI,+BACE,uCAAA,CACA,gBzCsxHN,CyCjxHE,sBACE,MAAA,CACA,WzCmxHJ,CyC9wHA,aACE,azCixHF,CyCvwHE,4BAEE,aAAA,CADA,YzC2wHJ,CyCvwHI,wDAEE,2BAAA,CADA,wBzC0wHN,CyCpwHE,+BAKE,2CAAA,CAEA,+BAAA,CADA,gCAAA,CADA,sBAAA,CAHA,mBAAA,CACA,gBAAA,CAFA,azC4wHJ,CyCnwHI,qCAEE,UAAA,CACA,UAAA,CAFA,azCuwHN,CK94HI,0CoCsJF,8BACE,iBzC4vHF,CyClvHE,wSAGE,ezCwvHJ,CyCpvHE,sCAEE,mBAAA,CACA,eAAA,CADA,oBAAA,CADA,kBAAA,CAAA,mBzCwvHJ,CACF,C0CrlII,yDAIE,+BAAA,CACA,8BAAA,CAFA,aAAA,CADA,QAAA,CADA,iB1C2lIN,C0CnlII,uBAEE,uCAAA,CADA,c1CslIN,C0CjiIM,iHAEE,WAlDkB,CAiDlB,kB1C4iIR,C0C7iIM,6HAEE,WAlDkB,CAiDlB,kB1CwjIR,C0CzjIM,6HAEE,WAlDkB,CAiDlB,kB1CokIR,C0CrkIM,oHAEE,WAlDkB,CAiDlB,kB1CglIR,C0CjlIM,0HAEE,WAlDkB,CAiDlB,kB1C4lIR,C0C7lIM,uHAEE,WAlDkB,CAiDlB,kB1CwmIR,C0CzmIM,uHAEE,WAlDkB,CAiDlB,kB1ConIR,C0CrnIM,6HAEE,WAlDkB,CAiDlB,kB1CgoIR,C0CjoIM,yCAEE,WAlDkB,CAiDlB,kB1CooIR,C0CroIM,yCAEE,WAlDkB,CAiDlB,kB1CwoIR,C0CzoIM,0CAEE,WAlDkB,CAiDlB,kB1C4oIR,C0C7oIM,uCAEE,WAlDkB,CAiDlB,kB1CgpIR,C0CjpIM,wCAEE,WAlDkB,CAiDlB,kB1CopIR,C0CrpIM,sCAEE,WAlDkB,CAiDlB,kB1CwpIR,C0CzpIM,wCAEE,WAlDkB,CAiDlB,kB1C4pIR,C0C7pIM,oCAEE,WAlDkB,CAiDlB,kB1CgqIR,C0CjqIM,2CAEE,WAlDkB,CAiDlB,kB1CoqIR,C0CrqIM,qCAEE,WAlDkB,CAiDlB,kB1CwqIR,C0CzqIM,oCAEE,WAlDkB,CAiDlB,kB1C4qIR,C0C7qIM,kCAEE,WAlDkB,CAiDlB,kB1CgrIR,C0CjrIM,qCAEE,WAlDkB,CAiDlB,kB1CorIR,C0CrrIM,mCAEE,WAlDkB,CAiDlB,kB1CwrIR,C0CzrIM,qCAEE,WAlDkB,CAiDlB,kB1C4rIR,C0C7rIM,wCAEE,WAlDkB,CAiDlB,kB1CgsIR,C0CjsIM,sCAEE,WAlDkB,CAiDlB,kB1CosIR,C0CrsIM,2CAEE,WAlDkB,CAiDlB,kB1CwsIR,C0C7rIM,iCAEE,WAPkB,CAMlB,iB1CgsIR,C0CjsIM,uCAEE,WAPkB,CAMlB,iB1CosIR,C0CrsIM,mCAEE,WAPkB,CAMlB,iB1CwsIR,C2C1xIA,MACE,2LAAA,CACA,yL3C6xIF,C2CpxIE,wBAKE,mBAAA,CAHA,YAAA,CACA,qBAAA,CACA,YAAA,CAHA,iB3C2xIJ,C2CjxII,8BAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,O3CqxIN,C2ChxIM,qCACE,0B3CkxIR,C2CrvIM,kEACE,0C3CuvIR,C2CjvIE,2BAME,uBAAA,CADA,+DAAA,CAJA,YAAA,CACA,cAAA,CACA,aAAA,CACA,oB3CqvIJ,C2ChvII,aATF,2BAUI,gB3CmvIJ,CACF,C2ChvII,cAGE,+BACE,iB3CgvIN,C2C7uIM,sCAQE,qCAAA,CANA,QAAA,CAKA,UAAA,CAHA,aAAA,CAEA,UAAA,CAHA,MAAA,CAFA,iBAAA,CAaA,2CAAA,CALA,2DACE,CAGF,kDAAA,CARA,+B3CqvIR,CACF,C2CvuII,8CACE,Y3CyuIN,C2CruII,iCAUE,+BAAA,CACA,6BAAA,CALA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,gBAAA,CACA,eAAA,CAFA,8BAAA,CAMA,+BAAA,CAGA,2CACE,CANF,kBAAA,CALA,U3CivIN,C2CluIM,aAII,6CACE,O3CiuIV,C2CluIQ,8CACE,O3CouIV,C2CruIQ,8CACE,O3CuuIV,C2CxuIQ,8CACE,O3C0uIV,C2C3uIQ,8CACE,O3C6uIV,C2C9uIQ,8CACE,O3CgvIV,C2CjvIQ,8CACE,O3CmvIV,C2CpvIQ,8CACE,O3CsvIV,C2CvvIQ,8CACE,O3CyvIV,C2C1vIQ,+CACE,Q3C4vIV,C2C7vIQ,+CACE,Q3C+vIV,C2ChwIQ,+CACE,Q3CkwIV,C2CnwIQ,+CACE,Q3CqwIV,C2CtwIQ,+CACE,Q3CwwIV,C2CzwIQ,+CACE,Q3C2wIV,C2C5wIQ,+CACE,Q3C8wIV,C2C/wIQ,+CACE,Q3CixIV,C2ClxIQ,+CACE,Q3CoxIV,C2CrxIQ,+CACE,Q3CuxIV,C2CxxIQ,+CACE,Q3C0xIV,CACF,C2CrxIM,uCACE,gC3CuxIR,C2CnxIM,oDACE,a3CqxIR,C2ChxII,yCACE,S3CkxIN,C2C9wIM,2CACE,aAAA,CACA,8B3CgxIR,C2C1wIE,4BACE,U3C4wIJ,C2CzwII,aAJF,4BAKI,gB3C4wIJ,CACF,C2CxwIE,0BACE,Y3C0wIJ,C2CvwII,aAJF,0BAKI,a3C0wIJ,C2CtwIM,sCACE,O3CwwIR,C2CzwIM,uCACE,O3C2wIR,C2C5wIM,uCACE,O3C8wIR,C2C/wIM,uCACE,O3CixIR,C2ClxIM,uCACE,O3CoxIR,C2CrxIM,uCACE,O3CuxIR,C2CxxIM,uCACE,O3C0xIR,C2C3xIM,uCACE,O3C6xIR,C2C9xIM,uCACE,O3CgyIR,C2CjyIM,wCACE,Q3CmyIR,C2CpyIM,wCACE,Q3CsyIR,C2CvyIM,wCACE,Q3CyyIR,C2C1yIM,wCACE,Q3C4yIR,C2C7yIM,wCACE,Q3C+yIR,C2ChzIM,wCACE,Q3CkzIR,C2CnzIM,wCACE,Q3CqzIR,C2CtzIM,wCACE,Q3CwzIR,C2CzzIM,wCACE,Q3C2zIR,C2C5zIM,wCACE,Q3C8zIR,C2C/zIM,wCACE,Q3Ci0IR,CACF,C2C3zII,+FAEE,Q3C6zIN,C2C1zIM,yGACE,wBAAA,CACA,yB3C6zIR,C2CpzIM,2DAEE,wBAAA,CACA,yBAAA,CAFA,Q3CwzIR,C2CjzIM,iEACE,Q3CmzIR,C2ChzIQ,qLAGE,wBAAA,CACA,yBAAA,CAFA,Q3CozIV,C2C9yIQ,6FACE,wBAAA,CACA,yB3CgzIV,C2C3yIM,yDACE,kB3C6yIR,C2CxyII,sCACE,Q3C0yIN,C2CryIE,2BAEE,iBAAA,CAOA,kBAAA,CAHA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,YAAA,CACA,gBAAA,CAEA,mBAAA,CAGA,gCAAA,CAPA,W3C8yIJ,C2CpyII,iCAEE,uDAAA,CADA,+B3CuyIN,C2ClyII,iCAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,8CAAA,CAAA,sCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,+CACE,CATF,U3C4yIN,C2C7xIE,4BAOE,yEACE,CANF,YAAA,CAGA,aAAA,CAFA,qBAAA,CAGA,mBAAA,CALA,iBAAA,CAYA,wBAAA,CATA,Y3CmyIJ,C2CvxII,sCACE,wB3CyxIN,C2CrxII,oCACE,S3CuxIN,C2CnxII,kCAGE,wEACE,CAFF,mBAAA,CADA,O3CuxIN,C2C7wIM,uDACE,8CAAA,CAAA,sC3C+wIR,CKt5II,0CsCqJF,wDAEE,kB3CuwIF,C2CzwIA,wDAEE,mB3CuwIF,C2CzwIA,8CAGE,eAAA,CAFA,eAAA,CAGA,iC3CqwIF,C2CjwIE,8DACE,mB3CowIJ,C2CrwIE,8DACE,kB3CowIJ,C2CrwIE,oDAEE,U3CmwIJ,C2C/vIE,8EAEE,kB3CkwIJ,C2CpwIE,8EAEE,mB3CkwIJ,C2CpwIE,8EAGE,kB3CiwIJ,C2CpwIE,8EAGE,mB3CiwIJ,C2CpwIE,oEACE,U3CmwIJ,C2C7vIE,8EAEE,mB3CgwIJ,C2ClwIE,8EAEE,kB3CgwIJ,C2ClwIE,8EAGE,mB3C+vIJ,C2ClwIE,8EAGE,kB3C+vIJ,C2ClwIE,oEACE,U3CiwIJ,CACF,C2CnvIE,cAHF,olDAII,gC3CsvIF,C2CnvIE,g8GACE,uC3CqvIJ,CACF,C2ChvIA,4sDACE,+B3CmvIF,C2C/uIA,wmDACE,a3CkvIF,C4CtnJA,MACE,qWAAA,CACA,8W5CynJF,C4ChnJE,4BAEE,oBAAA,CADA,iB5ConJJ,C4C/mJI,sDAEE,S5CknJN,C4CpnJI,sDAEE,U5CknJN,C4CpnJI,4CACE,iBAAA,CAEA,S5CinJN,C4C5mJE,+CAEE,SAAA,CADA,U5C+mJJ,C4C1mJE,kDAEE,W5CqnJJ,C4CvnJE,kDAEE,Y5CqnJJ,C4CvnJE,wCAOE,qDAAA,CADA,UAAA,CADA,aAAA,CAGA,0CAAA,CAAA,kCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,SAAA,CACA,Y5CmnJJ,C4CxmJE,gEACE,wB1B2Wa,C0B1Wb,mDAAA,CAAA,2C5C0mJJ,C6C1pJA,aAQE,wBACE,Y7CypJF,CACF,C8CnqJA,QACE,8DAAA,CAGA,+CAAA,CACA,iEAAA,CACA,oDAAA,CACA,sDAAA,CACA,mDAAA,CAGA,qEAAA,CACA,qEAAA,CACA,wEAAA,CACA,0EAAA,CACA,wEAAA,CACA,yEAAA,CACA,kEAAA,CACA,+DAAA,CACA,oEAAA,CACA,oEAAA,CACA,mEAAA,CACA,gEAAA,CACA,uEAAA,CACA,mEAAA,CACA,qEAAA,CACA,oEAAA,CACA,gEAAA,CACA,wEAAA,CACA,qEAAA,CACA,+D9CiqJF,C8C3pJA,SAEE,kBAAA,CADA,Y9C+pJF,C+CjsJE,kBAUE,cAAA,CATA,YAAA,CACA,kEACE,CAQF,Y/C6rJJ,C+CzrJI,sDACE,gB/C2rJN,C+CrrJI,oFAKE,wDAAA,CACA,mBAAA,CAJA,aAAA,CAEA,QAAA,CADA,aAAA,CAIA,sC/CurJN,C+ClrJM,iOACE,kBAAA,CACA,8B/CqrJR,C+CjrJM,6FACE,iBAAA,CAAA,c/CorJR,C+ChrJM,2HACE,Y/CmrJR,C+C/qJM,wHACE,e/CkrJR,C+CnqJI,yMAGE,eAAA,CAAA,Y/C2qJN,C+C7pJI,ybAOE,W/CmqJN,C+C/pJI,8BACE,eAAA,CAAA,Y/CiqJN,CK7lJI,mC2ChKA,8BACE,UhDqwJJ,CgDtwJE,8BACE,WhDqwJJ,CgDtwJE,8BAGE,kBhDmwJJ,CgDtwJE,8BAGE,iBhDmwJJ,CgDtwJE,oBAKE,mBAAA,CADA,YAAA,CAFA,ahDowJJ,CgD9vJI,kCACE,WhDiwJN,CgDlwJI,kCACE,UhDiwJN,CgDlwJI,kCAEE,iBAAA,CAAA,chDgwJN,CgDlwJI,kCAEE,aAAA,CAAA,kBhDgwJN,CACF","file":"main.css"} \ No newline at end of file diff --git a/main/cli/check/index.html b/main/cli/check/index.html index 72225126a..5da64616d 100644 --- a/main/cli/check/index.html +++ b/main/cli/check/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2320,7 +2320,7 @@ Checking the catalog - + @@ -2331,7 +2331,7 @@ Checking the catalog - + @@ -2342,7 +2342,7 @@ Checking the catalog - + diff --git a/main/cli/debug/index.html b/main/cli/debug/index.html index ae66b40d0..c90e0d693 100644 --- a/main/cli/debug/index.html +++ b/main/cli/debug/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2632,7 +2632,7 @@ Example of multiple arguments - + @@ -2643,7 +2643,7 @@ Example of multiple arguments - + @@ -2654,7 +2654,7 @@ Example of multiple arguments - + diff --git a/main/cli/exec/index.html b/main/cli/exec/index.html index 6e5bb22f4..3302bc93a 100644 --- a/main/cli/exec/index.html +++ b/main/cli/exec/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2825,7 +2825,7 @@ Example - + @@ -2836,7 +2836,7 @@ Example - + @@ -2847,7 +2847,7 @@ Example - + diff --git a/main/cli/get-inventory-information/index.html b/main/cli/get-inventory-information/index.html index 66f981d71..8522e13c2 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2517,7 +2517,7 @@ Example - + @@ -2528,7 +2528,7 @@ Example - + @@ -2539,7 +2539,7 @@ Example - + diff --git a/main/cli/inv-from-ansible/index.html b/main/cli/inv-from-ansible/index.html index c36ec8a8b..c32b4d88e 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2385,7 +2385,7 @@ Command output - + @@ -2396,7 +2396,7 @@ Command output - + @@ -2407,7 +2407,7 @@ Command output - + diff --git a/main/cli/inv-from-cvp/index.html b/main/cli/inv-from-cvp/index.html index db12c0fee..58ea6abbe 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2386,7 +2386,7 @@ Creating an inventory fr - + @@ -2397,7 +2397,7 @@ Creating an inventory fr - + @@ -2408,7 +2408,7 @@ Creating an inventory fr - + diff --git a/main/cli/nrfu/index.html b/main/cli/nrfu/index.html index af83ed585..e4814d923 100644 --- a/main/cli/nrfu/index.html +++ b/main/cli/nrfu/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3185,7 +3185,7 @@ Dry-run mode - + @@ -3196,7 +3196,7 @@ Dry-run mode - + @@ -3207,7 +3207,7 @@ Dry-run mode - + diff --git a/main/cli/overview/index.html b/main/cli/overview/index.html index 7ae34083d..de7dfc32e 100644 --- a/main/cli/overview/index.html +++ b/main/cli/overview/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2500,7 +2500,7 @@ Shell Completion - + @@ -2511,7 +2511,7 @@ Shell Completion - + @@ -2522,7 +2522,7 @@ Shell Completion - + diff --git a/main/cli/tag-management/index.html b/main/cli/tag-management/index.html index 0bd503e15..e4c5b1edd 100644 --- a/main/cli/tag-management/index.html +++ b/main/cli/tag-management/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2841,7 +2841,7 @@ Example - + @@ -2852,7 +2852,7 @@ Example - + @@ -2863,7 +2863,7 @@ Example - + diff --git a/main/contribution/index.html b/main/contribution/index.html index 818cbe7a9..184709092 100644 --- a/main/contribution/index.html +++ b/main/contribution/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2812,7 +2812,7 @@ Continuous Integration - + @@ -2823,7 +2823,7 @@ Continuous Integration - + @@ -2834,7 +2834,7 @@ Continuous Integration - + diff --git a/main/faq/index.html b/main/faq/index.html index 9e540805e..45f0c4469 100644 --- a/main/faq/index.html +++ b/main/faq/index.html @@ -18,7 +18,7 @@ - + @@ -26,7 +26,7 @@ - + @@ -131,7 +131,7 @@ - + @@ -228,7 +228,7 @@ - + ANTA on Github @@ -264,7 +264,7 @@ - + Arista Network Test Automation - ANTA @@ -274,7 +274,7 @@ - + ANTA on Github @@ -2588,7 +2588,7 @@ Still facing issues? - + @@ -2599,7 +2599,7 @@ Still facing issues? - + @@ -2610,7 +2610,7 @@ Still facing issues? - + diff --git a/main/getting-started/index.html b/main/getting-started/index.html index 1d2129f24..3a3fe708b 100644 --- a/main/getting-started/index.html +++ b/main/getting-started/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2968,7 +2968,7 @@ Basic usage in a Python script - + @@ -2979,7 +2979,7 @@ Basic usage in a Python script - + @@ -2990,7 +2990,7 @@ Basic usage in a Python script - + diff --git a/main/index.html b/main/index.html index 673ccba42..29105cdbb 100644 --- a/main/index.html +++ b/main/index.html @@ -18,7 +18,7 @@ - + @@ -26,7 +26,7 @@ - + @@ -131,7 +131,7 @@ - + @@ -228,7 +228,7 @@ - + ANTA on Github @@ -264,7 +264,7 @@ - + Arista Network Test Automation - ANTA @@ -274,7 +274,7 @@ - + ANTA on Github @@ -2526,7 +2526,7 @@ Credits - + @@ -2537,7 +2537,7 @@ Credits - + @@ -2548,7 +2548,7 @@ Credits - + diff --git a/main/requirements-and-installation/index.html b/main/requirements-and-installation/index.html index 9705edfd0..1e081fc3c 100644 --- a/main/requirements-and-installation/index.html +++ b/main/requirements-and-installation/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2583,7 +2583,7 @@ EOS Requirements - + @@ -2594,7 +2594,7 @@ EOS Requirements - + @@ -2605,7 +2605,7 @@ EOS Requirements - + diff --git a/main/search/search_index.json b/main/search/search_index.json index 4ff562a6b..c53e00eff 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: As a Python library in your own application The ANTA CLI "},{"location":"#install-anta-library","title":"Install ANTA library","text":"The library will NOT install the necessary dependencies for the CLI. # Install ANTA as a library\npip install anta\n"},{"location":"#install-anta-cli","title":"Install ANTA CLI","text":"If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx is a tool to install and run python applications in isolated environments. Refer to pipx instructions to install on your system. pipx installs ANTA in an isolated python environment and makes it available globally. This is not recommended if you plan to contribute to ANTA # Install ANTA CLI with pipx\n$ pipx install anta[cli]\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 You can also still choose to install it with directly with pip: pip install anta[cli]\n"},{"location":"#documentation","title":"Documentation","text":"The documentation is published on ANTA package website."},{"location":"#contribution-guide","title":"Contribution guide","text":"Contributions are welcome. Please refer to the contribution guide"},{"location":"#credits","title":"Credits","text":"Thank you to Jeremy Schulman for aio-eapi. 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":"Contribution model is based on a fork-model. Don\u2019t push to aristanetworks/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-environment","title":"Create a development environment","text":"Run the following commands to create an ANTA development environment: # Clone repository\n$ git clone https://github.com/aristanetworks/anta.git\n$ cd anta\n\n# Install ANTA in editable mode and its development tools\n$ pip install -e .[dev]\n# To also install the CLI\n$ pip install -e .[dev,cli]\n\n# Verify installation\n$ pip list -e\nPackage Version Editable project location\n------- ------- -------------------------\nanta 1.1.0 /mnt/lab/projects/anta\n Then, tox is configured 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\npy39 -> Run pytest with py39\npy310 -> Run pytest with py310\npy311 -> Run pytest with py311\npy312 -> Run pytest with py312\nreport -> Generate coverage report\n"},{"location":"contribution/#code-linting","title":"Code linting","text":"tox -e lint\n[...]\nlint: commands[0]> ruff check .\nAll checks passed!\nlint: commands[1]> ruff format . --check\n158 files already formatted\nlint: commands[2]> pylint anta\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\nlint: commands[3]> pylint tests\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\n lint: OK (22.69=setup[2.19]+cmd[0.02,0.02,9.71,10.75] seconds)\n congratulations :) (22.72 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 68 source files\ntype: commands[1]> mypy --config-file=pyproject.toml tests\nSuccess: no issues found in 82 source files\n type: OK (31.15=setup[14.62]+cmd[6.05,10.48] seconds)\n congratulations :) (31.18 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.units.anta_tests 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.units.anta_tests import test\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 pre-commit\ntrim trailing whitespace.................................................Passed\nfix end of files.........................................................Passed\ncheck for added large files..............................................Passed\ncheck for merge conflicts................................................Passed\nCheck and insert license on Python files.................................Passed\nCheck and insert license on Markdown files...............................Passed\nRun Ruff linter..........................................................Passed\nRun Ruff formatter.......................................................Passed\nCheck code style with pylint.............................................Passed\nChecks for common misspellings in text files.............................Passed\nCheck typing with mypy...................................................Passed\nCheck Markdown files style...............................................Passed\n"},{"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. The results can be viewed here."},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#a-local-os-error-occurred-while-connecting-to-a-device","title":"A local OS error occurred while connecting to a device","text":"A local OS error occurred while connecting to a device When running ANTA, you can receive A local OS error occurred while connecting to <device> errors. The underlying OSError exception can have various reasons: [Errno 24] Too many open files or [Errno 16] Device or resource busy. This usually means that the operating system refused to open a new file descriptor (or socket) for the ANTA process. This might be due to the hard limit for open file descriptors currently set for the ANTA process. At startup, ANTA sets the soft limit of its process to the hard limit up to 16384. This is because the soft limit is usually 1024 and the hard limit is usually higher (depends on the system). If the hard limit of the ANTA process is still lower than the number of selected tests in ANTA, the ANTA process may request to the operating system too many file descriptors and get an error, a WARNING is displayed at startup if this is the case."},{"location":"faq/#solution","title":"Solution","text":"One solution could be to raise the hard limit for the user starting the ANTA process. You can get the current hard limit for a user using the command ulimit -n -H while logged in. Create the file /etc/security/limits.d/10-anta.conf with the following content: <user> hard nofile <value>\n The user is the one with which the ANTA process is started. The value is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again."},{"location":"faq/#timeout-error-in-the-logs","title":"Timeout error in the logs","text":"Timeout error in the logs When running ANTA, you can receive <Foo>Timeout errors in the logs (could be ReadTimeout, WriteTimeout, ConnectTimeout or PoolTimeout). More details on the timeouts of the underlying library are available here: https://www.python-httpx.org/advanced/timeouts. This might be due to the time the host on which ANTA is run takes to reach the target devices (for instance if going through firewalls, NATs, \u2026) or when a lot of tests are being run at the same time on a device (eAPI has a queue mechanism to avoid exhausting EOS resources because of a high number of simultaneous eAPI requests)."},{"location":"faq/#solution_1","title":"Solution","text":"Use the timeout option. As an example for the nrfu command: anta nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml --timeout 50 text\n The previous command set a couple of options for ANTA NRFU, one them being the timeout command, by default, when running ANTA from CLI, it is set to 30s. The timeout is increased to 50s to allow ANTA to wait for API calls a little longer."},{"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_2","title":"Solution","text":" 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 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_3","title":"Solution","text":" 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_4","title":"Solution","text":" 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 follow the troubleshooting instructions and report the issue in our GitHub repository."},{"location":"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 install ANTA package is to run Python (>=3.9) and its pip package to install: pip install anta[cli]\n For more details about how to install package, please see the requirements and installation 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: s1-spine1\n tags: ['fabric', 'spine']\n - host: 192.168.0.11\n name: s1-spine2\n tags: ['fabric', 'spine']\n - host: 192.168.0.12\n name: s1-leaf1\n tags: ['fabric', 'leaf']\n - host: 192.168.0.13\n name: s1-leaf2\n tags: ['fabric', 'leaf']\n - host: 192.168.0.14\n name: s1-leaf3\n tags: ['fabric', 'leaf']\n - host: 192.168.0.15\n name: s1-leaf3\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: ---\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\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":""},{"location":"getting-started/#cli","title":"CLI","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. Usage: 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 selected inventory devices.\n Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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 Note The following examples shows how to pass all the CLI options. See how to use environment variables instead in the CLI overview"},{"location":"getting-started/#default-report-using-table","title":"Default report using table","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n `# table is default if not provided` \\\n table\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:01] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.058. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.069. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:02] INFO Running ANTA tests completed in: 0:00:00.969. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 s1-spine1 \u2502 VerifyMlagConfigSanity \u2502 skipped \u2502 MLAG is disabled \u2502 Verifies there are no MLAG config-sanity \u2502 MLAG \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502 inconsistencies. \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-spine1 \u2502 VerifyEOSVersion \u2502 failure \u2502 device is running version \u2502 Verifies the EOS version of the device. \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 \"4.32.2F-38195967.4322F (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)\" not in expected versions: \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['4.25.4M', '4.26.1F', \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 '4.28.3M-28837868.4283M (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n[...]\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyTerminAttrVersion \u2502 failure \u2502 device is running TerminAttr version \u2502 Verifies the TerminAttr version of the \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 v1.34.0 and is not in the allowed list: \u2502 device. \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['v1.22.1'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyZeroTouch \u2502 success \u2502 \u2502 Verifies ZeroTouch is disabled \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\u2500\u2500\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\u2500\u2500\u2500\u2500\u2500\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 arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n 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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:52:39] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.057. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.068. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:52:40] INFO Running ANTA tests completed in: 0:00:00.863. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\n\ns1-spine1 :: VerifyEOSVersion :: FAILURE(device is running version \"4.32.2F-38195967.4322F (engineering build)\" not in expected versions: ['4.25.4M', '4.26.1F',\n'4.28.3M-28837868.4283M (engineering build)'])\ns1-spine1 :: VerifyTerminAttrVersion :: FAILURE(device is running TerminAttr version v1.34.0 and is not in the allowed list: ['v1.22.1'])\ns1-spine1 :: VerifyZeroTouch :: SUCCESS()\ns1-spine1 :: VerifyMlagConfigSanity :: SKIPPED(MLAG is disabled)\n"},{"location":"getting-started/#report-in-json-format","title":"Report in JSON format","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable `\\\n `# --enable-password <password> `\\\n --catalog ./catalog.yml \\\n json\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:11] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.053. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.065. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:12] INFO Running ANTA tests completed in: 0:00:00.857. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \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\": \"s1-spine1\",\n \"test\": \"VerifyNTP\",\n \"categories\": [\n \"system\"\n ],\n \"description\": \"Verifies if NTP is synchronised.\",\n \"result\": \"success\",\n \"messages\": [],\n \"custom_field\": null\n },\n {\n \"name\": \"s1-spine1\",\n \"test\": \"VerifyMlagConfigSanity\",\n \"categories\": [\n \"mlag\"\n ],\n \"description\": \"Verifies there are no MLAG config-sanity inconsistencies.\",\n \"result\": \"skipped\",\n \"messages\": [\n \"MLAG is disabled\"\n ],\n \"custom_field\": null\n },\n [...]\n"},{"location":"getting-started/#basic-usage-in-a-python-script","title":"Basic usage in a Python script","text":"# 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\"\"\"Example script for ANTA.\n\nusage:\n\npython anta_runner.py\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport sys\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.cli.nrfu.utils import anta_progress_bar\nfrom anta.inventory import AntaInventory\nfrom anta.logger import Log, setup_logging\nfrom anta.models import AntaTest\nfrom anta.result_manager import ResultManager\nfrom anta.runner import main as anta_runner\n\n# setup logging\nsetup_logging(Log.INFO, Path(\"/tmp/anta.log\"))\nLOGGER = logging.getLogger()\nSCRIPT_LOG_PREFIX = \"[bold magenta][ANTA RUNNER SCRIPT][/] \" # For convenience purpose - there are nicer way to do this.\n\n\n# NOTE: The inventory and catalog files are not delivered with this script\nUSERNAME = \"admin\"\nPASSWORD = \"admin\"\nCATALOG_PATH = Path(\"/tmp/anta_catalog.yml\")\nINVENTORY_PATH = Path(\"/tmp/anta_inventory.yml\")\n\n# Load catalog file\ntry:\n catalog = AntaCatalog.parse(CATALOG_PATH)\nexcept Exception:\n LOGGER.exception(\"%s Catalog failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Catalog loaded!\", SCRIPT_LOG_PREFIX)\n\n# Load inventory\ntry:\n inventory = AntaInventory.parse(INVENTORY_PATH, username=USERNAME, password=PASSWORD)\nexcept Exception:\n LOGGER.exception(\"%s Inventory failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Inventory loaded!\", SCRIPT_LOG_PREFIX)\n\n# Create result manager object\nmanager = ResultManager()\n\n# Launch ANTA\nLOGGER.info(\"%s Starting ANTA runner...\", SCRIPT_LOG_PREFIX)\nwith anta_progress_bar() as AntaTest.progress:\n # Set dry_run to True to avoid connecting to the devices\n asyncio.run(anta_runner(manager, inventory, catalog, dry_run=False))\n\nLOGGER.info(\"%s ANTA run completed!\", SCRIPT_LOG_PREFIX)\n\n# Manipulate the test result object\nfor test_result in manager.results:\n LOGGER.info(\"%s %s:%s:%s\", SCRIPT_LOG_PREFIX, test_result.name, test_result.test, test_result.result)\n"},{"location":"requirements-and-installation/","title":"Installation","text":""},{"location":"requirements-and-installation/#python-version","title":"Python version","text":"Python 3 (>=3.9) is required: python --version\nPython 3.11.8\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-library-from-pypi-server","title":"Install library from Pypi server","text":"pip install anta\n Warning This command alone will not install the ANTA CLI requirements. "},{"location":"requirements-and-installation/#install-anta-cli-as-an-application-with-pipx","title":"Install ANTA CLI as an application with pipx","text":"pipx is a tool to install and run python applications in isolated environments. If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx installs ANTA in an isolated python environment and makes it available globally. pipx install anta[cli]\n Info Please take the time to read through the installation instructions of pipx before getting started."},{"location":"requirements-and-installation/#install-cli-from-pypi-server","title":"Install CLI from Pypi server","text":"Alternatively, pip install with cli extra is enough to install the ANTA CLI. pip install anta[cli]\n"},{"location":"requirements-and-installation/#install-anta-from-github","title":"Install ANTA from github","text":"pip install git+https://github.com/aristanetworks/anta.git\npip install git+https://github.com/aristanetworks/anta.git#egg=anta[cli]\n\n# You can even specify the branch, tag or commit:\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>#egg=anta[cli]\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 v1.1.0\n"},{"location":"requirements-and-installation/#eos-requirements","title":"EOS Requirements","text":"To get ANTA working, the targeted Arista EOS devices must have eAPI enabled. They need to use 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":"troubleshooting/","title":"Troubleshooting ANTA","text":"A couple of things to check when hitting an issue with ANTA: flowchart LR\n A>Hitting an issue with ANTA] --> B{Is my issue <br >listed in the FAQ?}\n B -- Yes --> C{Does the FAQ solution<br />works for me?}\n C -- Yes --> V(((Victory)))\n B -->|No| E{Is my problem<br />mentioned in one<br />of the open issues?}\n C -->|No| E\n E -- Yes --> F{Has the issue been<br />fixed in a newer<br />release or in main?}\n F -- Yes --> U[Upgrade]\n E -- No ---> H((Follow the steps below<br />and open a Github issue))\n U --> I{Did it fix<br /> your problem}\n I -- Yes --> V\n I -- No --> H\n F -- No ----> G((Add a comment on the <br />issue indicating you<br >are hitting this and<br />describing your setup<br /> and adding your logs.))\n\n click B \"../faq\" \"FAQ\"\n click E \"https://github.com/aristanetworks/anta/issues\"\n click H \"https://github.com/aristanetworks/anta/issues\"\n style A stroke:#f00,stroke-width:2px"},{"location":"troubleshooting/#capturing-logs","title":"Capturing logs","text":"To help document the issue in Github, it is important to capture some logs so the developers can understand what is affecting your system. No logs mean that the first question asked on the issue will probably be \u201cCan you share some logs please?\u201d. ANTA provides very verbose logs when using the DEBUG level. When using DEBUG log level with a log file, the DEBUG logging level is not sent to stdout, but only to the file. Danger On real deployments, do not use DEBUG logging level without setting a log file at the same time. To save the logs to a file called anta.log, use the following flags: # Where ANTA_COMMAND is one of nrfu, debug, get, exec, check\nanta -l DEBUG \u2013log-file anta.log <ANTA_COMMAND>\n See anta --help for more information. These have to precede the nrfu cmd. Tip Remember that in ANTA, each level of command has its own options and they can only be set at this level. so the -l and --log-file MUST be between anta and the ANTA_COMMAND. similarly, all the nrfu options MUST be set between the nrfu and the ANTA_NRFU_SUBCOMMAND (json, text, table or tpl-report). As an example, for the nrfu command, it would look like: anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#anta_debug-environment-variable","title":"ANTA_DEBUG environment variable","text":"Warning Do not use this if you do not know why. This produces a lot of logs and can create confusion if you do not know what to look for. The environment variable ANTA_DEBUG=true enable ANTA Debug Mode. This flag is used by various functions in ANTA: when set to true, the function will display or log more information. In particular, when an Exception occurs in the code and this variable is set, the logging function used by ANTA is different to also produce the Python traceback for debugging. This typically needs to be done when opening a GitHub issue and an Exception is seen at runtime. Example: ANTA_DEBUG=true anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#troubleshooting-on-eos","title":"Troubleshooting on EOS","text":"ANTA is using a specific ID in eAPI requests towards EOS. This allows for easier eAPI requests debugging on the device using EOS configuration trace CapiApp setting UwsgiRequestContext/4,CapiUwsgiServer/4 to set up CapiApp agent logs. Then, you can view agent logs using: bash tail -f /var/log/agents/CapiApp-*\n\n2024-05-15 15:32:54.056166 1429 UwsgiRequestContext 4 request content b'{\"jsonrpc\": \"2.0\", \"method\": \"runCmds\", \"params\": {\"version\": \"latest\", \"cmds\": [{\"cmd\": \"show ip route vrf default 10.255.0.3\", \"revision\": 4}], \"format\": \"json\", \"autoComplete\": false, \"expandAliases\": false}, \"id\": \"ANTA-VerifyRoutingTableEntry-132366530677328\"}'\n"},{"location":"usage-inventory-catalog/","title":"Inventory and Test catalog","text":"The ANTA framework needs 2 important inputs from the user to run: A device inventory 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 accessible via eAPI ranges: scan a range for devices accessible 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 in either YAML or JSON: ---\n<Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\n {\n \"<Python module>\": [\n {\n \"<AntaTest subclass>\": <AntaTest.Input compliant dictionary>\n }\n ]\n}\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 or equivalent in JSON: {\n \"anta.tests.connectivity\": [\n {\n \"VerifyReachability\": {\n \"result_overwrite\": {\n \"description\": \"Test with overwritten description\",\n \"categories\": [\n \"Overwritten category 1\"\n ],\n \"custom_field\": \"Test run by John Doe\"\n },\n \"filters\": {\n \"tags\": [\n \"leaf\"\n ]\n },\n \"hosts\": [\n {\n \"destination\": \"1.1.1.1\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n },\n {\n \"destination\": \"8.8.8.8\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n }\n ]\n }\n }\n ]\n}\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 or JSON file: anta.tests.software:\n - VerifyEOSVersion:\n # List of allowed EOS versions.\n versions:\n - 4.25.4M\n - 4.26.1F\n {\n \"anta.tests.software\": [\n {\n \"VerifyEOSVersion\": {\n \"versions\": [\n \"4.25.4M\",\n \"4.31.1F\"\n ]\n }\n }\n ]\n}\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 custom.tests.system Python module, the test catalog will be: custom.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":"usage-inventory-catalog/#example-script-to-merge-catalogs","title":"Example script to merge catalogs","text":"The following script reads all the files in intended/test_catalogs/ with names <device_name>-catalog.yml and merge them together inside one big catalog anta-catalog.yml using the new AntaCatalog.merge_catalogs() class method. # Copyright (c) 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\"\"\"Script that merge a collection of catalogs into one AntaCatalog.\"\"\"\n\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.models import AntaTest\n\nCATALOG_SUFFIX = \"-catalog.yml\"\nCATALOG_DIR = \"intended/test_catalogs/\"\n\nif __name__ == \"__main__\":\n catalogs = []\n for file in Path(CATALOG_DIR).glob(\"*\" + CATALOG_SUFFIX):\n device = str(file).removesuffix(CATALOG_SUFFIX).removeprefix(CATALOG_DIR)\n print(f\"Loading test catalog for device {device}\")\n catalog = AntaCatalog.parse(file)\n # Add the device name as a tag to all tests in the catalog\n for test in catalog.tests:\n test.inputs.filters = AntaTest.Input.Filters(tags={device})\n catalogs.append(catalog)\n\n # Merge all catalogs\n merged_catalog = AntaCatalog.merge_catalogs(catalogs)\n\n # Save the merged catalog to a file\n with Path(\"anta-catalog.yml\").open(\"w\") as f:\n f.write(merged_catalog.dump().yaml())\n Warning The AntaCatalog.merge() method is deprecated and will be removed in ANTA v2.0. Please use the AntaCatalog.merge_catalogs() class method instead."},{"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. "},{"location":"advanced_usages/as-python-lib/#examples","title":"Examples","text":""},{"location":"advanced_usages/as-python-lib/#parse-an-anta-inventory-file","title":"Parse an ANTA inventory file","text":"# Copyright (c) 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\"\"\"Script that parses an ANTA inventory file, connects to devices and print their status.\"\"\"\n\nimport asyncio\n\nfrom anta.inventory import AntaInventory\n\n\nasync def main(inv: AntaInventory) -> None:\n \"\"\"Read an AntaInventory and try to connect to every device in the inventory.\n\n 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\n\nif __name__ == \"__main__\":\n # Create the AntaInventory instance\n inventory = AntaInventory.parse(\n filename=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\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."},{"location":"advanced_usages/as-python-lib/#run-eos-commands","title":"Run EOS commands","text":"# Copyright (c) 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\"\"\"Script that runs a list of EOS commands on reachable devices.\"\"\"\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 \"\"\"Run a list of commands against each valid device in the inventory.\n\n Take an AntaInventory and a list of commands as string\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 -------\n dict[str, list[AntaCommand]]\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=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\n )\n\n # Create a list of commands with json output\n command_list = [\"show version\", \"show ip bgp summary\"]\n\n # Run the main asyncio entry point\n res = asyncio.run(main(inventory, command_list))\n\n pprint(res)\n"},{"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: 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 Caching can be disabled per device, network or range by setting the disable_cache key to True when defining 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. For tests developers, 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: set = None) -> None:\n super().__init__(name, tags, disable_cache=True)\n"},{"location":"advanced_usages/custom-tests/","title":"Developing ANTA tests","text":"Info 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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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":"Full AntaTest API documentation is available in the API documentation section"},{"location":"advanced_usages/custom-tests/#class-attributes","title":"Class Attributes","text":" name (str, optional): Name of the test. Used during reporting. By default set to the Class name. description (str, optional): A human readable description of your test. By default set to the first line of the docstring. 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":"Attributes: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. 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":"Full Input model documentation is available in API documentation section Attributes: Name Type Description result_overwrite ResultOverwrite | None Define fields to overwrite in the TestResult object."},{"location":"advanced_usages/custom-tests/#resultoverwrite-model","title":"ResultOverwrite model","text":"Full ResultOverwrite model documentation is available in API documentation section Attributes: Name Type Description description str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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 occurrence 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: 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. 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. 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. 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. 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, the first line is used as description of the test by default>\n \"\"\"\n\n # name = <override> # uncomment to override default behavior of name=Class Name\n # description = <override> # uncomment to override default behavior of description=first line of docstring\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 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 values 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, integrations with existing tools are not broken. This is done by using by default version=1 in eAPI calls. By default, ANTA uses version=\"latest\" in AntaCommand, but when developing tests, the revision MUST be provided when the outformat of the command is json. As explained earlier, this is to ensure that the eAPI always returns the same output model and that the test remains always valid from the day it was created. For some commands, you may also want to run them with a different revision or version. For instance, the VerifyBFDPeersHealth 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/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: Parse the command outputs using the self.instance_commands instance attribute. If needed, access the test inputs using the self.inputs instance attribute and write your conditional logic. 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_custom and the test is defined in anta_custom.dc_project Python module, the test catalog would look like: anta_custom.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(\n tests: list[AntaTestDefinition] | None = None,\n filename: str | Path | None = None,\n)\n Class representing an ANTA Catalog. It can be instantiated using its constructor or one of the static methods: parse(), from_list() or from_dict() Parameters: Name Type Description Default tests list[AntaTestDefinition] | None A list of AntaTestDefinition instances. None filename str | Path | None The path from which the catalog is loaded. None"},{"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.build_indexes","title":"build_indexes","text":"build_indexes(\n filtered_tests: set[str] | None = None,\n) -> None\n Indexes tests by their tags for quick access during filtering operations. If a filtered_tests set is provided, only the tests in this set will be indexed. This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests. Once the indexes are built, the indexes_built attribute is set to True. Source code in anta/catalog.py def build_indexes(self, filtered_tests: set[str] | None = None) -> None:\n \"\"\"Indexes tests by their tags for quick access during filtering operations.\n\n If a `filtered_tests` set is provided, only the tests in this set will be indexed.\n\n This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests.\n\n Once the indexes are built, the `indexes_built` attribute is set to True.\n \"\"\"\n for test in self.tests:\n # Skip tests that are not in the specified filtered_tests set\n if filtered_tests and test.test.name not in filtered_tests:\n continue\n\n # Indexing by tag\n if test.inputs.filters and (test_tags := test.inputs.filters.tags):\n for tag in test_tags:\n self.tag_to_tests[tag].add(test)\n else:\n self.tag_to_tests[None].add(test)\n\n self.indexes_built = True\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.clear_indexes","title":"clear_indexes","text":"clear_indexes() -> None\n Clear this AntaCatalog instance indexes. Source code in anta/catalog.py def clear_indexes(self) -> None:\n \"\"\"Clear this AntaCatalog instance indexes.\"\"\"\n self._init_indexes()\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.dump","title":"dump","text":"dump() -> AntaCatalogFile\n Return an AntaCatalogFile instance from this AntaCatalog instance. Returns: Type Description AntaCatalogFile An AntaCatalogFile instance containing tests of this AntaCatalog instance. Source code in anta/catalog.py def dump(self) -> AntaCatalogFile:\n \"\"\"Return an AntaCatalogFile instance from this AntaCatalog instance.\n\n Returns\n -------\n AntaCatalogFile\n An AntaCatalogFile instance containing tests of this AntaCatalog instance.\n \"\"\"\n root: dict[ImportString[Any], list[AntaTestDefinition]] = {}\n for test in self.tests:\n # Cannot use AntaTest.module property as the class is not instantiated\n root.setdefault(test.test.__module__, []).append(test)\n return AntaCatalogFile(root=root)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.from_dict","title":"from_dict staticmethod","text":"from_dict(\n data: RawCatalogInput,\n filename: str | Path | None = None,\n) -> 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. Parameters: Name Type Description Default data RawCatalogInput Python dictionary used to instantiate the AntaCatalog instance. required filename str | Path | None value to be set as AntaCatalog instance attribute None Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 dictionary content. 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 Parameters\n ----------\n data\n Python dictionary used to instantiate the AntaCatalog instance.\n filename\n value to be set as AntaCatalog instance attribute\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' dictionary content.\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. Parameters: Name Type Description Default data ListAntaTestTuples Python list used to instantiate the AntaCatalog instance. required Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 list content. 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 Parameters\n ----------\n data\n Python list used to instantiate the AntaCatalog instance.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' list content.\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(\n tags: set[str], *, strict: bool = False\n) -> set[AntaTestDefinition]\n Return all tests that match a given set of tags, according to the specified strictness. Parameters: Name Type Description Default tags set[str] The tags to filter tests by. If empty, return all tests without tags. required strict bool If True, returns only tests that contain all specified tags (intersection). If False, returns tests that contain any of the specified tags (union). False Returns: Type Description set[AntaTestDefinition] A set of tests that match the given tags. Raises: Type Description ValueError If the indexes have not been built prior to method call. Source code in anta/catalog.py def get_tests_by_tags(self, tags: set[str], *, strict: bool = False) -> set[AntaTestDefinition]:\n \"\"\"Return all tests that match a given set of tags, according to the specified strictness.\n\n Parameters\n ----------\n tags\n The tags to filter tests by. If empty, return all tests without tags.\n strict\n If True, returns only tests that contain all specified tags (intersection).\n If False, returns tests that contain any of the specified tags (union).\n\n Returns\n -------\n set[AntaTestDefinition]\n A set of tests that match the given tags.\n\n Raises\n ------\n ValueError\n If the indexes have not been built prior to method call.\n \"\"\"\n if not self.indexes_built:\n msg = \"Indexes have not been built yet. Call build_indexes() first.\"\n raise ValueError(msg)\n if not tags:\n return self.tag_to_tests[None]\n\n filtered_sets = [self.tag_to_tests[tag] for tag in tags if tag in self.tag_to_tests]\n if not filtered_sets:\n return set()\n\n if strict:\n return set.intersection(*filtered_sets)\n return set.union(*filtered_sets)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge","title":"merge","text":"merge(catalog: AntaCatalog) -> AntaCatalog\n Merge two AntaCatalog instances. Warning This method is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead. Parameters: Name Type Description Default catalog AntaCatalog AntaCatalog instance to merge to this instance. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of the two instances. Source code in anta/catalog.py def merge(self, catalog: AntaCatalog) -> AntaCatalog:\n \"\"\"Merge two AntaCatalog instances.\n\n Warning\n -------\n This method is deprecated and will be removed in ANTA v2.0. Use `AntaCatalog.merge_catalogs()` instead.\n\n Parameters\n ----------\n catalog\n AntaCatalog instance to merge to this instance.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of the two instances.\n \"\"\"\n # TODO: Use a decorator to deprecate this method instead. See https://github.com/aristanetworks/anta/issues/754\n warn(\n message=\"AntaCatalog.merge() is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n return self.merge_catalogs([self, catalog])\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge_catalogs","title":"merge_catalogs classmethod","text":"merge_catalogs(catalogs: list[AntaCatalog]) -> AntaCatalog\n Merge multiple AntaCatalog instances. Parameters: Name Type Description Default catalogs list[AntaCatalog] A list of AntaCatalog instances to merge. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of all the input catalogs. Source code in anta/catalog.py @classmethod\ndef merge_catalogs(cls, catalogs: list[AntaCatalog]) -> AntaCatalog:\n \"\"\"Merge multiple AntaCatalog instances.\n\n Parameters\n ----------\n catalogs\n A list of AntaCatalog instances to merge.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of all the input catalogs.\n \"\"\"\n combined_tests = list(chain(*(catalog.tests for catalog in catalogs)))\n return cls(tests=combined_tests)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.parse","title":"parse staticmethod","text":"parse(\n filename: str | Path,\n file_format: Literal[\"yaml\", \"json\"] = \"yaml\",\n) -> AntaCatalog\n Create an AntaCatalog instance from a test catalog file. Parameters: Name Type Description Default filename str | Path Path to test catalog YAML or JSON file. required file_format Literal['yaml', 'json'] Format of the file, either \u2018yaml\u2019 or \u2018json\u2019. 'yaml' Returns: Type Description AntaCatalog An AntaCatalog populated with the file content. Source code in anta/catalog.py @staticmethod\ndef parse(filename: str | Path, file_format: Literal[\"yaml\", \"json\"] = \"yaml\") -> AntaCatalog:\n \"\"\"Create an AntaCatalog instance from a test catalog file.\n\n Parameters\n ----------\n filename\n Path to test catalog YAML or JSON file.\n file_format\n Format of the file, either 'yaml' or 'json'.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the file content.\n \"\"\"\n if file_format not in [\"yaml\", \"json\"]:\n message = f\"'{file_format}' is not a valid format for an AntaCatalog file. Only 'yaml' and 'json' are supported.\"\n raise ValueError(message)\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) if file_format == \"yaml\" else json_load(f)\n except (TypeError, YAMLError, OSError, ValueError) 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(\n **data: (\n type[AntaTest]\n | AntaTest.Input\n | dict[str, Any]\n | None\n )\n)\n Bases: BaseModel Define a test with its associated inputs. Attributes: Name Type Description test type[AntaTest] An AntaTest concrete subclass. inputs Input The associated AntaTest.Input subclass instance. https://docs.pydantic.dev/2.0/usage/validators/#using-validation-context-with-basemodel-initialization."},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.check_inputs","title":"check_inputs","text":"check_inputs() -> Self\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) -> Self:\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(\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. 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\"Could not instantiate inputs as type {type(data).__name__} is not valid\"\n raise ValueError(msg)\n"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.serialize_model","title":"serialize_model","text":"serialize_model() -> dict[str, AntaTest.Input]\n Serialize the AntaTestDefinition model. The dictionary representing the model will be look like: <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n Returns: Type Description dict A dictionary representing the model. Source code in anta/catalog.py @model_serializer()\ndef serialize_model(self) -> dict[str, AntaTest.Input]:\n \"\"\"Serialize the AntaTestDefinition model.\n\n The dictionary representing the model will be look like:\n ```\n <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n ```\n\n Returns\n -------\n dict\n A dictionary representing the model.\n \"\"\"\n return {self.test.__name__: self.inputs}\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: <Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\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 if not data:\n return data\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 isinstance(test_definition, AntaTestDefinition):\n test_definitions.append(test_definition)\n continue\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 return data\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.flatten_modules","title":"flatten_modules staticmethod","text":"flatten_modules(\n data: dict[str, Any], package: str | None = None\n) -> dict[ModuleType, list[Any]]\n Allow the user to provide a data structure with nested Python modules. Example anta.tests.routing:\n generic:\n - <AntaTestDefinition>\n bgp:\n - <AntaTestDefinition>\n anta.tests.routing.generic and anta.tests.routing.bgp are importable Python modules. 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:\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 elif isinstance(tests, list):\n # This is a list of AntaTestDefinition\n modules[module] = tests\n else:\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 return modules\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.to_json","title":"to_json","text":"to_json() -> str\n Return a JSON representation string of this model. Returns: Type Description str The JSON representation string of this model. Source code in anta/catalog.py def to_json(self) -> str:\n \"\"\"Return a JSON representation string of this model.\n\n Returns\n -------\n str\n The JSON representation string of this model.\n \"\"\"\n return self.model_dump_json(serialize_as_any=True, exclude_unset=True, indent=2)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/catalog.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return safe_dump(safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/csv_reporter/","title":"CSV reporter","text":"CSV Report management for ANTA."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv","title":"ReportCsv","text":"Build a CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_name: str = \"Test Name\",\n test_status: str = \"Test Status\",\n messages: str = \"Message(s)\",\n description: str = \"Test description\",\n categories: str = \"Test category\",\n)\n Headers for the CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.convert_to_list","title":"convert_to_list classmethod","text":"convert_to_list(result: TestResult) -> list[str]\n Convert a TestResult into a list of string for creating file content. Parameters: Name Type Description Default result TestResult A TestResult to convert into list. required Returns: Type Description list[str] TestResult converted into a list. Source code in anta/reporter/csv_reporter.py @classmethod\ndef convert_to_list(cls, result: TestResult) -> list[str]:\n \"\"\"Convert a TestResult into a list of string for creating file content.\n\n Parameters\n ----------\n result\n A TestResult to convert into list.\n\n Returns\n -------\n list[str]\n TestResult converted into a list.\n \"\"\"\n message = cls.split_list_to_txt_list(result.messages) if len(result.messages) > 0 else \"\"\n categories = cls.split_list_to_txt_list(convert_categories(result.categories)) if len(result.categories) > 0 else \"None\"\n return [\n str(result.name),\n result.test,\n result.result,\n message,\n result.description,\n categories,\n ]\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.generate","title":"generate classmethod","text":"generate(\n results: ResultManager, csv_filename: pathlib.Path\n) -> None\n Build CSV flle with tests results. Parameters: Name Type Description Default results ResultManager A ResultManager instance. required csv_filename Path File path where to save CSV data. required Raises: Type Description OSError if any is raised while writing the CSV file. Source code in anta/reporter/csv_reporter.py @classmethod\ndef generate(cls, results: ResultManager, csv_filename: pathlib.Path) -> None:\n \"\"\"Build CSV flle with tests results.\n\n Parameters\n ----------\n results\n A ResultManager instance.\n csv_filename\n File path where to save CSV data.\n\n Raises\n ------\n OSError\n if any is raised while writing the CSV file.\n \"\"\"\n headers = [\n cls.Headers.device,\n cls.Headers.test_name,\n cls.Headers.test_status,\n cls.Headers.messages,\n cls.Headers.description,\n cls.Headers.categories,\n ]\n\n try:\n with csv_filename.open(mode=\"w\", encoding=\"utf-8\") as csvfile:\n csvwriter = csv.writer(\n csvfile,\n delimiter=\",\",\n )\n csvwriter.writerow(headers)\n for entry in results.results:\n csvwriter.writerow(cls.convert_to_list(entry))\n except OSError as exc:\n message = f\"OSError caught while writing the CSV file '{csv_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.split_list_to_txt_list","title":"split_list_to_txt_list classmethod","text":"split_list_to_txt_list(\n usr_list: list[str], delimiter: str = \" - \"\n) -> str\n Split list to multi-lines string. Parameters: Name Type Description Default usr_list list[str] List of string to concatenate. required delimiter str A delimiter to use to start string. Defaults to None. ' - ' Returns: Type Description str Multi-lines string. Source code in anta/reporter/csv_reporter.py @classmethod\ndef split_list_to_txt_list(cls, usr_list: list[str], delimiter: str = \" - \") -> str:\n \"\"\"Split list to multi-lines string.\n\n Parameters\n ----------\n usr_list\n List of string to concatenate.\n delimiter\n A delimiter to use to start string. Defaults to None.\n\n Returns\n -------\n str\n Multi-lines string.\n\n \"\"\"\n return f\"{delimiter}\".join(f\"{line}\" for line in usr_list)\n"},{"location":"api/device/","title":"Device","text":""},{"location":"api/device/#antadevice-base-class","title":"AntaDevice base class","text":""},{"location":"api/device/#anta.device.AntaDevice","title":"AntaDevice","text":"AntaDevice(\n name: str,\n tags: set[str] | None = None,\n *,\n disable_cache: bool = False\n)\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 str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. cache Cache | None In-memory cache from aiocache library for this device (None if cache is disabled). cache_locks dict Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled. Parameters: Name Type Description Default name str Device name. required tags set[str] | None Tags for this device. None disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AntaDevice.cache_statistics","title":"cache_statistics property","text":"cache_statistics: dict[str, Any] | None\n Return 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.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AntaDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AntaDevice.\"\"\"\n return (\n f\"AntaDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AntaDevice._collect","title":"_collect abstractmethod async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output. This abstract coroutine can be used to implement any command collection method for a device in ANTA. The _collect() implementation needs to populate the output attribute of the AntaCommand object passed as argument. If a failure occurs, the _collect() implementation is expected to catch the exception and implement proper logging, the output attribute of the AntaCommand object passed as argument would be None in this case. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py @abstractmethod\nasync def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None:\n \"\"\"Collect device command output.\n\n This abstract coroutine can be used to implement any command collection method\n for a device in ANTA.\n\n The `_collect()` implementation needs to populate the `output` attribute\n of the `AntaCommand` object passed as argument.\n\n If a failure occurs, the `_collect()` implementation is expected to catch the\n exception and implement proper logging, the `output` attribute of the\n `AntaCommand` object passed as argument would be `None` in this case.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n"},{"location":"api/device/#anta.device.AntaDevice.collect","title":"collect async","text":"collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> 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. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect(self, command: AntaCommand, *, collection_id: str | None = None) -> 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 Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\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, collection_id=collection_id)\n await self.cache.set(command.uid, command.output) # pylint: disable=no-member\n else:\n await self._collect(command=command, collection_id=collection_id)\n"},{"location":"api/device/#anta.device.AntaDevice.collect_commands","title":"collect_commands async","text":"collect_commands(\n commands: list[AntaCommand],\n *,\n collection_id: str | None = None\n) -> None\n Collect multiple commands. Parameters: Name Type Description Default commands list[AntaCommand] The commands to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect_commands(self, commands: list[AntaCommand], *, collection_id: str | None = None) -> None:\n \"\"\"Collect multiple commands.\n\n Parameters\n ----------\n commands\n The commands to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n await asyncio.gather(*(self.collect(command=command, collection_id=collection_id) for command in commands))\n"},{"location":"api/device/#anta.device.AntaDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device, usually through SCP. It is not mandatory to implement this for a valid AntaDevice subclass. Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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\n - `is_online`: When the device IP is reachable and a port can be open.\n\n - `established`: When a command execution succeeds.\n\n - `hw_model`: The hardware model of the device.\n \"\"\"\n"},{"location":"api/device/#async-eos-device-class","title":"Async EOS device class","text":""},{"location":"api/device/#anta.device.AsyncEOSDevice","title":"AsyncEOSDevice","text":"AsyncEOSDevice(\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: set[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)\n Bases: AntaDevice Implementation of AntaDevice for EOS using aio-eapi. Attributes: Name Type Description name str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. Parameters: Name Type Description Default host str Device FQDN or IP. required username str Username to connect to eAPI and SSH. required password str Password to connect to eAPI and SSH. required name str | None Device name. None enable bool Collect commands using privileged mode. False enable_password str | None Password used to gain privileged access on EOS. None port int | None eAPI port. Defaults to 80 is proto is \u2018http\u2019 or 443 if proto is \u2018https\u2019. None ssh_port int | None SSH port. 22 tags set[str] | None Tags for this device. None timeout float | None Timeout value in seconds for outgoing API calls. None insecure bool Disable SSH Host Key validation. False proto Literal['http', 'https'] eAPI protocol. Value can be \u2018http\u2019 or \u2018https\u2019. 'https' disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AsyncEOSDevice.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AsyncEOSDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AsyncEOSDevice.\"\"\"\n return (\n f\"AsyncEOSDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r}, \"\n f\"host={self._session.host!r}, \"\n f\"eapi_port={self._session.port!r}, \"\n f\"username={self._ssh_opts.username!r}, \"\n f\"enable={self.enable!r}, \"\n f\"insecure={self._ssh_opts.known_hosts is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AsyncEOSDevice._collect","title":"_collect async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output from EOS using aio-eapi. Supports outformat json and text as output structure. Gain privileged access using the enable_password attribute of the AntaDevice instance if populated. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None: # noqa: C901 function is too complex - because of many required except blocks\n \"\"\"Collect device command output from EOS using aio-eapi.\n\n Supports outformat `json` and `text` as output structure.\n Gain privileged access using the `enable_password` attribute\n of the `AntaDevice` instance if populated.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n commands: list[dict[str, str | int]] = []\n if self.enable and self._enable_password is not None:\n commands.append(\n {\n \"cmd\": \"enable\",\n \"input\": str(self._enable_password),\n },\n )\n elif self.enable:\n # No password\n commands.append({\"cmd\": \"enable\"})\n commands += [{\"cmd\": command.command, \"revision\": command.revision}] if command.revision else [{\"cmd\": command.command}]\n try:\n response: list[dict[str, Any] | str] = await self._session.cli(\n commands=commands,\n ofmt=command.ofmt,\n version=command.version,\n req_id=f\"ANTA-{collection_id}-{id(command)}\" if collection_id else f\"ANTA-{id(command)}\",\n ) # type: ignore[assignment] # multiple commands returns a list\n # Do not keep response of 'enable' command\n command.output = response[-1]\n except asynceapi.EapiCommandError as e:\n # This block catches exceptions related to EOS issuing an error.\n command.errors = e.errors\n if command.requires_privileges:\n logger.error(\n \"Command '%s' requires privileged mode on %s. Verify user permissions and if the `enable` option is required.\", command.command, self.name\n )\n if command.supported:\n logger.error(\"Command '%s' failed on %s: %s\", command.command, self.name, e.errors[0] if len(e.errors) == 1 else e.errors)\n else:\n logger.debug(\"Command '%s' is not supported on '%s' (%s)\", command.command, self.name, self.hw_model)\n except TimeoutException as e:\n # This block catches Timeout exceptions.\n command.errors = [exc_to_str(e)]\n timeouts = self._session.timeout.as_dict()\n logger.error(\n \"%s occurred while sending a command to %s. Consider increasing the timeout.\\nCurrent timeouts: Connect: %s | Read: %s | Write: %s | Pool: %s\",\n exc_to_str(e),\n self.name,\n timeouts[\"connect\"],\n timeouts[\"read\"],\n timeouts[\"write\"],\n timeouts[\"pool\"],\n )\n except (ConnectError, OSError) as e:\n # This block catches OSError and socket issues related exceptions.\n command.errors = [exc_to_str(e)]\n if (isinstance(exc := e.__cause__, httpcore.ConnectError) and isinstance(os_error := exc.__context__, OSError)) or isinstance(os_error := e, OSError): # pylint: disable=no-member\n if isinstance(os_error.__cause__, OSError):\n os_error = os_error.__cause__\n logger.error(\"A local OS error occurred while connecting to %s: %s.\", self.name, os_error)\n else:\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n except HTTPError as e:\n # This block catches most of the httpx Exceptions and logs a general message.\n command.errors = [exc_to_str(e)]\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n logger.debug(\"%s: %s\", self.name, command)\n"},{"location":"api/device/#anta.device.AsyncEOSDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device using asyncssh.scp(). Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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() function 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 = AntaCommand(command=\"show version\")\n await self._collect(show_version)\n if not show_version.collected:\n logger.warning(\"Cannot get hardware information from device %s\", self.name)\n else:\n self.hw_model = show_version.json_output.get(\"modelName\", None)\n if self.hw_model is None:\n logger.critical(\"Cannot parse 'show version' returned by device %s\", self.name)\n # in some cases it is possible that 'modelName' comes back empty\n # and it is nice to get a meaninfule error message\n elif self.hw_model == \"\":\n logger.critical(\"Got an empty 'modelName' in the 'show version' returned by device %s\", self.name)\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.devices","title":"devices property","text":"devices: list[AntaDevice]\n List of AntaDevice in this inventory."},{"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. Parameters: Name Type Description Default device AntaDevice Device object to be added. required Source code in anta/inventory/__init__.py def add_device(self, device: AntaDevice) -> None:\n \"\"\"Add a device to final inventory.\n\n Parameters\n ----------\n device\n 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(\n *,\n established_only: bool = False,\n tags: set[str] | None = None,\n devices: set[str] | None = None\n) -> AntaInventory\n Return a filtered inventory. Parameters: Name Type Description Default established_only bool Whether or not to include only established devices. False tags set[str] | None Tags to filter devices. None devices set[str] | None Names to filter devices. None 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: set[str] | None = None, devices: set[str] | None = None) -> AntaInventory:\n \"\"\"Return a filtered inventory.\n\n Parameters\n ----------\n established_only\n Whether or not to include only established devices.\n tags\n Tags to filter devices.\n devices\n Names to filter devices.\n\n Returns\n -------\n AntaInventory\n An inventory with filtered AntaDevice objects.\n \"\"\"\n\n def _filter_devices(device: AntaDevice) -> bool:\n \"\"\"Select the devices based on the inputs `tags`, `devices` and `established_only`.\"\"\"\n if tags is not None and all(tag not in tags for tag in device.tags):\n return False\n if devices is None or device.name in devices:\n return bool(not established_only or device.established)\n return False\n\n filtered_devices: list[AntaDevice] = list(filter(_filter_devices, self.values()))\n result = AntaInventory()\n for device in filtered_devices:\n result.add_device(device)\n return result\n"},{"location":"api/inventory/#anta.inventory.AntaInventory.parse","title":"parse staticmethod","text":"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. The inventory devices are AsyncEOSDevice instances. Parameters: Name Type Description Default filename str | Path Path to device inventory YAML file. required username str Username to use to connect to devices. required password str Password to use to connect to devices. required enable_password str | None Enable password to use if required. None timeout float | None Timeout value in seconds for outgoing API calls. None enable bool Whether or not the commands need to be run in enable mode towards the devices. False insecure bool Disable SSH Host Key validation. False disable_cache bool Disable cache globally. False 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 Parameters\n ----------\n filename\n Path to device inventory YAML file.\n username\n Username to use to connect to devices.\n password\n Password to use to connect to devices.\n enable_password\n Enable password to use if required.\n timeout\n Timeout value in seconds for outgoing API calls.\n enable\n Whether or not the commands need to be run in enable mode towards the devices.\n insecure\n Disable SSH Host Key validation.\n disable_cache\n Disable cache globally.\n\n Raises\n ------\n InventoryRootKeyError\n Root key of inventory is missing.\n InventoryIncorrectSchemaError\n 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 Device inventory input model."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryInput.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/inventory/models.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return yaml.safe_dump(yaml.safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryHost","title":"AntaInventoryHost","text":" Bases: BaseModel Host entry of AntaInventoryInput. Attributes: Name Type Description host Hostname | IPvAnyAddress IP Address or FQDN of the device. port Port | None Custom eAPI port to use. name str | None Custom name of the device. tags set[str] Tags of the device. disable_cache bool Disable cache for this device."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryNetwork","title":"AntaInventoryNetwork","text":" Bases: BaseModel Network entry of AntaInventoryInput. Attributes: Name Type Description network IPvAnyNetwork Subnet to use for scanning. tags set[str] Tags of the devices in this network. disable_cache bool Disable cache for all devices in this network."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryRange","title":"AntaInventoryRange","text":" Bases: BaseModel IP Range entry of AntaInventoryInput. Attributes: Name Type Description start IPvAnyAddress IPv4 or IPv6 address for the beginning of the range. stop IPvAnyAddress IPv4 or IPv6 address for the end of the range. tags set[str] Tags of the devices in this IP range. disable_cache bool Disable cache for all devices in this IP range."},{"location":"api/md_reporter/","title":"Markdown reporter","text":"Markdown report generator for ANTA test results."},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport","title":"ANTAReport","text":"ANTAReport(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generate the # ANTA Report section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the # ANTA Report section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `# ANTA Report` section of the markdown report.\"\"\"\n self.write_heading(heading_level=1)\n toc = MD_REPORT_TOC\n self.mdfile.write(toc + \"\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase","title":"MDReportBase","text":"MDReportBase(mdfile: TextIOWrapper, results: ResultManager)\n Bases: ABC Base class for all sections subclasses. Every subclasses must implement the generate_section method that uses the ResultManager object to generate and write content to the provided markdown file. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_heading_name","title":"generate_heading_name","text":"generate_heading_name() -> str\n Generate a formatted heading name based on the class name. Returns: Type Description str Formatted header name. Example ANTAReport will become ANTA Report. TestResultsSummary will become Test Results Summary. Source code in anta/reporter/md_reporter.py def generate_heading_name(self) -> str:\n \"\"\"Generate a formatted heading name based on the class name.\n\n Returns\n -------\n str\n Formatted header name.\n\n Example\n -------\n - `ANTAReport` will become `ANTA Report`.\n - `TestResultsSummary` will become `Test Results Summary`.\n \"\"\"\n class_name = self.__class__.__name__\n\n # Split the class name into words, keeping acronyms together\n words = re.findall(r\"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\\d|\\W|$)|\\d+\", class_name)\n\n # Capitalize each word, but keep acronyms in all caps\n formatted_words = [word if word.isupper() else word.capitalize() for word in words]\n\n return \" \".join(formatted_words)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of a markdown table for a specific report section. Subclasses can implement this method to generate the content of the table rows. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of a markdown table for a specific report section.\n\n Subclasses can implement this method to generate the content of the table rows.\n \"\"\"\n msg = \"Subclasses should implement this method\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_section","title":"generate_section abstractmethod","text":"generate_section() -> None\n Abstract method to generate a specific section of the markdown report. Must be implemented by subclasses. Source code in anta/reporter/md_reporter.py @abstractmethod\ndef generate_section(self) -> None:\n \"\"\"Abstract method to generate a specific section of the markdown report.\n\n Must be implemented by subclasses.\n \"\"\"\n msg = \"Must be implemented by subclasses\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.safe_markdown","title":"safe_markdown","text":"safe_markdown(text: str | None) -> str\n Escape markdown characters in the text to prevent markdown rendering issues. Parameters: Name Type Description Default text str | None The text to escape markdown characters from. required Returns: Type Description str The text with escaped markdown characters. Source code in anta/reporter/md_reporter.py def safe_markdown(self, text: str | None) -> str:\n \"\"\"Escape markdown characters in the text to prevent markdown rendering issues.\n\n Parameters\n ----------\n text\n The text to escape markdown characters from.\n\n Returns\n -------\n str\n The text with escaped markdown characters.\n \"\"\"\n # Custom field from a TestResult object can be None\n if text is None:\n return \"\"\n\n # Replace newlines with spaces to keep content on one line\n text = text.replace(\"\\n\", \" \")\n\n # Replace backticks with single quotes\n return text.replace(\"`\", \"'\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_heading","title":"write_heading","text":"write_heading(heading_level: int) -> None\n Write a markdown heading to the markdown file. The heading name used is the class name. Parameters: Name Type Description Default heading_level int The level of the heading (1-6). required Example ## Test Results Summary Source code in anta/reporter/md_reporter.py def write_heading(self, heading_level: int) -> None:\n \"\"\"Write a markdown heading to the markdown file.\n\n The heading name used is the class name.\n\n Parameters\n ----------\n heading_level\n The level of the heading (1-6).\n\n Example\n -------\n `## Test Results Summary`\n \"\"\"\n # Ensure the heading level is within the valid range of 1 to 6\n heading_level = max(1, min(heading_level, 6))\n heading_name = self.generate_heading_name()\n heading = \"#\" * heading_level + \" \" + heading_name\n self.mdfile.write(f\"{heading}\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_table","title":"write_table","text":"write_table(\n table_heading: list[str], *, last_table: bool = False\n) -> None\n Write a markdown table with a table heading and multiple rows to the markdown file. Parameters: Name Type Description Default table_heading list[str] List of strings to join for the table heading. required last_table bool Flag to determine if it\u2019s the last table of the markdown file to avoid unnecessary new line. Defaults to False. False Source code in anta/reporter/md_reporter.py def write_table(self, table_heading: list[str], *, last_table: bool = False) -> None:\n \"\"\"Write a markdown table with a table heading and multiple rows to the markdown file.\n\n Parameters\n ----------\n table_heading\n List of strings to join for the table heading.\n last_table\n Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.\n \"\"\"\n self.mdfile.write(\"\\n\".join(table_heading) + \"\\n\")\n for row in self.generate_rows():\n self.mdfile.write(row)\n if not last_table:\n self.mdfile.write(\"\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator","title":"MDReportGenerator","text":"Class responsible for generating a Markdown report based on the provided ResultManager object. It aggregates different report sections, each represented by a subclass of MDReportBase, and sequentially generates their content into a markdown file. The generate class method will loop over all the section subclasses and call their generate_section method. The final report will be generated in the same order as the sections list of the method."},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator.generate","title":"generate classmethod","text":"generate(results: ResultManager, md_filename: Path) -> None\n Generate and write the various sections of the markdown report. Parameters: Name Type Description Default results ResultManager The ResultsManager instance containing all test results. required md_filename Path The path to the markdown file to write the report into. required Source code in anta/reporter/md_reporter.py @classmethod\ndef generate(cls, results: ResultManager, md_filename: Path) -> None:\n \"\"\"Generate and write the various sections of the markdown report.\n\n Parameters\n ----------\n results\n The ResultsManager instance containing all test results.\n md_filename\n The path to the markdown file to write the report into.\n \"\"\"\n try:\n with md_filename.open(\"w\", encoding=\"utf-8\") as mdfile:\n sections: list[MDReportBase] = [\n ANTAReport(mdfile, results),\n TestResultsSummary(mdfile, results),\n SummaryTotals(mdfile, results),\n SummaryTotalsDeviceUnderTest(mdfile, results),\n SummaryTotalsPerCategory(mdfile, results),\n TestResults(mdfile, results),\n ]\n for section in sections:\n section.generate_section()\n except OSError as exc:\n message = f\"OSError caught while writing the Markdown file '{md_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals","title":"SummaryTotals","text":"SummaryTotals(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals table.\"\"\"\n yield (\n f\"| {self.results.get_total_results()} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SUCCESS})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SKIPPED})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.FAILURE})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.ERROR})} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest","title":"SummaryTotalsDeviceUnderTest","text":"SummaryTotalsDeviceUnderTest(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Devices Under Tests section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals device under test table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals device under test table.\"\"\"\n for device, stat in self.results.device_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n categories_skipped = \", \".join(sorted(convert_categories(list(stat.categories_skipped))))\n categories_failed = \", \".join(sorted(convert_categories(list(stat.categories_failed))))\n yield (\n f\"| {device} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} | {stat.tests_error_count} \"\n f\"| {categories_skipped or '-'} | {categories_failed or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Devices Under Tests section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Devices Under Tests` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory","title":"SummaryTotalsPerCategory","text":"SummaryTotalsPerCategory(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Per Category section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals per category table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals per category table.\"\"\"\n for category, stat in self.results.sorted_category_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n yield (\n f\"| {category} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} \"\n f\"| {stat.tests_error_count} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Per Category section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Per Category` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults","title":"TestResults","text":"TestResults(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generates the ## Test Results section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the all test results table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the all test results table.\"\"\"\n for result in self.results.get_results(sort_by=[\"name\", \"test\"]):\n messages = self.safe_markdown(\", \".join(result.messages))\n categories = \", \".join(convert_categories(result.categories))\n yield (\n f\"| {result.name or '-'} | {categories or '-'} | {result.test or '-'} \"\n f\"| {result.description or '-'} | {self.safe_markdown(result.custom_field) or '-'} | {result.result or '-'} | {messages or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n self.write_table(table_heading=self.TABLE_HEADING, last_table=True)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary","title":"TestResultsSummary","text":"TestResultsSummary(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ## Test Results Summary section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results Summary section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results Summary` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n"},{"location":"api/models/","title":"Test models","text":""},{"location":"api/models/#test-definition","title":"Test definition","text":""},{"location":"api/models/#anta.models.AntaTest","title":"AntaTest","text":"AntaTest(\n device: AntaDevice,\n inputs: dict[str, Any] | AntaTest.Input | None = None,\n eos_data: list[dict[Any, Any] | str] | None = None,\n)\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 '''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 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: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. Parameters: Name Type Description Default device AntaDevice AntaDevice instance on which the test will be run. required inputs dict[str, Any] | Input | None Dictionary of attributes used to instantiate the AntaTest.Input instance. None eos_data list[dict[Any, Any] | str] | None Populate outputs of the test commands instead of collecting from devices. This list must have the same length and order than the instance_commands instance attribute. None"},{"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 Return 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 Return a list of all the commands that have failed."},{"location":"api/models/#anta.models.AntaTest.module","title":"module property","text":"module: str\n Return the Python module in which this AntaTest class is defined."},{"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: Name Type Description result_overwrite ResultOverwrite | None 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 set[str] | None Tag of devices on which to run 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 str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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(\n function: F,\n) -> Callable[..., Coroutine[Any, Any, TestResult]]\n Decorate the test() method in child classes. This decorator implements (in this order): Instantiate the command outputs if eos_data is provided to the test() method Collect the commands from the device Run the test() method 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 Parameters\n ----------\n self\n The test instance.\n eos_data\n 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\n Any keyword argument to pass to the test.\n\n Returns\n -------\n TestResult\n The TestResult instance attribute populated with error status if any.\n\n \"\"\"\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 AntaTest.update_progress()\n return self.result\n\n if cmds := self.failed_commands:\n unsupported_commands = [f\"'{c.command}' is not supported on {self.device.hw_model}\" for c in cmds if not c.supported]\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 else:\n self.result.is_error(message=\"\\n\".join([f\"{c.command} has failed: {', '.join(c.errors)}\" for c in cmds]))\n AntaTest.update_progress()\n return self.result\n\n try:\n function(self, **kwargs)\n except Exception as e: # noqa: BLE001\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 # TODO: find a correct way to time test execution\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, collection_id=self.name)\n except Exception as e: # noqa: BLE001\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]:\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 _ = template\n msg = f\"AntaTemplate are provided but render() method has not been implemented for {self.module}.{self.__class__.__name__}\"\n raise NotImplementedError(msg)\n"},{"location":"api/models/#anta.models.AntaTest.save_commands_data","title":"save_commands_data","text":"save_commands_data(\n eos_data: list[dict[str, Any] | str]\n) -> 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 reason\")\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 reason\")\n ```\n\n \"\"\"\n"},{"location":"api/models/#command-definition","title":"Command definition","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 str Device command. version Literal[1, 'latest'] eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision | None eAPI revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt Literal['json', 'text'] eAPI output - json or text. output dict[str, Any] | str | None Output of the command. Only defined if there was no errors. template AntaTemplate | None AntaTemplate object used to render this command. errors list[str] If the command execution fails, eAPI returns a list of strings detailing the error(s). params AntaParamsBaseModel Pydantic Model containing the variables values used to render the template. use_cache bool Enable or disable caching for this AntaCommand if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaCommand.collected","title":"collected property","text":"collected: bool\n Return True if the command has been collected, False otherwise. A command that has not been collected could have returned an error. See error property."},{"location":"api/models/#anta.models.AntaCommand.error","title":"error property","text":"error: bool\n Return True if the command returned an error, False otherwise."},{"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.requires_privileges","title":"requires_privileges property","text":"requires_privileges: bool\n Return True if the command requires privileged mode, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"location":"api/models/#anta.models.AntaCommand.supported","title":"supported property","text":"supported: bool\n Return True if the command is supported on the device hardware platform, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"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/#anta.models.AntaTemplate","title":"AntaTemplate","text":"AntaTemplate(\n template: str,\n version: Literal[1, \"latest\"] = \"latest\",\n revision: Revision | None = None,\n ofmt: Literal[\"json\", \"text\"] = \"json\",\n *,\n use_cache: bool = True\n)\n 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: \u2018show vlan {vlan_id}\u2019. version eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt eAPI output - json or text. use_cache Enable or disable caching for this AntaTemplate if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaTemplate.__repr__","title":"__repr__","text":"__repr__() -> str\n Return the representation of the class. Copying pydantic model style, excluding params_schema Source code in anta/models.py def __repr__(self) -> str:\n \"\"\"Return the representation of the class.\n\n Copying pydantic model style, excluding `params_schema`\n \"\"\"\n return \" \".join(f\"{a}={v!r}\" for a, v in vars(self).items() if a != \"params_schema\")\n"},{"location":"api/models/#anta.models.AntaTemplate.render","title":"render","text":"render(**params: str | int | bool) -> AntaCommand\n Render an AntaCommand from an AntaTemplate instance. Keep the parameters used in the AntaTemplate instance. Parameters: Name Type Description Default params str | int | bool Dictionary of variables with string values to render the Python f-string. {} Returns: Type Description AntaCommand The rendered AntaCommand. This AntaCommand instance have a template attribute that references this AntaTemplate instance. Raises: Type Description AntaTemplateRenderError If a parameter is missing to render the AntaTemplate instance. Source code in anta/models.py def render(self, **params: str | int | bool) -> AntaCommand:\n \"\"\"Render an AntaCommand from an AntaTemplate instance.\n\n Keep the parameters used in the AntaTemplate instance.\n\n Parameters\n ----------\n params\n Dictionary of variables with string values to render the Python f-string.\n\n Returns\n -------\n AntaCommand\n The rendered AntaCommand.\n This AntaCommand instance have a template attribute that references this\n AntaTemplate instance.\n\n Raises\n ------\n AntaTemplateRenderError\n If a parameter is missing to render the AntaTemplate instance.\n \"\"\"\n try:\n command = self.template.format(**params)\n except (KeyError, SyntaxError) as e:\n raise AntaTemplateRenderError(self, e.args[0]) from e\n return AntaCommand(\n command=command,\n ofmt=self.ofmt,\n version=self.version,\n revision=self.revision,\n template=self,\n params=self.params_schema(**params),\n use_cache=self.use_cache,\n )\n"},{"location":"api/reporters/","title":"Other reporters","text":"Report management for ANTA."},{"location":"api/reporters/#anta.reporter.ReportJinja","title":"ReportJinja","text":"ReportJinja(template_path: pathlib.Path)\n Report builder based on a Jinja2 template."},{"location":"api/reporters/#anta.reporter.ReportJinja.render","title":"render","text":"render(\n data: list[dict[str, Any]],\n *,\n trim_blocks: bool = True,\n lstrip_blocks: bool = True\n) -> str\n Build a report based on a Jinja2 template. Report is built based on a J2 template provided by user. Data structure sent to template is: Example >>> print(ResultManager.json)\n[\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n]\n Parameters: Name Type Description Default data list[dict[str, Any]] List of results from ResultManager.results. required trim_blocks bool enable trim_blocks for J2 rendering. True lstrip_blocks bool enable lstrip_blocks for J2 rendering. True Returns: Type Description str Rendered template Source code in anta/reporter/__init__.py def render(self, data: list[dict[str, Any]], *, trim_blocks: bool = True, lstrip_blocks: bool = True) -> str:\n \"\"\"Build a report based on a Jinja2 template.\n\n Report is built based on a J2 template provided by user.\n Data structure sent to template is:\n\n Example\n -------\n ```\n >>> print(ResultManager.json)\n [\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n ]\n ```\n\n Parameters\n ----------\n data\n List of results from `ResultManager.results`.\n trim_blocks\n enable trim_blocks for J2 rendering.\n lstrip_blocks\n enable lstrip_blocks for J2 rendering.\n\n Returns\n -------\n str\n Rendered template\n\n \"\"\"\n with self.template_path.open(encoding=\"utf-8\") as file_:\n template = Template(file_.read(), trim_blocks=trim_blocks, lstrip_blocks=lstrip_blocks)\n\n return template.render({\"data\": data})\n"},{"location":"api/reporters/#anta.reporter.ReportTable","title":"ReportTable","text":"TableReport Generate a Table based on TestResult."},{"location":"api/reporters/#anta.reporter.ReportTable.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_case: str = \"Test Name\",\n number_of_success: str = \"# of success\",\n number_of_failure: str = \"# of failure\",\n number_of_skipped: str = \"# of skipped\",\n number_of_errors: str = \"# of errors\",\n list_of_error_nodes: str = \"List of failed or error nodes\",\n list_of_error_tests: str = \"List of failed or error test cases\",\n)\n Headers for the table report."},{"location":"api/reporters/#anta.reporter.ReportTable.report_all","title":"report_all","text":"report_all(\n manager: ResultManager, title: str = \"All tests results\"\n) -> Table\n Create a table report with all tests for one or all devices. Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required title str Title for the report. Defaults to \u2018All tests results\u2019. 'All tests results' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_all(self, manager: ResultManager, title: str = \"All tests results\") -> Table:\n \"\"\"Create a table report with all tests for one or all devices.\n\n Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n title\n Title for the report. Defaults to 'All tests results'.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\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 def add_line(result: TestResult) -> None:\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(convert_categories(result.categories))\n table.add_row(str(result.name), result.test, state, message, result.description, categories)\n\n for result in manager.results:\n add_line(result)\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_devices","title":"report_summary_devices","text":"report_summary_devices(\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table\n Create a table report with result aggregated per device. Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required devices list[str] | None List of device names to include. None to select all devices. None title str Title of the report. 'Summary per device' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_devices(\n self,\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per device.\n\n Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n devices\n List of device names to include. None to select all devices.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.device,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_tests,\n ]\n table = self._build_headers(headers=headers, table=table)\n for device, stats in sorted(manager.device_stats.items()):\n if devices is None or device in devices:\n table.add_row(\n device,\n str(stats.tests_success_count),\n str(stats.tests_skipped_count),\n str(stats.tests_failure_count),\n str(stats.tests_error_count),\n \", \".join(stats.tests_failure),\n )\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_tests","title":"report_summary_tests","text":"report_summary_tests(\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table\n Create a table report with result aggregated per test. Create table with full output: Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required tests list[str] | None List of test names to include. None to select all tests. None title str Title of the report. 'Summary per test' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_tests(\n self,\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per test.\n\n Create table with full output:\n Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n tests\n List of test names to include. None to select all tests.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.test_case,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_nodes,\n ]\n table = self._build_headers(headers=headers, table=table)\n for test, stats in sorted(manager.test_stats.items()):\n if tests is None or test in tests:\n table.add_row(\n test,\n str(stats.devices_success_count),\n str(stats.devices_skipped_count),\n str(stats.devices_failure_count),\n str(stats.devices_error_count),\n \", \".join(stats.devices_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":" options:\n filters: [\"!^_[^_]\", \"!^__len__\"]\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager","title":"ResultManager","text":"ResultManager()\n Helper to manage Test Results and generate reports. Examples Create Inventory: inventory_anta = AntaInventory.parse(\n filename='examples/inventory.yml',\n username='ansible',\n password='ansible',\n)\n Create Result Manager: manager = ResultManager()\n Run tests for all connected devices: for device in inventory_anta.get_inventory().devices:\n manager.add(\n VerifyNTP(device=device).test()\n )\n manager.add(\n VerifyEOSVersion(device=device).test(version='4.28.3M')\n )\n Print result in native format: manager.results\n[\n TestResult(\n name=\"pf1\",\n test=\"VerifyZeroTouch\",\n categories=[\"configuration\"],\n description=\"Verifies ZeroTouch is disabled\",\n result=\"success\",\n messages=[],\n custom_field=None,\n ),\n TestResult(\n name=\"pf1\",\n test='VerifyNTP',\n categories=[\"software\"],\n categories=['system'],\n description='Verifies if NTP is synchronised.',\n result='failure',\n messages=[\"The device is not synchronized with the configured NTP server(s): 'NTP is disabled.'\"],\n custom_field=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."},{"location":"api/result_manager/#anta.result_manager.ResultManager.json","title":"json property","text":"json: str\n Get a JSON representation of the results."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results","title":"results property writable","text":"results: list[TestResult]\n Get the list of TestResult."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results_by_status","title":"results_by_status cached property","text":"results_by_status: dict[AntaTestStatus, list[TestResult]]\n A cached property that returns the results grouped by status."},{"location":"api/result_manager/#anta.result_manager.ResultManager.sorted_category_stats","title":"sorted_category_stats property","text":"sorted_category_stats: dict[str, CategoryStats]\n A property that returns the category_stats dictionary sorted by key name."},{"location":"api/result_manager/#anta.result_manager.ResultManager.__len__","title":"__len__","text":"__len__() -> int\n Implement len method to count number of results. Source code in anta/result_manager/__init__.py def __len__(self) -> int:\n \"\"\"Implement __len__ method to count number of results.\"\"\"\n return len(self._result_entries)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.add","title":"add","text":"add(result: TestResult) -> None\n Add a result to the ResultManager instance. The result is added to the internal list of results and the overall status of the ResultManager instance is updated based on the added test status. Parameters: Name Type Description Default result TestResult TestResult to add to the ResultManager instance. required Source code in anta/result_manager/__init__.py def add(self, result: TestResult) -> None:\n \"\"\"Add a result to the ResultManager instance.\n\n The result is added to the internal list of results and the overall status\n of the ResultManager instance is updated based on the added test status.\n\n Parameters\n ----------\n result\n TestResult to add to the ResultManager instance.\n \"\"\"\n self._result_entries.append(result)\n self._update_status(result.result)\n self._update_stats(result)\n\n # Every time a new result is added, we need to clear the cached property\n self.__dict__.pop(\"results_by_status\", None)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter","title":"filter","text":"filter(hide: set[AntaTestStatus]) -> ResultManager\n Get a filtered ResultManager based on test status. Parameters: Name Type Description Default hide set[AntaTestStatus] Set of AntaTestStatus enum members to select tests to hide based on their status. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter(self, hide: set[AntaTestStatus]) -> ResultManager:\n \"\"\"Get a filtered ResultManager based on test status.\n\n Parameters\n ----------\n hide\n Set of AntaTestStatus enum members to select tests to hide based on their status.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n possible_statuses = set(AntaTestStatus)\n manager = ResultManager()\n manager.results = self.get_results(possible_statuses - hide)\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_devices","title":"filter_by_devices","text":"filter_by_devices(devices: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific devices. Parameters: Name Type Description Default devices set[str] Set of device names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_devices(self, devices: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific devices.\n\n Parameters\n ----------\n devices\n Set of device names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.name in devices]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_tests","title":"filter_by_tests","text":"filter_by_tests(tests: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific tests. Parameters: Name Type Description Default tests set[str] Set of test names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_tests(self, tests: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific tests.\n\n Parameters\n ----------\n tests\n Set of test names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.test in tests]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_devices","title":"get_devices","text":"get_devices() -> set[str]\n Get the set of all the device names. Returns: Type Description set[str] Set of device names. Source code in anta/result_manager/__init__.py def get_devices(self) -> set[str]:\n \"\"\"Get the set of all the device names.\n\n Returns\n -------\n set[str]\n Set of device names.\n \"\"\"\n return {str(result.name) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_results","title":"get_results","text":"get_results(\n status: set[AntaTestStatus] | None = None,\n sort_by: list[str] | None = None,\n) -> list[TestResult]\n Get the results, optionally filtered by status and sorted by TestResult fields. If no status is provided, all results are returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None sort_by list[str] | None Optional list of TestResult fields to sort the results. None Returns: Type Description list[TestResult] List of results. Source code in anta/result_manager/__init__.py def get_results(self, status: set[AntaTestStatus] | None = None, sort_by: list[str] | None = None) -> list[TestResult]:\n \"\"\"Get the results, optionally filtered by status and sorted by TestResult fields.\n\n If no status is provided, all results are returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n sort_by\n Optional list of TestResult fields to sort the results.\n\n Returns\n -------\n list[TestResult]\n List of results.\n \"\"\"\n # Return all results if no status is provided, otherwise return results for multiple statuses\n results = self._result_entries if status is None else list(chain.from_iterable(self.results_by_status.get(status, []) for status in status))\n\n if sort_by:\n accepted_fields = TestResult.model_fields.keys()\n if not set(sort_by).issubset(set(accepted_fields)):\n msg = f\"Invalid sort_by fields: {sort_by}. Accepted fields are: {list(accepted_fields)}\"\n raise ValueError(msg)\n results = sorted(results, key=lambda result: [getattr(result, field) for field in sort_by])\n\n return results\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_tests","title":"get_tests","text":"get_tests() -> set[str]\n Get the set of all the test names. Returns: Type Description set[str] Set of test names. Source code in anta/result_manager/__init__.py def get_tests(self) -> set[str]:\n \"\"\"Get the set of all the test names.\n\n Returns\n -------\n set[str]\n Set of test names.\n \"\"\"\n return {str(result.test) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_total_results","title":"get_total_results","text":"get_total_results(\n status: set[AntaTestStatus] | None = None,\n) -> int\n Get the total number of results, optionally filtered by status. If no status is provided, the total number of results is returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None Returns: Type Description int Total number of results. Source code in anta/result_manager/__init__.py def get_total_results(self, status: set[AntaTestStatus] | None = None) -> int:\n \"\"\"Get the total number of results, optionally filtered by status.\n\n If no status is provided, the total number of results is returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n\n Returns\n -------\n int\n Total number of results.\n \"\"\"\n if status is None:\n # Return the total number of results\n return sum(len(results) for results in self.results_by_status.values())\n\n # Return the total number of results for multiple statuses\n return sum(len(self.results_by_status.get(status, [])) for status in status)\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/#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 str Name of the device where the test was run. test str Name of the test run on the device. categories list[str] List of categories the TestResult belongs to. Defaults to the AntaTest categories. description str Description of the TestResult. Defaults to the AntaTest description. result AntaTestStatus Result of the test. Must be one of the AntaTestStatus Enum values: unset, success, failure, error or skipped. messages list[str] Messages to report after the test, if any. custom_field str | None 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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_error(self, message: str | None = None) -> None:\n \"\"\"Set status to error.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_failure(self, message: str | None = None) -> None:\n \"\"\"Set status to failure.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_skipped(self, message: str | None = None) -> None:\n \"\"\"Set status to skipped.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_success(self, message: str | None = None) -> None:\n \"\"\"Set status to success.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.SUCCESS, message)\n"},{"location":"api/runner/","title":"Runner","text":""},{"location":"api/runner/#anta.runner","title":"runner","text":"ANTA runner function."},{"location":"api/runner/#anta.runner.adjust_rlimit_nofile","title":"adjust_rlimit_nofile","text":"adjust_rlimit_nofile() -> tuple[int, int]\n Adjust the maximum number of open file descriptors for the ANTA process. The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable. If the ANTA_NOFILE environment variable is not set or is invalid, DEFAULT_NOFILE is used. Returns: Type Description tuple[int, int] The new soft and hard limits for open file descriptors. Source code in anta/runner.py def adjust_rlimit_nofile() -> tuple[int, int]:\n \"\"\"Adjust the maximum number of open file descriptors for the ANTA process.\n\n The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable.\n\n If the `ANTA_NOFILE` environment variable is not set or is invalid, `DEFAULT_NOFILE` is used.\n\n Returns\n -------\n tuple[int, int]\n The new soft and hard limits for open file descriptors.\n \"\"\"\n try:\n nofile = int(os.environ.get(\"ANTA_NOFILE\", DEFAULT_NOFILE))\n except ValueError as exception:\n logger.warning(\"The ANTA_NOFILE environment variable value is invalid: %s\\nDefault to %s.\", exc_to_str(exception), DEFAULT_NOFILE)\n nofile = DEFAULT_NOFILE\n\n limits = resource.getrlimit(resource.RLIMIT_NOFILE)\n logger.debug(\"Initial limit numbers for open file descriptors for the current ANTA process: Soft Limit: %s | Hard Limit: %s\", limits[0], limits[1])\n nofile = min(limits[1], nofile)\n logger.debug(\"Setting soft limit for open file descriptors for the current ANTA process to %s\", nofile)\n resource.setrlimit(resource.RLIMIT_NOFILE, (nofile, limits[1]))\n return resource.getrlimit(resource.RLIMIT_NOFILE)\n"},{"location":"api/runner/#anta.runner.get_coroutines","title":"get_coroutines","text":"get_coroutines(\n selected_tests: defaultdict[\n AntaDevice, set[AntaTestDefinition]\n ],\n manager: ResultManager,\n) -> list[Coroutine[Any, Any, TestResult]]\n Get the coroutines for the ANTA run. Parameters: Name Type Description Default selected_tests defaultdict[AntaDevice, set[AntaTestDefinition]] A mapping of devices to the tests to run. The selected tests are generated by the prepare_tests function. required manager ResultManager A ResultManager required Returns: Type Description list[Coroutine[Any, Any, TestResult]] The list of coroutines to run. Source code in anta/runner.py def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]], manager: ResultManager) -> list[Coroutine[Any, Any, TestResult]]:\n \"\"\"Get the coroutines for the ANTA run.\n\n Parameters\n ----------\n selected_tests\n A mapping of devices to the tests to run. The selected tests are generated by the `prepare_tests` function.\n manager\n A ResultManager\n\n Returns\n -------\n list[Coroutine[Any, Any, TestResult]]\n The list of coroutines to run.\n \"\"\"\n coros = []\n for device, test_definitions in selected_tests.items():\n for test in test_definitions:\n try:\n test_instance = test.test(device=device, inputs=test.inputs)\n manager.add(test_instance.result)\n coros.append(test_instance.test())\n except Exception as e: # noqa: PERF203, BLE001\n # An AntaTest instance is potentially user-defined code.\n # We need to catch everything and exit gracefully with an error message.\n message = \"\\n\".join(\n [\n f\"There is an error when creating test {test.test.__module__}.{test.test.__name__}.\",\n f\"If this is not a custom test implementation: {GITHUB_SUGGESTION}\",\n ],\n )\n anta_log_exception(e, message, logger)\n return coros\n"},{"location":"api/runner/#anta.runner.log_cache_statistics","title":"log_cache_statistics","text":"log_cache_statistics(devices: list[AntaDevice]) -> None\n Log cache statistics for each device in the inventory. Parameters: Name Type Description Default devices list[AntaDevice] List of devices in the inventory. required Source code in anta/runner.py def log_cache_statistics(devices: list[AntaDevice]) -> None:\n \"\"\"Log cache statistics for each device in the inventory.\n\n Parameters\n ----------\n devices\n List of devices in the inventory.\n \"\"\"\n for device in devices:\n if device.cache_statistics is not None:\n msg = (\n f\"Cache statistics for '{device.name}': \"\n f\"{device.cache_statistics['cache_hits']} hits / {device.cache_statistics['total_commands_sent']} \"\n f\"command(s) ({device.cache_statistics['cache_hit_ratio']})\"\n )\n logger.info(msg)\n else:\n logger.info(\"Caching is not enabled on %s\", device.name)\n"},{"location":"api/runner/#anta.runner.main","title":"main async","text":"main(\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False\n) -> None\n Run ANTA. Use this as an entrypoint to the test framework in your script. ResultManager object gets updated with the test results. Parameters: Name Type Description Default manager ResultManager ResultManager object to populate with the test results. required inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required devices set[str] | None Devices on which to run tests. None means all devices. These may come from the --device / -d CLI option in NRFU. None tests set[str] | None Tests to run against devices. None means all tests. These may come from the --test / -t CLI option in NRFU. None tags set[str] | None Tags to filter devices from the inventory. These may come from the --tags CLI option in NRFU. None established_only bool Include only established device(s). True dry_run bool Build the list of coroutine to run and stop before test execution. False Source code in anta/runner.py @cprofile()\nasync def main( # noqa: PLR0913\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False,\n) -> None:\n \"\"\"Run ANTA.\n\n Use this as an entrypoint to the test framework in your script.\n ResultManager object gets updated with the test results.\n\n Parameters\n ----------\n manager\n ResultManager object to populate with the test results.\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n devices\n Devices on which to run tests. None means all devices. These may come from the `--device / -d` CLI option in NRFU.\n tests\n Tests to run against devices. None means all tests. These may come from the `--test / -t` CLI option in NRFU.\n tags\n Tags to filter devices from the inventory. These may come from the `--tags` CLI option in NRFU.\n established_only\n Include only established device(s).\n dry_run\n Build the list of coroutine to run and stop before test execution.\n \"\"\"\n # Adjust the maximum number of open file descriptors for the ANTA process\n limits = adjust_rlimit_nofile()\n\n if not catalog.tests:\n logger.info(\"The list of tests is empty, exiting\")\n return\n\n with Catchtime(logger=logger, message=\"Preparing ANTA NRFU Run\"):\n # Setup the inventory\n selected_inventory = inventory if dry_run else await setup_inventory(inventory, tags, devices, established_only=established_only)\n if selected_inventory is None:\n return\n\n with Catchtime(logger=logger, message=\"Preparing the tests\"):\n selected_tests = prepare_tests(selected_inventory, catalog, tests, tags)\n if selected_tests is None:\n return\n final_tests_count = sum(len(tests) for tests in selected_tests.values())\n\n run_info = (\n \"--- ANTA NRFU Run Information ---\\n\"\n f\"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\\n\"\n f\"Total number of selected tests: {final_tests_count}\\n\"\n f\"Maximum number of open file descriptors for the current ANTA process: {limits[0]}\\n\"\n \"---------------------------------\"\n )\n\n logger.info(run_info)\n\n if final_tests_count > limits[0]:\n logger.warning(\n \"The number of concurrent tests is higher than the open file descriptors limit for this ANTA process.\\n\"\n \"Errors may occur while running the tests.\\n\"\n \"Please consult the ANTA FAQ.\"\n )\n\n coroutines = get_coroutines(selected_tests, manager)\n\n if dry_run:\n logger.info(\"Dry-run mode, exiting before running the tests.\")\n for coro in coroutines:\n coro.close()\n return\n\n if AntaTest.progress is not None:\n AntaTest.nrfu_task = AntaTest.progress.add_task(\"Running NRFU Tests...\", total=len(coroutines))\n\n with Catchtime(logger=logger, message=\"Running ANTA tests\"):\n await asyncio.gather(*coroutines)\n\n log_cache_statistics(selected_inventory.devices)\n"},{"location":"api/runner/#anta.runner.prepare_tests","title":"prepare_tests","text":"prepare_tests(\n inventory: AntaInventory,\n catalog: AntaCatalog,\n tests: set[str] | None,\n tags: set[str] | None,\n) -> (\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n)\n Prepare the tests to run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required tests set[str] | None Tests to run against devices. None means all tests. required tags set[str] | None Tags to filter devices from the inventory. required Returns: Type Description defaultdict[AntaDevice, set[AntaTestDefinition]] | None A mapping of devices to the tests to run or None if there are no tests to run. Source code in anta/runner.py def prepare_tests(\n inventory: AntaInventory, catalog: AntaCatalog, tests: set[str] | None, tags: set[str] | None\n) -> defaultdict[AntaDevice, set[AntaTestDefinition]] | None:\n \"\"\"Prepare the tests to run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n tests\n Tests to run against devices. None means all tests.\n tags\n Tags to filter devices from the inventory.\n\n Returns\n -------\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n A mapping of devices to the tests to run or None if there are no tests to run.\n \"\"\"\n # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests\n catalog.build_indexes(filtered_tests=tests)\n\n # Using a set to avoid inserting duplicate tests\n device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)\n\n total_test_count = 0\n\n # Create the device to tests mapping from the tags\n for device in inventory.devices:\n if tags:\n # If there are CLI tags, execute tests with matching tags for this device\n if not (matching_tags := tags.intersection(device.tags)):\n # The device does not have any selected tag, skipping\n continue\n device_to_tests[device].update(catalog.get_tests_by_tags(matching_tags))\n else:\n # If there is no CLI tags, execute all tests that do not have any tags\n device_to_tests[device].update(catalog.tag_to_tests[None])\n\n # Then add the tests with matching tags from device tags\n device_to_tests[device].update(catalog.get_tests_by_tags(device.tags))\n\n total_test_count += len(device_to_tests[device])\n\n if total_test_count == 0:\n msg = (\n f\"There are no tests{f' matching the tags {tags} ' if tags else ' '}to run in the current test catalog and device inventory, please verify your inputs.\"\n )\n logger.warning(msg)\n return None\n\n return device_to_tests\n"},{"location":"api/runner/#anta.runner.setup_inventory","title":"setup_inventory async","text":"setup_inventory(\n inventory: AntaInventory,\n tags: set[str] | None,\n devices: set[str] | None,\n *,\n established_only: bool\n) -> AntaInventory | None\n Set up the inventory for the ANTA run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required tags set[str] | None Tags to filter devices from the inventory. required devices set[str] | None Devices on which to run tests. None means all devices. required established_only bool If True use return only devices where a connection is established. required Returns: Type Description AntaInventory | None The filtered inventory or None if there are no devices to run tests on. Source code in anta/runner.py async def setup_inventory(inventory: AntaInventory, tags: set[str] | None, devices: set[str] | None, *, established_only: bool) -> AntaInventory | None:\n \"\"\"Set up the inventory for the ANTA run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n tags\n Tags to filter devices from the inventory.\n devices\n Devices on which to run tests. None means all devices.\n established_only\n If True use return only devices where a connection is established.\n\n Returns\n -------\n AntaInventory | None\n The filtered inventory or None if there are no devices to run tests on.\n \"\"\"\n if len(inventory) == 0:\n logger.info(\"The inventory is empty, exiting\")\n return None\n\n # Filter the inventory based on the CLI provided tags and devices if any\n selected_inventory = inventory.get_inventory(tags=tags, devices=devices) if tags or devices else inventory\n\n with Catchtime(logger=logger, message=\"Connecting to devices\"):\n # Connect to the devices\n await selected_inventory.connect_inventory()\n\n # Remove devices that are unreachable\n selected_inventory = selected_inventory.get_inventory(established_only=established_only)\n\n # If there are no devices in the inventory after filtering, exit\n if not selected_inventory.devices:\n msg = f'No reachable device {f\"matching the tags {tags} \" if tags else \"\"}was found.{f\" Selected devices: {devices} \" if devices is not None else \"\"}'\n logger.warning(msg)\n return None\n\n return selected_inventory\n"},{"location":"api/test.cvx/","title":"Test.cvx","text":""},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX","title":"VerifyManagementCVX","text":"Verifies the management CVX global status. Expected Results Success: The test will pass if the management CVX global status matches the expected status. Failure: The test will fail if the management CVX global status does not match the expected status. Examples anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n Source code in anta/tests/cvx.py class VerifyManagementCVX(AntaTest):\n \"\"\"Verifies the management CVX global status.\n\n Expected Results\n ----------------\n * Success: The test will pass if the management CVX global status matches the expected status.\n * Failure: The test will fail if the management CVX global status does not match the expected status.\n\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyManagementCVX test.\"\"\"\n\n enabled: bool\n \"\"\"Whether management CVX must be enabled (True) or disabled (False).\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyManagementCVX.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n cluster_status = command_output[\"clusterStatus\"]\n if (cluster_state := cluster_status.get(\"enabled\")) != self.inputs.enabled:\n self.result.is_failure(f\"Management CVX status is not valid: {cluster_state}\")\n"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX-attributes","title":"Inputs","text":"Name Type Description Default enabled bool Whether management CVX must be enabled (True) or disabled (False). -"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyMcsClientMounts","title":"VerifyMcsClientMounts","text":"Verify if all MCS client mounts are in mountStateMountComplete. Expected Results Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete. Failure: The test will fail even if one switch\u2019s MCS client mount status is not mountStateMountComplete. Examples anta.tests.cvx:\n- VerifyMcsClientMounts:\n Source code in anta/tests/cvx.py class VerifyMcsClientMounts(AntaTest):\n \"\"\"Verify if all MCS client mounts are in mountStateMountComplete.\n\n Expected Results\n ----------------\n * Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete.\n * Failure: The test will fail even if one switch's MCS client mount status is not mountStateMountComplete.\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyMcsClientMounts:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx mounts\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyMcsClientMounts.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n mount_states = command_output[\"mountStates\"]\n mcs_mount_state_detected = False\n for mount_state in mount_states:\n if not mount_state[\"type\"].startswith(\"Mcs\"):\n continue\n mcs_mount_state_detected = True\n if (state := mount_state[\"state\"]) != \"mountStateMountComplete\":\n self.result.is_failure(f\"MCS Client mount states are not valid: {state}\")\n\n if not mcs_mount_state_detected:\n self.result.is_failure(\"MCS Client mount states are not present\")\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authentication\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authorization\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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.avt/","title":"Adaptive Virtual Topology","text":""},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTPathHealth","title":"VerifyAVTPathHealth","text":"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs. Expected Results Success: The test will pass if all AVT paths for all VRFs are active and valid. Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid. Examples anta.tests.avt:\n - VerifyAVTPathHealth:\n Source code in anta/tests/avt.py class VerifyAVTPathHealth(AntaTest):\n \"\"\"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for all VRFs are active and valid.\n * Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTPathHealth:\n ```\n \"\"\"\n\n description = \"Verifies the status of all AVT paths for all VRFs.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTPathHealth.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output.get(\"vrfs\", {})\n\n # Check if AVT is configured\n if not command_output:\n self.result.is_failure(\"Adaptive virtual topology paths are not configured.\")\n return\n\n # Iterate over each VRF\n for vrf, vrf_data in command_output.items():\n # Iterate over each AVT path\n for profile, avt_path in vrf_data.get(\"avts\", {}).items():\n for path, flags in avt_path.get(\"avtPaths\", {}).items():\n # Get the status of the AVT path\n valid = flags[\"flags\"][\"valid\"]\n active = flags[\"flags\"][\"active\"]\n\n # Check the status of the AVT path\n if not valid and not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid and not active.\")\n elif not valid:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid.\")\n elif not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is not active.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole","title":"VerifyAVTRole","text":"Verifies the Adaptive Virtual Topology (AVT) role of a device. Expected Results Success: The test will pass if the AVT role of the device matches the expected role. Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role. Examples anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n Source code in anta/tests/avt.py class VerifyAVTRole(AntaTest):\n \"\"\"Verifies the Adaptive Virtual Topology (AVT) role of a device.\n\n Expected Results\n ----------------\n * Success: The test will pass if the AVT role of the device matches the expected role.\n * Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n ```\n \"\"\"\n\n description = \"Verifies the AVT role of a device.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTRole test.\"\"\"\n\n role: str\n \"\"\"Expected AVT role of the device.\"\"\"\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTRole.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output\n\n # Check if the AVT role matches the expected role\n if self.inputs.role != command_output.get(\"role\"):\n self.result.is_failure(f\"Expected AVT role as `{self.inputs.role}`, but found `{command_output.get('role')}` instead.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole-attributes","title":"Inputs","text":"Name Type Description Default role str Expected AVT role of the device. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath","title":"VerifyAVTSpecificPath","text":"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF. Expected Results Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided. If multiple paths are configured, the test will pass only if all the paths are valid and active. Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid, or does not match the specified type. Examples anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n Source code in anta/tests/avt.py class VerifyAVTSpecificPath(AntaTest):\n \"\"\"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided.\n If multiple paths are configured, the test will pass only if all the paths are valid and active.\n * Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid,\n or does not match the specified type.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n ```\n \"\"\"\n\n description = \"Verifies the status and type of an AVT path for a specified VRF.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show adaptive-virtual-topology path vrf {vrf} avt {avt_name} destination {destination}\")\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTSpecificPath test.\"\"\"\n\n avt_paths: list[AVTPaths]\n \"\"\"List of AVT paths to verify.\"\"\"\n\n class AVTPaths(BaseModel):\n \"\"\"Model for the details of AVT paths.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"The VRF for the AVT path. Defaults to 'default' if not provided.\"\"\"\n avt_name: str\n \"\"\"Name of the adaptive virtual topology.\"\"\"\n destination: IPv4Address\n \"\"\"The IPv4 address of the AVT peer.\"\"\"\n next_hop: IPv4Address\n \"\"\"The IPv4 address of the next hop for the AVT peer.\"\"\"\n path_type: str | None = None\n \"\"\"The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input AVT path/peer.\"\"\"\n return [template.render(vrf=path.vrf, avt_name=path.avt_name, destination=path.destination) for path in self.inputs.avt_paths]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTSpecificPath.\"\"\"\n # Assume the test is successful until a failure is detected\n self.result.is_success()\n\n # Process each command in the instance\n for command, input_avt in zip(self.instance_commands, self.inputs.avt_paths):\n # Extract the command output and parameters\n vrf = command.params.vrf\n avt_name = command.params.avt_name\n peer = str(command.params.destination)\n\n command_output = command.json_output.get(\"vrfs\", {})\n\n # If no AVT is configured, mark the test as failed and skip to the next command\n if not command_output:\n self.result.is_failure(f\"AVT configuration for peer '{peer}' under topology '{avt_name}' in VRF '{vrf}' is not found.\")\n continue\n\n # Extract the AVT paths\n avt_paths = get_value(command_output, f\"{vrf}.avts.{avt_name}.avtPaths\")\n next_hop, input_path_type = str(input_avt.next_hop), input_avt.path_type\n\n nexthop_path_found = path_type_found = False\n\n # Check each AVT path\n for path, path_data in avt_paths.items():\n # If the path does not match the expected next hop, skip to the next path\n if path_data.get(\"nexthopAddr\") != next_hop:\n continue\n\n nexthop_path_found = True\n path_type = \"direct\" if get_value(path_data, \"flags.directPath\") else \"multihop\"\n\n # If the path type does not match the expected path type, skip to the next path\n if input_path_type and path_type != input_path_type:\n continue\n\n path_type_found = True\n valid = get_value(path_data, \"flags.valid\")\n active = get_value(path_data, \"flags.active\")\n\n # Check the path status and type against the expected values\n if not all([valid, active]):\n failure_reasons = []\n if not get_value(path_data, \"flags.active\"):\n failure_reasons.append(\"inactive\")\n if not get_value(path_data, \"flags.valid\"):\n failure_reasons.append(\"invalid\")\n # Construct the failure message prefix\n failed_log = f\"AVT path '{path}' for topology '{avt_name}' in VRF '{vrf}'\"\n self.result.is_failure(f\"{failed_log} is {', '.join(failure_reasons)}.\")\n\n # If no matching next hop or path type was found, mark the test as failed\n if not nexthop_path_found or not path_type_found:\n self.result.is_failure(\n f\"No '{input_path_type}' path found with next-hop address '{next_hop}' for AVT peer '{peer}' under topology '{avt_name}' in VRF '{vrf}'.\"\n )\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"Inputs","text":"Name Type Description Default avt_paths list[AVTPaths] List of AVT paths to verify. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"AVTPaths","text":"Name Type Description Default vrf str The VRF for the AVT path. Defaults to 'default' if not provided. 'default' avt_name str Name of the adaptive virtual topology. - destination IPv4Address The IPv4 address of the AVT peer. - next_hop IPv4Address The IPv4 address of the next hop for the AVT peer. - path_type str | None The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered. None"},{"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 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\", revision=1),\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 categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\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 tx_interval = bfd_peers.tx_interval\n rx_interval = bfd_peers.rx_interval\n multiplier = bfd_peers.multiplier\n\n # Check if BFD peer configured\n bfd_output = get_value(\n self.instance_commands[0].json_output,\n f\"vrfs..{vrf}..ipv4Neighbors..{peer}..peerStats..\",\n separator=\"..\",\n )\n if not bfd_output:\n failures[peer] = {vrf: \"Not Configured\"}\n continue\n\n # Convert interval timer(s) into milliseconds to be consistent with the inputs.\n bfd_details = bfd_output.get(\"peerStatsDetail\", {})\n op_tx_interval = bfd_details.get(\"operTxInterval\") // 1000\n op_rx_interval = bfd_details.get(\"operRxInterval\") // 1000\n detect_multiplier = bfd_details.get(\"detectMult\")\n intervals_ok = op_tx_interval == tx_interval and op_rx_interval == rx_interval and detect_multiplier == multiplier\n\n # Check timers of BFD peer\n if not intervals_ok:\n failures[peer] = {\n vrf: {\n \"tx_interval\": op_tx_interval,\n \"rx_interval\": op_rx_interval,\n \"multiplier\": detect_multiplier,\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.VerifyBFDPeersRegProtocols","title":"VerifyBFDPeersRegProtocols","text":"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered. Expected Results Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s). Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s). Examples anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n Source code in anta/tests/bfd.py class VerifyBFDPeersRegProtocols(AntaTest):\n \"\"\"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered.\n\n Expected Results\n ----------------\n * Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s).\n * Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s).\n\n Examples\n --------\n ```yaml\n anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBFDPeersRegProtocols 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 protocols: list[BfdProtocol]\n \"\"\"List of protocols to be verified.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBFDPeersRegProtocols.\"\"\"\n # Initialize failure messages\n failures: dict[Any, Any] = {}\n\n # Iterating over BFD peers, extract the parameters and command output\n for bfd_peer in self.inputs.bfd_peers:\n peer = str(bfd_peer.peer_address)\n vrf = bfd_peer.vrf\n protocols = bfd_peer.protocols\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 registered protocols\n difference = set(protocols) - set(get_value(bfd_output, \"peerStatsDetail.apps\"))\n\n if difference:\n failures[peer] = {vrf: sorted(difference)}\n\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BFD peers are not configured or have non-registered protocol(s):\\n{failures}\")\n"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-attributes","title":"Inputs","text":"Name Type Description Default bfd_peers list[BFDPeer] List of IPv4 BFD peers. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-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' protocols list[BfdProtocol] List of protocols to be verified. -"},{"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 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\", revision=1)]\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 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.VerifyRunningConfigLines","title":"VerifyRunningConfigLines","text":"Verifies the given regular expression patterns are present in the running-config. Warning Since this uses regular expression searches on the whole running-config, it can drastically impact performance and should only be used if no other test is available. If possible, try using another ANTA test that is more specific. Expected Results Success: The test will pass if all the patterns are found in the running-config. Failure: The test will fail if any of the patterns are NOT found in the running-config. Examples anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n Source code in anta/tests/configuration.py class VerifyRunningConfigLines(AntaTest):\n \"\"\"Verifies the given regular expression patterns are present in the running-config.\n\n !!! warning\n Since this uses regular expression searches on the whole running-config, it can\n drastically impact performance and should only be used if no other test is available.\n\n If possible, try using another ANTA test that is more specific.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the patterns are found in the running-config.\n * Failure: The test will fail if any of the patterns are NOT found in the running-config.\n\n Examples\n --------\n ```yaml\n anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n ```\n \"\"\"\n\n description = \"Search the Running-Config for the given RegEx patterns.\"\n categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show running-config\", ofmt=\"text\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyRunningConfigLines test.\"\"\"\n\n regex_patterns: list[RegexString]\n \"\"\"List of regular expressions.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRunningConfigLines.\"\"\"\n failure_msgs = []\n command_output = self.instance_commands[0].text_output\n\n for pattern in self.inputs.regex_patterns:\n re_search = re.compile(pattern, flags=re.MULTILINE)\n\n if not re_search.search(command_output):\n failure_msgs.append(f\"'{pattern}'\")\n\n if not failure_msgs:\n self.result.is_success()\n else:\n self.result.is_failure(\"Following patterns were not found: \" + \",\".join(failure_msgs))\n"},{"location":"api/tests.configuration/#anta.tests.configuration.VerifyRunningConfigLines-attributes","title":"Inputs","text":"Name Type Description Default regex_patterns list[RegexString] List of regular expressions. -"},{"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 categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show zerotouch\", revision=1)]\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/#tests","title":"Tests","text":""},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors","title":"VerifyLLDPNeighbors","text":"Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. This test performs the following checks for each specified LLDP neighbor: Confirming matching ports on both local and neighboring devices. Ensuring compatibility of device names and interface identifiers. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored. Expected Results Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table. The system name or port of the LLDP neighbor does not match the expected 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 the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.\n\n This test performs the following checks for each specified LLDP neighbor:\n\n 1. Confirming matching ports on both local and neighboring devices.\n 2. Ensuring compatibility of device names and interface identifiers.\n 3. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table.\n - The system name or port of the LLDP neighbor does not match the expected 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 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\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLLDPNeighbors test.\"\"\"\n\n neighbors: list[LLDPNeighbor]\n \"\"\"List of LLDP neighbors.\"\"\"\n Neighbor: ClassVar[type[Neighbor]] = Neighbor\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLLDPNeighbors.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output[\"lldpNeighbors\"]\n for neighbor in self.inputs.neighbors:\n if neighbor.port not in output:\n self.result.is_failure(f\"{neighbor} - Port not found\")\n continue\n\n if len(lldp_neighbor_info := output[neighbor.port][\"lldpNeighborInfo\"]) == 0:\n self.result.is_failure(f\"{neighbor} - No LLDP neighbors\")\n continue\n\n # Check if the system name and neighbor port matches\n match_found = any(\n info[\"systemName\"] == neighbor.neighbor_device and info[\"neighborInterfaceInfo\"][\"interfaceId_v2\"] == neighbor.neighbor_port\n for info in lldp_neighbor_info\n )\n if not match_found:\n failure_msg = [f\"{info['systemName']}/{info['neighborInterfaceInfo']['interfaceId_v2']}\" for info in lldp_neighbor_info]\n self.result.is_failure(f\"{neighbor} - Wrong LLDP neighbors: {', '.join(failure_msg)}\")\n"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors-attributes","title":"Inputs","text":"Name Type Description Default neighbors list[LLDPNeighbor] List of LLDP neighbors. -"},{"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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"connectivity\"]\n # Template uses '{size}{df_bit}' without space since df_bit includes leading space when enabled\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"ping vrf {vrf} {destination} source {source} size {size}{df_bit} repeat {repeat}\", revision=1)\n ]\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 Host: ClassVar[type[Host]] = Host\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each host in the input list.\"\"\"\n commands = []\n for host in self.inputs.hosts:\n # df_bit includes leading space when enabled, empty string when disabled\n df_bit = \" df-bit\" if host.df_bit else \"\"\n command = template.render(destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat, size=host.size, df_bit=df_bit)\n commands.append(command)\n return commands\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyReachability.\"\"\"\n self.result.is_success()\n\n for command, host in zip(self.instance_commands, self.inputs.hosts):\n if f\"{host.repeat} received\" not in command.json_output[\"messages\"][0]:\n self.result.is_failure(f\"{host} - Unreachable\")\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/#input-models","title":"Input models","text":""},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Host","title":"Host","text":"Model for a remote host to ping. 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 size int Specify datagram size. Defaults to 100. 100 df_bit bool Enable do not fragment bit in IP header. Defaults to False. False Source code in anta/input_models/connectivity.py class Host(BaseModel):\n \"\"\"Model for a remote host to ping.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\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 size: int = 100\n \"\"\"Specify datagram size. Defaults to 100.\"\"\"\n df_bit: bool = False\n \"\"\"Enable do not fragment bit in IP header. Defaults to False.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the Host for reporting.\n\n Examples\n --------\n Host 10.1.1.1 (src: 10.2.2.2, vrf: mgmt, size: 100B, repeat: 2)\n\n \"\"\"\n df_status = \", df-bit: enabled\" if self.df_bit else \"\"\n return f\"Host {self.destination} (src: {self.source}, vrf: {self.vrf}, size: {self.size}B, repeat: {self.repeat}{df_status})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.LLDPNeighbor","title":"LLDPNeighbor","text":"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information. Name Type Description Default port Interface The LLDP port for the local device. - neighbor_device str The system name of the LLDP neighbor device. - neighbor_port Interface The LLDP port on the neighboring device. - Source code in anta/input_models/connectivity.py class LLDPNeighbor(BaseModel):\n \"\"\"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n port: Interface\n \"\"\"The LLDP port for the local device.\"\"\"\n neighbor_device: str\n \"\"\"The system name of the LLDP neighbor device.\"\"\"\n neighbor_port: Interface\n \"\"\"The LLDP port on the neighboring device.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the LLDPNeighbor for reporting.\n\n Examples\n --------\n Port Ethernet1 (Neighbor: DC1-SPINE2, Neighbor Port: Ethernet2)\n\n \"\"\"\n return f\"Port {self.port} (Neighbor: {self.neighbor_device}, Neighbor Port: {self.neighbor_port})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor","title":"Neighbor","text":"Alias for the LLDPNeighbor model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the LLDPNeighbor model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/connectivity.py class Neighbor(LLDPNeighbor): # pragma: no cover\n \"\"\"Alias for the LLDPNeighbor model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the LLDPNeighbor model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor.__init__","title":"__init__","text":"__init__(**data: Any) -> None\n Source code in anta/input_models/connectivity.py def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"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 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\", revision=1)]\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 break\n else:\n self.result.is_failure(\"Aboot component not found\")\n return\n\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 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\", revision=1)]\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_failure(\"Error in running test - Component FixedSystemvrm1 not found in 'show version'\")\n"},{"location":"api/tests.flow_tracking/","title":"Flow Tracking","text":""},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus","title":"VerifyHardwareFlowTrackerStatus","text":"Verifies if hardware flow tracking is running and an input tracker is active. This test optionally verifies the tracker interval/timeout and exporter configuration. Expected Results Success: The test will pass if hardware flow tracking is running and an input tracker is active. Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active, or the tracker interval/timeout and exporter configuration does not match the expected values. Examples anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n Source code in anta/tests/flow_tracking.py class VerifyHardwareFlowTrackerStatus(AntaTest):\n \"\"\"Verifies if hardware flow tracking is running and an input tracker is active.\n\n This test optionally verifies the tracker interval/timeout and exporter configuration.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware flow tracking is running and an input tracker is active.\n * Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active,\n or the tracker interval/timeout and exporter configuration does not match the expected values.\n\n Examples\n --------\n ```yaml\n anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n ```\n \"\"\"\n\n description = (\n \"Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration.\"\n )\n categories: ClassVar[list[str]] = [\"flow tracking\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show flow tracking hardware tracker {name}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyHardwareFlowTrackerStatus test.\"\"\"\n\n trackers: list[FlowTracker]\n \"\"\"List of flow trackers to verify.\"\"\"\n\n class FlowTracker(BaseModel):\n \"\"\"Detail of a flow tracker.\"\"\"\n\n name: str\n \"\"\"Name of the flow tracker.\"\"\"\n\n record_export: RecordExport | None = None\n \"\"\"Record export configuration for the flow tracker.\"\"\"\n\n exporters: list[Exporter] | None = None\n \"\"\"List of exporters for the flow tracker.\"\"\"\n\n class RecordExport(BaseModel):\n \"\"\"Record export configuration.\"\"\"\n\n on_inactive_timeout: int\n \"\"\"Timeout in milliseconds for exporting records when inactive.\"\"\"\n\n on_interval: int\n \"\"\"Interval in milliseconds for exporting records.\"\"\"\n\n class Exporter(BaseModel):\n \"\"\"Detail of an exporter.\"\"\"\n\n name: str\n \"\"\"Name of the exporter.\"\"\"\n\n local_interface: str\n \"\"\"Local interface used by the exporter.\"\"\"\n\n template_interval: int\n \"\"\"Template interval in milliseconds for the exporter.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each hardware tracker.\"\"\"\n return [template.render(name=tracker.name) for tracker in self.inputs.trackers]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareFlowTrackerStatus.\"\"\"\n self.result.is_success()\n for command, tracker_input in zip(self.instance_commands, self.inputs.trackers):\n hardware_tracker_name = command.params.name\n record_export = tracker_input.record_export.model_dump() if tracker_input.record_export else None\n exporters = [exporter.model_dump() for exporter in tracker_input.exporters] if tracker_input.exporters else None\n command_output = command.json_output\n\n # Check if hardware flow tracking is configured\n if not command_output.get(\"running\"):\n self.result.is_failure(\"Hardware flow tracking is not running.\")\n return\n\n # Check if the input hardware tracker is configured\n tracker_info = command_output[\"trackers\"].get(hardware_tracker_name)\n if not tracker_info:\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not configured.\")\n continue\n\n # Check if the input hardware tracker is active\n if not tracker_info.get(\"active\"):\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not active.\")\n continue\n\n # Check the input hardware tracker timeouts\n failure_msg = \"\"\n if record_export:\n record_export_failure = validate_record_export(record_export, tracker_info)\n if record_export_failure:\n failure_msg += record_export_failure\n\n # Check the input hardware tracker exporters' configuration\n if exporters:\n exporters_failure = validate_exporters(exporters, tracker_info)\n if exporters_failure:\n failure_msg += exporters_failure\n\n if failure_msg:\n self.result.is_failure(f\"{hardware_tracker_name}: {failure_msg}\\n\")\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Inputs","text":"Name Type Description Default trackers list[FlowTracker] List of flow trackers to verify. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"FlowTracker","text":"Name Type Description Default name str Name of the flow tracker. - record_export RecordExport | None Record export configuration for the flow tracker. None exporters list[Exporter] | None List of exporters for the flow tracker. None"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"RecordExport","text":"Name Type Description Default on_inactive_timeout int Timeout in milliseconds for exporting records when inactive. - on_interval int Interval in milliseconds for exporting records. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Exporter","text":"Name Type Description Default name str Name of the exporter. - local_interface str Local interface used by the exporter. - template_interval int Template interval in milliseconds for the exporter. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_exporters","title":"validate_exporters","text":"validate_exporters(\n exporters: list[dict[str, str]],\n tracker_info: dict[str, str],\n) -> str\n Validate the exporter configurations against the tracker info. Parameters: Name Type Description Default exporters list[dict[str, str]] The list of expected exporter configurations. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str Failure message if any exporter configuration does not match. Source code in anta/tests/flow_tracking.py def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the exporter configurations against the tracker info.\n\n Parameters\n ----------\n exporters\n The list of expected exporter configurations.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n Failure message if any exporter configuration does not match.\n \"\"\"\n failed_log = \"\"\n for exporter in exporters:\n exporter_name = exporter[\"name\"]\n actual_exporter_info = tracker_info[\"exporters\"].get(exporter_name)\n if not actual_exporter_info:\n failed_log += f\"\\nExporter `{exporter_name}` is not configured.\"\n continue\n\n expected_exporter_data = {\"local interface\": exporter[\"local_interface\"], \"template interval\": exporter[\"template_interval\"]}\n actual_exporter_data = {\"local interface\": actual_exporter_info[\"localIntf\"], \"template interval\": actual_exporter_info[\"templateInterval\"]}\n\n if expected_exporter_data != actual_exporter_data:\n failed_msg = get_failed_logs(expected_exporter_data, actual_exporter_data)\n failed_log += f\"\\nExporter `{exporter_name}`: {failed_msg}\"\n return failed_log\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_record_export","title":"validate_record_export","text":"validate_record_export(\n record_export: dict[str, str],\n tracker_info: dict[str, str],\n) -> str\n Validate the record export configuration against the tracker info. Parameters: Name Type Description Default record_export dict[str, str] The expected record export configuration. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str A failure message if the record export configuration does not match, otherwise blank string. Source code in anta/tests/flow_tracking.py def validate_record_export(record_export: dict[str, str], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the record export configuration against the tracker info.\n\n Parameters\n ----------\n record_export\n The expected record export configuration.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n A failure message if the record export configuration does not match, otherwise blank string.\n \"\"\"\n failed_log = \"\"\n actual_export = {\"inactive timeout\": tracker_info.get(\"inactiveTimeout\"), \"interval\": tracker_info.get(\"activeInterval\")}\n expected_export = {\"inactive timeout\": record_export.get(\"on_inactive_timeout\"), \"interval\": record_export.get(\"on_interval\")}\n if actual_export != expected_export:\n failed_log = get_failed_logs(expected_export, actual_export)\n return failed_log\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 description = \"Verifies if a GreenT policy other than the default is created.\"\n categories: ClassVar[list[str]] = [\"greent\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show monitor telemetry postcard policy profile\", revision=1)]\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 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\", revision=1)]\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 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\", revision=1)]\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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment power\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment cooling\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show inventory\", revision=2)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature transceiver\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=2)]\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 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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show lacp counters all-ports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters discards\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces status\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters errors\", revision=1)]\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 description = \"Verifies the interface IPv4 addresses.\"\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip interface {interface}\", revision=2)]\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 [template.render(interface=interface.name) for interface in self.inputs.interfaces]\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 for interface in self.inputs.interfaces:\n if interface.name == intf:\n input_interface_detail = interface\n break\n else:\n self.result.is_failure(f\"Could not find `{intf}` in the input interfaces. {GITHUB_SUGGESTION}\")\n continue\n\n input_primary_ip = str(input_interface_detail.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 (param_secondary_ips := input_interface_detail.secondary_ips) is not None:\n input_secondary_ips = sorted([str(network) for network in param_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. This test has been implemented for full-duplex interfaces only. 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. Error: The test will error out if the device has at least one non full-duplex interface. 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 This test has been implemented for full-duplex interfaces only.\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 * Error: The test will error out if the device has at least one non full-duplex interface.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfaceUtilization:\n threshold: 70.0\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show interfaces counters rates\", revision=1),\n AntaCommand(command=\"show interfaces\", revision=1),\n ]\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 # The utilization logic has been implemented for full-duplex interfaces only\n if ((duplex := (interface := interfaces[\"interfaces\"][intf]).get(\"duplex\", None)) is not None and duplex != duplex_full) or (\n (members := interface.get(\"memberInterfaces\", None)) is not None and any(stats[\"duplex\"] != duplex_full for stats in members.values())\n ):\n self.result.is_failure(f\"Interface {intf} or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented.\")\n return\n\n if (bandwidth := interfaces[\"interfaces\"][intf][\"bandwidth\"]) == 0:\n self.logger.debug(\"Interface %s has been ignored due to null bandwidth value\", intf)\n continue\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.VerifyInterfacesSpeed","title":"VerifyInterfacesSpeed","text":"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces. If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input. If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input. Expected Results Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex. Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex. Examples anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n Source code in anta/tests/interfaces.py class VerifyInterfacesSpeed(AntaTest):\n \"\"\"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces.\n\n - If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input.\n - If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input.\n\n Expected Results\n ----------------\n * Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex.\n * Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n class Input(AntaTest.Input):\n \"\"\"Inputs for the VerifyInterfacesSpeed test.\"\"\"\n\n interfaces: list[InterfaceDetail]\n \"\"\"List of interfaces to be tested\"\"\"\n\n class InterfaceDetail(BaseModel):\n \"\"\"Detail of an interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"The name of the interface.\"\"\"\n auto: bool\n \"\"\"The auto-negotiation status of the interface.\"\"\"\n speed: float = Field(ge=1, le=1000)\n \"\"\"The speed of the interface in Gigabits per second. Valid range is 1 to 1000.\"\"\"\n lanes: None | int = Field(None, ge=1, le=8)\n \"\"\"The number of lanes in the interface. Valid range is 1 to 8. This field is optional.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesSpeed.\"\"\"\n self.result.is_success()\n command_output = self.instance_commands[0].json_output\n\n # Iterate over all the interfaces\n for interface in self.inputs.interfaces:\n intf = interface.name\n\n # Check if interface exists\n if not (interface_output := get_value(command_output, f\"interfaces.{intf}\")):\n self.result.is_failure(f\"Interface `{intf}` is not found.\")\n continue\n\n auto_negotiation = interface_output.get(\"autoNegotiate\")\n actual_lanes = interface_output.get(\"lanes\")\n\n # Collecting actual interface details\n actual_interface_output = {\n \"auto negotiation\": auto_negotiation if interface.auto is True else None,\n \"duplex mode\": interface_output.get(\"duplex\"),\n \"speed\": interface_output.get(\"bandwidth\"),\n \"lanes\": actual_lanes if interface.lanes is not None else None,\n }\n\n # Forming expected interface details\n expected_interface_output = {\n \"auto negotiation\": \"success\" if interface.auto is True else None,\n \"duplex mode\": \"duplexFull\",\n \"speed\": interface.speed * BPS_GBPS_CONVERSIONS,\n \"lanes\": interface.lanes,\n }\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n for output in [actual_interface_output, expected_interface_output]:\n # Convert speed to Gbps for readability\n if output[\"speed\"] is not None:\n output[\"speed\"] = f\"{custom_division(output['speed'], BPS_GBPS_CONVERSIONS)}Gbps\"\n failed_log = get_failed_logs(expected_interface_output, actual_interface_output)\n self.result.is_failure(f\"For interface {intf}:{failed_log}\\n\")\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceDetail] List of interfaces to be tested -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"InterfaceDetail","text":"Name Type Description Default name EthernetInterface The name of the interface. - auto bool The auto-negotiation status of the interface. - speed float The speed of the interface in Gigabits per second. Valid range is 1 to 1000. Field(ge=1, le=1000) lanes None | int The number of lanes in the interface. Valid range is 1 to 8. This field is optional. Field(None, ge=1, le=8)"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus","title":"VerifyInterfacesStatus","text":"Verifies the operational states of specified interfaces to ensure they match expected configurations. This test performs the following checks for each specified interface: If line_protocol_status is defined, both status and line_protocol_status are verified for the specified interface. If line_protocol_status is not provided but the status is \u201cup\u201d, it is assumed that both the status and line protocol should be \u201cup\u201d. If the interface status is not \u201cup\u201d, only the interface\u2019s status is validated, with no line protocol check performed. Expected Results Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces. Failure: If any of the following occur: The specified interface is not configured. The specified interface status and line protocol status does not match the expected operational state for any interface. 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 the operational states of specified interfaces to ensure they match expected configurations.\n\n This test performs the following checks for each specified interface:\n\n 1. If `line_protocol_status` is defined, both `status` and `line_protocol_status` are verified for the specified interface.\n 2. If `line_protocol_status` is not provided but the `status` is \"up\", it is assumed that both the status and line protocol should be \"up\".\n 3. If the interface `status` is not \"up\", only the interface's status is validated, with no line protocol check performed.\n\n Expected Results\n ----------------\n * Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces.\n * Failure: If any of the following occur:\n - The specified interface is not configured.\n - The specified interface status and line protocol status does not match the expected operational state for any interface.\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesStatus.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output\n for interface in self.inputs.interfaces:\n if (intf_status := get_value(command_output[\"interfaceDescriptions\"], interface.name, separator=\"..\")) is None:\n self.result.is_failure(f\"{interface.name} - Not configured\")\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 actual_state = f\"Expected: {interface.status}/{interface.line_protocol_status}, Actual: {status}/{proto}\"\n self.result.is_failure(f\"{interface.name} - {actual_state}\")\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\"):\n self.result.is_failure(f\"{interface.name} - Expected: up/up, Actual: {status}/{proto}\")\n elif interface.status != status:\n self.result.is_failure(f\"{interface.name} - Expected: {interface.status}, Actual: {status}\")\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.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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip virtual-router\", revision=2)]\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 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\", revision=1)]\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 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\", revision=1)]\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.VerifyLACPInterfacesStatus","title":"VerifyLACPInterfacesStatus","text":"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces. Verifies that the interface is a member of the LACP port channel. Ensures that the synchronization is established. Ensures the interfaces are in the correct state for collecting and distributing traffic. Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \u201cslow\u201d mode, is the default setting.) Expected Results Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct. Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct. Examples anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n Source code in anta/tests/interfaces.py class VerifyLACPInterfacesStatus(AntaTest):\n \"\"\"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces.\n\n - Verifies that the interface is a member of the LACP port channel.\n - Ensures that the synchronization is established.\n - Ensures the interfaces are in the correct state for collecting and distributing traffic.\n - Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \"slow\" mode, is the default setting.)\n\n Expected Results\n ----------------\n * Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct.\n * Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show lacp interface {interface}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLACPInterfacesStatus test.\"\"\"\n\n interfaces: list[LACPInterface]\n \"\"\"List of LACP member interface.\"\"\"\n\n class LACPInterface(BaseModel):\n \"\"\"Model for an LACP member interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"Ethernet interface to validate.\"\"\"\n portchannel: PortChannelInterface\n \"\"\"Port Channel in which the interface is bundled.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each interface in the input list.\"\"\"\n return [template.render(interface=interface.name) for interface in self.inputs.interfaces]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLACPInterfacesStatus.\"\"\"\n self.result.is_success()\n\n # Member port verification parameters.\n member_port_details = [\"activity\", \"aggregation\", \"synchronization\", \"collecting\", \"distributing\", \"timeout\"]\n\n # Iterating over command output for different interfaces\n for command, input_entry in zip(self.instance_commands, self.inputs.interfaces):\n interface = input_entry.name\n portchannel = input_entry.portchannel\n\n # Verify if a PortChannel is configured with the provided interface\n if not (interface_details := get_value(command.json_output, f\"portChannels.{portchannel}.interfaces.{interface}\")):\n self.result.is_failure(f\"Interface '{interface}' is not configured to be a member of LACP '{portchannel}'.\")\n continue\n\n # Verify the interface is bundled in port channel.\n actor_port_status = interface_details.get(\"actorPortStatus\")\n if actor_port_status != \"bundled\":\n message = f\"For Interface {interface}:\\nExpected `bundled` as the local port status, but found `{actor_port_status}` instead.\\n\"\n self.result.is_failure(message)\n continue\n\n # Collecting actor and partner port details\n actor_port_details = interface_details.get(\"actorPortState\", {})\n partner_port_details = interface_details.get(\"partnerPortState\", {})\n\n # Collecting actual interface details\n actual_interface_output = {\n \"actor_port_details\": {param: actor_port_details.get(param, \"NotFound\") for param in member_port_details},\n \"partner_port_details\": {param: partner_port_details.get(param, \"NotFound\") for param in member_port_details},\n }\n\n # Forming expected interface details\n expected_details = {param: param != \"timeout\" for param in member_port_details}\n expected_interface_output = {\"actor_port_details\": expected_details, \"partner_port_details\": expected_details}\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n message = f\"For Interface {interface}:\\n\"\n actor_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"actor_port_details\", {}), actual_interface_output.get(\"actor_port_details\", {})\n )\n partner_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"partner_port_details\", {}), actual_interface_output.get(\"partner_port_details\", {})\n )\n\n if actor_port_failed_log:\n message += f\"Actor port details:{actor_port_failed_log}\\n\"\n if partner_port_failed_log:\n message += f\"Partner port details:{partner_port_failed_log}\\n\"\n\n self.result.is_failure(message)\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[LACPInterface] List of LACP member interface. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"LACPInterface","text":"Name Type Description Default name EthernetInterface Ethernet interface to validate. - portchannel PortChannelInterface Port Channel in which the interface is bundled. -"},{"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 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\", revision=1)]\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: # pragma: no branch\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show port-channel\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip interface brief\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show storm-control\", revision=1)]\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.interfaces/#input-models","title":"Input models","text":""},{"location":"api/tests.interfaces/#anta.input_models.interfaces.InterfaceState","title":"InterfaceState","text":"Model for an interface state. 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 Source code in anta/input_models/interfaces.py 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"},{"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 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\", revision=1)]\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 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 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. This test performs the following checks: Retrieves the device\u2019s configured FQDN Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message includes the complete FQDN of the device Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the device\u2019s complete FQDN. Failure: If any of the following occur: The test message is not found in recent logs The log message does not include the device\u2019s FQDN The FQDN in the log message doesn\u2019t match the configured 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 This test performs the following checks:\n\n 1. Retrieves the device's configured FQDN\n 2. Sends a test log message at the **informational** level\n 3. Retrieves the most recent logs (last 30 seconds)\n 4. Verifies that the test message includes the complete FQDN of the device\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the device's complete FQDN.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The log message does not include the device's FQDN\n - The FQDN in the log message doesn't match the configured FQDN\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingHostname:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show hostname\", revision=1),\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingHostname validation\", ofmt=\"text\"),\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 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. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message was successfully logged Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are being generated and the test message is found in recent logs. Failure: If any of the following occur: The test message is not found in recent logs The logging system is not capturing new messages No logs are being 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 This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message was successfully logged\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are being generated and the test message is found in recent logs.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The logging system is not capturing new messages\n - No logs are being generated\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingLogsGeneration:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingLogsGeneration validation\", ofmt=\"text\"),\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 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 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 VerifyLoggingSourceIntf 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 VerifyLoggingSourceIntf.\"\"\"\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 appropriate timestamp. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message is present with a high-resolution RFC3339 timestamp format Example format: 2024-01-25T15:30:45.123456+00:00 Includes microsecond precision Contains timezone offset Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the correct high-resolution RFC3339 timestamp format. Failure: If any of the following occur: The test message is not found in recent logs The timestamp format does not match the expected RFC3339 format 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 appropriate timestamp.\n\n This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message is present with a high-resolution RFC3339 timestamp format\n - Example format: `2024-01-25T15:30:45.123456+00:00`\n - Includes microsecond precision\n - Contains timezone offset\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the correct high-resolution RFC3339 timestamp format.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The timestamp format does not match the expected RFC3339 format\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingTimestamp:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingTimestamp validation\", ofmt=\"text\"),\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(\n logger: logging.Logger, command_output: str\n) -> str\n Parse show logging output and gets operational logging states used in the tests in this module. Parameters: Name Type Description Default logger Logger The logger object. required command_output str The show logging output. required 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 Parameters\n ----------\n logger\n The logger object.\n command_output\n The `show logging` output.\n\n Returns\n -------\n str\n 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":"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 Adaptive Virtual Topology BFD Configuration Connectivity Field Notices Flow Tracking GreenT Hardware Interfaces LANZ Logging MLAG Multicast Profiles PTP Router Path Selection Routing Generic Routing BGP Routing ISIS Routing OSPF Security Services SNMP Software STP STUN 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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag config-sanity\", revision=1)]\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 command_output[\"mlagActive\"] 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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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.path_selection/","title":"Router Path Selection","text":""},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifyPathsHealth","title":"VerifyPathsHealth","text":"Verifies the path and telemetry state of all paths under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if all path states under router path-selection are either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and their telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if any path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifyPathsHealth:\n Source code in anta/tests/path_selection.py class VerifyPathsHealth(AntaTest):\n \"\"\"Verifies the path and telemetry state of all paths under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if all path states under router path-selection are either 'IPsec established' or 'Resolved'\n and their telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if any path state is not 'IPsec established' or 'Resolved',\n or the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifyPathsHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show path-selection paths\", revision=1)]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyPathsHealth.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"dpsPeers\"]\n\n # If no paths are configured for router path-selection, the test fails\n if not command_output:\n self.result.is_failure(\"No path configured for router path-selection.\")\n return\n\n # Check the state of each path\n for peer, peer_data in command_output.items():\n for group, group_data in peer_data[\"dpsGroups\"].items():\n for path_data in group_data[\"dpsPaths\"].values():\n path_state = path_data[\"state\"]\n session = path_data[\"dpsSessions\"][\"0\"][\"active\"]\n\n # If the path state of any path is not 'ipsecEstablished' or 'routeResolved', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for peer {peer} in path-group {group} is `{path_state}`.\")\n\n # If the telemetry state of any path is inactive, the test fails\n elif not session:\n self.result.is_failure(f\"Telemetry state for peer {peer} in path-group {group} is `inactive`.\")\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath","title":"VerifySpecificPath","text":"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if the path state under router path-selection is either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if the path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or if the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n Source code in anta/tests/path_selection.py class VerifySpecificPath(AntaTest):\n \"\"\"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if the path state under router path-selection is either 'IPsec established' or 'Resolved'\n and telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if the path state is not 'IPsec established' or 'Resolved',\n or if the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show path-selection paths peer {peer} path-group {group} source {source} destination {destination}\", revision=1)\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificPath test.\"\"\"\n\n paths: list[RouterPath]\n \"\"\"List of router paths to verify.\"\"\"\n\n class RouterPath(BaseModel):\n \"\"\"Detail of a router path.\"\"\"\n\n peer: IPv4Address\n \"\"\"Static peer IPv4 address.\"\"\"\n\n path_group: str\n \"\"\"Router path group name.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of path.\"\"\"\n\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of path.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each router path.\"\"\"\n return [\n template.render(peer=path.peer, group=path.path_group, source=path.source_address, destination=path.destination_address) for path in self.inputs.paths\n ]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificPath.\"\"\"\n self.result.is_success()\n\n # Check the state of each path\n for command in self.instance_commands:\n peer = command.params.peer\n path_group = command.params.group\n source = command.params.source\n destination = command.params.destination\n command_output = command.json_output.get(\"dpsPeers\", [])\n\n # If the peer is not configured for the path group, the test fails\n if not command_output:\n self.result.is_failure(f\"Path `peer: {peer} source: {source} destination: {destination}` is not configured for path-group `{path_group}`.\")\n continue\n\n # Extract the state of the path\n path_output = get_value(command_output, f\"{peer}..dpsGroups..{path_group}..dpsPaths\", separator=\"..\")\n path_state = next(iter(path_output.values())).get(\"state\")\n session = get_value(next(iter(path_output.values())), \"dpsSessions.0.active\")\n\n # If the state of the path is not 'ipsecEstablished' or 'routeResolved', or the telemetry state is 'inactive', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `{path_state}`.\")\n elif not session:\n self.result.is_failure(\n f\"Telemetry state for path `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `inactive`.\"\n )\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"Inputs","text":"Name Type Description Default paths list[RouterPath] List of router paths to verify. -"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"RouterPath","text":"Name Type Description Default peer IPv4Address Static peer IPv4 address. - path_group str Router path group name. - source_address IPv4Address Source IPv4 address of path. - destination_address IPv4Address Destination IPv4 address of path. -"},{"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 description = \"Verifies the device TCAM profile.\"\n categories: ClassVar[list[str]] = [\"profiles\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hardware tcam profile\", revision=1)]\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 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\", revision=1)]\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\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 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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpLockStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpModeStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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 on the device. 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 on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpOffset:\n ```\n \"\"\"\n\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\", revision=1)]\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 description = \"Verifies the PTP interfaces state.\"\n categories: ClassVar[list[str]] = [\"ptp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", revision=2)]\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/#tests","title":"Tests","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 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\", revision=3)]\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 the advertised and received routes of BGP peers. 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 the advertised and received routes of BGP peers.\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show bgp neighbors {peer} advertised-routes vrf {vrf}\", revision=3),\n AntaTemplate(template=\"show bgp neighbors {peer} routes vrf {vrf}\", revision=3),\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 [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\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 = command.params.peer\n vrf = command.params.vrf\n for input_entry in self.inputs.bgp_peers:\n if str(input_entry.peer_address) == peer and input_entry.vrf == vrf:\n advertised_routes = input_entry.advertised_routes\n received_routes = input_entry.received_routes\n break\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 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\", revision=3)]\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 given address families. This test performs the following checks for each specified address family: Confirms that the specified VRF is configured. Counts the number of peers that are: If check_peer_state is set to True, Counts the number of BGP peers that are in the Established state and have successfully negotiated the specified AFI/SAFI If check_peer_state is set to False, skips validation of the Established state and AFI/SAFI negotiation. Expected Results Success: If the count of BGP peers matches the expected count with check_peer_state enabled/disabled. Failure: If any of the following occur: The specified VRF is not configured. The BGP peer count does not match expected value with check_peer_state enabled/disabled.\u201d 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 given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Confirms that the specified VRF is configured.\n 2. Counts the number of peers that are:\n - If `check_peer_state` is set to True, Counts the number of BGP peers that are in the `Established` state and\n have successfully negotiated the specified AFI/SAFI\n - If `check_peer_state` is set to False, skips validation of the `Established` state and AFI/SAFI negotiation.\n\n Expected Results\n ----------------\n * Success: If the count of BGP peers matches the expected count with `check_peer_state` enabled/disabled.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - The BGP peer count does not match expected value with `check_peer_state` enabled/disabled.\"\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp summary vrf all\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerCount test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'num_peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.num_peers is None:\n msg = f\"{af} 'num_peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerCount.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n peers_data = vrf_output.get(\"peers\", {}).values()\n if not address_family.check_peer_state:\n # Count the number of peers without considering the state and negotiated AFI/SAFI check if the count matches the expected count\n peer_count = sum(1 for peer_data in peers_data if address_family.eos_key in peer_data)\n else:\n # Count the number of established peers with negotiated AFI/SAFI\n peer_count = sum(\n 1\n for peer_data in peers_data\n if peer_data.get(\"peerState\") == \"Established\" and get_value(peer_data, f\"{address_family.eos_key}.afiSafiState\") == \"negotiated\"\n )\n\n # Check if the count matches the expected count\n if address_family.num_peers != peer_count:\n self.result.is_failure(f\"{address_family} - Expected: {address_family.num_peers}, Actual: {peer_count}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats","title":"VerifyBGPPeerDropStats","text":"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s). By default, all drop statistics counters will be checked for any non-zero values. An optional list of specific drop statistics can be provided for granular testing. Expected Results Success: The test will pass if the BGP peer\u2019s drop statistic(s) are zero. Failure: The test will fail if the BGP peer\u2019s drop statistic(s) are non-zero/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerDropStats(AntaTest):\n \"\"\"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s).\n\n By default, all drop statistics counters will be checked for any non-zero values.\n An optional list of specific drop statistics can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's drop statistic(s) are zero.\n * Failure: The test will fail if the BGP peer's drop statistic(s) are non-zero/Not Found or peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerDropStats 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 drop_stats: list[BgpDropStats] | None = None\n \"\"\"Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerDropStats.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n drop_statistics = input_entry.drop_stats\n\n # Verify BGP peer\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify BGP peer's drop stats\n drop_stats_output = peer_detail.get(\"dropStats\", {})\n\n # In case drop stats not provided, It will check all drop statistics\n if not drop_statistics:\n drop_statistics = drop_stats_output\n\n # Verify BGP peer's drop stats\n drop_stats_not_ok = {\n drop_stat: drop_stats_output.get(drop_stat, \"Not Found\") for drop_stat in drop_statistics if drop_stats_output.get(drop_stat, \"Not Found\")\n }\n if any(drop_stats_not_ok):\n failures[peer] = {vrf: drop_stats_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero NLRI drop statistics counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats-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.VerifyBGPPeerDropStats-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' drop_stats list[BgpDropStats] | None Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics. None"},{"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 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\", revision=3)]\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. Supports strict: True to verify that only the specified capabilities are configured, requiring an exact match. 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 strict: False\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 Supports `strict: True` to verify that only the specified capabilities are configured, requiring an exact match.\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 strict: False\n capabilities:\n - ipv4Unicast\n ```\n \"\"\"\n\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\", revision=3)]\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 strict: bool = False\n \"\"\"If True, requires exact matching of provided capabilities. Defaults to False.\"\"\"\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 # Fetching the capabilities output.\n bgp_output = get_value(bgp_output, \"neighborCapabilities.multiprotocolCaps\")\n\n if bgp_peer.strict and sorted(capabilities) != sorted(bgp_output):\n failure[\"bgp_peers\"][peer][vrf] = {\n \"status\": f\"Expected only `{', '.join(capabilities)}` capabilities should be listed but found `{', '.join(bgp_output)}` instead.\"\n }\n failures = deep_update(failures, failure)\n continue\n\n # Check each capability\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' strict bool If True, requires exact matching of provided capabilities. Defaults to False. False capabilities list[MultiProtocolCaps] List of multiprotocol capabilities to be verified. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit","title":"VerifyBGPPeerRouteLimit","text":"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s). Expected Results Success: The test will pass if the BGP peer\u2019s maximum routes and, if provided, the maximum routes warning limit are equal to the given limits. Failure: The test will fail if the BGP peer\u2019s maximum routes do not match the given limit, or if the maximum routes warning limit is provided and does not match the given limit, or if the peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerRouteLimit(AntaTest):\n \"\"\"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's maximum routes and, if provided, the maximum routes warning limit are equal to the given limits.\n * Failure: The test will fail if the BGP peer's maximum routes do not match the given limit, or if the maximum routes warning limit is provided\n and does not match the given limit, or if the peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerRouteLimit 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 maximum_routes: int = Field(ge=0, le=4294967294)\n \"\"\"The maximum allowable number of BGP routes, `0` means unlimited.\"\"\"\n warning_limit: int = Field(default=0, ge=0, le=4294967294)\n \"\"\"Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerRouteLimit.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n maximum_routes = input_entry.maximum_routes\n warning_limit = input_entry.warning_limit\n failure: dict[Any, Any] = {}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify maximum routes configured.\n if (actual_routes := peer_detail.get(\"maxTotalRoutes\", \"Not Found\")) != maximum_routes:\n failure[\"Maximum total routes\"] = actual_routes\n\n # Verify warning limit if given.\n if warning_limit and (actual_warning_limit := peer_detail.get(\"totalRoutesWarnLimit\", \"Not Found\")) != warning_limit:\n failure[\"Warning limit\"] = actual_warning_limit\n\n # Updated failures if any.\n if failure:\n failures[peer] = {vrf: failure}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peer(s) are not configured or maximum routes and maximum routes warning limit is not correct:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit-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.VerifyBGPPeerRouteLimit-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' maximum_routes int The maximum allowable number of BGP routes, `0` means unlimited. Field(ge=0, le=4294967294) warning_limit int Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit. Field(default=0, ge=0, le=4294967294)"},{"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 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\", revision=3)]\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.VerifyBGPPeerUpdateErrors","title":"VerifyBGPPeerUpdateErrors","text":"Verifies BGP update error counters for the provided BGP IPv4 peer(s). By default, all update error counters will be checked for any non-zero values. An optional list of specific update error counters can be provided for granular testing. Note: For \u201cdisabledAfiSafi\u201d error counter field, checking that it\u2019s not \u201cNone\u201d versus 0. Expected Results Success: The test will pass if the BGP peer\u2019s update error counter(s) are zero/None. Failure: The test will fail if the BGP peer\u2019s update error counter(s) are non-zero/not None/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerUpdateErrors(AntaTest):\n \"\"\"Verifies BGP update error counters for the provided BGP IPv4 peer(s).\n\n By default, all update error counters will be checked for any non-zero values.\n An optional list of specific update error counters can be provided for granular testing.\n\n Note: For \"disabledAfiSafi\" error counter field, checking that it's not \"None\" versus 0.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's update error counter(s) are zero/None.\n * Failure: The test will fail if the BGP peer's update error counter(s) are non-zero/not None/Not Found or\n peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerUpdateErrors 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 update_errors: list[BgpUpdateError] | None = None\n \"\"\"Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerUpdateErrors.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n update_error_counters = input_entry.update_errors\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Getting the BGP peer's error counters output.\n error_counters_output = peer_detail.get(\"peerInUpdateErrors\", {})\n\n # In case update error counters not provided, It will check all the update error counters.\n if not update_error_counters:\n update_error_counters = error_counters_output\n\n # verifying the error counters.\n error_counters_not_ok = {\n (\"disabledAfiSafi\" if error_counter == \"disabledAfiSafi\" else error_counter): value\n for error_counter in update_error_counters\n if (value := error_counters_output.get(error_counter, \"Not Found\")) != \"None\" and value != 0\n }\n if error_counters_not_ok:\n failures[peer] = {vrf: error_counters_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero update error counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerUpdateErrors-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.VerifyBGPPeerUpdateErrors-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' update_errors list[BgpUpdateError] | None Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters. None"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth","title":"VerifyBGPPeersHealth","text":"Verifies the health of BGP peers for given address families. This test performs the following checks for each specified address family: Validates that the VRF is configured. Checks if there are any peers for the given AFI/SAFI. For each relevant peer: Verifies that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Checks that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified address families and their peers. Failure: If any of the following occur: The specified VRF is not configured. No peers are found for a given AFI/SAFI. Any BGP session is not in the Established state. The AFI/SAFI state is not \u2018negotiated\u2019 for any peer. Any TCP message queue (input or output) is not empty when check_tcp_queues is True (default). 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 check_tcp_queues: false\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeersHealth(AntaTest):\n \"\"\"Verifies the health of BGP peers for given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Validates that the VRF is configured.\n 2. Checks if there are any peers for the given AFI/SAFI.\n 3. For each relevant peer:\n - Verifies that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Checks that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified address families and their peers.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - No peers are found for a given AFI/SAFI.\n - Any BGP session is not in the `Established` state.\n - The AFI/SAFI state is not 'negotiated' for any peer.\n - Any TCP message queue (input or output) is not empty when `check_tcp_queues` is `True` (default).\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 check_tcp_queues: false\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeersHealth test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeersHealth.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n # Check if any peers are found for this AFI/SAFI\n relevant_peers = [\n peer for peer in vrf_output.get(\"peerList\", []) if get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\") is not None\n ]\n\n if not relevant_peers:\n self.result.is_failure(f\"{address_family} - No peers found\")\n continue\n\n for peer in relevant_peers:\n # Check if the BGP session is established\n if peer[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session state is not established; State: {peer['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers","title":"VerifyBGPSpecificPeers","text":"Verifies the health of specific BGP peer(s) for given address families. This test performs the following checks for each specified address family and peer: Confirms that the specified VRF is configured. For each specified peer: Verifies that the peer is found in the BGP configuration. Checks that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Ensures that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified peers in all address families. Failure: If any of the following occur: The specified VRF is not configured. A specified peer is not found in the BGP configuration. The BGP session for a peer is not in the Established state. The AFI/SAFI state is not negotiated for a peer. Any TCP message queue (input or output) is not empty for a peer when check_tcp_queues is True (default). 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) for given address families.\n\n This test performs the following checks for each specified address family and peer:\n\n 1. Confirms that the specified VRF is configured.\n 2. For each specified peer:\n - Verifies that the peer is found in the BGP configuration.\n - Checks that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Ensures that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified peers in all address families.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - A specified peer is not found in the BGP configuration.\n - The BGP session for a peer is not in the `Established` state.\n - The AFI/SAFI state is not `negotiated` for a peer.\n - Any TCP message queue (input or output) is not empty for a peer when `check_tcp_queues` is `True` (default).\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPSpecificPeers test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.peers is None:\n msg = f\"{af} 'peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPSpecificPeers.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n for peer in address_family.peers:\n peer_ip = str(peer)\n\n # Check if the peer is found\n if (peer_data := get_item(vrf_output[\"peerList\"], \"peerAddress\", peer_ip)) is None:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Not configured\")\n continue\n\n # Check if the BGP session is established\n if peer_data[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session state is not established; State: {peer_data['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer_data, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not capability_status:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated\")\n\n if capability_status and not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer_data[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer_data[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"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 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\", revision=3)]\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.VerifyBgpRouteMaps","title":"VerifyBgpRouteMaps","text":"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s). Expected Results Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF. Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction. Examples anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n Source code in anta/tests/routing/bgp.py class VerifyBgpRouteMaps(AntaTest):\n \"\"\"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF.\n * Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBgpRouteMaps 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 inbound_route_map: str | None = None\n \"\"\"Inbound route map applied, defaults to None.\"\"\"\n outbound_route_map: str | None = None\n \"\"\"Outbound route map applied, defaults to None.\"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpPeer class.\n\n At least one of 'inbound' or 'outbound' route-map must be provided.\n \"\"\"\n if not (self.inbound_route_map or self.outbound_route_map):\n msg = \"At least one of 'inbound_route_map' or 'outbound_route_map' must be provided.\"\n raise ValueError(msg)\n return self\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBgpRouteMaps.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n inbound_route_map = input_entry.inbound_route_map\n outbound_route_map = input_entry.outbound_route_map\n failure: dict[Any, Any] = {vrf: {}}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify Inbound route-map\n if inbound_route_map and (inbound_map := peer_detail.get(\"routeMapInbound\", \"Not Configured\")) != inbound_route_map:\n failure[vrf].update({\"Inbound route-map\": inbound_map})\n\n # Verify Outbound route-map\n if outbound_route_map and (outbound_map := peer_detail.get(\"routeMapOutbound\", \"Not Configured\")) != outbound_route_map:\n failure[vrf].update({\"Outbound route-map\": outbound_map})\n\n if failure[vrf]:\n failures[peer] = failure\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(\n f\"The following BGP peers are not configured or has an incorrect or missing route map in either the inbound or outbound direction:\\n{failures}\"\n )\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBgpRouteMaps-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.VerifyBgpRouteMaps-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' inbound_route_map str | None Inbound route map applied, defaults to None. None outbound_route_map str | None Outbound route map applied, defaults to None. None"},{"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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp evpn route-type mac-ip {address} vni {vni}\", revision=2)]\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=str(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 = 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/#input-models","title":"Input models","text":""},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily","title":"BgpAddressFamily","text":"Model for a BGP address family. Name Type Description Default afi Afi BGP Address Family Identifier (AFI). - safi Safi | None BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`. None vrf str Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`. If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`. These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6. 'default' num_peers PositiveInt | None Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test. None peers list[IPv4Address | IPv6Address] | None List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test. None check_tcp_queues bool Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`. Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests. True check_peer_state bool Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`. Can be enabled in the `VerifyBGPPeerCount` tests. False Source code in anta/input_models/routing/bgp.py class BgpAddressFamily(BaseModel):\n \"\"\"Model for a BGP address family.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n afi: Afi\n \"\"\"BGP Address Family Identifier (AFI).\"\"\"\n safi: Safi | None = None\n \"\"\"BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`.\"\"\"\n vrf: str = \"default\"\n \"\"\"Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`.\n\n If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`.\n\n These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6.\n \"\"\"\n num_peers: PositiveInt | None = None\n \"\"\"Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test.\"\"\"\n peers: list[IPv4Address | IPv6Address] | None = None\n \"\"\"List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test.\"\"\"\n check_tcp_queues: bool = True\n \"\"\"Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`.\n\n Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests.\n \"\"\"\n check_peer_state: bool = False\n \"\"\"Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`.\n\n Can be enabled in the `VerifyBGPPeerCount` tests.\n \"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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 @property\n def eos_key(self) -> str:\n \"\"\"AFI/SAFI EOS key representation.\"\"\"\n # Pydantic handles the validation of the AFI/SAFI combination, so we can ignore error handling here.\n return AFI_SAFI_EOS_KEY[(self.afi, self.safi)]\n\n def __str__(self) -> str:\n \"\"\"Return a string representation of the BgpAddressFamily model. Used in failure messages.\n\n Examples\n --------\n - AFI:ipv4 SAFI:unicast VRF:default\n - AFI:evpn\n \"\"\"\n base_string = f\"AFI: {self.afi}\"\n if self.safi is not None:\n base_string += f\" SAFI: {self.safi}\"\n if self.afi in [\"ipv4\", \"ipv6\"]:\n base_string += f\" VRF: {self.vrf}\"\n return base_string\n"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily.validate_inputs","title":"validate_inputs","text":"validate_inputs() -> Self\n Validate the inputs provided to the BgpAddressFamily class. If afi is either ipv4 or ipv6, safi must be provided. If afi is not ipv4 or ipv6, safi must NOT be provided and vrf must be default. Source code in anta/input_models/routing/bgp.py @model_validator(mode=\"after\")\ndef validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAfi","title":"BgpAfi","text":"Alias for the BgpAddressFamily model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the BgpAddressFamily model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/routing/bgp.py class BgpAfi(BgpAddressFamily): # pragma: no cover\n \"\"\"Alias for the BgpAddressFamily model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the BgpAddressFamily model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the BgpAfi class, emitting a deprecation warning.\"\"\"\n warn(\n message=\"BgpAfi model is deprecated and will be removed in ANTA v2.0.0. Use the BgpAddressFamily model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\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. 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.\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 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 categories: ClassVar[list[str]] = [\"routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show ip route vrf {vrf} {route}\", revision=4),\n AntaTemplate(template=\"show ip route vrf {vrf}\", revision=4),\n ]\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 collect: Literal[\"one\", \"all\"] = \"one\"\n \"\"\"Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one`\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for the input vrf.\"\"\"\n if template == VerifyRoutingTableEntry.commands[0] and self.inputs.collect == \"one\":\n return [template.render(vrf=self.inputs.vrf, route=route) for route in self.inputs.routes]\n\n if template == VerifyRoutingTableEntry.commands[1] and self.inputs.collect == \"all\":\n return [template.render(vrf=self.inputs.vrf)]\n\n return []\n\n @staticmethod\n @cache\n def ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRoutingTableEntry.\"\"\"\n commands_output_route_ips = set()\n\n for command in self.instance_commands:\n command_output_vrf = command.json_output[\"vrfs\"][self.inputs.vrf]\n commands_output_route_ips |= {self.ip_interface_ip(route) for route in command_output_vrf[\"routes\"]}\n\n missing_routes = [str(route) for route in self.inputs.routes if route not in commands_output_route_ips]\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. - collect Literal['one', 'all'] Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one` 'one'"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableEntry.ip_interface_ip","title":"ip_interface_ip cached staticmethod","text":"ip_interface_ip(route: str) -> IPv4Address\n Return the IP address of the provided ip route with mask. Source code in anta/tests/routing/generic.py @staticmethod\n@cache\ndef ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n"},{"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 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: PositiveInteger\n \"\"\"Expected minimum routing table size.\"\"\"\n maximum: PositiveInteger\n \"\"\"Expected maximum routing table size.\"\"\"\n\n @model_validator(mode=\"after\")\n def check_min_max(self) -> Self:\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 PositiveInteger Expected minimum routing table size. - maximum PositiveInteger Expected maximum routing table size. -"},{"location":"api/tests.routing.isis/","title":"ISIS","text":""},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode","title":"VerifyISISInterfaceMode","text":"Verifies ISIS Interfaces are running in correct mode. Expected Results Success: The test will pass if all listed interfaces are running in correct mode. Failure: The test will fail if any of the listed interfaces is not running in correct mode. Skipped: The test will be skipped if no ISIS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISInterfaceMode(AntaTest):\n \"\"\"Verifies ISIS Interfaces are running in correct mode.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces are running in correct mode.\n * Failure: The test will fail if any of the listed interfaces is not running in correct mode.\n * Skipped: The test will be skipped if no ISIS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n ```\n \"\"\"\n\n description = \"Verifies interface mode for IS-IS\"\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceState]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceState(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n mode: Literal[\"point-to-point\", \"broadcast\", \"passive\"]\n \"\"\"Number of IS-IS neighbors.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF where the interface should be configured\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISInterfaceMode.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # Check for p2p interfaces\n for interface in self.inputs.interfaces:\n interface_data = _get_interface_data(\n interface=interface.name,\n vrf=interface.vrf,\n command_output=command_output,\n )\n # Check for correct VRF\n if interface_data is not None:\n interface_type = get_value(dictionary=interface_data, key=\"interfaceType\", default=\"unset\")\n # Check for interfaceType\n if interface.mode == \"point-to-point\" and interface.mode != interface_type:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in {interface.mode} reporting {interface_type}\")\n # Check for passive\n elif interface.mode == \"passive\":\n json_path = f\"intfLevels.{interface.level}.passive\"\n if interface_data is None or get_value(dictionary=interface_data, key=json_path, default=False) is False:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in passive mode\")\n else:\n self.result.is_failure(f\"Interface {interface.name} not found in VRF {interface.vrf}\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceState] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"InterfaceState","text":"Name Type Description Default name Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 mode Literal['point-to-point', 'broadcast', 'passive'] Number of IS-IS neighbors. - vrf str VRF where the interface should be configured 'default'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount","title":"VerifyISISNeighborCount","text":"Verifies number of IS-IS neighbors per level and per interface. Expected Results Success: The test will pass if the number of neighbors is correct. Failure: The test will fail if the number of neighbors is incorrect. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborCount(AntaTest):\n \"\"\"Verifies number of IS-IS neighbors per level and per interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if the number of neighbors is correct.\n * Failure: The test will fail if the number of neighbors is incorrect.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceCount]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceCount(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: int = 2\n \"\"\"IS-IS level to check.\"\"\"\n count: int\n \"\"\"Number of IS-IS neighbors.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborCount.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n isis_neighbor_count = _get_isis_neighbors_count(command_output)\n if len(isis_neighbor_count) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n for interface in self.inputs.interfaces:\n eos_data = [ifl_data for ifl_data in isis_neighbor_count if ifl_data[\"interface\"] == interface.name and ifl_data[\"level\"] == interface.level]\n if not eos_data:\n self.result.is_failure(f\"No neighbor detected for interface {interface.name}\")\n continue\n if eos_data[0][\"count\"] != interface.count:\n self.result.is_failure(\n f\"Interface {interface.name}: \"\n f\"expected Level {interface.level}: count {interface.count}, \"\n f\"got Level {eos_data[0]['level']}: count {eos_data[0]['count']}\"\n )\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceCount] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"InterfaceCount","text":"Name Type Description Default name Interface Interface name to check. - level int IS-IS level to check. 2 count int Number of IS-IS neighbors. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborState","title":"VerifyISISNeighborState","text":"Verifies all IS-IS neighbors are in UP state. Expected Results Success: The test will pass if all IS-IS neighbors are in UP state. Failure: The test will fail if some IS-IS neighbors are not in UP state. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborState(AntaTest):\n \"\"\"Verifies all IS-IS neighbors are in UP state.\n\n Expected Results\n ----------------\n * Success: The test will pass if all IS-IS neighbors are in UP state.\n * Failure: The test will fail if some IS-IS neighbors are not in UP state.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis neighbors\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborState.\"\"\"\n command_output = self.instance_commands[0].json_output\n if _count_isis_neighbor(command_output) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n self.result.is_success()\n not_full_neighbors = _get_not_full_isis_neighbors(command_output)\n if not_full_neighbors:\n self.result.is_failure(f\"Some neighbors are not in the correct state (UP): {not_full_neighbors}.\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments","title":"VerifyISISSegmentRoutingAdjacencySegments","text":"Verify that all expected Adjacency segments are correctly visible for each interface. Expected Results Success: The test will pass if all listed interfaces have correct adjacencies. Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies. Skipped: The test will be skipped if no ISIS SR Adjacency is found. Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):\n \"\"\"Verify that all expected Adjacency segments are correctly visible for each interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces have correct adjacencies.\n * Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies.\n * Skipped: The test will be skipped if no ISIS SR Adjacency is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing adjacency-segments\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingAdjacencySegments test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n segments: list[Segment]\n \"\"\"List of Adjacency segments configured in this instance.\"\"\"\n\n class Segment(BaseModel):\n \"\"\"Segment model definition.\"\"\"\n\n interface: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n sid_origin: Literal[\"dynamic\"] = \"dynamic\"\n \"\"\"Adjacency type\"\"\"\n address: IPv4Address\n \"\"\"IP address of remote end of segment.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingAdjacencySegments.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routging.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n for input_segment in instance.segments:\n eos_segment = _get_adjacency_segment_data_by_neighbor(\n neighbor=str(input_segment.address),\n instance=instance.name,\n vrf=instance.vrf,\n command_output=command_output,\n )\n if eos_segment is None:\n failure_message.append(f\"Your segment has not been found: {input_segment}.\")\n\n elif (\n eos_segment[\"localIntf\"] != input_segment.interface\n or eos_segment[\"level\"] != input_segment.level\n or eos_segment[\"sidOrigin\"] != input_segment.sid_origin\n ):\n failure_message.append(f\"Your segment is not correct: Expected: {input_segment} - Found: {eos_segment}.\")\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' segments list[Segment] List of Adjacency segments configured in this instance. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Segment","text":"Name Type Description Default interface Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 sid_origin Literal['dynamic'] Adjacency type 'dynamic' address IPv4Address IP address of remote end of segment. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane","title":"VerifyISISSegmentRoutingDataplane","text":"Verify dataplane of a list of ISIS-SR instances. Expected Results Success: The test will pass if all instances have correct dataplane configured Failure: The test will fail if one of the instances has incorrect dataplane configured Skipped: The test will be skipped if ISIS is not running Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingDataplane(AntaTest):\n \"\"\"Verify dataplane of a list of ISIS-SR instances.\n\n Expected Results\n ----------------\n * Success: The test will pass if all instances have correct dataplane configured\n * Failure: The test will fail if one of the instances has incorrect dataplane configured\n * Skipped: The test will be skipped if ISIS is not running\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingDataplane test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n dataplane: Literal[\"MPLS\", \"mpls\", \"unset\"] = \"MPLS\"\n \"\"\"Configured dataplane for the instance.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingDataplane.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routing.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n eos_dataplane = get_value(dictionary=command_output, key=f\"vrfs.{instance.vrf}.isisInstances.{instance.name}.dataPlane\", default=None)\n if instance.dataplane.upper() != eos_dataplane:\n failure_message.append(f\"ISIS instance {instance.name} is not running dataplane {instance.dataplane} ({eos_dataplane})\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' dataplane Literal['MPLS', 'mpls', 'unset'] Configured dataplane for the instance. 'MPLS'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels","title":"VerifyISISSegmentRoutingTunnels","text":"Verify ISIS-SR tunnels computed by device. Expected Results Success: The test will pass if all listed tunnels are computed on device. Failure: The test will fail if one of the listed tunnels is missing. Skipped: The test will be skipped if ISIS-SR is not configured. Examples anta.tests.routing:\nisis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingTunnels(AntaTest):\n \"\"\"Verify ISIS-SR tunnels computed by device.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed tunnels are computed on device.\n * Failure: The test will fail if one of the listed tunnels is missing.\n * Skipped: The test will be skipped if ISIS-SR is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing tunnel\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingTunnels test.\"\"\"\n\n entries: list[Entry]\n \"\"\"List of tunnels to check on device.\"\"\"\n\n class Entry(BaseModel):\n \"\"\"Definition of a tunnel entry.\"\"\"\n\n endpoint: IPv4Network\n \"\"\"Endpoint IP of the tunnel.\"\"\"\n vias: list[Vias] | None = None\n \"\"\"Optional list of path to reach endpoint.\"\"\"\n\n class Vias(BaseModel):\n \"\"\"Definition of a tunnel path.\"\"\"\n\n nexthop: IPv4Address | None = None\n \"\"\"Nexthop of the tunnel. If None, then it is not tested. Default: None\"\"\"\n type: Literal[\"ip\", \"tunnel\"] | None = None\n \"\"\"Type of the tunnel. If None, then it is not tested. Default: None\"\"\"\n interface: Interface | None = None\n \"\"\"Interface of the tunnel. If None, then it is not tested. Default: None\"\"\"\n tunnel_id: Literal[\"TI-LFA\", \"ti-lfa\", \"unset\"] | None = None\n \"\"\"Computation method of the tunnel. If None, then it is not tested. Default: None\"\"\"\n\n def _eos_entry_lookup(self, search_value: IPv4Network, entries: dict[str, Any], search_key: str = \"endpoint\") -> dict[str, Any] | None:\n return next(\n (entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == str(search_value)),\n None,\n )\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingTunnels.\n\n This method performs the main test logic for verifying ISIS Segment Routing tunnels.\n It checks the command output, initiates defaults, and performs various checks on the tunnels.\n \"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n # initiate defaults\n failure_message = []\n\n if len(command_output[\"entries\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n for input_entry in self.inputs.entries:\n eos_entry = self._eos_entry_lookup(search_value=input_entry.endpoint, entries=command_output[\"entries\"])\n if eos_entry is None:\n failure_message.append(f\"Tunnel to {input_entry} is not found.\")\n elif input_entry.vias is not None:\n failure_src = []\n for via_input in input_entry.vias:\n if not self._check_tunnel_type(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel type\")\n if not self._check_tunnel_nexthop(via_input, eos_entry):\n failure_src.append(\"incorrect nexthop\")\n if not self._check_tunnel_interface(via_input, eos_entry):\n failure_src.append(\"incorrect interface\")\n if not self._check_tunnel_id(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel ID\")\n\n if failure_src:\n failure_message.append(f\"Tunnel to {input_entry.endpoint!s} is incorrect: {', '.join(failure_src)}\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n\n def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input tunnel type to check.\n eos_entry : dict[str, Any]\n The EOS entry containing the tunnel types.\n\n Returns\n -------\n bool\n True if the tunnel type matches any of the tunnel types in `eos_entry`, False otherwise.\n \"\"\"\n if via_input.type is not None:\n return any(\n via_input.type\n == get_value(\n dictionary=eos_via,\n key=\"type\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel nexthop matches the given input.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel nexthop matches, False otherwise.\n \"\"\"\n if via_input.nexthop is not None:\n return any(\n str(via_input.nexthop)\n == get_value(\n dictionary=eos_via,\n key=\"nexthop\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel interface exists in the given EOS entry.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel interface exists, False otherwise.\n \"\"\"\n if via_input.interface is not None:\n return any(\n via_input.interface\n == get_value(\n dictionary=eos_via,\n key=\"interface\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input vias to check.\n eos_entry : dict[str, Any])\n The EOS entry to compare against.\n\n Returns\n -------\n bool\n True if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias, False otherwise.\n \"\"\"\n if via_input.tunnel_id is not None:\n return any(\n via_input.tunnel_id.upper()\n == get_value(\n dictionary=eos_via,\n key=\"tunnelId.type\",\n default=\"undefined\",\n ).upper()\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Inputs","text":"Name Type Description Default entries list[Entry] List of tunnels to check on device. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Entry","text":"Name Type Description Default endpoint IPv4Network Endpoint IP of the tunnel. - vias list[Vias] | None Optional list of path to reach endpoint. None"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Vias","text":"Name Type Description Default nexthop IPv4Address | None Nexthop of the tunnel. If None, then it is not tested. Default: None None type Literal['ip', 'tunnel'] | None Type of the tunnel. If None, then it is not tested. Default: None None interface Interface | None Interface of the tunnel. If None, then it is not tested. Default: None None tunnel_id Literal['TI-LFA', 'ti-lfa', 'unset'] | None Computation method of the tunnel. If None, then it is not tested. Default: None None"},{"location":"api/tests.routing.ospf/","title":"OSPF","text":""},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFMaxLSA","title":"VerifyOSPFMaxLSA","text":"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold. Expected Results Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold. Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold. Skipped: The test will be skipped if no OSPF instance is found. Examples anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n Source code in anta/tests/routing/ospf.py class VerifyOSPFMaxLSA(AntaTest):\n \"\"\"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold.\n * Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold.\n * Skipped: The test will be skipped if no OSPF instance is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n ```\n \"\"\"\n\n description = \"Verifies all OSPF instances did not cross the maximum LSA threshold.\"\n categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyOSPFMaxLSA.\"\"\"\n command_output = self.instance_commands[0].json_output\n ospf_instance_info = _get_ospf_max_lsa_info(command_output)\n if not ospf_instance_info:\n self.result.is_skipped(\"No OSPF instance found.\")\n return\n all_instances_within_threshold = all(instance[\"numLsa\"] <= instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100) for instance in ospf_instance_info)\n if all_instances_within_threshold:\n self.result.is_success()\n else:\n exceeded_instances = [\n instance[\"instance\"] for instance in ospf_instance_info if instance[\"numLsa\"] > instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100)\n ]\n self.result.is_failure(f\"OSPF Instances {exceeded_instances} crossed the maximum LSA threshold.\")\n"},{"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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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.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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ip access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ipv6 access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show management security ssl certificate\", revision=1),\n AntaCommand(command=\"show clock\", revision=1),\n ]\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) -> Self:\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 get_args(RsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for RSA encryption. Allowed sizes are {get_args(RsaKeySize)}.\"\n raise ValueError(msg)\n\n if self.encryption_algorithm == \"ECDSA\" and self.key_size not in get_args(EcdsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for ECDSA encryption. Allowed sizes are {get_args(EcdsaKeySize)}.\"\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner login\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner motd\", revision=1)]\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.VerifyHardwareEntropy","title":"VerifyHardwareEntropy","text":"Verifies hardware entropy generation is enabled on device. Expected Results Success: The test will pass if hardware entropy generation is enabled. Failure: The test will fail if hardware entropy generation is not enabled. Examples anta.tests.security:\n - VerifyHardwareEntropy:\n Source code in anta/tests/security.py class VerifyHardwareEntropy(AntaTest):\n \"\"\"Verifies hardware entropy generation is enabled on device.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware entropy generation is enabled.\n * Failure: The test will fail if hardware entropy generation is not enabled.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyHardwareEntropy:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management security\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareEntropy.\"\"\"\n command_output = self.instance_commands[0].json_output\n\n # Check if hardware entropy generation is enabled.\n if not command_output.get(\"hardwareEntropyEnabled\"):\n self.result.is_failure(\"Hardware entropy generation is disabled.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.security/#anta.tests.security.VerifyIPSecConnHealth","title":"VerifyIPSecConnHealth","text":"Verifies all IPv4 security connections. Expected Results Success: The test will pass if all the IPv4 security connections are established in all vrf. Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf. Examples anta.tests.security:\n - VerifyIPSecConnHealth:\n Source code in anta/tests/security.py class VerifyIPSecConnHealth(AntaTest):\n \"\"\"Verifies all IPv4 security connections.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the IPv4 security connections are established in all vrf.\n * Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyIPSecConnHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip security connection vrf all\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyIPSecConnHealth.\"\"\"\n self.result.is_success()\n failure_conn = []\n command_output = self.instance_commands[0].json_output[\"connections\"]\n\n # Check if IP security connection is configured\n if not command_output:\n self.result.is_failure(\"No IPv4 security connection configured.\")\n return\n\n # Iterate over all ipsec connections\n for conn_data in command_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n failure_conn.append(f\"source:{source} destination:{destination} vrf:{vrf}\")\n if failure_conn:\n failure_msg = \"\\n\".join(failure_conn)\n self.result.is_failure(f\"The following IPv4 security connections are not established:\\n{failure_msg}.\")\n"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip access-lists {acl}\", revision=1)]\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) 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, acl in zip(self.instance_commands, self.inputs.ipv4_access_lists):\n # Collecting input ACL details\n acl_name = command_output.params.acl\n # Retrieve the expected entries from the inputs\n acl_entries = acl.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 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\", revision=1)]\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 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\", revision=1)]\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 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 try:\n line = next(line for line in command_output.split(\"\\n\") if line.startswith(\"SSHD status\"))\n except StopIteration:\n self.result.is_failure(\"Could not find SSH status in returned output.\")\n return\n status = line.split()[-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.VerifySpecificIPSecConn","title":"VerifySpecificIPSecConn","text":"Verifies the state of IPv4 security connections for a specified peer. It optionally allows for the verification of a specific path for a peer by providing source and destination addresses. If these addresses are not provided, it will verify all paths for the specified peer. Expected Results Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF. Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF. Examples anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n Source code in anta/tests/security.py class VerifySpecificIPSecConn(AntaTest):\n \"\"\"Verifies the state of IPv4 security connections for a specified peer.\n\n It optionally allows for the verification of a specific path for a peer by providing source and destination addresses.\n If these addresses are not provided, it will verify all paths for the specified peer.\n\n Expected Results\n ----------------\n * Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF.\n * Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n ```\n \"\"\"\n\n description = \"Verifies IPv4 security connections for a peer.\"\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip security connection vrf {vrf} path peer {peer}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificIPSecConn test.\"\"\"\n\n ip_security_connections: list[IPSecPeers]\n \"\"\"List of IP4v security peers.\"\"\"\n\n class IPSecPeers(BaseModel):\n \"\"\"Details of IPv4 security peers.\"\"\"\n\n peer: IPv4Address\n \"\"\"IPv4 address of the peer.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"Optional VRF for the IP security peer.\"\"\"\n\n connections: list[IPSecConn] | None = None\n \"\"\"Optional list of IPv4 security connections of a peer.\"\"\"\n\n class IPSecConn(BaseModel):\n \"\"\"Details of IPv4 security connections for a peer.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of the connection.\"\"\"\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of the connection.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input IP Sec connection.\"\"\"\n return [template.render(peer=conn.peer, vrf=conn.vrf) for conn in self.inputs.ip_security_connections]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificIPSecConn.\"\"\"\n self.result.is_success()\n for command_output, input_peer in zip(self.instance_commands, self.inputs.ip_security_connections):\n conn_output = command_output.json_output[\"connections\"]\n peer = command_output.params.peer\n vrf = command_output.params.vrf\n conn_input = input_peer.connections\n\n # Check if IPv4 security connection is configured\n if not conn_output:\n self.result.is_failure(f\"No IPv4 security connection configured for peer `{peer}`.\")\n continue\n\n # If connection details are not provided then check all connections of a peer\n if conn_input is None:\n for conn_data in conn_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source} destination:{destination} vrf:{vrf}` for peer `{peer}` is `Established` \"\n f\"but found `{state}` instead.\"\n )\n continue\n\n # Create a dictionary of existing connections for faster lookup\n existing_connections = {\n (conn_data.get(\"saddr\"), conn_data.get(\"daddr\"), conn_data.get(\"tunnelNs\")): next(iter(conn_data[\"pathDict\"].values()))\n for conn_data in conn_output.values()\n }\n for connection in conn_input:\n source_input = str(connection.source_address)\n destination_input = str(connection.destination_address)\n\n if (source_input, destination_input, vrf) in existing_connections:\n existing_state = existing_connections[(source_input, destination_input, vrf)]\n if existing_state != \"Established\":\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` \"\n f\"for peer `{peer}` is `Established` but found `{existing_state}` instead.\"\n )\n else:\n self.result.is_failure(\n f\"IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` for peer `{peer}` is not found.\"\n )\n"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"Inputs","text":"Name Type Description Default ip_security_connections list[IPSecPeers] List of IP4v security peers. -"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecPeers","text":"Name Type Description Default peer IPv4Address IPv4 address of the peer. - vrf str Optional VRF for the IP security peer. 'default' connections list[IPSecConn] | None Optional list of IPv4 security connections of a peer. None"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecConn","text":"Name Type Description Default source_address IPv4Address Source IPv4 address of the connection. - destination_address IPv4Address Destination IPv4 address of the connection. -"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management telnet\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=1)]\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. This test performs the following checks for each specified DNS Server: Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF. Ensuring an appropriate priority level. 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 any of the following conditions are met: The provided DNS server is not configured. The provided DNS server with designated VRF and priority does not match the expected information. 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 This test performs the following checks for each specified DNS Server:\n\n 1. Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF.\n 2. Ensuring an appropriate priority level.\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 any of the following conditions are met:\n - The provided DNS server is not configured.\n - The provided DNS server with designated VRF and priority does not match the expected information.\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 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\", revision=1)]\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 DnsServer: ClassVar[type[DnsServer]] = DnsServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyDNSServers.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"nameServerConfigs\"]\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 # Check if the DNS server is configured with specified VRF.\n if (output := get_dict_superset(command_output, input_dict)) is None:\n self.result.is_failure(f\"{server} - Not configured\")\n continue\n\n # Check if the DNS server priority matches with expected.\n if output[\"priority\"] != priority:\n self.result.is_failure(f\"{server} - Incorrect priority; Priority: {output['priority']}\")\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.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 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 categories: ClassVar[list[str]] = [\"services\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hostname\", revision=1)]\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.services/#input-models","title":"Input models","text":""},{"location":"api/tests.services/#anta.input_models.services.DnsServer","title":"DnsServer","text":"Model for a DNS server configuration. Name Type Description Default server_address IPv4Address | IPv6Address The IPv4 or IPv6 address of the DNS server. - vrf str The VRF instance in which the DNS server resides. Defaults to 'default'. 'default' priority int The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest. Field(ge=0, le=4) Source code in anta/input_models/services.py class DnsServer(BaseModel):\n \"\"\"Model for a DNS server configuration.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: IPv4Address | IPv6Address\n \"\"\"The IPv4 or IPv6 address of the DNS server.\"\"\"\n vrf: str = \"default\"\n \"\"\"The VRF instance in which the DNS server resides. Defaults to 'default'.\"\"\"\n priority: int = Field(ge=0, le=4)\n \"\"\"The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the DnsServer for reporting.\n\n Examples\n --------\n Server 10.0.0.1 (VRF: default, Priority: 1)\n \"\"\"\n return f\"Server {self.server_address} (VRF: {self.vrf}, Priority: {self.priority})\"\n"},{"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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP contact is configured.\n if not (contact := get_value(self.instance_commands[0].json_output, \"contact.contact\")):\n self.result.is_failure(\"SNMP contact is not configured.\")\n return\n\n # Verifies the expected SNMP contact.\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.VerifySnmpErrorCounters","title":"VerifySnmpErrorCounters","text":"Verifies the SNMP error counters. By default, all error counters will be checked for any non-zero values. An optional list of specific error counters can be provided for granular testing. Expected Results Success: The test will pass if the SNMP error counter(s) are zero/None. Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured. Examples ```yaml anta.tests.snmp: - VerifySnmpErrorCounters: error_counters: - inVersionErrs - inBadCommunityNames Source code in anta/tests/snmp.py class VerifySnmpErrorCounters(AntaTest):\n \"\"\"Verifies the SNMP error counters.\n\n By default, all error counters will be checked for any non-zero values.\n An optional list of specific error counters can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP error counter(s) are zero/None.\n * Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpErrorCounters:\n error_counters:\n - inVersionErrs\n - inBadCommunityNames\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpErrorCounters test.\"\"\"\n\n error_counters: list[SnmpErrorCounter] | None = None\n \"\"\"Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpErrorCounters.\"\"\"\n error_counters = self.inputs.error_counters\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (snmp_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP error counters not provided, It will check all the error counters.\n if not error_counters:\n error_counters = list(get_args(SnmpErrorCounter))\n\n error_counters_not_ok = {counter: value for counter in error_counters if (value := snmp_counters.get(counter))}\n\n # Check if any failures\n if not error_counters_not_ok:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP error counters are not found or have non-zero error counters:\\n{error_counters_not_ok}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpErrorCounters-attributes","title":"Inputs","text":"Name Type Description Default error_counters list[SnmpErrorCounter] | None Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters. None"},{"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 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\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP location is configured.\n if not (location := get_value(self.instance_commands[0].json_output, \"location.location\")):\n self.result.is_failure(\"SNMP location is not configured.\")\n return\n\n # Verifies the expected SNMP location.\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.VerifySnmpPDUCounters","title":"VerifySnmpPDUCounters","text":"Verifies the SNMP PDU counters. By default, all SNMP PDU counters will be checked for any non-zero values. An optional list of specific SNMP PDU(s) can be provided for granular testing. Expected Results Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero. Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found. Examples anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n Source code in anta/tests/snmp.py class VerifySnmpPDUCounters(AntaTest):\n \"\"\"Verifies the SNMP PDU counters.\n\n By default, all SNMP PDU counters will be checked for any non-zero values.\n An optional list of specific SNMP PDU(s) can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero.\n * Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpPDUCounters test.\"\"\"\n\n pdus: list[SnmpPdu] | None = None\n \"\"\"Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpPDUCounters.\"\"\"\n snmp_pdus = self.inputs.pdus\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (pdu_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP PDUs not provided, It will check all the update error counters.\n if not snmp_pdus:\n snmp_pdus = list(get_args(SnmpPdu))\n\n failures = {pdu: value for pdu in snmp_pdus if (value := pdu_counters.get(pdu, \"Not Found\")) == \"Not Found\" or value == 0}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP PDU counters are not found or have zero PDU counters:\\n{failures}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpPDUCounters-attributes","title":"Inputs","text":"Name Type Description Default pdus list[SnmpPdu] | None Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters. None"},{"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 description = \"Verifies if the SNMP agent is enabled.\"\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show extensions\", revision=2),\n AntaCommand(command=\"show boot-extensions\", revision=1),\n ]\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 description = \"Verifies the EOS version of the device.\"\n categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree blockedports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree counters\", revision=1)]\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 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\", revision=1)]\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 vlan_id = command.params.vlan\n if not (topologies := get_value(command.json_output, \"topologies\")):\n not_configured.append(vlan_id)\n else:\n interfaces_not_forwarding = []\n for value in topologies.values():\n if vlan_id and 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 forwarding 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show spanning-tree vlan {vlan}\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree root detail\", revision=1)]\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.stp/#anta.tests.stp.VerifyStpTopologyChanges","title":"VerifyStpTopologyChanges","text":"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold. Expected Results Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold. Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold, indicating potential instability in the topology. Examples anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n Source code in anta/tests/stp.py class VerifyStpTopologyChanges(AntaTest):\n \"\"\"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold.\n * Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold,\n indicating potential instability in the topology.\n\n Examples\n --------\n ```yaml\n anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree topology status detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStpTopologyChanges test.\"\"\"\n\n threshold: int\n \"\"\"The threshold number of changes in the STP topology.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStpTopologyChanges.\"\"\"\n failures: dict[str, Any] = {\"topologies\": {}}\n\n command_output = self.instance_commands[0].json_output\n stp_topologies = command_output.get(\"topologies\", {})\n\n # verifies all available topologies except the \"NoStp\" topology.\n stp_topologies.pop(\"NoStp\", None)\n\n # Verify the STP topology(s).\n if not stp_topologies:\n self.result.is_failure(\"STP is not configured.\")\n return\n\n # Verifies the number of changes across all interfaces\n for topology, topology_details in stp_topologies.items():\n interfaces = {\n interface: {\"Number of changes\": num_of_changes}\n for interface, details in topology_details.get(\"interfaces\", {}).items()\n if (num_of_changes := details.get(\"numChanges\")) > self.inputs.threshold\n }\n if interfaces:\n failures[\"topologies\"][topology] = interfaces\n\n if failures[\"topologies\"]:\n self.result.is_failure(f\"The following STP topologies are not configured or number of changes not within the threshold:\\n{failures}\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.stp/#anta.tests.stp.VerifyStpTopologyChanges-attributes","title":"Inputs","text":"Name Type Description Default threshold int The threshold number of changes in the STP topology. -"},{"location":"api/tests.stun/","title":"STUN","text":""},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient","title":"VerifyStunClient","text":"Verifies STUN client settings, including local IP/port and optionally public IP/port. Expected Results Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port. Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect. Examples anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n Source code in anta/tests/stun.py class VerifyStunClient(AntaTest):\n \"\"\"Verifies STUN client settings, including local IP/port and optionally public IP/port.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port.\n * Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show stun client translations {source_address} {source_port}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStunClient test.\"\"\"\n\n stun_clients: list[ClientAddress]\n\n class ClientAddress(BaseModel):\n \"\"\"Source and public address/port details of STUN client.\"\"\"\n\n source_address: IPv4Address\n \"\"\"IPv4 source address of STUN client.\"\"\"\n source_port: Port = 4500\n \"\"\"Source port number for STUN client.\"\"\"\n public_address: IPv4Address | None = None\n \"\"\"Optional IPv4 public address of STUN client.\"\"\"\n public_port: Port | None = None\n \"\"\"Optional public port number for STUN client.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each STUN translation.\"\"\"\n return [template.render(source_address=client.source_address, source_port=client.source_port) for client in self.inputs.stun_clients]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunClient.\"\"\"\n self.result.is_success()\n\n # Iterate over each command output and corresponding client input\n for command, client_input in zip(self.instance_commands, self.inputs.stun_clients):\n bindings = command.json_output[\"bindings\"]\n source_address = str(command.params.source_address)\n source_port = command.params.source_port\n\n # If no bindings are found for the STUN client, mark the test as a failure and continue with the next client\n if not bindings:\n self.result.is_failure(f\"STUN client transaction for source `{source_address}:{source_port}` is not found.\")\n continue\n\n # Extract the public address and port from the client input\n public_address = client_input.public_address\n public_port = client_input.public_port\n\n # Extract the transaction ID from the bindings\n transaction_id = next(iter(bindings.keys()))\n\n # Prepare the actual and expected STUN data for comparison\n actual_stun_data = {\n \"source ip\": get_value(bindings, f\"{transaction_id}.sourceAddress.ip\"),\n \"source port\": get_value(bindings, f\"{transaction_id}.sourceAddress.port\"),\n }\n expected_stun_data = {\"source ip\": source_address, \"source port\": source_port}\n\n # If public address is provided, add it to the actual and expected STUN data\n if public_address is not None:\n actual_stun_data[\"public ip\"] = get_value(bindings, f\"{transaction_id}.publicAddress.ip\")\n expected_stun_data[\"public ip\"] = str(public_address)\n\n # If public port is provided, add it to the actual and expected STUN data\n if public_port is not None:\n actual_stun_data[\"public port\"] = get_value(bindings, f\"{transaction_id}.publicAddress.port\")\n expected_stun_data[\"public port\"] = public_port\n\n # If the actual STUN data does not match the expected STUN data, mark the test as failure\n if actual_stun_data != expected_stun_data:\n failed_log = get_failed_logs(expected_stun_data, actual_stun_data)\n self.result.is_failure(f\"For STUN source `{source_address}:{source_port}`:{failed_log}\")\n"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"ClientAddress","text":"Name Type Description Default source_address IPv4Address IPv4 source address of STUN client. - source_port Port Source port number for STUN client. 4500 public_address IPv4Address | None Optional IPv4 public address of STUN client. None public_port Port | None Optional public port number for STUN client. None"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunServer","title":"VerifyStunServer","text":"Verifies the STUN server status is enabled and running. Expected Results Success: The test will pass if the STUN server status is enabled and running. Failure: The test will fail if the STUN server is disabled or not running. Examples anta.tests.stun:\n - VerifyStunServer:\n Source code in anta/tests/stun.py class VerifyStunServer(AntaTest):\n \"\"\"Verifies the STUN server status is enabled and running.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN server status is enabled and running.\n * Failure: The test will fail if the STUN server is disabled or not running.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunServer:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show stun server status\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunServer.\"\"\"\n command_output = self.instance_commands[0].json_output\n status_disabled = not command_output.get(\"enabled\")\n not_running = command_output.get(\"pid\") == 0\n\n if status_disabled and not_running:\n self.result.is_failure(\"STUN server status is disabled and not running.\")\n elif status_disabled:\n self.result.is_failure(\"STUN server status is disabled.\")\n elif not_running:\n self.result.is_failure(\"STUN server is not running.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.system/","title":"System","text":""},{"location":"api/tests.system/#tests","title":"Tests","text":""},{"location":"api/tests.system/#anta.tests.system.VerifyAgentLogs","title":"VerifyAgentLogs","text":"Verifies there are no agent crash reports. 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 there are no agent crash reports.\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show processes top once\", revision=1)]\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 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\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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.VerifyNTPAssociations","title":"VerifyNTPAssociations","text":"Verifies the Network Time Protocol (NTP) associations. Expected Results Success: The test will pass if the Primary NTP server (marked as preferred) has the condition \u2018sys.peer\u2019 and all other NTP servers have the condition \u2018candidate\u2019. Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition \u2018sys.peer\u2019 or if any other NTP server does not have the condition \u2018candidate\u2019. Examples anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n Source code in anta/tests/system.py class VerifyNTPAssociations(AntaTest):\n \"\"\"Verifies the Network Time Protocol (NTP) associations.\n\n Expected Results\n ----------------\n * Success: The test will pass if the Primary NTP server (marked as preferred) has the condition 'sys.peer' and\n all other NTP servers have the condition 'candidate'.\n * Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition 'sys.peer' or\n if any other NTP server does not have the condition 'candidate'.\n\n Examples\n --------\n ```yaml\n anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ntp associations\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyNTPAssociations test.\"\"\"\n\n ntp_servers: list[NTPServer]\n \"\"\"List of NTP servers.\"\"\"\n NTPServer: ClassVar[type[NTPServer]] = NTPServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyNTPAssociations.\"\"\"\n self.result.is_success()\n\n if not (peers := get_value(self.instance_commands[0].json_output, \"peers\")):\n self.result.is_failure(\"No NTP peers configured\")\n return\n\n # Iterate over each NTP server.\n for ntp_server in self.inputs.ntp_servers:\n server_address = str(ntp_server.server_address)\n\n # We check `peerIpAddr` in the peer details - covering IPv4Address input, or the peer key - covering Hostname input.\n matching_peer = next((peer for peer, peer_details in peers.items() if (server_address in {peer_details[\"peerIpAddr\"], peer})), None)\n\n if not matching_peer:\n self.result.is_failure(f\"{ntp_server} - Not configured\")\n continue\n\n # Collecting the expected/actual NTP peer details.\n exp_condition = \"sys.peer\" if ntp_server.preferred else \"candidate\"\n exp_stratum = ntp_server.stratum\n act_condition = get_value(peers[matching_peer], \"condition\")\n act_stratum = get_value(peers[matching_peer], \"stratumLevel\")\n\n if act_condition != exp_condition or act_stratum != exp_stratum:\n self.result.is_failure(f\"{ntp_server} - Bad association; Condition: {act_condition}, Stratum: {act_stratum}\")\n"},{"location":"api/tests.system/#anta.tests.system.VerifyNTPAssociations-attributes","title":"Inputs","text":"Name Type Description Default ntp_servers list[NTPServer] List of NTP servers. -"},{"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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show reload cause\", revision=1)]\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 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 description = \"Verifies the device uptime.\"\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show uptime\", revision=1)]\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.system/#input-models","title":"Input models","text":""},{"location":"api/tests.system/#anta.input_models.system.NTPServer","title":"NTPServer","text":"Model for a NTP server. Name Type Description Default server_address Hostname | IPv4Address The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name. For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output. - preferred bool Optional preferred for NTP server. If not provided, it defaults to `False`. False stratum int NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized. Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state. Field(ge=0, le=16) Source code in anta/input_models/system.py class NTPServer(BaseModel):\n \"\"\"Model for a NTP server.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: Hostname | IPv4Address\n \"\"\"The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration\n of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name.\n For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output.\"\"\"\n preferred: bool = False\n \"\"\"Optional preferred for NTP server. If not provided, it defaults to `False`.\"\"\"\n stratum: int = Field(ge=0, le=16)\n \"\"\"NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized.\n Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Representation of the NTPServer model.\"\"\"\n return f\"{self.server_address} (Preferred: {self.preferred}, Stratum: {self.stratum})\"\n"},{"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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\", revision=1)]\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 description = \"Verifies the Vxlan1 interface status.\"\n categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 there are no VXLAN config-sanity inconsistencies. 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 there are no VXLAN config-sanity inconsistencies.\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan config-sanity\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vni\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vtep\", revision=1)]\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[\n str, AfterValidator(aaa_group_prefix)\n]\n"},{"location":"api/types/#anta.custom_types.Afi","title":"Afi module-attribute","text":"Afi = Literal[\n \"ipv4\",\n \"ipv6\",\n \"vpn-ipv4\",\n \"vpn-ipv6\",\n \"evpn\",\n \"rt-membership\",\n \"path-selection\",\n \"link-state\",\n]\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.BfdProtocol","title":"BfdProtocol module-attribute","text":"BfdProtocol = Literal[\n \"bgp\",\n \"isis\",\n \"lag\",\n \"ospf\",\n \"ospfv3\",\n \"pim\",\n \"route-input\",\n \"static-bfd\",\n \"static-route\",\n \"vrrp\",\n \"vxlan\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpDropStats","title":"BgpDropStats module-attribute","text":"BgpDropStats = Literal[\n \"inDropAsloop\",\n \"inDropClusterIdLoop\",\n \"inDropMalformedMpbgp\",\n \"inDropOrigId\",\n \"inDropNhLocal\",\n \"inDropNhAfV6\",\n \"prefixDroppedMartianV4\",\n \"prefixDroppedMaxRouteLimitViolatedV4\",\n \"prefixDroppedMartianV6\",\n \"prefixDroppedMaxRouteLimitViolatedV6\",\n \"prefixLuDroppedV4\",\n \"prefixLuDroppedMartianV4\",\n \"prefixLuDroppedMaxRouteLimitViolatedV4\",\n \"prefixLuDroppedV6\",\n \"prefixLuDroppedMartianV6\",\n \"prefixLuDroppedMaxRouteLimitViolatedV6\",\n \"prefixEvpnDroppedUnsupportedRouteType\",\n \"prefixBgpLsDroppedReceptionUnsupported\",\n \"outDropV4LocalAddr\",\n \"outDropV6LocalAddr\",\n \"prefixVpnIpv4DroppedImportMatchFailure\",\n \"prefixVpnIpv4DroppedMaxRouteLimitViolated\",\n \"prefixVpnIpv6DroppedImportMatchFailure\",\n \"prefixVpnIpv6DroppedMaxRouteLimitViolated\",\n \"prefixEvpnDroppedImportMatchFailure\",\n \"prefixEvpnDroppedMaxRouteLimitViolated\",\n \"prefixRtMembershipDroppedLocalAsReject\",\n \"prefixRtMembershipDroppedMaxRouteLimitViolated\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpUpdateError","title":"BgpUpdateError module-attribute","text":"BgpUpdateError = Literal[\n \"inUpdErrWithdraw\",\n \"inUpdErrIgnore\",\n \"inUpdErrDisableAfiSafi\",\n \"disabledAfiSafi\",\n \"lastUpdErrTime\",\n]\n"},{"location":"api/types/#anta.custom_types.EcdsaKeySize","title":"EcdsaKeySize module-attribute","text":"EcdsaKeySize = Literal[256, 384, 512]\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[\n \"acl\",\n \"arp-inspection\",\n \"bpduguard\",\n \"dot1x-session-replace\",\n \"hitless-reload-down\",\n \"lacp-rate-limit\",\n \"link-flap\",\n \"no-internal-vlan\",\n \"portchannelguard\",\n \"portsec\",\n \"tapagg\",\n \"uplink-failure-detection\",\n]\n"},{"location":"api/types/#anta.custom_types.EthernetInterface","title":"EthernetInterface module-attribute","text":"EthernetInterface = Annotated[\n str,\n Field(pattern=\"^Ethernet[0-9]+(\\\\/[0-9]+)*$\"),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\n"},{"location":"api/types/#anta.custom_types.Hostname","title":"Hostname module-attribute","text":"Hostname = Annotated[\n str, Field(pattern=REGEXP_TYPE_HOSTNAME)\n]\n"},{"location":"api/types/#anta.custom_types.Interface","title":"Interface module-attribute","text":"Interface = Annotated[\n str,\n Field(pattern=REGEXP_TYPE_EOS_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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[\n str,\n BeforeValidator(\n bgp_multiprotocol_capabilities_abbreviations\n ),\n]\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.PortChannelInterface","title":"PortChannelInterface module-attribute","text":"PortChannelInterface = Annotated[\n str,\n Field(pattern=REGEX_TYPE_PORTCHANNEL),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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.REGEXP_BGP_IPV4_MPLS_LABELS","title":"REGEXP_BGP_IPV4_MPLS_LABELS module-attribute","text":"REGEXP_BGP_IPV4_MPLS_LABELS = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?label(s)?)\\\\b\"\n)\n Match IPv4 MPLS Labels."},{"location":"api/types/#anta.custom_types.REGEXP_BGP_L2VPN_AFI","title":"REGEXP_BGP_L2VPN_AFI module-attribute","text":"REGEXP_BGP_L2VPN_AFI = \"\\\\b(l2[\\\\s\\\\-]?vpn[\\\\s\\\\-]?evpn)\\\\b\"\n Match L2VPN EVPN AFI."},{"location":"api/types/#anta.custom_types.REGEXP_EOS_BLACKLIST_CMDS","title":"REGEXP_EOS_BLACKLIST_CMDS module-attribute","text":"REGEXP_EOS_BLACKLIST_CMDS = [\n \"^reload.*\",\n \"^conf\\\\w*\\\\s*(terminal|session)*\",\n \"^wr\\\\w*\\\\s*\\\\w+\",\n]\n List of regular expressions to blacklist from eos commands."},{"location":"api/types/#anta.custom_types.REGEXP_INTERFACE_ID","title":"REGEXP_INTERFACE_ID module-attribute","text":"REGEXP_INTERFACE_ID = '\\\\d+(\\\\/\\\\d+)*(\\\\.\\\\d+)?'\n Match Interface ID lilke 1/1.1."},{"location":"api/types/#anta.custom_types.REGEXP_PATH_MARKERS","title":"REGEXP_PATH_MARKERS module-attribute","text":"REGEXP_PATH_MARKERS = '[\\\\\\\\\\\\/\\\\s]'\n Match directory path from string."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_EOS_INTERFACE","title":"REGEXP_TYPE_EOS_INTERFACE module-attribute","text":"REGEXP_TYPE_EOS_INTERFACE = \"^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\\\\/[0-9]+)*(\\\\.[0-9]+)?$\"\n Match EOS interface types like Ethernet1/1, Vlan1, Loopback1, etc."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_HOSTNAME","title":"REGEXP_TYPE_HOSTNAME module-attribute","text":"REGEXP_TYPE_HOSTNAME = \"^(([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 Match hostname like my-hostname, my-hostname-1, my-hostname-1-2."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_VXLAN_SRC_INTERFACE","title":"REGEXP_TYPE_VXLAN_SRC_INTERFACE module-attribute","text":"REGEXP_TYPE_VXLAN_SRC_INTERFACE = \"^(Loopback)([0-9]|[1-9][0-9]{1,2}|[1-7][0-9]{3}|8[01][0-9]{2}|819[01])$\"\n Match Vxlan source interface like Loopback10."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_MPLS_VPN","title":"REGEX_BGP_IPV4_MPLS_VPN module-attribute","text":"REGEX_BGP_IPV4_MPLS_VPN = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?vpn)\\\\b\"\n)\n Match IPv4 MPLS VPN."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_UNICAST","title":"REGEX_BGP_IPV4_UNICAST module-attribute","text":"REGEX_BGP_IPV4_UNICAST = (\n \"\\\\b(ipv4[\\\\s\\\\-]?uni[\\\\s\\\\-]?cast)\\\\b\"\n)\n Match IPv4 Unicast."},{"location":"api/types/#anta.custom_types.REGEX_TYPE_PORTCHANNEL","title":"REGEX_TYPE_PORTCHANNEL module-attribute","text":"REGEX_TYPE_PORTCHANNEL = '^Port-Channel[0-9]{1,6}$'\n Match Port Channel interface like Port-Channel5."},{"location":"api/types/#anta.custom_types.RegexString","title":"RegexString module-attribute","text":"RegexString = Annotated[str, AfterValidator(validate_regex)]\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[\n \"unicast\", \"multicast\", \"labeled-unicast\", \"sr-te\"\n]\n"},{"location":"api/types/#anta.custom_types.SnmpErrorCounter","title":"SnmpErrorCounter module-attribute","text":"SnmpErrorCounter = Literal[\n \"inVersionErrs\",\n \"inBadCommunityNames\",\n \"inBadCommunityUses\",\n \"inParseErrs\",\n \"outTooBigErrs\",\n \"outNoSuchNameErrs\",\n \"outBadValueErrs\",\n \"outGeneralErrs\",\n]\n"},{"location":"api/types/#anta.custom_types.SnmpPdu","title":"SnmpPdu module-attribute","text":"SnmpPdu = Literal[\n \"inGetPdus\",\n \"inGetNextPdus\",\n \"inSetPdus\",\n \"outGetResponsePdus\",\n \"outTrapPdus\",\n]\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[\n str,\n Field(pattern=REGEXP_TYPE_VXLAN_SRC_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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(\n value: str,\n) -> str\n Abbreviations for different BGP multiprotocol capabilities. Examples IPv4 Unicast L2vpnEVPN ipv4 MPLS Labels ipv4Mplsvpn 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 REGEXP_BGP_L2VPN_AFI: \"l2VpnEvpn\",\n REGEXP_BGP_IPV4_MPLS_LABELS: \"ipv4MplsLabels\",\n REGEX_BGP_IPV4_MPLS_VPN: \"ipv4MplsVpn\",\n REGEX_BGP_IPV4_UNICAST: \"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(REGEXP_INTERFACE_ID)\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 return next((f\"{full_name}{intf_id}\" for alias, full_name in alias_map.items() if v.lower().startswith(alias)), 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 vlan -> Vlan loopback -> Loopback 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 v != \"\" and not v[0].isupper():\n return f\"{v[0].upper()}{v[1:]}\"\n return v\n"},{"location":"api/types/#anta.custom_types.validate_regex","title":"validate_regex","text":"validate_regex(value: str) -> str\n Validate that the input value is a valid regex format. Source code in anta/custom_types.py def validate_regex(value: str) -> str:\n \"\"\"Validate that the input value is a valid regex format.\"\"\"\n try:\n re.compile(value)\n except re.error as e:\n msg = f\"Invalid regex: {e}\"\n raise ValueError(msg) from e\n return value\n"},{"location":"cli/check/","title":"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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n --help Show this message and exit.\n"},{"location":"cli/debug/","title":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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":"Usage: 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.\n\n 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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"},{"location":"cli/debug/#example-of-multiple-arguments","title":"Example of multiple arguments","text":"Warning If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters. 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":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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_2024-04-09_15_56_19]\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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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 [DEPRECATED] Ensure devices have 'aaa authorization\n exec default local' configured (required for SCP on\n EOS). THIS WILL CHANGE THE CONFIGURATION OF YOUR\n 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. Warning ANTA can automatically configure aaa authorization exec default local using the anta exec collect-tech-support --configure option but this option is deprecated and will be removed in ANTA 2.0.0. 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":"The ANTA CLI offers multiple commands to access data from your local inventory."},{"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 (visit this page to learn more about tags). The --connected option allows to display only the devices where a connection has been established."},{"location":"cli/get-inventory-information/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 By default, anta get inventory only provides information that doesn\u2019t rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, use the --connected option."},{"location":"cli/get-inventory-information/#example","title":"Example","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 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={'DC1-SPINE1', 'DC1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC1-SPINE2': AsyncEOSDevice(\n name='DC1-SPINE2',\n tags={'DC1', 'SPINE', 'DC1-SPINE2'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE1': AsyncEOSDevice(\n name='DC2-SPINE1',\n tags={'DC2', 'DC2-SPINE1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE2': AsyncEOSDevice(\n name='DC2-SPINE2',\n tags={'DC2', 'DC2-SPINE2', 'SPINE'},\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=False,\n insecure=False\n )\n}\n"},{"location":"cli/inv-from-ansible/","title":"Inventory from Ansible","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\n NOTE: This command does not support inline vaulted variables. Make sure to\n comment them out.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var:\n ANTA_INVENTORY; required]\n --overwrite Do not prompt when overriding current inventory\n [env var: ANTA_GET_FROM_ANSIBLE_OVERWRITE]\n -g, --ansible-group TEXT Ansible group to filter\n --ansible-inventory FILE Path to your ansible inventory file to read\n [required]\n --help Show this message and exit.\n Warnings anta get from-ansible does not support inline vaulted variables, comment them out to generate your inventory. If the vaulted variable is necessary to build the inventory (e.g. ansible_host), it needs to be unvaulted for from-ansible command to work.\u201d The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritance 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: ---\nall:\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 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"},{"location":"cli/inv-from-cvp/","title":"Inventory from CVP","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. Info The current implementation only works with on-premises CloudVision instances, not with CloudVision as a Service (CVaaS)."},{"location":"cli/inv-from-cvp/#command-overview","title":"Command overview","text":"Usage: anta get from-cvp [OPTIONS]\n\n Build ANTA inventory from CloudVision.\n\n NOTE: Only username/password authentication is supported for on-premises CloudVision instances.\n Token authentication for both on-premises and CloudVision as a Service (CVaaS) is not supported.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var: ANTA_INVENTORY;\n required]\n --overwrite Do not prompt when overriding current inventory [env\n var: ANTA_GET_FROM_CVP_OVERWRITE]\n -host, --host TEXT CloudVision instance FQDN or IP [required]\n -u, --username TEXT CloudVision username [required]\n -p, --password TEXT CloudVision password [required]\n -c, --container TEXT CloudVision container where devices are configured\n --ignore-cert By default connection to CV will use HTTPS\n certificate, set this flag to disable it [env var:\n ANTA_GET_FROM_CVP_IGNORE_CERT]\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":"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 report Table report JSON report Custom template report CSV report Markdown report "},{"location":"cli/nrfu/#nrfu-command-overview","title":"NRFU Command overview","text":"Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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. Refer to the dedicated page for more information."},{"location":"cli/nrfu/#device-and-test-filtering","title":"Device and test filtering","text":"Options --device and --test can be used to target one or multiple devices and/or tests to run in your environment. The options can be repeated. Example: anta nrfu --device leaf1a --device leaf1b --test VerifyUptime --test VerifyReloadCause."},{"location":"cli/nrfu/#hide-results","title":"Hide results","text":"Option --hide can be used to hide test results in the output or report file based on their status. The option can be repeated. Example: anta nrfu --hide error --hide skipped."},{"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":"Usage: anta nrfu text [OPTIONS]\n\n ANTA command to check network states with text result.\n\nOptions:\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example","title":"Example","text":"anta nrfu --device DC1-LEAF1A text\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":"Usage: anta nrfu table [OPTIONS]\n\n ANTA command to check network states with table result.\n\nOptions:\n --group-by [device|test] Group result by test or device.\n --help Show this message and exit.\n 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 --device spine1 table\n anta nrfu --test VerifyZeroTouch table\n "},{"location":"cli/nrfu/#performing-nrfu-with-json-rendering","title":"Performing NRFU with JSON rendering","text":"The JSON rendering command in NRFU testing will generate an output of all test results in JSON format."},{"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 JSON 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. If specified, no output will be displayed in the terminal. This is useful for further processing or integration with other tools."},{"location":"cli/nrfu/#example_1","title":"Example","text":"anta nrfu --tags LEAF json\n"},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-csv-file","title":"Performing NRFU and saving results in a CSV file","text":"The csv command in NRFU testing is useful for generating a CSV file with all tests result. This file can be easily analyzed and filtered by operator for reporting purposes."},{"location":"cli/nrfu/#command-overview_3","title":"Command overview","text":"anta nrfu csv --help\nUsage: anta nrfu csv [OPTIONS]\n\n ANTA command to check network states with CSV result.\n\nOptions:\n --csv-output FILE Path to save report as a CSV file [env var:\n ANTA_NRFU_CSV_CSV_OUTPUT]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_2","title":"Example","text":""},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-markdown-file","title":"Performing NRFU and saving results in a Markdown file","text":"The md-report command in NRFU testing generates a comprehensive Markdown report containing various sections, including detailed statistics for devices and test categories."},{"location":"cli/nrfu/#command-overview_4","title":"Command overview","text":"anta nrfu md-report --help\n\nUsage: anta nrfu md-report [OPTIONS]\n\n ANTA command to check network state with Markdown report.\n\nOptions:\n --md-output FILE Path to save the report as a Markdown file [env var:\n ANTA_NRFU_MD_REPORT_MD_OUTPUT; required]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_3","title":"Example","text":""},{"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_5","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_4","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/nrfu/#dry-run-mode","title":"Dry-run mode","text":"It is possible to run anta nrfu --dry-run to execute ANTA up to the point where it should communicate with the network to execute the tests. When using --dry-run, all inventory devices are assumed to be online. This can be useful to check how many tests would be run using the catalog and inventory. "},{"location":"cli/overview/","title":"Overview","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."},{"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 selected inventory devices.\n"},{"location":"cli/overview/#anta-environment-variables","title":"ANTA environment 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_CATALOG=tests.yml\n Then, run the CLI without options: anta nrfu\n Note All environment variables may not be needed for every commands. Refer to <command> --help for the comprehensive environment variables names. Below are the environment 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":"ANTA uses tags to define test-to-device mappings (tests run on devices with matching tags) and the --tags CLI option acts as a filter to execute specific test/device combinations."},{"location":"cli/tag-management/#defining-tags","title":"Defining tags","text":""},{"location":"cli/tag-management/#device-tags","title":"Device tags","text":"Device tags can be defined in the inventory: anta_inventory:\n hosts:\n - name: leaf1\n host: leaf1.anta.arista.com\n tags: [\"leaf\"]\n - name: leaf2\n host: leaf2.anta.arista.com\n tags: [\"leaf\"]\n - name: spine1\n host: spine1.anta.arista.com\n tags: [\"spine\"]\n Each device also has its own name automatically added as a tag: $ anta get inventory\nCurrent inventory content is:\n{\n 'leaf1': AsyncEOSDevice(\n name='leaf1',\n tags={'leaf', 'leaf1'}, <--\n [...]\n host='leaf1.anta.arista.com',\n [...]\n ),\n 'leaf2': AsyncEOSDevice(\n name='leaf2',\n tags={'leaf', 'leaf2'}, <--\n [...]\n host='leaf2.anta.arista.com',\n [...]\n ),\n 'spine1': AsyncEOSDevice(\n name='spine1',\n tags={'spine1', 'spine'}, <--\n [...]\n host='spine1.anta.arista.com',\n [...]\n )\n}\n"},{"location":"cli/tag-management/#test-tags","title":"Test tags","text":"Tags can be defined in the test catalog to restrict tests to tagged devices: anta.tests.system:\n - VerifyUptime:\n minimum: 10\n filters:\n tags: ['spine']\n - VerifyUptime:\n minimum: 9\n filters:\n tags: ['leaf']\n - VerifyReloadCause:\n filters:\n tags: ['spine', 'leaf']\n - VerifyCoredump:\n - VerifyAgentLogs:\n - VerifyCPUUtilization:\n - VerifyMemoryUtilization:\n - VerifyFileSystemUtilization:\n - VerifyNTP:\n\nanta.tests.mlag:\n - VerifyMlagStatus:\n filters:\n tags: ['leaf']\n\nanta.tests.interfaces:\n - VerifyL3MTU:\n mtu: 1500\n filters:\n tags: ['spine']\n A tag used to filter a test can also be a device name Use different input values for a specific test Leverage tags to define different input values for a specific test. See the VerifyUptime example above."},{"location":"cli/tag-management/#using-tags","title":"Using tags","text":"Command Description No --tags option Run all tests on all devices according to the tag definitions in your inventory and test catalog. Tests without tags are executed on all devices. --tags leaf Run all tests marked with the leaf tag on all devices configured with the leaf tag. All other tests are ignored. --tags leaf,spine Run all tests marked with the leaf tag on all devices configured with the leaf tag.Run all tests marked with the spine tag on all devices configured with the spine tag. All other tests are ignored."},{"location":"cli/tag-management/#examples","title":"Examples","text":"The following examples use the inventory and test catalog defined above."},{"location":"cli/tag-management/#no-tags-option","title":"No --tags option","text":"Tests without tags are run on all devices. Tests with tags will only run on devices with matching tags. $ anta nrfu table --group-by device\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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 27\n---------------------------------\n Summary per device\n\u250f\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\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 leaf1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 leaf2 \u2502 7 \u2502 1 \u2502 1 \u2502 0 \u2502 VerifyAgentLogs \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 spine1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u2514\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/#single-tag","title":"Single tag","text":"With a tag specified, only tests matching this tag will be run on matching devices. $ anta nrfu --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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (2 established)\nTotal number of selected tests: 6\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\n In this case, only leaf devices defined in the inventory are used to run tests marked with the leaf in the test catalog."},{"location":"cli/tag-management/#multiple-tags","title":"Multiple tags","text":"It is possible to use multiple tags using the --tags tag1,tag2 syntax. $ anta nrfu --tags leaf,spine 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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 15\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyL3MTU :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyL3MTU :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyReloadCause :: SUCCESS\nspine1 :: VerifyMlagStatus :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyL3MTU :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\n"},{"location":"cli/tag-management/#obtaining-all-configured-tags","title":"Obtaining all configured tags","text":"As most ANTA commands accommodate tag filtering, this command is useful for enumerating all tags configured in the inventory. Running the anta get tags command will return a list of all tags configured in the inventory."},{"location":"cli/tag-management/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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/tag-management/#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 \"leaf\",\n \"leaf1\",\n \"leaf2\",\n \"spine\",\n \"spine1\"\n]\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: As a Python library in your own application The ANTA CLI "},{"location":"#install-anta-library","title":"Install ANTA library","text":"The library will NOT install the necessary dependencies for the CLI. # Install ANTA as a library\npip install anta\n"},{"location":"#install-anta-cli","title":"Install ANTA CLI","text":"If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx is a tool to install and run python applications in isolated environments. Refer to pipx instructions to install on your system. pipx installs ANTA in an isolated python environment and makes it available globally. This is not recommended if you plan to contribute to ANTA # Install ANTA CLI with pipx\n$ pipx install anta[cli]\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 You can also still choose to install it with directly with pip: pip install anta[cli]\n"},{"location":"#documentation","title":"Documentation","text":"The documentation is published on ANTA package website."},{"location":"#contribution-guide","title":"Contribution guide","text":"Contributions are welcome. Please refer to the contribution guide"},{"location":"#credits","title":"Credits","text":"Thank you to Jeremy Schulman for aio-eapi. 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":"Contribution model is based on a fork-model. Don\u2019t push to aristanetworks/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-environment","title":"Create a development environment","text":"Run the following commands to create an ANTA development environment: # Clone repository\n$ git clone https://github.com/aristanetworks/anta.git\n$ cd anta\n\n# Install ANTA in editable mode and its development tools\n$ pip install -e .[dev]\n# To also install the CLI\n$ pip install -e .[dev,cli]\n\n# Verify installation\n$ pip list -e\nPackage Version Editable project location\n------- ------- -------------------------\nanta 1.1.0 /mnt/lab/projects/anta\n Then, tox is configured 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\npy39 -> Run pytest with py39\npy310 -> Run pytest with py310\npy311 -> Run pytest with py311\npy312 -> Run pytest with py312\nreport -> Generate coverage report\n"},{"location":"contribution/#code-linting","title":"Code linting","text":"tox -e lint\n[...]\nlint: commands[0]> ruff check .\nAll checks passed!\nlint: commands[1]> ruff format . --check\n158 files already formatted\nlint: commands[2]> pylint anta\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\nlint: commands[3]> pylint tests\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\n lint: OK (22.69=setup[2.19]+cmd[0.02,0.02,9.71,10.75] seconds)\n congratulations :) (22.72 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 68 source files\ntype: commands[1]> mypy --config-file=pyproject.toml tests\nSuccess: no issues found in 82 source files\n type: OK (31.15=setup[14.62]+cmd[6.05,10.48] seconds)\n congratulations :) (31.18 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.units.anta_tests 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.units.anta_tests import test\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 pre-commit\ntrim trailing whitespace.................................................Passed\nfix end of files.........................................................Passed\ncheck for added large files..............................................Passed\ncheck for merge conflicts................................................Passed\nCheck and insert license on Python files.................................Passed\nCheck and insert license on Markdown files...............................Passed\nRun Ruff linter..........................................................Passed\nRun Ruff formatter.......................................................Passed\nCheck code style with pylint.............................................Passed\nChecks for common misspellings in text files.............................Passed\nCheck typing with mypy...................................................Passed\nCheck Markdown files style...............................................Passed\n"},{"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. The results can be viewed here."},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#a-local-os-error-occurred-while-connecting-to-a-device","title":"A local OS error occurred while connecting to a device","text":"A local OS error occurred while connecting to a device When running ANTA, you can receive A local OS error occurred while connecting to <device> errors. The underlying OSError exception can have various reasons: [Errno 24] Too many open files or [Errno 16] Device or resource busy. This usually means that the operating system refused to open a new file descriptor (or socket) for the ANTA process. This might be due to the hard limit for open file descriptors currently set for the ANTA process. At startup, ANTA sets the soft limit of its process to the hard limit up to 16384. This is because the soft limit is usually 1024 and the hard limit is usually higher (depends on the system). If the hard limit of the ANTA process is still lower than the number of selected tests in ANTA, the ANTA process may request to the operating system too many file descriptors and get an error, a WARNING is displayed at startup if this is the case."},{"location":"faq/#solution","title":"Solution","text":"One solution could be to raise the hard limit for the user starting the ANTA process. You can get the current hard limit for a user using the command ulimit -n -H while logged in. Create the file /etc/security/limits.d/10-anta.conf with the following content: <user> hard nofile <value>\n The user is the one with which the ANTA process is started. The value is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again."},{"location":"faq/#timeout-error-in-the-logs","title":"Timeout error in the logs","text":"Timeout error in the logs When running ANTA, you can receive <Foo>Timeout errors in the logs (could be ReadTimeout, WriteTimeout, ConnectTimeout or PoolTimeout). More details on the timeouts of the underlying library are available here: https://www.python-httpx.org/advanced/timeouts. This might be due to the time the host on which ANTA is run takes to reach the target devices (for instance if going through firewalls, NATs, \u2026) or when a lot of tests are being run at the same time on a device (eAPI has a queue mechanism to avoid exhausting EOS resources because of a high number of simultaneous eAPI requests)."},{"location":"faq/#solution_1","title":"Solution","text":"Use the timeout option. As an example for the nrfu command: anta nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml --timeout 50 text\n The previous command set a couple of options for ANTA NRFU, one them being the timeout command, by default, when running ANTA from CLI, it is set to 30s. The timeout is increased to 50s to allow ANTA to wait for API calls a little longer."},{"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_2","title":"Solution","text":" 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 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_3","title":"Solution","text":" 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_4","title":"Solution","text":" 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 follow the troubleshooting instructions and report the issue in our GitHub repository."},{"location":"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 install ANTA package is to run Python (>=3.9) and its pip package to install: pip install anta[cli]\n For more details about how to install package, please see the requirements and installation 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: s1-spine1\n tags: ['fabric', 'spine']\n - host: 192.168.0.11\n name: s1-spine2\n tags: ['fabric', 'spine']\n - host: 192.168.0.12\n name: s1-leaf1\n tags: ['fabric', 'leaf']\n - host: 192.168.0.13\n name: s1-leaf2\n tags: ['fabric', 'leaf']\n - host: 192.168.0.14\n name: s1-leaf3\n tags: ['fabric', 'leaf']\n - host: 192.168.0.15\n name: s1-leaf3\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: ---\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\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":""},{"location":"getting-started/#cli","title":"CLI","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. Usage: 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 selected inventory devices.\n Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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 Note The following examples shows how to pass all the CLI options. See how to use environment variables instead in the CLI overview"},{"location":"getting-started/#default-report-using-table","title":"Default report using table","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n `# table is default if not provided` \\\n table\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:01] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.058. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.069. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:02] INFO Running ANTA tests completed in: 0:00:00.969. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 s1-spine1 \u2502 VerifyMlagConfigSanity \u2502 skipped \u2502 MLAG is disabled \u2502 Verifies there are no MLAG config-sanity \u2502 MLAG \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502 inconsistencies. \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-spine1 \u2502 VerifyEOSVersion \u2502 failure \u2502 device is running version \u2502 Verifies the EOS version of the device. \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 \"4.32.2F-38195967.4322F (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)\" not in expected versions: \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['4.25.4M', '4.26.1F', \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 '4.28.3M-28837868.4283M (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n[...]\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyTerminAttrVersion \u2502 failure \u2502 device is running TerminAttr version \u2502 Verifies the TerminAttr version of the \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 v1.34.0 and is not in the allowed list: \u2502 device. \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['v1.22.1'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyZeroTouch \u2502 success \u2502 \u2502 Verifies ZeroTouch is disabled \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\u2500\u2500\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\u2500\u2500\u2500\u2500\u2500\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 arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n 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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:52:39] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.057. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.068. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:52:40] INFO Running ANTA tests completed in: 0:00:00.863. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\n\ns1-spine1 :: VerifyEOSVersion :: FAILURE(device is running version \"4.32.2F-38195967.4322F (engineering build)\" not in expected versions: ['4.25.4M', '4.26.1F',\n'4.28.3M-28837868.4283M (engineering build)'])\ns1-spine1 :: VerifyTerminAttrVersion :: FAILURE(device is running TerminAttr version v1.34.0 and is not in the allowed list: ['v1.22.1'])\ns1-spine1 :: VerifyZeroTouch :: SUCCESS()\ns1-spine1 :: VerifyMlagConfigSanity :: SKIPPED(MLAG is disabled)\n"},{"location":"getting-started/#report-in-json-format","title":"Report in JSON format","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable `\\\n `# --enable-password <password> `\\\n --catalog ./catalog.yml \\\n json\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:11] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.053. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.065. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:12] INFO Running ANTA tests completed in: 0:00:00.857. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \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\": \"s1-spine1\",\n \"test\": \"VerifyNTP\",\n \"categories\": [\n \"system\"\n ],\n \"description\": \"Verifies if NTP is synchronised.\",\n \"result\": \"success\",\n \"messages\": [],\n \"custom_field\": null\n },\n {\n \"name\": \"s1-spine1\",\n \"test\": \"VerifyMlagConfigSanity\",\n \"categories\": [\n \"mlag\"\n ],\n \"description\": \"Verifies there are no MLAG config-sanity inconsistencies.\",\n \"result\": \"skipped\",\n \"messages\": [\n \"MLAG is disabled\"\n ],\n \"custom_field\": null\n },\n [...]\n"},{"location":"getting-started/#basic-usage-in-a-python-script","title":"Basic usage in a Python script","text":"# 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\"\"\"Example script for ANTA.\n\nusage:\n\npython anta_runner.py\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport sys\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.cli.nrfu.utils import anta_progress_bar\nfrom anta.inventory import AntaInventory\nfrom anta.logger import Log, setup_logging\nfrom anta.models import AntaTest\nfrom anta.result_manager import ResultManager\nfrom anta.runner import main as anta_runner\n\n# setup logging\nsetup_logging(Log.INFO, Path(\"/tmp/anta.log\"))\nLOGGER = logging.getLogger()\nSCRIPT_LOG_PREFIX = \"[bold magenta][ANTA RUNNER SCRIPT][/] \" # For convenience purpose - there are nicer way to do this.\n\n\n# NOTE: The inventory and catalog files are not delivered with this script\nUSERNAME = \"admin\"\nPASSWORD = \"admin\"\nCATALOG_PATH = Path(\"/tmp/anta_catalog.yml\")\nINVENTORY_PATH = Path(\"/tmp/anta_inventory.yml\")\n\n# Load catalog file\ntry:\n catalog = AntaCatalog.parse(CATALOG_PATH)\nexcept Exception:\n LOGGER.exception(\"%s Catalog failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Catalog loaded!\", SCRIPT_LOG_PREFIX)\n\n# Load inventory\ntry:\n inventory = AntaInventory.parse(INVENTORY_PATH, username=USERNAME, password=PASSWORD)\nexcept Exception:\n LOGGER.exception(\"%s Inventory failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Inventory loaded!\", SCRIPT_LOG_PREFIX)\n\n# Create result manager object\nmanager = ResultManager()\n\n# Launch ANTA\nLOGGER.info(\"%s Starting ANTA runner...\", SCRIPT_LOG_PREFIX)\nwith anta_progress_bar() as AntaTest.progress:\n # Set dry_run to True to avoid connecting to the devices\n asyncio.run(anta_runner(manager, inventory, catalog, dry_run=False))\n\nLOGGER.info(\"%s ANTA run completed!\", SCRIPT_LOG_PREFIX)\n\n# Manipulate the test result object\nfor test_result in manager.results:\n LOGGER.info(\"%s %s:%s:%s\", SCRIPT_LOG_PREFIX, test_result.name, test_result.test, test_result.result)\n"},{"location":"requirements-and-installation/","title":"Installation","text":""},{"location":"requirements-and-installation/#python-version","title":"Python version","text":"Python 3 (>=3.9) is required: python --version\nPython 3.11.8\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-library-from-pypi-server","title":"Install library from Pypi server","text":"pip install anta\n Warning This command alone will not install the ANTA CLI requirements. "},{"location":"requirements-and-installation/#install-anta-cli-as-an-application-with-pipx","title":"Install ANTA CLI as an application with pipx","text":"pipx is a tool to install and run python applications in isolated environments. If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx installs ANTA in an isolated python environment and makes it available globally. pipx install anta[cli]\n Info Please take the time to read through the installation instructions of pipx before getting started."},{"location":"requirements-and-installation/#install-cli-from-pypi-server","title":"Install CLI from Pypi server","text":"Alternatively, pip install with cli extra is enough to install the ANTA CLI. pip install anta[cli]\n"},{"location":"requirements-and-installation/#install-anta-from-github","title":"Install ANTA from github","text":"pip install git+https://github.com/aristanetworks/anta.git\npip install git+https://github.com/aristanetworks/anta.git#egg=anta[cli]\n\n# You can even specify the branch, tag or commit:\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>#egg=anta[cli]\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 v1.1.0\n"},{"location":"requirements-and-installation/#eos-requirements","title":"EOS Requirements","text":"To get ANTA working, the targeted Arista EOS devices must have eAPI enabled. They need to use 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":"troubleshooting/","title":"Troubleshooting ANTA","text":"A couple of things to check when hitting an issue with ANTA: flowchart LR\n A>Hitting an issue with ANTA] --> B{Is my issue <br >listed in the FAQ?}\n B -- Yes --> C{Does the FAQ solution<br />works for me?}\n C -- Yes --> V(((Victory)))\n B -->|No| E{Is my problem<br />mentioned in one<br />of the open issues?}\n C -->|No| E\n E -- Yes --> F{Has the issue been<br />fixed in a newer<br />release or in main?}\n F -- Yes --> U[Upgrade]\n E -- No ---> H((Follow the steps below<br />and open a Github issue))\n U --> I{Did it fix<br /> your problem}\n I -- Yes --> V\n I -- No --> H\n F -- No ----> G((Add a comment on the <br />issue indicating you<br >are hitting this and<br />describing your setup<br /> and adding your logs.))\n\n click B \"../faq\" \"FAQ\"\n click E \"https://github.com/aristanetworks/anta/issues\"\n click H \"https://github.com/aristanetworks/anta/issues\"\n style A stroke:#f00,stroke-width:2px"},{"location":"troubleshooting/#capturing-logs","title":"Capturing logs","text":"To help document the issue in Github, it is important to capture some logs so the developers can understand what is affecting your system. No logs mean that the first question asked on the issue will probably be \u201cCan you share some logs please?\u201d. ANTA provides very verbose logs when using the DEBUG level. When using DEBUG log level with a log file, the DEBUG logging level is not sent to stdout, but only to the file. Danger On real deployments, do not use DEBUG logging level without setting a log file at the same time. To save the logs to a file called anta.log, use the following flags: # Where ANTA_COMMAND is one of nrfu, debug, get, exec, check\nanta -l DEBUG \u2013log-file anta.log <ANTA_COMMAND>\n See anta --help for more information. These have to precede the nrfu cmd. Tip Remember that in ANTA, each level of command has its own options and they can only be set at this level. so the -l and --log-file MUST be between anta and the ANTA_COMMAND. similarly, all the nrfu options MUST be set between the nrfu and the ANTA_NRFU_SUBCOMMAND (json, text, table or tpl-report). As an example, for the nrfu command, it would look like: anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#anta_debug-environment-variable","title":"ANTA_DEBUG environment variable","text":"Warning Do not use this if you do not know why. This produces a lot of logs and can create confusion if you do not know what to look for. The environment variable ANTA_DEBUG=true enable ANTA Debug Mode. This flag is used by various functions in ANTA: when set to true, the function will display or log more information. In particular, when an Exception occurs in the code and this variable is set, the logging function used by ANTA is different to also produce the Python traceback for debugging. This typically needs to be done when opening a GitHub issue and an Exception is seen at runtime. Example: ANTA_DEBUG=true anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#troubleshooting-on-eos","title":"Troubleshooting on EOS","text":"ANTA is using a specific ID in eAPI requests towards EOS. This allows for easier eAPI requests debugging on the device using EOS configuration trace CapiApp setting UwsgiRequestContext/4,CapiUwsgiServer/4 to set up CapiApp agent logs. Then, you can view agent logs using: bash tail -f /var/log/agents/CapiApp-*\n\n2024-05-15 15:32:54.056166 1429 UwsgiRequestContext 4 request content b'{\"jsonrpc\": \"2.0\", \"method\": \"runCmds\", \"params\": {\"version\": \"latest\", \"cmds\": [{\"cmd\": \"show ip route vrf default 10.255.0.3\", \"revision\": 4}], \"format\": \"json\", \"autoComplete\": false, \"expandAliases\": false}, \"id\": \"ANTA-VerifyRoutingTableEntry-132366530677328\"}'\n"},{"location":"usage-inventory-catalog/","title":"Inventory and Test catalog","text":"The ANTA framework needs 2 important inputs from the user to run: A device inventory 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 accessible via eAPI ranges: scan a range for devices accessible 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 in either YAML or JSON: ---\n<Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\n {\n \"<Python module>\": [\n {\n \"<AntaTest subclass>\": <AntaTest.Input compliant dictionary>\n }\n ]\n}\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 or equivalent in JSON: {\n \"anta.tests.connectivity\": [\n {\n \"VerifyReachability\": {\n \"result_overwrite\": {\n \"description\": \"Test with overwritten description\",\n \"categories\": [\n \"Overwritten category 1\"\n ],\n \"custom_field\": \"Test run by John Doe\"\n },\n \"filters\": {\n \"tags\": [\n \"leaf\"\n ]\n },\n \"hosts\": [\n {\n \"destination\": \"1.1.1.1\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n },\n {\n \"destination\": \"8.8.8.8\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n }\n ]\n }\n }\n ]\n}\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 or JSON file: anta.tests.software:\n - VerifyEOSVersion:\n # List of allowed EOS versions.\n versions:\n - 4.25.4M\n - 4.26.1F\n {\n \"anta.tests.software\": [\n {\n \"VerifyEOSVersion\": {\n \"versions\": [\n \"4.25.4M\",\n \"4.31.1F\"\n ]\n }\n }\n ]\n}\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 custom.tests.system Python module, the test catalog will be: custom.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":"usage-inventory-catalog/#example-script-to-merge-catalogs","title":"Example script to merge catalogs","text":"The following script reads all the files in intended/test_catalogs/ with names <device_name>-catalog.yml and merge them together inside one big catalog anta-catalog.yml using the new AntaCatalog.merge_catalogs() class method. # Copyright (c) 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\"\"\"Script that merge a collection of catalogs into one AntaCatalog.\"\"\"\n\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.models import AntaTest\n\nCATALOG_SUFFIX = \"-catalog.yml\"\nCATALOG_DIR = \"intended/test_catalogs/\"\n\nif __name__ == \"__main__\":\n catalogs = []\n for file in Path(CATALOG_DIR).glob(\"*\" + CATALOG_SUFFIX):\n device = str(file).removesuffix(CATALOG_SUFFIX).removeprefix(CATALOG_DIR)\n print(f\"Loading test catalog for device {device}\")\n catalog = AntaCatalog.parse(file)\n # Add the device name as a tag to all tests in the catalog\n for test in catalog.tests:\n test.inputs.filters = AntaTest.Input.Filters(tags={device})\n catalogs.append(catalog)\n\n # Merge all catalogs\n merged_catalog = AntaCatalog.merge_catalogs(catalogs)\n\n # Save the merged catalog to a file\n with Path(\"anta-catalog.yml\").open(\"w\") as f:\n f.write(merged_catalog.dump().yaml())\n Warning The AntaCatalog.merge() method is deprecated and will be removed in ANTA v2.0. Please use the AntaCatalog.merge_catalogs() class method instead."},{"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. "},{"location":"advanced_usages/as-python-lib/#examples","title":"Examples","text":""},{"location":"advanced_usages/as-python-lib/#parse-an-anta-inventory-file","title":"Parse an ANTA inventory file","text":"# Copyright (c) 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\"\"\"Script that parses an ANTA inventory file, connects to devices and print their status.\"\"\"\n\nimport asyncio\n\nfrom anta.inventory import AntaInventory\n\n\nasync def main(inv: AntaInventory) -> None:\n \"\"\"Read an AntaInventory and try to connect to every device in the inventory.\n\n 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\n\nif __name__ == \"__main__\":\n # Create the AntaInventory instance\n inventory = AntaInventory.parse(\n filename=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\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."},{"location":"advanced_usages/as-python-lib/#run-eos-commands","title":"Run EOS commands","text":"# Copyright (c) 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\"\"\"Script that runs a list of EOS commands on reachable devices.\"\"\"\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 \"\"\"Run a list of commands against each valid device in the inventory.\n\n Take an AntaInventory and a list of commands as string\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 -------\n dict[str, list[AntaCommand]]\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=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\n )\n\n # Create a list of commands with json output\n command_list = [\"show version\", \"show ip bgp summary\"]\n\n # Run the main asyncio entry point\n res = asyncio.run(main(inventory, command_list))\n\n pprint(res)\n"},{"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: 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 Caching can be disabled per device, network or range by setting the disable_cache key to True when defining 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. For tests developers, 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: set = None) -> None:\n super().__init__(name, tags, disable_cache=True)\n"},{"location":"advanced_usages/custom-tests/","title":"Developing ANTA tests","text":"Info 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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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":"Full AntaTest API documentation is available in the API documentation section"},{"location":"advanced_usages/custom-tests/#class-attributes","title":"Class Attributes","text":" name (str, optional): Name of the test. Used during reporting. By default set to the Class name. description (str, optional): A human readable description of your test. By default set to the first line of the docstring. 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":"Attributes: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. 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":"Full Input model documentation is available in API documentation section Attributes: Name Type Description result_overwrite ResultOverwrite | None Define fields to overwrite in the TestResult object."},{"location":"advanced_usages/custom-tests/#resultoverwrite-model","title":"ResultOverwrite model","text":"Full ResultOverwrite model documentation is available in API documentation section Attributes: Name Type Description description str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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 occurrence 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: 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. 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. 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. 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. 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, the first line is used as description of the test by default>\n \"\"\"\n\n # name = <override> # uncomment to override default behavior of name=Class Name\n # description = <override> # uncomment to override default behavior of description=first line of docstring\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 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 values 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, integrations with existing tools are not broken. This is done by using by default version=1 in eAPI calls. By default, ANTA uses version=\"latest\" in AntaCommand, but when developing tests, the revision MUST be provided when the outformat of the command is json. As explained earlier, this is to ensure that the eAPI always returns the same output model and that the test remains always valid from the day it was created. For some commands, you may also want to run them with a different revision or version. For instance, the VerifyBFDPeersHealth 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/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: Parse the command outputs using the self.instance_commands instance attribute. If needed, access the test inputs using the self.inputs instance attribute and write your conditional logic. 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_custom and the test is defined in anta_custom.dc_project Python module, the test catalog would look like: anta_custom.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(\n tests: list[AntaTestDefinition] | None = None,\n filename: str | Path | None = None,\n)\n Class representing an ANTA Catalog. It can be instantiated using its constructor or one of the static methods: parse(), from_list() or from_dict() Parameters: Name Type Description Default tests list[AntaTestDefinition] | None A list of AntaTestDefinition instances. None filename str | Path | None The path from which the catalog is loaded. None"},{"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.build_indexes","title":"build_indexes","text":"build_indexes(\n filtered_tests: set[str] | None = None,\n) -> None\n Indexes tests by their tags for quick access during filtering operations. If a filtered_tests set is provided, only the tests in this set will be indexed. This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests. Once the indexes are built, the indexes_built attribute is set to True. Source code in anta/catalog.py def build_indexes(self, filtered_tests: set[str] | None = None) -> None:\n \"\"\"Indexes tests by their tags for quick access during filtering operations.\n\n If a `filtered_tests` set is provided, only the tests in this set will be indexed.\n\n This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests.\n\n Once the indexes are built, the `indexes_built` attribute is set to True.\n \"\"\"\n for test in self.tests:\n # Skip tests that are not in the specified filtered_tests set\n if filtered_tests and test.test.name not in filtered_tests:\n continue\n\n # Indexing by tag\n if test.inputs.filters and (test_tags := test.inputs.filters.tags):\n for tag in test_tags:\n self.tag_to_tests[tag].add(test)\n else:\n self.tag_to_tests[None].add(test)\n\n self.indexes_built = True\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.clear_indexes","title":"clear_indexes","text":"clear_indexes() -> None\n Clear this AntaCatalog instance indexes. Source code in anta/catalog.py def clear_indexes(self) -> None:\n \"\"\"Clear this AntaCatalog instance indexes.\"\"\"\n self._init_indexes()\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.dump","title":"dump","text":"dump() -> AntaCatalogFile\n Return an AntaCatalogFile instance from this AntaCatalog instance. Returns: Type Description AntaCatalogFile An AntaCatalogFile instance containing tests of this AntaCatalog instance. Source code in anta/catalog.py def dump(self) -> AntaCatalogFile:\n \"\"\"Return an AntaCatalogFile instance from this AntaCatalog instance.\n\n Returns\n -------\n AntaCatalogFile\n An AntaCatalogFile instance containing tests of this AntaCatalog instance.\n \"\"\"\n root: dict[ImportString[Any], list[AntaTestDefinition]] = {}\n for test in self.tests:\n # Cannot use AntaTest.module property as the class is not instantiated\n root.setdefault(test.test.__module__, []).append(test)\n return AntaCatalogFile(root=root)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.from_dict","title":"from_dict staticmethod","text":"from_dict(\n data: RawCatalogInput,\n filename: str | Path | None = None,\n) -> 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. Parameters: Name Type Description Default data RawCatalogInput Python dictionary used to instantiate the AntaCatalog instance. required filename str | Path | None value to be set as AntaCatalog instance attribute None Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 dictionary content. 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 Parameters\n ----------\n data\n Python dictionary used to instantiate the AntaCatalog instance.\n filename\n value to be set as AntaCatalog instance attribute\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' dictionary content.\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. Parameters: Name Type Description Default data ListAntaTestTuples Python list used to instantiate the AntaCatalog instance. required Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 list content. 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 Parameters\n ----------\n data\n Python list used to instantiate the AntaCatalog instance.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' list content.\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(\n tags: set[str], *, strict: bool = False\n) -> set[AntaTestDefinition]\n Return all tests that match a given set of tags, according to the specified strictness. Parameters: Name Type Description Default tags set[str] The tags to filter tests by. If empty, return all tests without tags. required strict bool If True, returns only tests that contain all specified tags (intersection). If False, returns tests that contain any of the specified tags (union). False Returns: Type Description set[AntaTestDefinition] A set of tests that match the given tags. Raises: Type Description ValueError If the indexes have not been built prior to method call. Source code in anta/catalog.py def get_tests_by_tags(self, tags: set[str], *, strict: bool = False) -> set[AntaTestDefinition]:\n \"\"\"Return all tests that match a given set of tags, according to the specified strictness.\n\n Parameters\n ----------\n tags\n The tags to filter tests by. If empty, return all tests without tags.\n strict\n If True, returns only tests that contain all specified tags (intersection).\n If False, returns tests that contain any of the specified tags (union).\n\n Returns\n -------\n set[AntaTestDefinition]\n A set of tests that match the given tags.\n\n Raises\n ------\n ValueError\n If the indexes have not been built prior to method call.\n \"\"\"\n if not self.indexes_built:\n msg = \"Indexes have not been built yet. Call build_indexes() first.\"\n raise ValueError(msg)\n if not tags:\n return self.tag_to_tests[None]\n\n filtered_sets = [self.tag_to_tests[tag] for tag in tags if tag in self.tag_to_tests]\n if not filtered_sets:\n return set()\n\n if strict:\n return set.intersection(*filtered_sets)\n return set.union(*filtered_sets)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge","title":"merge","text":"merge(catalog: AntaCatalog) -> AntaCatalog\n Merge two AntaCatalog instances. Warning This method is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead. Parameters: Name Type Description Default catalog AntaCatalog AntaCatalog instance to merge to this instance. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of the two instances. Source code in anta/catalog.py def merge(self, catalog: AntaCatalog) -> AntaCatalog:\n \"\"\"Merge two AntaCatalog instances.\n\n Warning\n -------\n This method is deprecated and will be removed in ANTA v2.0. Use `AntaCatalog.merge_catalogs()` instead.\n\n Parameters\n ----------\n catalog\n AntaCatalog instance to merge to this instance.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of the two instances.\n \"\"\"\n # TODO: Use a decorator to deprecate this method instead. See https://github.com/aristanetworks/anta/issues/754\n warn(\n message=\"AntaCatalog.merge() is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n return self.merge_catalogs([self, catalog])\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge_catalogs","title":"merge_catalogs classmethod","text":"merge_catalogs(catalogs: list[AntaCatalog]) -> AntaCatalog\n Merge multiple AntaCatalog instances. Parameters: Name Type Description Default catalogs list[AntaCatalog] A list of AntaCatalog instances to merge. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of all the input catalogs. Source code in anta/catalog.py @classmethod\ndef merge_catalogs(cls, catalogs: list[AntaCatalog]) -> AntaCatalog:\n \"\"\"Merge multiple AntaCatalog instances.\n\n Parameters\n ----------\n catalogs\n A list of AntaCatalog instances to merge.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of all the input catalogs.\n \"\"\"\n combined_tests = list(chain(*(catalog.tests for catalog in catalogs)))\n return cls(tests=combined_tests)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.parse","title":"parse staticmethod","text":"parse(\n filename: str | Path,\n file_format: Literal[\"yaml\", \"json\"] = \"yaml\",\n) -> AntaCatalog\n Create an AntaCatalog instance from a test catalog file. Parameters: Name Type Description Default filename str | Path Path to test catalog YAML or JSON file. required file_format Literal['yaml', 'json'] Format of the file, either \u2018yaml\u2019 or \u2018json\u2019. 'yaml' Returns: Type Description AntaCatalog An AntaCatalog populated with the file content. Source code in anta/catalog.py @staticmethod\ndef parse(filename: str | Path, file_format: Literal[\"yaml\", \"json\"] = \"yaml\") -> AntaCatalog:\n \"\"\"Create an AntaCatalog instance from a test catalog file.\n\n Parameters\n ----------\n filename\n Path to test catalog YAML or JSON file.\n file_format\n Format of the file, either 'yaml' or 'json'.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the file content.\n \"\"\"\n if file_format not in [\"yaml\", \"json\"]:\n message = f\"'{file_format}' is not a valid format for an AntaCatalog file. Only 'yaml' and 'json' are supported.\"\n raise ValueError(message)\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) if file_format == \"yaml\" else json_load(f)\n except (TypeError, YAMLError, OSError, ValueError) 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(\n **data: (\n type[AntaTest]\n | AntaTest.Input\n | dict[str, Any]\n | None\n )\n)\n Bases: BaseModel Define a test with its associated inputs. Attributes: Name Type Description test type[AntaTest] An AntaTest concrete subclass. inputs Input The associated AntaTest.Input subclass instance. https://docs.pydantic.dev/2.0/usage/validators/#using-validation-context-with-basemodel-initialization."},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.check_inputs","title":"check_inputs","text":"check_inputs() -> Self\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) -> Self:\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(\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. 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\"Could not instantiate inputs as type {type(data).__name__} is not valid\"\n raise ValueError(msg)\n"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.serialize_model","title":"serialize_model","text":"serialize_model() -> dict[str, AntaTest.Input]\n Serialize the AntaTestDefinition model. The dictionary representing the model will be look like: <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n Returns: Type Description dict A dictionary representing the model. Source code in anta/catalog.py @model_serializer()\ndef serialize_model(self) -> dict[str, AntaTest.Input]:\n \"\"\"Serialize the AntaTestDefinition model.\n\n The dictionary representing the model will be look like:\n ```\n <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n ```\n\n Returns\n -------\n dict\n A dictionary representing the model.\n \"\"\"\n return {self.test.__name__: self.inputs}\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: <Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\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 if not data:\n return data\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 isinstance(test_definition, AntaTestDefinition):\n test_definitions.append(test_definition)\n continue\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 return data\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.flatten_modules","title":"flatten_modules staticmethod","text":"flatten_modules(\n data: dict[str, Any], package: str | None = None\n) -> dict[ModuleType, list[Any]]\n Allow the user to provide a data structure with nested Python modules. Example anta.tests.routing:\n generic:\n - <AntaTestDefinition>\n bgp:\n - <AntaTestDefinition>\n anta.tests.routing.generic and anta.tests.routing.bgp are importable Python modules. 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:\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 elif isinstance(tests, list):\n # This is a list of AntaTestDefinition\n modules[module] = tests\n else:\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 return modules\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.to_json","title":"to_json","text":"to_json() -> str\n Return a JSON representation string of this model. Returns: Type Description str The JSON representation string of this model. Source code in anta/catalog.py def to_json(self) -> str:\n \"\"\"Return a JSON representation string of this model.\n\n Returns\n -------\n str\n The JSON representation string of this model.\n \"\"\"\n return self.model_dump_json(serialize_as_any=True, exclude_unset=True, indent=2)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/catalog.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return safe_dump(safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/csv_reporter/","title":"CSV reporter","text":"CSV Report management for ANTA."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv","title":"ReportCsv","text":"Build a CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_name: str = \"Test Name\",\n test_status: str = \"Test Status\",\n messages: str = \"Message(s)\",\n description: str = \"Test description\",\n categories: str = \"Test category\",\n)\n Headers for the CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.convert_to_list","title":"convert_to_list classmethod","text":"convert_to_list(result: TestResult) -> list[str]\n Convert a TestResult into a list of string for creating file content. Parameters: Name Type Description Default result TestResult A TestResult to convert into list. required Returns: Type Description list[str] TestResult converted into a list. Source code in anta/reporter/csv_reporter.py @classmethod\ndef convert_to_list(cls, result: TestResult) -> list[str]:\n \"\"\"Convert a TestResult into a list of string for creating file content.\n\n Parameters\n ----------\n result\n A TestResult to convert into list.\n\n Returns\n -------\n list[str]\n TestResult converted into a list.\n \"\"\"\n message = cls.split_list_to_txt_list(result.messages) if len(result.messages) > 0 else \"\"\n categories = cls.split_list_to_txt_list(convert_categories(result.categories)) if len(result.categories) > 0 else \"None\"\n return [\n str(result.name),\n result.test,\n result.result,\n message,\n result.description,\n categories,\n ]\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.generate","title":"generate classmethod","text":"generate(\n results: ResultManager, csv_filename: pathlib.Path\n) -> None\n Build CSV flle with tests results. Parameters: Name Type Description Default results ResultManager A ResultManager instance. required csv_filename Path File path where to save CSV data. required Raises: Type Description OSError if any is raised while writing the CSV file. Source code in anta/reporter/csv_reporter.py @classmethod\ndef generate(cls, results: ResultManager, csv_filename: pathlib.Path) -> None:\n \"\"\"Build CSV flle with tests results.\n\n Parameters\n ----------\n results\n A ResultManager instance.\n csv_filename\n File path where to save CSV data.\n\n Raises\n ------\n OSError\n if any is raised while writing the CSV file.\n \"\"\"\n headers = [\n cls.Headers.device,\n cls.Headers.test_name,\n cls.Headers.test_status,\n cls.Headers.messages,\n cls.Headers.description,\n cls.Headers.categories,\n ]\n\n try:\n with csv_filename.open(mode=\"w\", encoding=\"utf-8\") as csvfile:\n csvwriter = csv.writer(\n csvfile,\n delimiter=\",\",\n )\n csvwriter.writerow(headers)\n for entry in results.results:\n csvwriter.writerow(cls.convert_to_list(entry))\n except OSError as exc:\n message = f\"OSError caught while writing the CSV file '{csv_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.split_list_to_txt_list","title":"split_list_to_txt_list classmethod","text":"split_list_to_txt_list(\n usr_list: list[str], delimiter: str = \" - \"\n) -> str\n Split list to multi-lines string. Parameters: Name Type Description Default usr_list list[str] List of string to concatenate. required delimiter str A delimiter to use to start string. Defaults to None. ' - ' Returns: Type Description str Multi-lines string. Source code in anta/reporter/csv_reporter.py @classmethod\ndef split_list_to_txt_list(cls, usr_list: list[str], delimiter: str = \" - \") -> str:\n \"\"\"Split list to multi-lines string.\n\n Parameters\n ----------\n usr_list\n List of string to concatenate.\n delimiter\n A delimiter to use to start string. Defaults to None.\n\n Returns\n -------\n str\n Multi-lines string.\n\n \"\"\"\n return f\"{delimiter}\".join(f\"{line}\" for line in usr_list)\n"},{"location":"api/device/","title":"Device","text":""},{"location":"api/device/#antadevice-base-class","title":"AntaDevice base class","text":""},{"location":"api/device/#anta.device.AntaDevice","title":"AntaDevice","text":"AntaDevice(\n name: str,\n tags: set[str] | None = None,\n *,\n disable_cache: bool = False\n)\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 str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. cache Cache | None In-memory cache from aiocache library for this device (None if cache is disabled). cache_locks dict Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled. Parameters: Name Type Description Default name str Device name. required tags set[str] | None Tags for this device. None disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AntaDevice.cache_statistics","title":"cache_statistics property","text":"cache_statistics: dict[str, Any] | None\n Return 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.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AntaDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AntaDevice.\"\"\"\n return (\n f\"AntaDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AntaDevice._collect","title":"_collect abstractmethod async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output. This abstract coroutine can be used to implement any command collection method for a device in ANTA. The _collect() implementation needs to populate the output attribute of the AntaCommand object passed as argument. If a failure occurs, the _collect() implementation is expected to catch the exception and implement proper logging, the output attribute of the AntaCommand object passed as argument would be None in this case. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py @abstractmethod\nasync def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None:\n \"\"\"Collect device command output.\n\n This abstract coroutine can be used to implement any command collection method\n for a device in ANTA.\n\n The `_collect()` implementation needs to populate the `output` attribute\n of the `AntaCommand` object passed as argument.\n\n If a failure occurs, the `_collect()` implementation is expected to catch the\n exception and implement proper logging, the `output` attribute of the\n `AntaCommand` object passed as argument would be `None` in this case.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n"},{"location":"api/device/#anta.device.AntaDevice.collect","title":"collect async","text":"collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> 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. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect(self, command: AntaCommand, *, collection_id: str | None = None) -> 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 Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\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, collection_id=collection_id)\n await self.cache.set(command.uid, command.output) # pylint: disable=no-member\n else:\n await self._collect(command=command, collection_id=collection_id)\n"},{"location":"api/device/#anta.device.AntaDevice.collect_commands","title":"collect_commands async","text":"collect_commands(\n commands: list[AntaCommand],\n *,\n collection_id: str | None = None\n) -> None\n Collect multiple commands. Parameters: Name Type Description Default commands list[AntaCommand] The commands to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect_commands(self, commands: list[AntaCommand], *, collection_id: str | None = None) -> None:\n \"\"\"Collect multiple commands.\n\n Parameters\n ----------\n commands\n The commands to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n await asyncio.gather(*(self.collect(command=command, collection_id=collection_id) for command in commands))\n"},{"location":"api/device/#anta.device.AntaDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device, usually through SCP. It is not mandatory to implement this for a valid AntaDevice subclass. Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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\n - `is_online`: When the device IP is reachable and a port can be open.\n\n - `established`: When a command execution succeeds.\n\n - `hw_model`: The hardware model of the device.\n \"\"\"\n"},{"location":"api/device/#async-eos-device-class","title":"Async EOS device class","text":""},{"location":"api/device/#anta.device.AsyncEOSDevice","title":"AsyncEOSDevice","text":"AsyncEOSDevice(\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: set[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)\n Bases: AntaDevice Implementation of AntaDevice for EOS using aio-eapi. Attributes: Name Type Description name str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. Parameters: Name Type Description Default host str Device FQDN or IP. required username str Username to connect to eAPI and SSH. required password str Password to connect to eAPI and SSH. required name str | None Device name. None enable bool Collect commands using privileged mode. False enable_password str | None Password used to gain privileged access on EOS. None port int | None eAPI port. Defaults to 80 is proto is \u2018http\u2019 or 443 if proto is \u2018https\u2019. None ssh_port int | None SSH port. 22 tags set[str] | None Tags for this device. None timeout float | None Timeout value in seconds for outgoing API calls. None insecure bool Disable SSH Host Key validation. False proto Literal['http', 'https'] eAPI protocol. Value can be \u2018http\u2019 or \u2018https\u2019. 'https' disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AsyncEOSDevice.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AsyncEOSDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AsyncEOSDevice.\"\"\"\n return (\n f\"AsyncEOSDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r}, \"\n f\"host={self._session.host!r}, \"\n f\"eapi_port={self._session.port!r}, \"\n f\"username={self._ssh_opts.username!r}, \"\n f\"enable={self.enable!r}, \"\n f\"insecure={self._ssh_opts.known_hosts is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AsyncEOSDevice._collect","title":"_collect async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output from EOS using aio-eapi. Supports outformat json and text as output structure. Gain privileged access using the enable_password attribute of the AntaDevice instance if populated. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None: # noqa: C901 function is too complex - because of many required except blocks\n \"\"\"Collect device command output from EOS using aio-eapi.\n\n Supports outformat `json` and `text` as output structure.\n Gain privileged access using the `enable_password` attribute\n of the `AntaDevice` instance if populated.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n commands: list[dict[str, str | int]] = []\n if self.enable and self._enable_password is not None:\n commands.append(\n {\n \"cmd\": \"enable\",\n \"input\": str(self._enable_password),\n },\n )\n elif self.enable:\n # No password\n commands.append({\"cmd\": \"enable\"})\n commands += [{\"cmd\": command.command, \"revision\": command.revision}] if command.revision else [{\"cmd\": command.command}]\n try:\n response: list[dict[str, Any] | str] = await self._session.cli(\n commands=commands,\n ofmt=command.ofmt,\n version=command.version,\n req_id=f\"ANTA-{collection_id}-{id(command)}\" if collection_id else f\"ANTA-{id(command)}\",\n ) # type: ignore[assignment] # multiple commands returns a list\n # Do not keep response of 'enable' command\n command.output = response[-1]\n except asynceapi.EapiCommandError as e:\n # This block catches exceptions related to EOS issuing an error.\n command.errors = e.errors\n if command.requires_privileges:\n logger.error(\n \"Command '%s' requires privileged mode on %s. Verify user permissions and if the `enable` option is required.\", command.command, self.name\n )\n if command.supported:\n logger.error(\"Command '%s' failed on %s: %s\", command.command, self.name, e.errors[0] if len(e.errors) == 1 else e.errors)\n else:\n logger.debug(\"Command '%s' is not supported on '%s' (%s)\", command.command, self.name, self.hw_model)\n except TimeoutException as e:\n # This block catches Timeout exceptions.\n command.errors = [exc_to_str(e)]\n timeouts = self._session.timeout.as_dict()\n logger.error(\n \"%s occurred while sending a command to %s. Consider increasing the timeout.\\nCurrent timeouts: Connect: %s | Read: %s | Write: %s | Pool: %s\",\n exc_to_str(e),\n self.name,\n timeouts[\"connect\"],\n timeouts[\"read\"],\n timeouts[\"write\"],\n timeouts[\"pool\"],\n )\n except (ConnectError, OSError) as e:\n # This block catches OSError and socket issues related exceptions.\n command.errors = [exc_to_str(e)]\n if (isinstance(exc := e.__cause__, httpcore.ConnectError) and isinstance(os_error := exc.__context__, OSError)) or isinstance(os_error := e, OSError): # pylint: disable=no-member\n if isinstance(os_error.__cause__, OSError):\n os_error = os_error.__cause__\n logger.error(\"A local OS error occurred while connecting to %s: %s.\", self.name, os_error)\n else:\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n except HTTPError as e:\n # This block catches most of the httpx Exceptions and logs a general message.\n command.errors = [exc_to_str(e)]\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n logger.debug(\"%s: %s\", self.name, command)\n"},{"location":"api/device/#anta.device.AsyncEOSDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device using asyncssh.scp(). Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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() function 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 = AntaCommand(command=\"show version\")\n await self._collect(show_version)\n if not show_version.collected:\n logger.warning(\"Cannot get hardware information from device %s\", self.name)\n else:\n self.hw_model = show_version.json_output.get(\"modelName\", None)\n if self.hw_model is None:\n logger.critical(\"Cannot parse 'show version' returned by device %s\", self.name)\n # in some cases it is possible that 'modelName' comes back empty\n # and it is nice to get a meaninfule error message\n elif self.hw_model == \"\":\n logger.critical(\"Got an empty 'modelName' in the 'show version' returned by device %s\", self.name)\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.devices","title":"devices property","text":"devices: list[AntaDevice]\n List of AntaDevice in this inventory."},{"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. Parameters: Name Type Description Default device AntaDevice Device object to be added. required Source code in anta/inventory/__init__.py def add_device(self, device: AntaDevice) -> None:\n \"\"\"Add a device to final inventory.\n\n Parameters\n ----------\n device\n 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(\n *,\n established_only: bool = False,\n tags: set[str] | None = None,\n devices: set[str] | None = None\n) -> AntaInventory\n Return a filtered inventory. Parameters: Name Type Description Default established_only bool Whether or not to include only established devices. False tags set[str] | None Tags to filter devices. None devices set[str] | None Names to filter devices. None 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: set[str] | None = None, devices: set[str] | None = None) -> AntaInventory:\n \"\"\"Return a filtered inventory.\n\n Parameters\n ----------\n established_only\n Whether or not to include only established devices.\n tags\n Tags to filter devices.\n devices\n Names to filter devices.\n\n Returns\n -------\n AntaInventory\n An inventory with filtered AntaDevice objects.\n \"\"\"\n\n def _filter_devices(device: AntaDevice) -> bool:\n \"\"\"Select the devices based on the inputs `tags`, `devices` and `established_only`.\"\"\"\n if tags is not None and all(tag not in tags for tag in device.tags):\n return False\n if devices is None or device.name in devices:\n return bool(not established_only or device.established)\n return False\n\n filtered_devices: list[AntaDevice] = list(filter(_filter_devices, self.values()))\n result = AntaInventory()\n for device in filtered_devices:\n result.add_device(device)\n return result\n"},{"location":"api/inventory/#anta.inventory.AntaInventory.parse","title":"parse staticmethod","text":"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. The inventory devices are AsyncEOSDevice instances. Parameters: Name Type Description Default filename str | Path Path to device inventory YAML file. required username str Username to use to connect to devices. required password str Password to use to connect to devices. required enable_password str | None Enable password to use if required. None timeout float | None Timeout value in seconds for outgoing API calls. None enable bool Whether or not the commands need to be run in enable mode towards the devices. False insecure bool Disable SSH Host Key validation. False disable_cache bool Disable cache globally. False 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 Parameters\n ----------\n filename\n Path to device inventory YAML file.\n username\n Username to use to connect to devices.\n password\n Password to use to connect to devices.\n enable_password\n Enable password to use if required.\n timeout\n Timeout value in seconds for outgoing API calls.\n enable\n Whether or not the commands need to be run in enable mode towards the devices.\n insecure\n Disable SSH Host Key validation.\n disable_cache\n Disable cache globally.\n\n Raises\n ------\n InventoryRootKeyError\n Root key of inventory is missing.\n InventoryIncorrectSchemaError\n 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 Device inventory input model."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryInput.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/inventory/models.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return yaml.safe_dump(yaml.safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryHost","title":"AntaInventoryHost","text":" Bases: BaseModel Host entry of AntaInventoryInput. Attributes: Name Type Description host Hostname | IPvAnyAddress IP Address or FQDN of the device. port Port | None Custom eAPI port to use. name str | None Custom name of the device. tags set[str] Tags of the device. disable_cache bool Disable cache for this device."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryNetwork","title":"AntaInventoryNetwork","text":" Bases: BaseModel Network entry of AntaInventoryInput. Attributes: Name Type Description network IPvAnyNetwork Subnet to use for scanning. tags set[str] Tags of the devices in this network. disable_cache bool Disable cache for all devices in this network."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryRange","title":"AntaInventoryRange","text":" Bases: BaseModel IP Range entry of AntaInventoryInput. Attributes: Name Type Description start IPvAnyAddress IPv4 or IPv6 address for the beginning of the range. stop IPvAnyAddress IPv4 or IPv6 address for the end of the range. tags set[str] Tags of the devices in this IP range. disable_cache bool Disable cache for all devices in this IP range."},{"location":"api/md_reporter/","title":"Markdown reporter","text":"Markdown report generator for ANTA test results."},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport","title":"ANTAReport","text":"ANTAReport(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generate the # ANTA Report section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the # ANTA Report section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `# ANTA Report` section of the markdown report.\"\"\"\n self.write_heading(heading_level=1)\n toc = MD_REPORT_TOC\n self.mdfile.write(toc + \"\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase","title":"MDReportBase","text":"MDReportBase(mdfile: TextIOWrapper, results: ResultManager)\n Bases: ABC Base class for all sections subclasses. Every subclasses must implement the generate_section method that uses the ResultManager object to generate and write content to the provided markdown file. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_heading_name","title":"generate_heading_name","text":"generate_heading_name() -> str\n Generate a formatted heading name based on the class name. Returns: Type Description str Formatted header name. Example ANTAReport will become ANTA Report. TestResultsSummary will become Test Results Summary. Source code in anta/reporter/md_reporter.py def generate_heading_name(self) -> str:\n \"\"\"Generate a formatted heading name based on the class name.\n\n Returns\n -------\n str\n Formatted header name.\n\n Example\n -------\n - `ANTAReport` will become `ANTA Report`.\n - `TestResultsSummary` will become `Test Results Summary`.\n \"\"\"\n class_name = self.__class__.__name__\n\n # Split the class name into words, keeping acronyms together\n words = re.findall(r\"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\\d|\\W|$)|\\d+\", class_name)\n\n # Capitalize each word, but keep acronyms in all caps\n formatted_words = [word if word.isupper() else word.capitalize() for word in words]\n\n return \" \".join(formatted_words)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of a markdown table for a specific report section. Subclasses can implement this method to generate the content of the table rows. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of a markdown table for a specific report section.\n\n Subclasses can implement this method to generate the content of the table rows.\n \"\"\"\n msg = \"Subclasses should implement this method\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_section","title":"generate_section abstractmethod","text":"generate_section() -> None\n Abstract method to generate a specific section of the markdown report. Must be implemented by subclasses. Source code in anta/reporter/md_reporter.py @abstractmethod\ndef generate_section(self) -> None:\n \"\"\"Abstract method to generate a specific section of the markdown report.\n\n Must be implemented by subclasses.\n \"\"\"\n msg = \"Must be implemented by subclasses\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.safe_markdown","title":"safe_markdown","text":"safe_markdown(text: str | None) -> str\n Escape markdown characters in the text to prevent markdown rendering issues. Parameters: Name Type Description Default text str | None The text to escape markdown characters from. required Returns: Type Description str The text with escaped markdown characters. Source code in anta/reporter/md_reporter.py def safe_markdown(self, text: str | None) -> str:\n \"\"\"Escape markdown characters in the text to prevent markdown rendering issues.\n\n Parameters\n ----------\n text\n The text to escape markdown characters from.\n\n Returns\n -------\n str\n The text with escaped markdown characters.\n \"\"\"\n # Custom field from a TestResult object can be None\n if text is None:\n return \"\"\n\n # Replace newlines with spaces to keep content on one line\n text = text.replace(\"\\n\", \" \")\n\n # Replace backticks with single quotes\n return text.replace(\"`\", \"'\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_heading","title":"write_heading","text":"write_heading(heading_level: int) -> None\n Write a markdown heading to the markdown file. The heading name used is the class name. Parameters: Name Type Description Default heading_level int The level of the heading (1-6). required Example ## Test Results Summary Source code in anta/reporter/md_reporter.py def write_heading(self, heading_level: int) -> None:\n \"\"\"Write a markdown heading to the markdown file.\n\n The heading name used is the class name.\n\n Parameters\n ----------\n heading_level\n The level of the heading (1-6).\n\n Example\n -------\n `## Test Results Summary`\n \"\"\"\n # Ensure the heading level is within the valid range of 1 to 6\n heading_level = max(1, min(heading_level, 6))\n heading_name = self.generate_heading_name()\n heading = \"#\" * heading_level + \" \" + heading_name\n self.mdfile.write(f\"{heading}\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_table","title":"write_table","text":"write_table(\n table_heading: list[str], *, last_table: bool = False\n) -> None\n Write a markdown table with a table heading and multiple rows to the markdown file. Parameters: Name Type Description Default table_heading list[str] List of strings to join for the table heading. required last_table bool Flag to determine if it\u2019s the last table of the markdown file to avoid unnecessary new line. Defaults to False. False Source code in anta/reporter/md_reporter.py def write_table(self, table_heading: list[str], *, last_table: bool = False) -> None:\n \"\"\"Write a markdown table with a table heading and multiple rows to the markdown file.\n\n Parameters\n ----------\n table_heading\n List of strings to join for the table heading.\n last_table\n Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.\n \"\"\"\n self.mdfile.write(\"\\n\".join(table_heading) + \"\\n\")\n for row in self.generate_rows():\n self.mdfile.write(row)\n if not last_table:\n self.mdfile.write(\"\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator","title":"MDReportGenerator","text":"Class responsible for generating a Markdown report based on the provided ResultManager object. It aggregates different report sections, each represented by a subclass of MDReportBase, and sequentially generates their content into a markdown file. The generate class method will loop over all the section subclasses and call their generate_section method. The final report will be generated in the same order as the sections list of the method."},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator.generate","title":"generate classmethod","text":"generate(results: ResultManager, md_filename: Path) -> None\n Generate and write the various sections of the markdown report. Parameters: Name Type Description Default results ResultManager The ResultsManager instance containing all test results. required md_filename Path The path to the markdown file to write the report into. required Source code in anta/reporter/md_reporter.py @classmethod\ndef generate(cls, results: ResultManager, md_filename: Path) -> None:\n \"\"\"Generate and write the various sections of the markdown report.\n\n Parameters\n ----------\n results\n The ResultsManager instance containing all test results.\n md_filename\n The path to the markdown file to write the report into.\n \"\"\"\n try:\n with md_filename.open(\"w\", encoding=\"utf-8\") as mdfile:\n sections: list[MDReportBase] = [\n ANTAReport(mdfile, results),\n TestResultsSummary(mdfile, results),\n SummaryTotals(mdfile, results),\n SummaryTotalsDeviceUnderTest(mdfile, results),\n SummaryTotalsPerCategory(mdfile, results),\n TestResults(mdfile, results),\n ]\n for section in sections:\n section.generate_section()\n except OSError as exc:\n message = f\"OSError caught while writing the Markdown file '{md_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals","title":"SummaryTotals","text":"SummaryTotals(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals table.\"\"\"\n yield (\n f\"| {self.results.get_total_results()} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SUCCESS})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SKIPPED})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.FAILURE})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.ERROR})} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest","title":"SummaryTotalsDeviceUnderTest","text":"SummaryTotalsDeviceUnderTest(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Devices Under Tests section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals device under test table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals device under test table.\"\"\"\n for device, stat in self.results.device_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n categories_skipped = \", \".join(sorted(convert_categories(list(stat.categories_skipped))))\n categories_failed = \", \".join(sorted(convert_categories(list(stat.categories_failed))))\n yield (\n f\"| {device} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} | {stat.tests_error_count} \"\n f\"| {categories_skipped or '-'} | {categories_failed or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Devices Under Tests section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Devices Under Tests` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory","title":"SummaryTotalsPerCategory","text":"SummaryTotalsPerCategory(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Per Category section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals per category table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals per category table.\"\"\"\n for category, stat in self.results.sorted_category_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n yield (\n f\"| {category} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} \"\n f\"| {stat.tests_error_count} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Per Category section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Per Category` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults","title":"TestResults","text":"TestResults(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generates the ## Test Results section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the all test results table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the all test results table.\"\"\"\n for result in self.results.get_results(sort_by=[\"name\", \"test\"]):\n messages = self.safe_markdown(\", \".join(result.messages))\n categories = \", \".join(convert_categories(result.categories))\n yield (\n f\"| {result.name or '-'} | {categories or '-'} | {result.test or '-'} \"\n f\"| {result.description or '-'} | {self.safe_markdown(result.custom_field) or '-'} | {result.result or '-'} | {messages or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n self.write_table(table_heading=self.TABLE_HEADING, last_table=True)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary","title":"TestResultsSummary","text":"TestResultsSummary(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ## Test Results Summary section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results Summary section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results Summary` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n"},{"location":"api/models/","title":"Test models","text":""},{"location":"api/models/#test-definition","title":"Test definition","text":""},{"location":"api/models/#anta.models.AntaTest","title":"AntaTest","text":"AntaTest(\n device: AntaDevice,\n inputs: dict[str, Any] | AntaTest.Input | None = None,\n eos_data: list[dict[Any, Any] | str] | None = None,\n)\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 '''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 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: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. Parameters: Name Type Description Default device AntaDevice AntaDevice instance on which the test will be run. required inputs dict[str, Any] | Input | None Dictionary of attributes used to instantiate the AntaTest.Input instance. None eos_data list[dict[Any, Any] | str] | None Populate outputs of the test commands instead of collecting from devices. This list must have the same length and order than the instance_commands instance attribute. None"},{"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 Return 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 Return a list of all the commands that have failed."},{"location":"api/models/#anta.models.AntaTest.module","title":"module property","text":"module: str\n Return the Python module in which this AntaTest class is defined."},{"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: Name Type Description result_overwrite ResultOverwrite | None 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 set[str] | None Tag of devices on which to run 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 str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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(\n function: F,\n) -> Callable[..., Coroutine[Any, Any, TestResult]]\n Decorate the test() method in child classes. This decorator implements (in this order): Instantiate the command outputs if eos_data is provided to the test() method Collect the commands from the device Run the test() method 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 Parameters\n ----------\n self\n The test instance.\n eos_data\n 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\n Any keyword argument to pass to the test.\n\n Returns\n -------\n TestResult\n The TestResult instance attribute populated with error status if any.\n\n \"\"\"\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 AntaTest.update_progress()\n return self.result\n\n if cmds := self.failed_commands:\n unsupported_commands = [f\"'{c.command}' is not supported on {self.device.hw_model}\" for c in cmds if not c.supported]\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 else:\n self.result.is_error(message=\"\\n\".join([f\"{c.command} has failed: {', '.join(c.errors)}\" for c in cmds]))\n AntaTest.update_progress()\n return self.result\n\n try:\n function(self, **kwargs)\n except Exception as e: # noqa: BLE001\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 # TODO: find a correct way to time test execution\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, collection_id=self.name)\n except Exception as e: # noqa: BLE001\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]:\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 _ = template\n msg = f\"AntaTemplate are provided but render() method has not been implemented for {self.module}.{self.__class__.__name__}\"\n raise NotImplementedError(msg)\n"},{"location":"api/models/#anta.models.AntaTest.save_commands_data","title":"save_commands_data","text":"save_commands_data(\n eos_data: list[dict[str, Any] | str]\n) -> 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 reason\")\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 reason\")\n ```\n\n \"\"\"\n"},{"location":"api/models/#command-definition","title":"Command definition","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 str Device command. version Literal[1, 'latest'] eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision | None eAPI revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt Literal['json', 'text'] eAPI output - json or text. output dict[str, Any] | str | None Output of the command. Only defined if there was no errors. template AntaTemplate | None AntaTemplate object used to render this command. errors list[str] If the command execution fails, eAPI returns a list of strings detailing the error(s). params AntaParamsBaseModel Pydantic Model containing the variables values used to render the template. use_cache bool Enable or disable caching for this AntaCommand if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaCommand.collected","title":"collected property","text":"collected: bool\n Return True if the command has been collected, False otherwise. A command that has not been collected could have returned an error. See error property."},{"location":"api/models/#anta.models.AntaCommand.error","title":"error property","text":"error: bool\n Return True if the command returned an error, False otherwise."},{"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.requires_privileges","title":"requires_privileges property","text":"requires_privileges: bool\n Return True if the command requires privileged mode, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"location":"api/models/#anta.models.AntaCommand.supported","title":"supported property","text":"supported: bool\n Return True if the command is supported on the device hardware platform, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"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/#anta.models.AntaTemplate","title":"AntaTemplate","text":"AntaTemplate(\n template: str,\n version: Literal[1, \"latest\"] = \"latest\",\n revision: Revision | None = None,\n ofmt: Literal[\"json\", \"text\"] = \"json\",\n *,\n use_cache: bool = True\n)\n 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: \u2018show vlan {vlan_id}\u2019. version eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt eAPI output - json or text. use_cache Enable or disable caching for this AntaTemplate if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaTemplate.__repr__","title":"__repr__","text":"__repr__() -> str\n Return the representation of the class. Copying pydantic model style, excluding params_schema Source code in anta/models.py def __repr__(self) -> str:\n \"\"\"Return the representation of the class.\n\n Copying pydantic model style, excluding `params_schema`\n \"\"\"\n return \" \".join(f\"{a}={v!r}\" for a, v in vars(self).items() if a != \"params_schema\")\n"},{"location":"api/models/#anta.models.AntaTemplate.render","title":"render","text":"render(**params: str | int | bool) -> AntaCommand\n Render an AntaCommand from an AntaTemplate instance. Keep the parameters used in the AntaTemplate instance. Parameters: Name Type Description Default params str | int | bool Dictionary of variables with string values to render the Python f-string. {} Returns: Type Description AntaCommand The rendered AntaCommand. This AntaCommand instance have a template attribute that references this AntaTemplate instance. Raises: Type Description AntaTemplateRenderError If a parameter is missing to render the AntaTemplate instance. Source code in anta/models.py def render(self, **params: str | int | bool) -> AntaCommand:\n \"\"\"Render an AntaCommand from an AntaTemplate instance.\n\n Keep the parameters used in the AntaTemplate instance.\n\n Parameters\n ----------\n params\n Dictionary of variables with string values to render the Python f-string.\n\n Returns\n -------\n AntaCommand\n The rendered AntaCommand.\n This AntaCommand instance have a template attribute that references this\n AntaTemplate instance.\n\n Raises\n ------\n AntaTemplateRenderError\n If a parameter is missing to render the AntaTemplate instance.\n \"\"\"\n try:\n command = self.template.format(**params)\n except (KeyError, SyntaxError) as e:\n raise AntaTemplateRenderError(self, e.args[0]) from e\n return AntaCommand(\n command=command,\n ofmt=self.ofmt,\n version=self.version,\n revision=self.revision,\n template=self,\n params=self.params_schema(**params),\n use_cache=self.use_cache,\n )\n"},{"location":"api/reporters/","title":"Other reporters","text":"Report management for ANTA."},{"location":"api/reporters/#anta.reporter.ReportJinja","title":"ReportJinja","text":"ReportJinja(template_path: pathlib.Path)\n Report builder based on a Jinja2 template."},{"location":"api/reporters/#anta.reporter.ReportJinja.render","title":"render","text":"render(\n data: list[dict[str, Any]],\n *,\n trim_blocks: bool = True,\n lstrip_blocks: bool = True\n) -> str\n Build a report based on a Jinja2 template. Report is built based on a J2 template provided by user. Data structure sent to template is: Example >>> print(ResultManager.json)\n[\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n]\n Parameters: Name Type Description Default data list[dict[str, Any]] List of results from ResultManager.results. required trim_blocks bool enable trim_blocks for J2 rendering. True lstrip_blocks bool enable lstrip_blocks for J2 rendering. True Returns: Type Description str Rendered template Source code in anta/reporter/__init__.py def render(self, data: list[dict[str, Any]], *, trim_blocks: bool = True, lstrip_blocks: bool = True) -> str:\n \"\"\"Build a report based on a Jinja2 template.\n\n Report is built based on a J2 template provided by user.\n Data structure sent to template is:\n\n Example\n -------\n ```\n >>> print(ResultManager.json)\n [\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n ]\n ```\n\n Parameters\n ----------\n data\n List of results from `ResultManager.results`.\n trim_blocks\n enable trim_blocks for J2 rendering.\n lstrip_blocks\n enable lstrip_blocks for J2 rendering.\n\n Returns\n -------\n str\n Rendered template\n\n \"\"\"\n with self.template_path.open(encoding=\"utf-8\") as file_:\n template = Template(file_.read(), trim_blocks=trim_blocks, lstrip_blocks=lstrip_blocks)\n\n return template.render({\"data\": data})\n"},{"location":"api/reporters/#anta.reporter.ReportTable","title":"ReportTable","text":"TableReport Generate a Table based on TestResult."},{"location":"api/reporters/#anta.reporter.ReportTable.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_case: str = \"Test Name\",\n number_of_success: str = \"# of success\",\n number_of_failure: str = \"# of failure\",\n number_of_skipped: str = \"# of skipped\",\n number_of_errors: str = \"# of errors\",\n list_of_error_nodes: str = \"List of failed or error nodes\",\n list_of_error_tests: str = \"List of failed or error test cases\",\n)\n Headers for the table report."},{"location":"api/reporters/#anta.reporter.ReportTable.report_all","title":"report_all","text":"report_all(\n manager: ResultManager, title: str = \"All tests results\"\n) -> Table\n Create a table report with all tests for one or all devices. Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required title str Title for the report. Defaults to \u2018All tests results\u2019. 'All tests results' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_all(self, manager: ResultManager, title: str = \"All tests results\") -> Table:\n \"\"\"Create a table report with all tests for one or all devices.\n\n Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n title\n Title for the report. Defaults to 'All tests results'.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\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 def add_line(result: TestResult) -> None:\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(convert_categories(result.categories))\n table.add_row(str(result.name), result.test, state, message, result.description, categories)\n\n for result in manager.results:\n add_line(result)\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_devices","title":"report_summary_devices","text":"report_summary_devices(\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table\n Create a table report with result aggregated per device. Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required devices list[str] | None List of device names to include. None to select all devices. None title str Title of the report. 'Summary per device' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_devices(\n self,\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per device.\n\n Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n devices\n List of device names to include. None to select all devices.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.device,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_tests,\n ]\n table = self._build_headers(headers=headers, table=table)\n for device, stats in sorted(manager.device_stats.items()):\n if devices is None or device in devices:\n table.add_row(\n device,\n str(stats.tests_success_count),\n str(stats.tests_skipped_count),\n str(stats.tests_failure_count),\n str(stats.tests_error_count),\n \", \".join(stats.tests_failure),\n )\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_tests","title":"report_summary_tests","text":"report_summary_tests(\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table\n Create a table report with result aggregated per test. Create table with full output: Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required tests list[str] | None List of test names to include. None to select all tests. None title str Title of the report. 'Summary per test' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_tests(\n self,\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per test.\n\n Create table with full output:\n Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n tests\n List of test names to include. None to select all tests.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.test_case,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_nodes,\n ]\n table = self._build_headers(headers=headers, table=table)\n for test, stats in sorted(manager.test_stats.items()):\n if tests is None or test in tests:\n table.add_row(\n test,\n str(stats.devices_success_count),\n str(stats.devices_skipped_count),\n str(stats.devices_failure_count),\n str(stats.devices_error_count),\n \", \".join(stats.devices_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":" options:\n filters: [\"!^_[^_]\", \"!^__len__\"]\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager","title":"ResultManager","text":"ResultManager()\n Helper to manage Test Results and generate reports. Examples Create Inventory: inventory_anta = AntaInventory.parse(\n filename='examples/inventory.yml',\n username='ansible',\n password='ansible',\n)\n Create Result Manager: manager = ResultManager()\n Run tests for all connected devices: for device in inventory_anta.get_inventory().devices:\n manager.add(\n VerifyNTP(device=device).test()\n )\n manager.add(\n VerifyEOSVersion(device=device).test(version='4.28.3M')\n )\n Print result in native format: manager.results\n[\n TestResult(\n name=\"pf1\",\n test=\"VerifyZeroTouch\",\n categories=[\"configuration\"],\n description=\"Verifies ZeroTouch is disabled\",\n result=\"success\",\n messages=[],\n custom_field=None,\n ),\n TestResult(\n name=\"pf1\",\n test='VerifyNTP',\n categories=[\"software\"],\n categories=['system'],\n description='Verifies if NTP is synchronised.',\n result='failure',\n messages=[\"The device is not synchronized with the configured NTP server(s): 'NTP is disabled.'\"],\n custom_field=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."},{"location":"api/result_manager/#anta.result_manager.ResultManager.json","title":"json property","text":"json: str\n Get a JSON representation of the results."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results","title":"results property writable","text":"results: list[TestResult]\n Get the list of TestResult."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results_by_status","title":"results_by_status cached property","text":"results_by_status: dict[AntaTestStatus, list[TestResult]]\n A cached property that returns the results grouped by status."},{"location":"api/result_manager/#anta.result_manager.ResultManager.sorted_category_stats","title":"sorted_category_stats property","text":"sorted_category_stats: dict[str, CategoryStats]\n A property that returns the category_stats dictionary sorted by key name."},{"location":"api/result_manager/#anta.result_manager.ResultManager.__len__","title":"__len__","text":"__len__() -> int\n Implement len method to count number of results. Source code in anta/result_manager/__init__.py def __len__(self) -> int:\n \"\"\"Implement __len__ method to count number of results.\"\"\"\n return len(self._result_entries)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.add","title":"add","text":"add(result: TestResult) -> None\n Add a result to the ResultManager instance. The result is added to the internal list of results and the overall status of the ResultManager instance is updated based on the added test status. Parameters: Name Type Description Default result TestResult TestResult to add to the ResultManager instance. required Source code in anta/result_manager/__init__.py def add(self, result: TestResult) -> None:\n \"\"\"Add a result to the ResultManager instance.\n\n The result is added to the internal list of results and the overall status\n of the ResultManager instance is updated based on the added test status.\n\n Parameters\n ----------\n result\n TestResult to add to the ResultManager instance.\n \"\"\"\n self._result_entries.append(result)\n self._update_status(result.result)\n self._update_stats(result)\n\n # Every time a new result is added, we need to clear the cached property\n self.__dict__.pop(\"results_by_status\", None)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter","title":"filter","text":"filter(hide: set[AntaTestStatus]) -> ResultManager\n Get a filtered ResultManager based on test status. Parameters: Name Type Description Default hide set[AntaTestStatus] Set of AntaTestStatus enum members to select tests to hide based on their status. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter(self, hide: set[AntaTestStatus]) -> ResultManager:\n \"\"\"Get a filtered ResultManager based on test status.\n\n Parameters\n ----------\n hide\n Set of AntaTestStatus enum members to select tests to hide based on their status.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n possible_statuses = set(AntaTestStatus)\n manager = ResultManager()\n manager.results = self.get_results(possible_statuses - hide)\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_devices","title":"filter_by_devices","text":"filter_by_devices(devices: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific devices. Parameters: Name Type Description Default devices set[str] Set of device names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_devices(self, devices: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific devices.\n\n Parameters\n ----------\n devices\n Set of device names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.name in devices]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_tests","title":"filter_by_tests","text":"filter_by_tests(tests: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific tests. Parameters: Name Type Description Default tests set[str] Set of test names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_tests(self, tests: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific tests.\n\n Parameters\n ----------\n tests\n Set of test names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.test in tests]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_devices","title":"get_devices","text":"get_devices() -> set[str]\n Get the set of all the device names. Returns: Type Description set[str] Set of device names. Source code in anta/result_manager/__init__.py def get_devices(self) -> set[str]:\n \"\"\"Get the set of all the device names.\n\n Returns\n -------\n set[str]\n Set of device names.\n \"\"\"\n return {str(result.name) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_results","title":"get_results","text":"get_results(\n status: set[AntaTestStatus] | None = None,\n sort_by: list[str] | None = None,\n) -> list[TestResult]\n Get the results, optionally filtered by status and sorted by TestResult fields. If no status is provided, all results are returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None sort_by list[str] | None Optional list of TestResult fields to sort the results. None Returns: Type Description list[TestResult] List of results. Source code in anta/result_manager/__init__.py def get_results(self, status: set[AntaTestStatus] | None = None, sort_by: list[str] | None = None) -> list[TestResult]:\n \"\"\"Get the results, optionally filtered by status and sorted by TestResult fields.\n\n If no status is provided, all results are returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n sort_by\n Optional list of TestResult fields to sort the results.\n\n Returns\n -------\n list[TestResult]\n List of results.\n \"\"\"\n # Return all results if no status is provided, otherwise return results for multiple statuses\n results = self._result_entries if status is None else list(chain.from_iterable(self.results_by_status.get(status, []) for status in status))\n\n if sort_by:\n accepted_fields = TestResult.model_fields.keys()\n if not set(sort_by).issubset(set(accepted_fields)):\n msg = f\"Invalid sort_by fields: {sort_by}. Accepted fields are: {list(accepted_fields)}\"\n raise ValueError(msg)\n results = sorted(results, key=lambda result: [getattr(result, field) for field in sort_by])\n\n return results\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_tests","title":"get_tests","text":"get_tests() -> set[str]\n Get the set of all the test names. Returns: Type Description set[str] Set of test names. Source code in anta/result_manager/__init__.py def get_tests(self) -> set[str]:\n \"\"\"Get the set of all the test names.\n\n Returns\n -------\n set[str]\n Set of test names.\n \"\"\"\n return {str(result.test) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_total_results","title":"get_total_results","text":"get_total_results(\n status: set[AntaTestStatus] | None = None,\n) -> int\n Get the total number of results, optionally filtered by status. If no status is provided, the total number of results is returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None Returns: Type Description int Total number of results. Source code in anta/result_manager/__init__.py def get_total_results(self, status: set[AntaTestStatus] | None = None) -> int:\n \"\"\"Get the total number of results, optionally filtered by status.\n\n If no status is provided, the total number of results is returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n\n Returns\n -------\n int\n Total number of results.\n \"\"\"\n if status is None:\n # Return the total number of results\n return sum(len(results) for results in self.results_by_status.values())\n\n # Return the total number of results for multiple statuses\n return sum(len(self.results_by_status.get(status, [])) for status in status)\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/#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 str Name of the device where the test was run. test str Name of the test run on the device. categories list[str] List of categories the TestResult belongs to. Defaults to the AntaTest categories. description str Description of the TestResult. Defaults to the AntaTest description. result AntaTestStatus Result of the test. Must be one of the AntaTestStatus Enum values: unset, success, failure, error or skipped. messages list[str] Messages to report after the test, if any. custom_field str | None 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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_error(self, message: str | None = None) -> None:\n \"\"\"Set status to error.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_failure(self, message: str | None = None) -> None:\n \"\"\"Set status to failure.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_skipped(self, message: str | None = None) -> None:\n \"\"\"Set status to skipped.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_success(self, message: str | None = None) -> None:\n \"\"\"Set status to success.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.SUCCESS, message)\n"},{"location":"api/runner/","title":"Runner","text":""},{"location":"api/runner/#anta.runner","title":"runner","text":"ANTA runner function."},{"location":"api/runner/#anta.runner.adjust_rlimit_nofile","title":"adjust_rlimit_nofile","text":"adjust_rlimit_nofile() -> tuple[int, int]\n Adjust the maximum number of open file descriptors for the ANTA process. The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable. If the ANTA_NOFILE environment variable is not set or is invalid, DEFAULT_NOFILE is used. Returns: Type Description tuple[int, int] The new soft and hard limits for open file descriptors. Source code in anta/runner.py def adjust_rlimit_nofile() -> tuple[int, int]:\n \"\"\"Adjust the maximum number of open file descriptors for the ANTA process.\n\n The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable.\n\n If the `ANTA_NOFILE` environment variable is not set or is invalid, `DEFAULT_NOFILE` is used.\n\n Returns\n -------\n tuple[int, int]\n The new soft and hard limits for open file descriptors.\n \"\"\"\n try:\n nofile = int(os.environ.get(\"ANTA_NOFILE\", DEFAULT_NOFILE))\n except ValueError as exception:\n logger.warning(\"The ANTA_NOFILE environment variable value is invalid: %s\\nDefault to %s.\", exc_to_str(exception), DEFAULT_NOFILE)\n nofile = DEFAULT_NOFILE\n\n limits = resource.getrlimit(resource.RLIMIT_NOFILE)\n logger.debug(\"Initial limit numbers for open file descriptors for the current ANTA process: Soft Limit: %s | Hard Limit: %s\", limits[0], limits[1])\n nofile = min(limits[1], nofile)\n logger.debug(\"Setting soft limit for open file descriptors for the current ANTA process to %s\", nofile)\n resource.setrlimit(resource.RLIMIT_NOFILE, (nofile, limits[1]))\n return resource.getrlimit(resource.RLIMIT_NOFILE)\n"},{"location":"api/runner/#anta.runner.get_coroutines","title":"get_coroutines","text":"get_coroutines(\n selected_tests: defaultdict[\n AntaDevice, set[AntaTestDefinition]\n ],\n manager: ResultManager,\n) -> list[Coroutine[Any, Any, TestResult]]\n Get the coroutines for the ANTA run. Parameters: Name Type Description Default selected_tests defaultdict[AntaDevice, set[AntaTestDefinition]] A mapping of devices to the tests to run. The selected tests are generated by the prepare_tests function. required manager ResultManager A ResultManager required Returns: Type Description list[Coroutine[Any, Any, TestResult]] The list of coroutines to run. Source code in anta/runner.py def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]], manager: ResultManager) -> list[Coroutine[Any, Any, TestResult]]:\n \"\"\"Get the coroutines for the ANTA run.\n\n Parameters\n ----------\n selected_tests\n A mapping of devices to the tests to run. The selected tests are generated by the `prepare_tests` function.\n manager\n A ResultManager\n\n Returns\n -------\n list[Coroutine[Any, Any, TestResult]]\n The list of coroutines to run.\n \"\"\"\n coros = []\n for device, test_definitions in selected_tests.items():\n for test in test_definitions:\n try:\n test_instance = test.test(device=device, inputs=test.inputs)\n manager.add(test_instance.result)\n coros.append(test_instance.test())\n except Exception as e: # noqa: PERF203, BLE001\n # An AntaTest instance is potentially user-defined code.\n # We need to catch everything and exit gracefully with an error message.\n message = \"\\n\".join(\n [\n f\"There is an error when creating test {test.test.__module__}.{test.test.__name__}.\",\n f\"If this is not a custom test implementation: {GITHUB_SUGGESTION}\",\n ],\n )\n anta_log_exception(e, message, logger)\n return coros\n"},{"location":"api/runner/#anta.runner.log_cache_statistics","title":"log_cache_statistics","text":"log_cache_statistics(devices: list[AntaDevice]) -> None\n Log cache statistics for each device in the inventory. Parameters: Name Type Description Default devices list[AntaDevice] List of devices in the inventory. required Source code in anta/runner.py def log_cache_statistics(devices: list[AntaDevice]) -> None:\n \"\"\"Log cache statistics for each device in the inventory.\n\n Parameters\n ----------\n devices\n List of devices in the inventory.\n \"\"\"\n for device in devices:\n if device.cache_statistics is not None:\n msg = (\n f\"Cache statistics for '{device.name}': \"\n f\"{device.cache_statistics['cache_hits']} hits / {device.cache_statistics['total_commands_sent']} \"\n f\"command(s) ({device.cache_statistics['cache_hit_ratio']})\"\n )\n logger.info(msg)\n else:\n logger.info(\"Caching is not enabled on %s\", device.name)\n"},{"location":"api/runner/#anta.runner.main","title":"main async","text":"main(\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False\n) -> None\n Run ANTA. Use this as an entrypoint to the test framework in your script. ResultManager object gets updated with the test results. Parameters: Name Type Description Default manager ResultManager ResultManager object to populate with the test results. required inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required devices set[str] | None Devices on which to run tests. None means all devices. These may come from the --device / -d CLI option in NRFU. None tests set[str] | None Tests to run against devices. None means all tests. These may come from the --test / -t CLI option in NRFU. None tags set[str] | None Tags to filter devices from the inventory. These may come from the --tags CLI option in NRFU. None established_only bool Include only established device(s). True dry_run bool Build the list of coroutine to run and stop before test execution. False Source code in anta/runner.py @cprofile()\nasync def main( # noqa: PLR0913\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False,\n) -> None:\n \"\"\"Run ANTA.\n\n Use this as an entrypoint to the test framework in your script.\n ResultManager object gets updated with the test results.\n\n Parameters\n ----------\n manager\n ResultManager object to populate with the test results.\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n devices\n Devices on which to run tests. None means all devices. These may come from the `--device / -d` CLI option in NRFU.\n tests\n Tests to run against devices. None means all tests. These may come from the `--test / -t` CLI option in NRFU.\n tags\n Tags to filter devices from the inventory. These may come from the `--tags` CLI option in NRFU.\n established_only\n Include only established device(s).\n dry_run\n Build the list of coroutine to run and stop before test execution.\n \"\"\"\n # Adjust the maximum number of open file descriptors for the ANTA process\n limits = adjust_rlimit_nofile()\n\n if not catalog.tests:\n logger.info(\"The list of tests is empty, exiting\")\n return\n\n with Catchtime(logger=logger, message=\"Preparing ANTA NRFU Run\"):\n # Setup the inventory\n selected_inventory = inventory if dry_run else await setup_inventory(inventory, tags, devices, established_only=established_only)\n if selected_inventory is None:\n return\n\n with Catchtime(logger=logger, message=\"Preparing the tests\"):\n selected_tests = prepare_tests(selected_inventory, catalog, tests, tags)\n if selected_tests is None:\n return\n final_tests_count = sum(len(tests) for tests in selected_tests.values())\n\n run_info = (\n \"--- ANTA NRFU Run Information ---\\n\"\n f\"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\\n\"\n f\"Total number of selected tests: {final_tests_count}\\n\"\n f\"Maximum number of open file descriptors for the current ANTA process: {limits[0]}\\n\"\n \"---------------------------------\"\n )\n\n logger.info(run_info)\n\n if final_tests_count > limits[0]:\n logger.warning(\n \"The number of concurrent tests is higher than the open file descriptors limit for this ANTA process.\\n\"\n \"Errors may occur while running the tests.\\n\"\n \"Please consult the ANTA FAQ.\"\n )\n\n coroutines = get_coroutines(selected_tests, manager)\n\n if dry_run:\n logger.info(\"Dry-run mode, exiting before running the tests.\")\n for coro in coroutines:\n coro.close()\n return\n\n if AntaTest.progress is not None:\n AntaTest.nrfu_task = AntaTest.progress.add_task(\"Running NRFU Tests...\", total=len(coroutines))\n\n with Catchtime(logger=logger, message=\"Running ANTA tests\"):\n await asyncio.gather(*coroutines)\n\n log_cache_statistics(selected_inventory.devices)\n"},{"location":"api/runner/#anta.runner.prepare_tests","title":"prepare_tests","text":"prepare_tests(\n inventory: AntaInventory,\n catalog: AntaCatalog,\n tests: set[str] | None,\n tags: set[str] | None,\n) -> (\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n)\n Prepare the tests to run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required tests set[str] | None Tests to run against devices. None means all tests. required tags set[str] | None Tags to filter devices from the inventory. required Returns: Type Description defaultdict[AntaDevice, set[AntaTestDefinition]] | None A mapping of devices to the tests to run or None if there are no tests to run. Source code in anta/runner.py def prepare_tests(\n inventory: AntaInventory, catalog: AntaCatalog, tests: set[str] | None, tags: set[str] | None\n) -> defaultdict[AntaDevice, set[AntaTestDefinition]] | None:\n \"\"\"Prepare the tests to run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n tests\n Tests to run against devices. None means all tests.\n tags\n Tags to filter devices from the inventory.\n\n Returns\n -------\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n A mapping of devices to the tests to run or None if there are no tests to run.\n \"\"\"\n # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests\n catalog.build_indexes(filtered_tests=tests)\n\n # Using a set to avoid inserting duplicate tests\n device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)\n\n total_test_count = 0\n\n # Create the device to tests mapping from the tags\n for device in inventory.devices:\n if tags:\n # If there are CLI tags, execute tests with matching tags for this device\n if not (matching_tags := tags.intersection(device.tags)):\n # The device does not have any selected tag, skipping\n continue\n device_to_tests[device].update(catalog.get_tests_by_tags(matching_tags))\n else:\n # If there is no CLI tags, execute all tests that do not have any tags\n device_to_tests[device].update(catalog.tag_to_tests[None])\n\n # Then add the tests with matching tags from device tags\n device_to_tests[device].update(catalog.get_tests_by_tags(device.tags))\n\n total_test_count += len(device_to_tests[device])\n\n if total_test_count == 0:\n msg = (\n f\"There are no tests{f' matching the tags {tags} ' if tags else ' '}to run in the current test catalog and device inventory, please verify your inputs.\"\n )\n logger.warning(msg)\n return None\n\n return device_to_tests\n"},{"location":"api/runner/#anta.runner.setup_inventory","title":"setup_inventory async","text":"setup_inventory(\n inventory: AntaInventory,\n tags: set[str] | None,\n devices: set[str] | None,\n *,\n established_only: bool\n) -> AntaInventory | None\n Set up the inventory for the ANTA run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required tags set[str] | None Tags to filter devices from the inventory. required devices set[str] | None Devices on which to run tests. None means all devices. required established_only bool If True use return only devices where a connection is established. required Returns: Type Description AntaInventory | None The filtered inventory or None if there are no devices to run tests on. Source code in anta/runner.py async def setup_inventory(inventory: AntaInventory, tags: set[str] | None, devices: set[str] | None, *, established_only: bool) -> AntaInventory | None:\n \"\"\"Set up the inventory for the ANTA run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n tags\n Tags to filter devices from the inventory.\n devices\n Devices on which to run tests. None means all devices.\n established_only\n If True use return only devices where a connection is established.\n\n Returns\n -------\n AntaInventory | None\n The filtered inventory or None if there are no devices to run tests on.\n \"\"\"\n if len(inventory) == 0:\n logger.info(\"The inventory is empty, exiting\")\n return None\n\n # Filter the inventory based on the CLI provided tags and devices if any\n selected_inventory = inventory.get_inventory(tags=tags, devices=devices) if tags or devices else inventory\n\n with Catchtime(logger=logger, message=\"Connecting to devices\"):\n # Connect to the devices\n await selected_inventory.connect_inventory()\n\n # Remove devices that are unreachable\n selected_inventory = selected_inventory.get_inventory(established_only=established_only)\n\n # If there are no devices in the inventory after filtering, exit\n if not selected_inventory.devices:\n msg = f'No reachable device {f\"matching the tags {tags} \" if tags else \"\"}was found.{f\" Selected devices: {devices} \" if devices is not None else \"\"}'\n logger.warning(msg)\n return None\n\n return selected_inventory\n"},{"location":"api/test.cvx/","title":"Test.cvx","text":""},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX","title":"VerifyManagementCVX","text":"Verifies the management CVX global status. Expected Results Success: The test will pass if the management CVX global status matches the expected status. Failure: The test will fail if the management CVX global status does not match the expected status. Examples anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n Source code in anta/tests/cvx.py class VerifyManagementCVX(AntaTest):\n \"\"\"Verifies the management CVX global status.\n\n Expected Results\n ----------------\n * Success: The test will pass if the management CVX global status matches the expected status.\n * Failure: The test will fail if the management CVX global status does not match the expected status.\n\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyManagementCVX test.\"\"\"\n\n enabled: bool\n \"\"\"Whether management CVX must be enabled (True) or disabled (False).\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyManagementCVX.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n cluster_status = command_output[\"clusterStatus\"]\n if (cluster_state := cluster_status.get(\"enabled\")) != self.inputs.enabled:\n self.result.is_failure(f\"Management CVX status is not valid: {cluster_state}\")\n"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX-attributes","title":"Inputs","text":"Name Type Description Default enabled bool Whether management CVX must be enabled (True) or disabled (False). -"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyMcsClientMounts","title":"VerifyMcsClientMounts","text":"Verify if all MCS client mounts are in mountStateMountComplete. Expected Results Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete. Failure: The test will fail even if one switch\u2019s MCS client mount status is not mountStateMountComplete. Examples anta.tests.cvx:\n- VerifyMcsClientMounts:\n Source code in anta/tests/cvx.py class VerifyMcsClientMounts(AntaTest):\n \"\"\"Verify if all MCS client mounts are in mountStateMountComplete.\n\n Expected Results\n ----------------\n * Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete.\n * Failure: The test will fail even if one switch's MCS client mount status is not mountStateMountComplete.\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyMcsClientMounts:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx mounts\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyMcsClientMounts.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n mount_states = command_output[\"mountStates\"]\n mcs_mount_state_detected = False\n for mount_state in mount_states:\n if not mount_state[\"type\"].startswith(\"Mcs\"):\n continue\n mcs_mount_state_detected = True\n if (state := mount_state[\"state\"]) != \"mountStateMountComplete\":\n self.result.is_failure(f\"MCS Client mount states are not valid: {state}\")\n\n if not mcs_mount_state_detected:\n self.result.is_failure(\"MCS Client mount states are not present\")\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authentication\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authorization\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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.avt/","title":"Adaptive Virtual Topology","text":""},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTPathHealth","title":"VerifyAVTPathHealth","text":"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs. Expected Results Success: The test will pass if all AVT paths for all VRFs are active and valid. Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid. Examples anta.tests.avt:\n - VerifyAVTPathHealth:\n Source code in anta/tests/avt.py class VerifyAVTPathHealth(AntaTest):\n \"\"\"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for all VRFs are active and valid.\n * Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTPathHealth:\n ```\n \"\"\"\n\n description = \"Verifies the status of all AVT paths for all VRFs.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTPathHealth.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output.get(\"vrfs\", {})\n\n # Check if AVT is configured\n if not command_output:\n self.result.is_failure(\"Adaptive virtual topology paths are not configured.\")\n return\n\n # Iterate over each VRF\n for vrf, vrf_data in command_output.items():\n # Iterate over each AVT path\n for profile, avt_path in vrf_data.get(\"avts\", {}).items():\n for path, flags in avt_path.get(\"avtPaths\", {}).items():\n # Get the status of the AVT path\n valid = flags[\"flags\"][\"valid\"]\n active = flags[\"flags\"][\"active\"]\n\n # Check the status of the AVT path\n if not valid and not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid and not active.\")\n elif not valid:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid.\")\n elif not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is not active.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole","title":"VerifyAVTRole","text":"Verifies the Adaptive Virtual Topology (AVT) role of a device. Expected Results Success: The test will pass if the AVT role of the device matches the expected role. Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role. Examples anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n Source code in anta/tests/avt.py class VerifyAVTRole(AntaTest):\n \"\"\"Verifies the Adaptive Virtual Topology (AVT) role of a device.\n\n Expected Results\n ----------------\n * Success: The test will pass if the AVT role of the device matches the expected role.\n * Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n ```\n \"\"\"\n\n description = \"Verifies the AVT role of a device.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTRole test.\"\"\"\n\n role: str\n \"\"\"Expected AVT role of the device.\"\"\"\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTRole.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output\n\n # Check if the AVT role matches the expected role\n if self.inputs.role != command_output.get(\"role\"):\n self.result.is_failure(f\"Expected AVT role as `{self.inputs.role}`, but found `{command_output.get('role')}` instead.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole-attributes","title":"Inputs","text":"Name Type Description Default role str Expected AVT role of the device. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath","title":"VerifyAVTSpecificPath","text":"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF. Expected Results Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided. If multiple paths are configured, the test will pass only if all the paths are valid and active. Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid, or does not match the specified type. Examples anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n Source code in anta/tests/avt.py class VerifyAVTSpecificPath(AntaTest):\n \"\"\"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided.\n If multiple paths are configured, the test will pass only if all the paths are valid and active.\n * Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid,\n or does not match the specified type.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n ```\n \"\"\"\n\n description = \"Verifies the status and type of an AVT path for a specified VRF.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show adaptive-virtual-topology path vrf {vrf} avt {avt_name} destination {destination}\")\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTSpecificPath test.\"\"\"\n\n avt_paths: list[AVTPaths]\n \"\"\"List of AVT paths to verify.\"\"\"\n\n class AVTPaths(BaseModel):\n \"\"\"Model for the details of AVT paths.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"The VRF for the AVT path. Defaults to 'default' if not provided.\"\"\"\n avt_name: str\n \"\"\"Name of the adaptive virtual topology.\"\"\"\n destination: IPv4Address\n \"\"\"The IPv4 address of the AVT peer.\"\"\"\n next_hop: IPv4Address\n \"\"\"The IPv4 address of the next hop for the AVT peer.\"\"\"\n path_type: str | None = None\n \"\"\"The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input AVT path/peer.\"\"\"\n return [template.render(vrf=path.vrf, avt_name=path.avt_name, destination=path.destination) for path in self.inputs.avt_paths]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTSpecificPath.\"\"\"\n # Assume the test is successful until a failure is detected\n self.result.is_success()\n\n # Process each command in the instance\n for command, input_avt in zip(self.instance_commands, self.inputs.avt_paths):\n # Extract the command output and parameters\n vrf = command.params.vrf\n avt_name = command.params.avt_name\n peer = str(command.params.destination)\n\n command_output = command.json_output.get(\"vrfs\", {})\n\n # If no AVT is configured, mark the test as failed and skip to the next command\n if not command_output:\n self.result.is_failure(f\"AVT configuration for peer '{peer}' under topology '{avt_name}' in VRF '{vrf}' is not found.\")\n continue\n\n # Extract the AVT paths\n avt_paths = get_value(command_output, f\"{vrf}.avts.{avt_name}.avtPaths\")\n next_hop, input_path_type = str(input_avt.next_hop), input_avt.path_type\n\n nexthop_path_found = path_type_found = False\n\n # Check each AVT path\n for path, path_data in avt_paths.items():\n # If the path does not match the expected next hop, skip to the next path\n if path_data.get(\"nexthopAddr\") != next_hop:\n continue\n\n nexthop_path_found = True\n path_type = \"direct\" if get_value(path_data, \"flags.directPath\") else \"multihop\"\n\n # If the path type does not match the expected path type, skip to the next path\n if input_path_type and path_type != input_path_type:\n continue\n\n path_type_found = True\n valid = get_value(path_data, \"flags.valid\")\n active = get_value(path_data, \"flags.active\")\n\n # Check the path status and type against the expected values\n if not all([valid, active]):\n failure_reasons = []\n if not get_value(path_data, \"flags.active\"):\n failure_reasons.append(\"inactive\")\n if not get_value(path_data, \"flags.valid\"):\n failure_reasons.append(\"invalid\")\n # Construct the failure message prefix\n failed_log = f\"AVT path '{path}' for topology '{avt_name}' in VRF '{vrf}'\"\n self.result.is_failure(f\"{failed_log} is {', '.join(failure_reasons)}.\")\n\n # If no matching next hop or path type was found, mark the test as failed\n if not nexthop_path_found or not path_type_found:\n self.result.is_failure(\n f\"No '{input_path_type}' path found with next-hop address '{next_hop}' for AVT peer '{peer}' under topology '{avt_name}' in VRF '{vrf}'.\"\n )\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"Inputs","text":"Name Type Description Default avt_paths list[AVTPaths] List of AVT paths to verify. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"AVTPaths","text":"Name Type Description Default vrf str The VRF for the AVT path. Defaults to 'default' if not provided. 'default' avt_name str Name of the adaptive virtual topology. - destination IPv4Address The IPv4 address of the AVT peer. - next_hop IPv4Address The IPv4 address of the next hop for the AVT peer. - path_type str | None The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered. None"},{"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 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\", revision=1),\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 categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\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 tx_interval = bfd_peers.tx_interval\n rx_interval = bfd_peers.rx_interval\n multiplier = bfd_peers.multiplier\n\n # Check if BFD peer configured\n bfd_output = get_value(\n self.instance_commands[0].json_output,\n f\"vrfs..{vrf}..ipv4Neighbors..{peer}..peerStats..\",\n separator=\"..\",\n )\n if not bfd_output:\n failures[peer] = {vrf: \"Not Configured\"}\n continue\n\n # Convert interval timer(s) into milliseconds to be consistent with the inputs.\n bfd_details = bfd_output.get(\"peerStatsDetail\", {})\n op_tx_interval = bfd_details.get(\"operTxInterval\") // 1000\n op_rx_interval = bfd_details.get(\"operRxInterval\") // 1000\n detect_multiplier = bfd_details.get(\"detectMult\")\n intervals_ok = op_tx_interval == tx_interval and op_rx_interval == rx_interval and detect_multiplier == multiplier\n\n # Check timers of BFD peer\n if not intervals_ok:\n failures[peer] = {\n vrf: {\n \"tx_interval\": op_tx_interval,\n \"rx_interval\": op_rx_interval,\n \"multiplier\": detect_multiplier,\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.VerifyBFDPeersRegProtocols","title":"VerifyBFDPeersRegProtocols","text":"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered. Expected Results Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s). Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s). Examples anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n Source code in anta/tests/bfd.py class VerifyBFDPeersRegProtocols(AntaTest):\n \"\"\"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered.\n\n Expected Results\n ----------------\n * Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s).\n * Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s).\n\n Examples\n --------\n ```yaml\n anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBFDPeersRegProtocols 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 protocols: list[BfdProtocol]\n \"\"\"List of protocols to be verified.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBFDPeersRegProtocols.\"\"\"\n # Initialize failure messages\n failures: dict[Any, Any] = {}\n\n # Iterating over BFD peers, extract the parameters and command output\n for bfd_peer in self.inputs.bfd_peers:\n peer = str(bfd_peer.peer_address)\n vrf = bfd_peer.vrf\n protocols = bfd_peer.protocols\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 registered protocols\n difference = set(protocols) - set(get_value(bfd_output, \"peerStatsDetail.apps\"))\n\n if difference:\n failures[peer] = {vrf: sorted(difference)}\n\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BFD peers are not configured or have non-registered protocol(s):\\n{failures}\")\n"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-attributes","title":"Inputs","text":"Name Type Description Default bfd_peers list[BFDPeer] List of IPv4 BFD peers. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-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' protocols list[BfdProtocol] List of protocols to be verified. -"},{"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 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\", revision=1)]\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 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.VerifyRunningConfigLines","title":"VerifyRunningConfigLines","text":"Verifies the given regular expression patterns are present in the running-config. Warning Since this uses regular expression searches on the whole running-config, it can drastically impact performance and should only be used if no other test is available. If possible, try using another ANTA test that is more specific. Expected Results Success: The test will pass if all the patterns are found in the running-config. Failure: The test will fail if any of the patterns are NOT found in the running-config. Examples anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n Source code in anta/tests/configuration.py class VerifyRunningConfigLines(AntaTest):\n \"\"\"Verifies the given regular expression patterns are present in the running-config.\n\n !!! warning\n Since this uses regular expression searches on the whole running-config, it can\n drastically impact performance and should only be used if no other test is available.\n\n If possible, try using another ANTA test that is more specific.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the patterns are found in the running-config.\n * Failure: The test will fail if any of the patterns are NOT found in the running-config.\n\n Examples\n --------\n ```yaml\n anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n ```\n \"\"\"\n\n description = \"Search the Running-Config for the given RegEx patterns.\"\n categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show running-config\", ofmt=\"text\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyRunningConfigLines test.\"\"\"\n\n regex_patterns: list[RegexString]\n \"\"\"List of regular expressions.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRunningConfigLines.\"\"\"\n failure_msgs = []\n command_output = self.instance_commands[0].text_output\n\n for pattern in self.inputs.regex_patterns:\n re_search = re.compile(pattern, flags=re.MULTILINE)\n\n if not re_search.search(command_output):\n failure_msgs.append(f\"'{pattern}'\")\n\n if not failure_msgs:\n self.result.is_success()\n else:\n self.result.is_failure(\"Following patterns were not found: \" + \",\".join(failure_msgs))\n"},{"location":"api/tests.configuration/#anta.tests.configuration.VerifyRunningConfigLines-attributes","title":"Inputs","text":"Name Type Description Default regex_patterns list[RegexString] List of regular expressions. -"},{"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 categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show zerotouch\", revision=1)]\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/#tests","title":"Tests","text":""},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors","title":"VerifyLLDPNeighbors","text":"Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. This test performs the following checks for each specified LLDP neighbor: Confirming matching ports on both local and neighboring devices. Ensuring compatibility of device names and interface identifiers. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored. Expected Results Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table. The system name or port of the LLDP neighbor does not match the expected 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 the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.\n\n This test performs the following checks for each specified LLDP neighbor:\n\n 1. Confirming matching ports on both local and neighboring devices.\n 2. Ensuring compatibility of device names and interface identifiers.\n 3. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table.\n - The system name or port of the LLDP neighbor does not match the expected 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 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\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLLDPNeighbors test.\"\"\"\n\n neighbors: list[LLDPNeighbor]\n \"\"\"List of LLDP neighbors.\"\"\"\n Neighbor: ClassVar[type[Neighbor]] = Neighbor\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLLDPNeighbors.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output[\"lldpNeighbors\"]\n for neighbor in self.inputs.neighbors:\n if neighbor.port not in output:\n self.result.is_failure(f\"{neighbor} - Port not found\")\n continue\n\n if len(lldp_neighbor_info := output[neighbor.port][\"lldpNeighborInfo\"]) == 0:\n self.result.is_failure(f\"{neighbor} - No LLDP neighbors\")\n continue\n\n # Check if the system name and neighbor port matches\n match_found = any(\n info[\"systemName\"] == neighbor.neighbor_device and info[\"neighborInterfaceInfo\"][\"interfaceId_v2\"] == neighbor.neighbor_port\n for info in lldp_neighbor_info\n )\n if not match_found:\n failure_msg = [f\"{info['systemName']}/{info['neighborInterfaceInfo']['interfaceId_v2']}\" for info in lldp_neighbor_info]\n self.result.is_failure(f\"{neighbor} - Wrong LLDP neighbors: {', '.join(failure_msg)}\")\n"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors-attributes","title":"Inputs","text":"Name Type Description Default neighbors list[LLDPNeighbor] List of LLDP neighbors. -"},{"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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"connectivity\"]\n # Template uses '{size}{df_bit}' without space since df_bit includes leading space when enabled\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"ping vrf {vrf} {destination} source {source} size {size}{df_bit} repeat {repeat}\", revision=1)\n ]\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 Host: ClassVar[type[Host]] = Host\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each host in the input list.\"\"\"\n commands = []\n for host in self.inputs.hosts:\n # df_bit includes leading space when enabled, empty string when disabled\n df_bit = \" df-bit\" if host.df_bit else \"\"\n command = template.render(destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat, size=host.size, df_bit=df_bit)\n commands.append(command)\n return commands\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyReachability.\"\"\"\n self.result.is_success()\n\n for command, host in zip(self.instance_commands, self.inputs.hosts):\n if f\"{host.repeat} received\" not in command.json_output[\"messages\"][0]:\n self.result.is_failure(f\"{host} - Unreachable\")\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/#input-models","title":"Input models","text":""},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Host","title":"Host","text":"Model for a remote host to ping. 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 size int Specify datagram size. Defaults to 100. 100 df_bit bool Enable do not fragment bit in IP header. Defaults to False. False Source code in anta/input_models/connectivity.py class Host(BaseModel):\n \"\"\"Model for a remote host to ping.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\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 size: int = 100\n \"\"\"Specify datagram size. Defaults to 100.\"\"\"\n df_bit: bool = False\n \"\"\"Enable do not fragment bit in IP header. Defaults to False.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the Host for reporting.\n\n Examples\n --------\n Host 10.1.1.1 (src: 10.2.2.2, vrf: mgmt, size: 100B, repeat: 2)\n\n \"\"\"\n df_status = \", df-bit: enabled\" if self.df_bit else \"\"\n return f\"Host {self.destination} (src: {self.source}, vrf: {self.vrf}, size: {self.size}B, repeat: {self.repeat}{df_status})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.LLDPNeighbor","title":"LLDPNeighbor","text":"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information. Name Type Description Default port Interface The LLDP port for the local device. - neighbor_device str The system name of the LLDP neighbor device. - neighbor_port Interface The LLDP port on the neighboring device. - Source code in anta/input_models/connectivity.py class LLDPNeighbor(BaseModel):\n \"\"\"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n port: Interface\n \"\"\"The LLDP port for the local device.\"\"\"\n neighbor_device: str\n \"\"\"The system name of the LLDP neighbor device.\"\"\"\n neighbor_port: Interface\n \"\"\"The LLDP port on the neighboring device.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the LLDPNeighbor for reporting.\n\n Examples\n --------\n Port Ethernet1 (Neighbor: DC1-SPINE2, Neighbor Port: Ethernet2)\n\n \"\"\"\n return f\"Port {self.port} (Neighbor: {self.neighbor_device}, Neighbor Port: {self.neighbor_port})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor","title":"Neighbor","text":"Alias for the LLDPNeighbor model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the LLDPNeighbor model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/connectivity.py class Neighbor(LLDPNeighbor): # pragma: no cover\n \"\"\"Alias for the LLDPNeighbor model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the LLDPNeighbor model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor.__init__","title":"__init__","text":"__init__(**data: Any) -> None\n Source code in anta/input_models/connectivity.py def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"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 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\", revision=1)]\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 break\n else:\n self.result.is_failure(\"Aboot component not found\")\n return\n\n self.result.is_success()\n incorrect_aboot_version = (\n (aboot_version.startswith(\"4.0.\") and int(aboot_version.split(\".\")[2]) < 7)\n or (aboot_version.startswith(\"4.1.\") and int(aboot_version.split(\".\")[2]) < 1)\n or (\n (aboot_version.startswith(\"6.0.\") and int(aboot_version.split(\".\")[2]) < 9)\n or (aboot_version.startswith(\"6.1.\") 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 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\", revision=1)]\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_failure(\"Error in running test - Component FixedSystemvrm1 not found in 'show version'\")\n"},{"location":"api/tests.flow_tracking/","title":"Flow Tracking","text":""},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus","title":"VerifyHardwareFlowTrackerStatus","text":"Verifies if hardware flow tracking is running and an input tracker is active. This test optionally verifies the tracker interval/timeout and exporter configuration. Expected Results Success: The test will pass if hardware flow tracking is running and an input tracker is active. Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active, or the tracker interval/timeout and exporter configuration does not match the expected values. Examples anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n Source code in anta/tests/flow_tracking.py class VerifyHardwareFlowTrackerStatus(AntaTest):\n \"\"\"Verifies if hardware flow tracking is running and an input tracker is active.\n\n This test optionally verifies the tracker interval/timeout and exporter configuration.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware flow tracking is running and an input tracker is active.\n * Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active,\n or the tracker interval/timeout and exporter configuration does not match the expected values.\n\n Examples\n --------\n ```yaml\n anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n ```\n \"\"\"\n\n description = (\n \"Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration.\"\n )\n categories: ClassVar[list[str]] = [\"flow tracking\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show flow tracking hardware tracker {name}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyHardwareFlowTrackerStatus test.\"\"\"\n\n trackers: list[FlowTracker]\n \"\"\"List of flow trackers to verify.\"\"\"\n\n class FlowTracker(BaseModel):\n \"\"\"Detail of a flow tracker.\"\"\"\n\n name: str\n \"\"\"Name of the flow tracker.\"\"\"\n\n record_export: RecordExport | None = None\n \"\"\"Record export configuration for the flow tracker.\"\"\"\n\n exporters: list[Exporter] | None = None\n \"\"\"List of exporters for the flow tracker.\"\"\"\n\n class RecordExport(BaseModel):\n \"\"\"Record export configuration.\"\"\"\n\n on_inactive_timeout: int\n \"\"\"Timeout in milliseconds for exporting records when inactive.\"\"\"\n\n on_interval: int\n \"\"\"Interval in milliseconds for exporting records.\"\"\"\n\n class Exporter(BaseModel):\n \"\"\"Detail of an exporter.\"\"\"\n\n name: str\n \"\"\"Name of the exporter.\"\"\"\n\n local_interface: str\n \"\"\"Local interface used by the exporter.\"\"\"\n\n template_interval: int\n \"\"\"Template interval in milliseconds for the exporter.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each hardware tracker.\"\"\"\n return [template.render(name=tracker.name) for tracker in self.inputs.trackers]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareFlowTrackerStatus.\"\"\"\n self.result.is_success()\n for command, tracker_input in zip(self.instance_commands, self.inputs.trackers):\n hardware_tracker_name = command.params.name\n record_export = tracker_input.record_export.model_dump() if tracker_input.record_export else None\n exporters = [exporter.model_dump() for exporter in tracker_input.exporters] if tracker_input.exporters else None\n command_output = command.json_output\n\n # Check if hardware flow tracking is configured\n if not command_output.get(\"running\"):\n self.result.is_failure(\"Hardware flow tracking is not running.\")\n return\n\n # Check if the input hardware tracker is configured\n tracker_info = command_output[\"trackers\"].get(hardware_tracker_name)\n if not tracker_info:\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not configured.\")\n continue\n\n # Check if the input hardware tracker is active\n if not tracker_info.get(\"active\"):\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not active.\")\n continue\n\n # Check the input hardware tracker timeouts\n failure_msg = \"\"\n if record_export:\n record_export_failure = validate_record_export(record_export, tracker_info)\n if record_export_failure:\n failure_msg += record_export_failure\n\n # Check the input hardware tracker exporters' configuration\n if exporters:\n exporters_failure = validate_exporters(exporters, tracker_info)\n if exporters_failure:\n failure_msg += exporters_failure\n\n if failure_msg:\n self.result.is_failure(f\"{hardware_tracker_name}: {failure_msg}\\n\")\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Inputs","text":"Name Type Description Default trackers list[FlowTracker] List of flow trackers to verify. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"FlowTracker","text":"Name Type Description Default name str Name of the flow tracker. - record_export RecordExport | None Record export configuration for the flow tracker. None exporters list[Exporter] | None List of exporters for the flow tracker. None"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"RecordExport","text":"Name Type Description Default on_inactive_timeout int Timeout in milliseconds for exporting records when inactive. - on_interval int Interval in milliseconds for exporting records. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Exporter","text":"Name Type Description Default name str Name of the exporter. - local_interface str Local interface used by the exporter. - template_interval int Template interval in milliseconds for the exporter. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_exporters","title":"validate_exporters","text":"validate_exporters(\n exporters: list[dict[str, str]],\n tracker_info: dict[str, str],\n) -> str\n Validate the exporter configurations against the tracker info. Parameters: Name Type Description Default exporters list[dict[str, str]] The list of expected exporter configurations. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str Failure message if any exporter configuration does not match. Source code in anta/tests/flow_tracking.py def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the exporter configurations against the tracker info.\n\n Parameters\n ----------\n exporters\n The list of expected exporter configurations.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n Failure message if any exporter configuration does not match.\n \"\"\"\n failed_log = \"\"\n for exporter in exporters:\n exporter_name = exporter[\"name\"]\n actual_exporter_info = tracker_info[\"exporters\"].get(exporter_name)\n if not actual_exporter_info:\n failed_log += f\"\\nExporter `{exporter_name}` is not configured.\"\n continue\n\n expected_exporter_data = {\"local interface\": exporter[\"local_interface\"], \"template interval\": exporter[\"template_interval\"]}\n actual_exporter_data = {\"local interface\": actual_exporter_info[\"localIntf\"], \"template interval\": actual_exporter_info[\"templateInterval\"]}\n\n if expected_exporter_data != actual_exporter_data:\n failed_msg = get_failed_logs(expected_exporter_data, actual_exporter_data)\n failed_log += f\"\\nExporter `{exporter_name}`: {failed_msg}\"\n return failed_log\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_record_export","title":"validate_record_export","text":"validate_record_export(\n record_export: dict[str, str],\n tracker_info: dict[str, str],\n) -> str\n Validate the record export configuration against the tracker info. Parameters: Name Type Description Default record_export dict[str, str] The expected record export configuration. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str A failure message if the record export configuration does not match, otherwise blank string. Source code in anta/tests/flow_tracking.py def validate_record_export(record_export: dict[str, str], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the record export configuration against the tracker info.\n\n Parameters\n ----------\n record_export\n The expected record export configuration.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n A failure message if the record export configuration does not match, otherwise blank string.\n \"\"\"\n failed_log = \"\"\n actual_export = {\"inactive timeout\": tracker_info.get(\"inactiveTimeout\"), \"interval\": tracker_info.get(\"activeInterval\")}\n expected_export = {\"inactive timeout\": record_export.get(\"on_inactive_timeout\"), \"interval\": record_export.get(\"on_interval\")}\n if actual_export != expected_export:\n failed_log = get_failed_logs(expected_export, actual_export)\n return failed_log\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 description = \"Verifies if a GreenT policy other than the default is created.\"\n categories: ClassVar[list[str]] = [\"greent\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show monitor telemetry postcard policy profile\", revision=1)]\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 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\", revision=1)]\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 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\", revision=1)]\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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment power\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment cooling\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show inventory\", revision=2)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature transceiver\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=2)]\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 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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show lacp counters all-ports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters discards\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces status\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters errors\", revision=1)]\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 description = \"Verifies the interface IPv4 addresses.\"\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip interface {interface}\", revision=2)]\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 [template.render(interface=interface.name) for interface in self.inputs.interfaces]\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 for interface in self.inputs.interfaces:\n if interface.name == intf:\n input_interface_detail = interface\n break\n else:\n self.result.is_failure(f\"Could not find `{intf}` in the input interfaces. {GITHUB_SUGGESTION}\")\n continue\n\n input_primary_ip = str(input_interface_detail.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 (param_secondary_ips := input_interface_detail.secondary_ips) is not None:\n input_secondary_ips = sorted([str(network) for network in param_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. This test has been implemented for full-duplex interfaces only. 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. Error: The test will error out if the device has at least one non full-duplex interface. 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 This test has been implemented for full-duplex interfaces only.\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 * Error: The test will error out if the device has at least one non full-duplex interface.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfaceUtilization:\n threshold: 70.0\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show interfaces counters rates\", revision=1),\n AntaCommand(command=\"show interfaces\", revision=1),\n ]\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 # The utilization logic has been implemented for full-duplex interfaces only\n if ((duplex := (interface := interfaces[\"interfaces\"][intf]).get(\"duplex\", None)) is not None and duplex != duplex_full) or (\n (members := interface.get(\"memberInterfaces\", None)) is not None and any(stats[\"duplex\"] != duplex_full for stats in members.values())\n ):\n self.result.is_failure(f\"Interface {intf} or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented.\")\n return\n\n if (bandwidth := interfaces[\"interfaces\"][intf][\"bandwidth\"]) == 0:\n self.logger.debug(\"Interface %s has been ignored due to null bandwidth value\", intf)\n continue\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.VerifyInterfacesSpeed","title":"VerifyInterfacesSpeed","text":"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces. If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input. If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input. Expected Results Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex. Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex. Examples anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n Source code in anta/tests/interfaces.py class VerifyInterfacesSpeed(AntaTest):\n \"\"\"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces.\n\n - If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input.\n - If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input.\n\n Expected Results\n ----------------\n * Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex.\n * Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n class Input(AntaTest.Input):\n \"\"\"Inputs for the VerifyInterfacesSpeed test.\"\"\"\n\n interfaces: list[InterfaceDetail]\n \"\"\"List of interfaces to be tested\"\"\"\n\n class InterfaceDetail(BaseModel):\n \"\"\"Detail of an interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"The name of the interface.\"\"\"\n auto: bool\n \"\"\"The auto-negotiation status of the interface.\"\"\"\n speed: float = Field(ge=1, le=1000)\n \"\"\"The speed of the interface in Gigabits per second. Valid range is 1 to 1000.\"\"\"\n lanes: None | int = Field(None, ge=1, le=8)\n \"\"\"The number of lanes in the interface. Valid range is 1 to 8. This field is optional.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesSpeed.\"\"\"\n self.result.is_success()\n command_output = self.instance_commands[0].json_output\n\n # Iterate over all the interfaces\n for interface in self.inputs.interfaces:\n intf = interface.name\n\n # Check if interface exists\n if not (interface_output := get_value(command_output, f\"interfaces.{intf}\")):\n self.result.is_failure(f\"Interface `{intf}` is not found.\")\n continue\n\n auto_negotiation = interface_output.get(\"autoNegotiate\")\n actual_lanes = interface_output.get(\"lanes\")\n\n # Collecting actual interface details\n actual_interface_output = {\n \"auto negotiation\": auto_negotiation if interface.auto is True else None,\n \"duplex mode\": interface_output.get(\"duplex\"),\n \"speed\": interface_output.get(\"bandwidth\"),\n \"lanes\": actual_lanes if interface.lanes is not None else None,\n }\n\n # Forming expected interface details\n expected_interface_output = {\n \"auto negotiation\": \"success\" if interface.auto is True else None,\n \"duplex mode\": \"duplexFull\",\n \"speed\": interface.speed * BPS_GBPS_CONVERSIONS,\n \"lanes\": interface.lanes,\n }\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n for output in [actual_interface_output, expected_interface_output]:\n # Convert speed to Gbps for readability\n if output[\"speed\"] is not None:\n output[\"speed\"] = f\"{custom_division(output['speed'], BPS_GBPS_CONVERSIONS)}Gbps\"\n failed_log = get_failed_logs(expected_interface_output, actual_interface_output)\n self.result.is_failure(f\"For interface {intf}:{failed_log}\\n\")\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceDetail] List of interfaces to be tested -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"InterfaceDetail","text":"Name Type Description Default name EthernetInterface The name of the interface. - auto bool The auto-negotiation status of the interface. - speed float The speed of the interface in Gigabits per second. Valid range is 1 to 1000. Field(ge=1, le=1000) lanes None | int The number of lanes in the interface. Valid range is 1 to 8. This field is optional. Field(None, ge=1, le=8)"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus","title":"VerifyInterfacesStatus","text":"Verifies the operational states of specified interfaces to ensure they match expected configurations. This test performs the following checks for each specified interface: If line_protocol_status is defined, both status and line_protocol_status are verified for the specified interface. If line_protocol_status is not provided but the status is \u201cup\u201d, it is assumed that both the status and line protocol should be \u201cup\u201d. If the interface status is not \u201cup\u201d, only the interface\u2019s status is validated, with no line protocol check performed. Expected Results Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces. Failure: If any of the following occur: The specified interface is not configured. The specified interface status and line protocol status does not match the expected operational state for any interface. 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 the operational states of specified interfaces to ensure they match expected configurations.\n\n This test performs the following checks for each specified interface:\n\n 1. If `line_protocol_status` is defined, both `status` and `line_protocol_status` are verified for the specified interface.\n 2. If `line_protocol_status` is not provided but the `status` is \"up\", it is assumed that both the status and line protocol should be \"up\".\n 3. If the interface `status` is not \"up\", only the interface's status is validated, with no line protocol check performed.\n\n Expected Results\n ----------------\n * Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces.\n * Failure: If any of the following occur:\n - The specified interface is not configured.\n - The specified interface status and line protocol status does not match the expected operational state for any interface.\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesStatus.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output\n for interface in self.inputs.interfaces:\n if (intf_status := get_value(command_output[\"interfaceDescriptions\"], interface.name, separator=\"..\")) is None:\n self.result.is_failure(f\"{interface.name} - Not configured\")\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 actual_state = f\"Expected: {interface.status}/{interface.line_protocol_status}, Actual: {status}/{proto}\"\n self.result.is_failure(f\"{interface.name} - {actual_state}\")\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\"):\n self.result.is_failure(f\"{interface.name} - Expected: up/up, Actual: {status}/{proto}\")\n elif interface.status != status:\n self.result.is_failure(f\"{interface.name} - Expected: {interface.status}, Actual: {status}\")\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.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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip virtual-router\", revision=2)]\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 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\", revision=1)]\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 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\", revision=1)]\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.VerifyLACPInterfacesStatus","title":"VerifyLACPInterfacesStatus","text":"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces. Verifies that the interface is a member of the LACP port channel. Ensures that the synchronization is established. Ensures the interfaces are in the correct state for collecting and distributing traffic. Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \u201cslow\u201d mode, is the default setting.) Expected Results Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct. Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct. Examples anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n Source code in anta/tests/interfaces.py class VerifyLACPInterfacesStatus(AntaTest):\n \"\"\"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces.\n\n - Verifies that the interface is a member of the LACP port channel.\n - Ensures that the synchronization is established.\n - Ensures the interfaces are in the correct state for collecting and distributing traffic.\n - Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \"slow\" mode, is the default setting.)\n\n Expected Results\n ----------------\n * Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct.\n * Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show lacp interface {interface}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLACPInterfacesStatus test.\"\"\"\n\n interfaces: list[LACPInterface]\n \"\"\"List of LACP member interface.\"\"\"\n\n class LACPInterface(BaseModel):\n \"\"\"Model for an LACP member interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"Ethernet interface to validate.\"\"\"\n portchannel: PortChannelInterface\n \"\"\"Port Channel in which the interface is bundled.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each interface in the input list.\"\"\"\n return [template.render(interface=interface.name) for interface in self.inputs.interfaces]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLACPInterfacesStatus.\"\"\"\n self.result.is_success()\n\n # Member port verification parameters.\n member_port_details = [\"activity\", \"aggregation\", \"synchronization\", \"collecting\", \"distributing\", \"timeout\"]\n\n # Iterating over command output for different interfaces\n for command, input_entry in zip(self.instance_commands, self.inputs.interfaces):\n interface = input_entry.name\n portchannel = input_entry.portchannel\n\n # Verify if a PortChannel is configured with the provided interface\n if not (interface_details := get_value(command.json_output, f\"portChannels.{portchannel}.interfaces.{interface}\")):\n self.result.is_failure(f\"Interface '{interface}' is not configured to be a member of LACP '{portchannel}'.\")\n continue\n\n # Verify the interface is bundled in port channel.\n actor_port_status = interface_details.get(\"actorPortStatus\")\n if actor_port_status != \"bundled\":\n message = f\"For Interface {interface}:\\nExpected `bundled` as the local port status, but found `{actor_port_status}` instead.\\n\"\n self.result.is_failure(message)\n continue\n\n # Collecting actor and partner port details\n actor_port_details = interface_details.get(\"actorPortState\", {})\n partner_port_details = interface_details.get(\"partnerPortState\", {})\n\n # Collecting actual interface details\n actual_interface_output = {\n \"actor_port_details\": {param: actor_port_details.get(param, \"NotFound\") for param in member_port_details},\n \"partner_port_details\": {param: partner_port_details.get(param, \"NotFound\") for param in member_port_details},\n }\n\n # Forming expected interface details\n expected_details = {param: param != \"timeout\" for param in member_port_details}\n expected_interface_output = {\"actor_port_details\": expected_details, \"partner_port_details\": expected_details}\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n message = f\"For Interface {interface}:\\n\"\n actor_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"actor_port_details\", {}), actual_interface_output.get(\"actor_port_details\", {})\n )\n partner_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"partner_port_details\", {}), actual_interface_output.get(\"partner_port_details\", {})\n )\n\n if actor_port_failed_log:\n message += f\"Actor port details:{actor_port_failed_log}\\n\"\n if partner_port_failed_log:\n message += f\"Partner port details:{partner_port_failed_log}\\n\"\n\n self.result.is_failure(message)\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[LACPInterface] List of LACP member interface. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"LACPInterface","text":"Name Type Description Default name EthernetInterface Ethernet interface to validate. - portchannel PortChannelInterface Port Channel in which the interface is bundled. -"},{"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 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\", revision=1)]\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: # pragma: no branch\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show port-channel\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip interface brief\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show storm-control\", revision=1)]\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.interfaces/#input-models","title":"Input models","text":""},{"location":"api/tests.interfaces/#anta.input_models.interfaces.InterfaceState","title":"InterfaceState","text":"Model for an interface state. 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 Source code in anta/input_models/interfaces.py 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"},{"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 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\", revision=1)]\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 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 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. This test performs the following checks: Retrieves the device\u2019s configured FQDN Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message includes the complete FQDN of the device Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the device\u2019s complete FQDN. Failure: If any of the following occur: The test message is not found in recent logs The log message does not include the device\u2019s FQDN The FQDN in the log message doesn\u2019t match the configured 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 This test performs the following checks:\n\n 1. Retrieves the device's configured FQDN\n 2. Sends a test log message at the **informational** level\n 3. Retrieves the most recent logs (last 30 seconds)\n 4. Verifies that the test message includes the complete FQDN of the device\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the device's complete FQDN.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The log message does not include the device's FQDN\n - The FQDN in the log message doesn't match the configured FQDN\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingHostname:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show hostname\", revision=1),\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingHostname validation\", ofmt=\"text\"),\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 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. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message was successfully logged Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are being generated and the test message is found in recent logs. Failure: If any of the following occur: The test message is not found in recent logs The logging system is not capturing new messages No logs are being 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 This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message was successfully logged\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are being generated and the test message is found in recent logs.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The logging system is not capturing new messages\n - No logs are being generated\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingLogsGeneration:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingLogsGeneration validation\", ofmt=\"text\"),\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 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 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 VerifyLoggingSourceIntf 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 VerifyLoggingSourceIntf.\"\"\"\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 appropriate timestamp. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message is present with a high-resolution RFC3339 timestamp format Example format: 2024-01-25T15:30:45.123456+00:00 Includes microsecond precision Contains timezone offset Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the correct high-resolution RFC3339 timestamp format. Failure: If any of the following occur: The test message is not found in recent logs The timestamp format does not match the expected RFC3339 format 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 appropriate timestamp.\n\n This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message is present with a high-resolution RFC3339 timestamp format\n - Example format: `2024-01-25T15:30:45.123456+00:00`\n - Includes microsecond precision\n - Contains timezone offset\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the correct high-resolution RFC3339 timestamp format.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The timestamp format does not match the expected RFC3339 format\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingTimestamp:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingTimestamp validation\", ofmt=\"text\"),\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(\n logger: logging.Logger, command_output: str\n) -> str\n Parse show logging output and gets operational logging states used in the tests in this module. Parameters: Name Type Description Default logger Logger The logger object. required command_output str The show logging output. required 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 Parameters\n ----------\n logger\n The logger object.\n command_output\n The `show logging` output.\n\n Returns\n -------\n str\n 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":"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 Adaptive Virtual Topology BFD Configuration Connectivity Field Notices Flow Tracking GreenT Hardware Interfaces LANZ Logging MLAG Multicast Profiles PTP Router Path Selection Routing Generic Routing BGP Routing ISIS Routing OSPF Security Services SNMP Software STP STUN 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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag config-sanity\", revision=1)]\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 command_output[\"mlagActive\"] 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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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.path_selection/","title":"Router Path Selection","text":""},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifyPathsHealth","title":"VerifyPathsHealth","text":"Verifies the path and telemetry state of all paths under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if all path states under router path-selection are either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and their telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if any path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifyPathsHealth:\n Source code in anta/tests/path_selection.py class VerifyPathsHealth(AntaTest):\n \"\"\"Verifies the path and telemetry state of all paths under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if all path states under router path-selection are either 'IPsec established' or 'Resolved'\n and their telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if any path state is not 'IPsec established' or 'Resolved',\n or the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifyPathsHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show path-selection paths\", revision=1)]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyPathsHealth.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"dpsPeers\"]\n\n # If no paths are configured for router path-selection, the test fails\n if not command_output:\n self.result.is_failure(\"No path configured for router path-selection.\")\n return\n\n # Check the state of each path\n for peer, peer_data in command_output.items():\n for group, group_data in peer_data[\"dpsGroups\"].items():\n for path_data in group_data[\"dpsPaths\"].values():\n path_state = path_data[\"state\"]\n session = path_data[\"dpsSessions\"][\"0\"][\"active\"]\n\n # If the path state of any path is not 'ipsecEstablished' or 'routeResolved', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for peer {peer} in path-group {group} is `{path_state}`.\")\n\n # If the telemetry state of any path is inactive, the test fails\n elif not session:\n self.result.is_failure(f\"Telemetry state for peer {peer} in path-group {group} is `inactive`.\")\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath","title":"VerifySpecificPath","text":"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if the path state under router path-selection is either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if the path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or if the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n Source code in anta/tests/path_selection.py class VerifySpecificPath(AntaTest):\n \"\"\"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if the path state under router path-selection is either 'IPsec established' or 'Resolved'\n and telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if the path state is not 'IPsec established' or 'Resolved',\n or if the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show path-selection paths peer {peer} path-group {group} source {source} destination {destination}\", revision=1)\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificPath test.\"\"\"\n\n paths: list[RouterPath]\n \"\"\"List of router paths to verify.\"\"\"\n\n class RouterPath(BaseModel):\n \"\"\"Detail of a router path.\"\"\"\n\n peer: IPv4Address\n \"\"\"Static peer IPv4 address.\"\"\"\n\n path_group: str\n \"\"\"Router path group name.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of path.\"\"\"\n\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of path.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each router path.\"\"\"\n return [\n template.render(peer=path.peer, group=path.path_group, source=path.source_address, destination=path.destination_address) for path in self.inputs.paths\n ]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificPath.\"\"\"\n self.result.is_success()\n\n # Check the state of each path\n for command in self.instance_commands:\n peer = command.params.peer\n path_group = command.params.group\n source = command.params.source\n destination = command.params.destination\n command_output = command.json_output.get(\"dpsPeers\", [])\n\n # If the peer is not configured for the path group, the test fails\n if not command_output:\n self.result.is_failure(f\"Path `peer: {peer} source: {source} destination: {destination}` is not configured for path-group `{path_group}`.\")\n continue\n\n # Extract the state of the path\n path_output = get_value(command_output, f\"{peer}..dpsGroups..{path_group}..dpsPaths\", separator=\"..\")\n path_state = next(iter(path_output.values())).get(\"state\")\n session = get_value(next(iter(path_output.values())), \"dpsSessions.0.active\")\n\n # If the state of the path is not 'ipsecEstablished' or 'routeResolved', or the telemetry state is 'inactive', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `{path_state}`.\")\n elif not session:\n self.result.is_failure(\n f\"Telemetry state for path `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `inactive`.\"\n )\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"Inputs","text":"Name Type Description Default paths list[RouterPath] List of router paths to verify. -"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"RouterPath","text":"Name Type Description Default peer IPv4Address Static peer IPv4 address. - path_group str Router path group name. - source_address IPv4Address Source IPv4 address of path. - destination_address IPv4Address Destination IPv4 address of path. -"},{"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 description = \"Verifies the device TCAM profile.\"\n categories: ClassVar[list[str]] = [\"profiles\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hardware tcam profile\", revision=1)]\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 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\", revision=1)]\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\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 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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpLockStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpModeStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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 on the device. 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 on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpOffset:\n ```\n \"\"\"\n\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\", revision=1)]\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 description = \"Verifies the PTP interfaces state.\"\n categories: ClassVar[list[str]] = [\"ptp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", revision=2)]\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/#tests","title":"Tests","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 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\", revision=3)]\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 the advertised and received routes of BGP peers. 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 the advertised and received routes of BGP peers.\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show bgp neighbors {peer} advertised-routes vrf {vrf}\", revision=3),\n AntaTemplate(template=\"show bgp neighbors {peer} routes vrf {vrf}\", revision=3),\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 [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\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 = command.params.peer\n vrf = command.params.vrf\n for input_entry in self.inputs.bgp_peers:\n if str(input_entry.peer_address) == peer and input_entry.vrf == vrf:\n advertised_routes = input_entry.advertised_routes\n received_routes = input_entry.received_routes\n break\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 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\", revision=3)]\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 given address families. This test performs the following checks for each specified address family: Confirms that the specified VRF is configured. Counts the number of peers that are: If check_peer_state is set to True, Counts the number of BGP peers that are in the Established state and have successfully negotiated the specified AFI/SAFI If check_peer_state is set to False, skips validation of the Established state and AFI/SAFI negotiation. Expected Results Success: If the count of BGP peers matches the expected count with check_peer_state enabled/disabled. Failure: If any of the following occur: The specified VRF is not configured. The BGP peer count does not match expected value with check_peer_state enabled/disabled.\u201d 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 given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Confirms that the specified VRF is configured.\n 2. Counts the number of peers that are:\n - If `check_peer_state` is set to True, Counts the number of BGP peers that are in the `Established` state and\n have successfully negotiated the specified AFI/SAFI\n - If `check_peer_state` is set to False, skips validation of the `Established` state and AFI/SAFI negotiation.\n\n Expected Results\n ----------------\n * Success: If the count of BGP peers matches the expected count with `check_peer_state` enabled/disabled.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - The BGP peer count does not match expected value with `check_peer_state` enabled/disabled.\"\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp summary vrf all\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerCount test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'num_peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.num_peers is None:\n msg = f\"{af} 'num_peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerCount.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n peers_data = vrf_output.get(\"peers\", {}).values()\n if not address_family.check_peer_state:\n # Count the number of peers without considering the state and negotiated AFI/SAFI check if the count matches the expected count\n peer_count = sum(1 for peer_data in peers_data if address_family.eos_key in peer_data)\n else:\n # Count the number of established peers with negotiated AFI/SAFI\n peer_count = sum(\n 1\n for peer_data in peers_data\n if peer_data.get(\"peerState\") == \"Established\" and get_value(peer_data, f\"{address_family.eos_key}.afiSafiState\") == \"negotiated\"\n )\n\n # Check if the count matches the expected count\n if address_family.num_peers != peer_count:\n self.result.is_failure(f\"{address_family} - Expected: {address_family.num_peers}, Actual: {peer_count}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats","title":"VerifyBGPPeerDropStats","text":"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s). By default, all drop statistics counters will be checked for any non-zero values. An optional list of specific drop statistics can be provided for granular testing. Expected Results Success: The test will pass if the BGP peer\u2019s drop statistic(s) are zero. Failure: The test will fail if the BGP peer\u2019s drop statistic(s) are non-zero/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerDropStats(AntaTest):\n \"\"\"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s).\n\n By default, all drop statistics counters will be checked for any non-zero values.\n An optional list of specific drop statistics can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's drop statistic(s) are zero.\n * Failure: The test will fail if the BGP peer's drop statistic(s) are non-zero/Not Found or peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerDropStats 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 drop_stats: list[BgpDropStats] | None = None\n \"\"\"Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerDropStats.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n drop_statistics = input_entry.drop_stats\n\n # Verify BGP peer\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify BGP peer's drop stats\n drop_stats_output = peer_detail.get(\"dropStats\", {})\n\n # In case drop stats not provided, It will check all drop statistics\n if not drop_statistics:\n drop_statistics = drop_stats_output\n\n # Verify BGP peer's drop stats\n drop_stats_not_ok = {\n drop_stat: drop_stats_output.get(drop_stat, \"Not Found\") for drop_stat in drop_statistics if drop_stats_output.get(drop_stat, \"Not Found\")\n }\n if any(drop_stats_not_ok):\n failures[peer] = {vrf: drop_stats_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero NLRI drop statistics counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats-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.VerifyBGPPeerDropStats-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' drop_stats list[BgpDropStats] | None Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics. None"},{"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 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\", revision=3)]\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. Supports strict: True to verify that only the specified capabilities are configured, requiring an exact match. 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 strict: False\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 Supports `strict: True` to verify that only the specified capabilities are configured, requiring an exact match.\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 strict: False\n capabilities:\n - ipv4Unicast\n ```\n \"\"\"\n\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\", revision=3)]\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 strict: bool = False\n \"\"\"If True, requires exact matching of provided capabilities. Defaults to False.\"\"\"\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 # Fetching the capabilities output.\n bgp_output = get_value(bgp_output, \"neighborCapabilities.multiprotocolCaps\")\n\n if bgp_peer.strict and sorted(capabilities) != sorted(bgp_output):\n failure[\"bgp_peers\"][peer][vrf] = {\n \"status\": f\"Expected only `{', '.join(capabilities)}` capabilities should be listed but found `{', '.join(bgp_output)}` instead.\"\n }\n failures = deep_update(failures, failure)\n continue\n\n # Check each capability\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' strict bool If True, requires exact matching of provided capabilities. Defaults to False. False capabilities list[MultiProtocolCaps] List of multiprotocol capabilities to be verified. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit","title":"VerifyBGPPeerRouteLimit","text":"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s). Expected Results Success: The test will pass if the BGP peer\u2019s maximum routes and, if provided, the maximum routes warning limit are equal to the given limits. Failure: The test will fail if the BGP peer\u2019s maximum routes do not match the given limit, or if the maximum routes warning limit is provided and does not match the given limit, or if the peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerRouteLimit(AntaTest):\n \"\"\"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's maximum routes and, if provided, the maximum routes warning limit are equal to the given limits.\n * Failure: The test will fail if the BGP peer's maximum routes do not match the given limit, or if the maximum routes warning limit is provided\n and does not match the given limit, or if the peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerRouteLimit 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 maximum_routes: int = Field(ge=0, le=4294967294)\n \"\"\"The maximum allowable number of BGP routes, `0` means unlimited.\"\"\"\n warning_limit: int = Field(default=0, ge=0, le=4294967294)\n \"\"\"Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerRouteLimit.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n maximum_routes = input_entry.maximum_routes\n warning_limit = input_entry.warning_limit\n failure: dict[Any, Any] = {}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify maximum routes configured.\n if (actual_routes := peer_detail.get(\"maxTotalRoutes\", \"Not Found\")) != maximum_routes:\n failure[\"Maximum total routes\"] = actual_routes\n\n # Verify warning limit if given.\n if warning_limit and (actual_warning_limit := peer_detail.get(\"totalRoutesWarnLimit\", \"Not Found\")) != warning_limit:\n failure[\"Warning limit\"] = actual_warning_limit\n\n # Updated failures if any.\n if failure:\n failures[peer] = {vrf: failure}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peer(s) are not configured or maximum routes and maximum routes warning limit is not correct:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit-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.VerifyBGPPeerRouteLimit-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' maximum_routes int The maximum allowable number of BGP routes, `0` means unlimited. Field(ge=0, le=4294967294) warning_limit int Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit. Field(default=0, ge=0, le=4294967294)"},{"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 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\", revision=3)]\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.VerifyBGPPeerUpdateErrors","title":"VerifyBGPPeerUpdateErrors","text":"Verifies BGP update error counters for the provided BGP IPv4 peer(s). By default, all update error counters will be checked for any non-zero values. An optional list of specific update error counters can be provided for granular testing. Note: For \u201cdisabledAfiSafi\u201d error counter field, checking that it\u2019s not \u201cNone\u201d versus 0. Expected Results Success: The test will pass if the BGP peer\u2019s update error counter(s) are zero/None. Failure: The test will fail if the BGP peer\u2019s update error counter(s) are non-zero/not None/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerUpdateErrors(AntaTest):\n \"\"\"Verifies BGP update error counters for the provided BGP IPv4 peer(s).\n\n By default, all update error counters will be checked for any non-zero values.\n An optional list of specific update error counters can be provided for granular testing.\n\n Note: For \"disabledAfiSafi\" error counter field, checking that it's not \"None\" versus 0.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's update error counter(s) are zero/None.\n * Failure: The test will fail if the BGP peer's update error counter(s) are non-zero/not None/Not Found or\n peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerUpdateErrors 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 update_errors: list[BgpUpdateError] | None = None\n \"\"\"Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerUpdateErrors.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n update_error_counters = input_entry.update_errors\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Getting the BGP peer's error counters output.\n error_counters_output = peer_detail.get(\"peerInUpdateErrors\", {})\n\n # In case update error counters not provided, It will check all the update error counters.\n if not update_error_counters:\n update_error_counters = error_counters_output\n\n # verifying the error counters.\n error_counters_not_ok = {\n (\"disabledAfiSafi\" if error_counter == \"disabledAfiSafi\" else error_counter): value\n for error_counter in update_error_counters\n if (value := error_counters_output.get(error_counter, \"Not Found\")) != \"None\" and value != 0\n }\n if error_counters_not_ok:\n failures[peer] = {vrf: error_counters_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero update error counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerUpdateErrors-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.VerifyBGPPeerUpdateErrors-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' update_errors list[BgpUpdateError] | None Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters. None"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth","title":"VerifyBGPPeersHealth","text":"Verifies the health of BGP peers for given address families. This test performs the following checks for each specified address family: Validates that the VRF is configured. Checks if there are any peers for the given AFI/SAFI. For each relevant peer: Verifies that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Checks that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified address families and their peers. Failure: If any of the following occur: The specified VRF is not configured. No peers are found for a given AFI/SAFI. Any BGP session is not in the Established state. The AFI/SAFI state is not \u2018negotiated\u2019 for any peer. Any TCP message queue (input or output) is not empty when check_tcp_queues is True (default). 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 check_tcp_queues: false\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeersHealth(AntaTest):\n \"\"\"Verifies the health of BGP peers for given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Validates that the VRF is configured.\n 2. Checks if there are any peers for the given AFI/SAFI.\n 3. For each relevant peer:\n - Verifies that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Checks that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified address families and their peers.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - No peers are found for a given AFI/SAFI.\n - Any BGP session is not in the `Established` state.\n - The AFI/SAFI state is not 'negotiated' for any peer.\n - Any TCP message queue (input or output) is not empty when `check_tcp_queues` is `True` (default).\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 check_tcp_queues: false\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeersHealth test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeersHealth.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n # Check if any peers are found for this AFI/SAFI\n relevant_peers = [\n peer for peer in vrf_output.get(\"peerList\", []) if get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\") is not None\n ]\n\n if not relevant_peers:\n self.result.is_failure(f\"{address_family} - No peers found\")\n continue\n\n for peer in relevant_peers:\n # Check if the BGP session is established\n if peer[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session state is not established; State: {peer['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers","title":"VerifyBGPSpecificPeers","text":"Verifies the health of specific BGP peer(s) for given address families. This test performs the following checks for each specified address family and peer: Confirms that the specified VRF is configured. For each specified peer: Verifies that the peer is found in the BGP configuration. Checks that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Ensures that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified peers in all address families. Failure: If any of the following occur: The specified VRF is not configured. A specified peer is not found in the BGP configuration. The BGP session for a peer is not in the Established state. The AFI/SAFI state is not negotiated for a peer. Any TCP message queue (input or output) is not empty for a peer when check_tcp_queues is True (default). 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) for given address families.\n\n This test performs the following checks for each specified address family and peer:\n\n 1. Confirms that the specified VRF is configured.\n 2. For each specified peer:\n - Verifies that the peer is found in the BGP configuration.\n - Checks that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Ensures that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified peers in all address families.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - A specified peer is not found in the BGP configuration.\n - The BGP session for a peer is not in the `Established` state.\n - The AFI/SAFI state is not `negotiated` for a peer.\n - Any TCP message queue (input or output) is not empty for a peer when `check_tcp_queues` is `True` (default).\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPSpecificPeers test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.peers is None:\n msg = f\"{af} 'peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPSpecificPeers.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n for peer in address_family.peers:\n peer_ip = str(peer)\n\n # Check if the peer is found\n if (peer_data := get_item(vrf_output[\"peerList\"], \"peerAddress\", peer_ip)) is None:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Not configured\")\n continue\n\n # Check if the BGP session is established\n if peer_data[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session state is not established; State: {peer_data['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer_data, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not capability_status:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated\")\n\n if capability_status and not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer_data[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer_data[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"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 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\", revision=3)]\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.VerifyBgpRouteMaps","title":"VerifyBgpRouteMaps","text":"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s). Expected Results Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF. Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction. Examples anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n Source code in anta/tests/routing/bgp.py class VerifyBgpRouteMaps(AntaTest):\n \"\"\"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF.\n * Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBgpRouteMaps 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 inbound_route_map: str | None = None\n \"\"\"Inbound route map applied, defaults to None.\"\"\"\n outbound_route_map: str | None = None\n \"\"\"Outbound route map applied, defaults to None.\"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpPeer class.\n\n At least one of 'inbound' or 'outbound' route-map must be provided.\n \"\"\"\n if not (self.inbound_route_map or self.outbound_route_map):\n msg = \"At least one of 'inbound_route_map' or 'outbound_route_map' must be provided.\"\n raise ValueError(msg)\n return self\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBgpRouteMaps.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n inbound_route_map = input_entry.inbound_route_map\n outbound_route_map = input_entry.outbound_route_map\n failure: dict[Any, Any] = {vrf: {}}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify Inbound route-map\n if inbound_route_map and (inbound_map := peer_detail.get(\"routeMapInbound\", \"Not Configured\")) != inbound_route_map:\n failure[vrf].update({\"Inbound route-map\": inbound_map})\n\n # Verify Outbound route-map\n if outbound_route_map and (outbound_map := peer_detail.get(\"routeMapOutbound\", \"Not Configured\")) != outbound_route_map:\n failure[vrf].update({\"Outbound route-map\": outbound_map})\n\n if failure[vrf]:\n failures[peer] = failure\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(\n f\"The following BGP peers are not configured or has an incorrect or missing route map in either the inbound or outbound direction:\\n{failures}\"\n )\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBgpRouteMaps-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.VerifyBgpRouteMaps-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' inbound_route_map str | None Inbound route map applied, defaults to None. None outbound_route_map str | None Outbound route map applied, defaults to None. None"},{"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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp evpn route-type mac-ip {address} vni {vni}\", revision=2)]\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=str(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 = 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/#input-models","title":"Input models","text":""},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily","title":"BgpAddressFamily","text":"Model for a BGP address family. Name Type Description Default afi Afi BGP Address Family Identifier (AFI). - safi Safi | None BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`. None vrf str Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`. If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`. These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6. 'default' num_peers PositiveInt | None Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test. None peers list[IPv4Address | IPv6Address] | None List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test. None check_tcp_queues bool Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`. Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests. True check_peer_state bool Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`. Can be enabled in the `VerifyBGPPeerCount` tests. False Source code in anta/input_models/routing/bgp.py class BgpAddressFamily(BaseModel):\n \"\"\"Model for a BGP address family.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n afi: Afi\n \"\"\"BGP Address Family Identifier (AFI).\"\"\"\n safi: Safi | None = None\n \"\"\"BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`.\"\"\"\n vrf: str = \"default\"\n \"\"\"Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`.\n\n If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`.\n\n These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6.\n \"\"\"\n num_peers: PositiveInt | None = None\n \"\"\"Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test.\"\"\"\n peers: list[IPv4Address | IPv6Address] | None = None\n \"\"\"List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test.\"\"\"\n check_tcp_queues: bool = True\n \"\"\"Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`.\n\n Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests.\n \"\"\"\n check_peer_state: bool = False\n \"\"\"Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`.\n\n Can be enabled in the `VerifyBGPPeerCount` tests.\n \"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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 @property\n def eos_key(self) -> str:\n \"\"\"AFI/SAFI EOS key representation.\"\"\"\n # Pydantic handles the validation of the AFI/SAFI combination, so we can ignore error handling here.\n return AFI_SAFI_EOS_KEY[(self.afi, self.safi)]\n\n def __str__(self) -> str:\n \"\"\"Return a string representation of the BgpAddressFamily model. Used in failure messages.\n\n Examples\n --------\n - AFI:ipv4 SAFI:unicast VRF:default\n - AFI:evpn\n \"\"\"\n base_string = f\"AFI: {self.afi}\"\n if self.safi is not None:\n base_string += f\" SAFI: {self.safi}\"\n if self.afi in [\"ipv4\", \"ipv6\"]:\n base_string += f\" VRF: {self.vrf}\"\n return base_string\n"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily.validate_inputs","title":"validate_inputs","text":"validate_inputs() -> Self\n Validate the inputs provided to the BgpAddressFamily class. If afi is either ipv4 or ipv6, safi must be provided. If afi is not ipv4 or ipv6, safi must NOT be provided and vrf must be default. Source code in anta/input_models/routing/bgp.py @model_validator(mode=\"after\")\ndef validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAfi","title":"BgpAfi","text":"Alias for the BgpAddressFamily model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the BgpAddressFamily model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/routing/bgp.py class BgpAfi(BgpAddressFamily): # pragma: no cover\n \"\"\"Alias for the BgpAddressFamily model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the BgpAddressFamily model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the BgpAfi class, emitting a deprecation warning.\"\"\"\n warn(\n message=\"BgpAfi model is deprecated and will be removed in ANTA v2.0.0. Use the BgpAddressFamily model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\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. 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.\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 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 categories: ClassVar[list[str]] = [\"routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show ip route vrf {vrf} {route}\", revision=4),\n AntaTemplate(template=\"show ip route vrf {vrf}\", revision=4),\n ]\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 collect: Literal[\"one\", \"all\"] = \"one\"\n \"\"\"Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one`\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for the input vrf.\"\"\"\n if template == VerifyRoutingTableEntry.commands[0] and self.inputs.collect == \"one\":\n return [template.render(vrf=self.inputs.vrf, route=route) for route in self.inputs.routes]\n\n if template == VerifyRoutingTableEntry.commands[1] and self.inputs.collect == \"all\":\n return [template.render(vrf=self.inputs.vrf)]\n\n return []\n\n @staticmethod\n @cache\n def ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRoutingTableEntry.\"\"\"\n commands_output_route_ips = set()\n\n for command in self.instance_commands:\n command_output_vrf = command.json_output[\"vrfs\"][self.inputs.vrf]\n commands_output_route_ips |= {self.ip_interface_ip(route) for route in command_output_vrf[\"routes\"]}\n\n missing_routes = [str(route) for route in self.inputs.routes if route not in commands_output_route_ips]\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. - collect Literal['one', 'all'] Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one` 'one'"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableEntry.ip_interface_ip","title":"ip_interface_ip cached staticmethod","text":"ip_interface_ip(route: str) -> IPv4Address\n Return the IP address of the provided ip route with mask. Source code in anta/tests/routing/generic.py @staticmethod\n@cache\ndef ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n"},{"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 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: PositiveInteger\n \"\"\"Expected minimum routing table size.\"\"\"\n maximum: PositiveInteger\n \"\"\"Expected maximum routing table size.\"\"\"\n\n @model_validator(mode=\"after\")\n def check_min_max(self) -> Self:\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 PositiveInteger Expected minimum routing table size. - maximum PositiveInteger Expected maximum routing table size. -"},{"location":"api/tests.routing.isis/","title":"ISIS","text":""},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode","title":"VerifyISISInterfaceMode","text":"Verifies ISIS Interfaces are running in correct mode. Expected Results Success: The test will pass if all listed interfaces are running in correct mode. Failure: The test will fail if any of the listed interfaces is not running in correct mode. Skipped: The test will be skipped if no ISIS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISInterfaceMode(AntaTest):\n \"\"\"Verifies ISIS Interfaces are running in correct mode.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces are running in correct mode.\n * Failure: The test will fail if any of the listed interfaces is not running in correct mode.\n * Skipped: The test will be skipped if no ISIS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n ```\n \"\"\"\n\n description = \"Verifies interface mode for IS-IS\"\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceState]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceState(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n mode: Literal[\"point-to-point\", \"broadcast\", \"passive\"]\n \"\"\"Number of IS-IS neighbors.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF where the interface should be configured\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISInterfaceMode.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # Check for p2p interfaces\n for interface in self.inputs.interfaces:\n interface_data = _get_interface_data(\n interface=interface.name,\n vrf=interface.vrf,\n command_output=command_output,\n )\n # Check for correct VRF\n if interface_data is not None:\n interface_type = get_value(dictionary=interface_data, key=\"interfaceType\", default=\"unset\")\n # Check for interfaceType\n if interface.mode == \"point-to-point\" and interface.mode != interface_type:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in {interface.mode} reporting {interface_type}\")\n # Check for passive\n elif interface.mode == \"passive\":\n json_path = f\"intfLevels.{interface.level}.passive\"\n if interface_data is None or get_value(dictionary=interface_data, key=json_path, default=False) is False:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in passive mode\")\n else:\n self.result.is_failure(f\"Interface {interface.name} not found in VRF {interface.vrf}\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceState] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"InterfaceState","text":"Name Type Description Default name Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 mode Literal['point-to-point', 'broadcast', 'passive'] Number of IS-IS neighbors. - vrf str VRF where the interface should be configured 'default'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount","title":"VerifyISISNeighborCount","text":"Verifies number of IS-IS neighbors per level and per interface. Expected Results Success: The test will pass if the number of neighbors is correct. Failure: The test will fail if the number of neighbors is incorrect. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborCount(AntaTest):\n \"\"\"Verifies number of IS-IS neighbors per level and per interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if the number of neighbors is correct.\n * Failure: The test will fail if the number of neighbors is incorrect.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceCount]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceCount(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: int = 2\n \"\"\"IS-IS level to check.\"\"\"\n count: int\n \"\"\"Number of IS-IS neighbors.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborCount.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n isis_neighbor_count = _get_isis_neighbors_count(command_output)\n if len(isis_neighbor_count) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n for interface in self.inputs.interfaces:\n eos_data = [ifl_data for ifl_data in isis_neighbor_count if ifl_data[\"interface\"] == interface.name and ifl_data[\"level\"] == interface.level]\n if not eos_data:\n self.result.is_failure(f\"No neighbor detected for interface {interface.name}\")\n continue\n if eos_data[0][\"count\"] != interface.count:\n self.result.is_failure(\n f\"Interface {interface.name}: \"\n f\"expected Level {interface.level}: count {interface.count}, \"\n f\"got Level {eos_data[0]['level']}: count {eos_data[0]['count']}\"\n )\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceCount] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"InterfaceCount","text":"Name Type Description Default name Interface Interface name to check. - level int IS-IS level to check. 2 count int Number of IS-IS neighbors. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborState","title":"VerifyISISNeighborState","text":"Verifies all IS-IS neighbors are in UP state. Expected Results Success: The test will pass if all IS-IS neighbors are in UP state. Failure: The test will fail if some IS-IS neighbors are not in UP state. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborState(AntaTest):\n \"\"\"Verifies all IS-IS neighbors are in UP state.\n\n Expected Results\n ----------------\n * Success: The test will pass if all IS-IS neighbors are in UP state.\n * Failure: The test will fail if some IS-IS neighbors are not in UP state.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis neighbors\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborState.\"\"\"\n command_output = self.instance_commands[0].json_output\n if _count_isis_neighbor(command_output) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n self.result.is_success()\n not_full_neighbors = _get_not_full_isis_neighbors(command_output)\n if not_full_neighbors:\n self.result.is_failure(f\"Some neighbors are not in the correct state (UP): {not_full_neighbors}.\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments","title":"VerifyISISSegmentRoutingAdjacencySegments","text":"Verify that all expected Adjacency segments are correctly visible for each interface. Expected Results Success: The test will pass if all listed interfaces have correct adjacencies. Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies. Skipped: The test will be skipped if no ISIS SR Adjacency is found. Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):\n \"\"\"Verify that all expected Adjacency segments are correctly visible for each interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces have correct adjacencies.\n * Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies.\n * Skipped: The test will be skipped if no ISIS SR Adjacency is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing adjacency-segments\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingAdjacencySegments test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n segments: list[Segment]\n \"\"\"List of Adjacency segments configured in this instance.\"\"\"\n\n class Segment(BaseModel):\n \"\"\"Segment model definition.\"\"\"\n\n interface: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n sid_origin: Literal[\"dynamic\"] = \"dynamic\"\n \"\"\"Adjacency type\"\"\"\n address: IPv4Address\n \"\"\"IP address of remote end of segment.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingAdjacencySegments.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routging.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n for input_segment in instance.segments:\n eos_segment = _get_adjacency_segment_data_by_neighbor(\n neighbor=str(input_segment.address),\n instance=instance.name,\n vrf=instance.vrf,\n command_output=command_output,\n )\n if eos_segment is None:\n failure_message.append(f\"Your segment has not been found: {input_segment}.\")\n\n elif (\n eos_segment[\"localIntf\"] != input_segment.interface\n or eos_segment[\"level\"] != input_segment.level\n or eos_segment[\"sidOrigin\"] != input_segment.sid_origin\n ):\n failure_message.append(f\"Your segment is not correct: Expected: {input_segment} - Found: {eos_segment}.\")\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' segments list[Segment] List of Adjacency segments configured in this instance. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Segment","text":"Name Type Description Default interface Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 sid_origin Literal['dynamic'] Adjacency type 'dynamic' address IPv4Address IP address of remote end of segment. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane","title":"VerifyISISSegmentRoutingDataplane","text":"Verify dataplane of a list of ISIS-SR instances. Expected Results Success: The test will pass if all instances have correct dataplane configured Failure: The test will fail if one of the instances has incorrect dataplane configured Skipped: The test will be skipped if ISIS is not running Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingDataplane(AntaTest):\n \"\"\"Verify dataplane of a list of ISIS-SR instances.\n\n Expected Results\n ----------------\n * Success: The test will pass if all instances have correct dataplane configured\n * Failure: The test will fail if one of the instances has incorrect dataplane configured\n * Skipped: The test will be skipped if ISIS is not running\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingDataplane test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n dataplane: Literal[\"MPLS\", \"mpls\", \"unset\"] = \"MPLS\"\n \"\"\"Configured dataplane for the instance.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingDataplane.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routing.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n eos_dataplane = get_value(dictionary=command_output, key=f\"vrfs.{instance.vrf}.isisInstances.{instance.name}.dataPlane\", default=None)\n if instance.dataplane.upper() != eos_dataplane:\n failure_message.append(f\"ISIS instance {instance.name} is not running dataplane {instance.dataplane} ({eos_dataplane})\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' dataplane Literal['MPLS', 'mpls', 'unset'] Configured dataplane for the instance. 'MPLS'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels","title":"VerifyISISSegmentRoutingTunnels","text":"Verify ISIS-SR tunnels computed by device. Expected Results Success: The test will pass if all listed tunnels are computed on device. Failure: The test will fail if one of the listed tunnels is missing. Skipped: The test will be skipped if ISIS-SR is not configured. Examples anta.tests.routing:\nisis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingTunnels(AntaTest):\n \"\"\"Verify ISIS-SR tunnels computed by device.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed tunnels are computed on device.\n * Failure: The test will fail if one of the listed tunnels is missing.\n * Skipped: The test will be skipped if ISIS-SR is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing tunnel\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingTunnels test.\"\"\"\n\n entries: list[Entry]\n \"\"\"List of tunnels to check on device.\"\"\"\n\n class Entry(BaseModel):\n \"\"\"Definition of a tunnel entry.\"\"\"\n\n endpoint: IPv4Network\n \"\"\"Endpoint IP of the tunnel.\"\"\"\n vias: list[Vias] | None = None\n \"\"\"Optional list of path to reach endpoint.\"\"\"\n\n class Vias(BaseModel):\n \"\"\"Definition of a tunnel path.\"\"\"\n\n nexthop: IPv4Address | None = None\n \"\"\"Nexthop of the tunnel. If None, then it is not tested. Default: None\"\"\"\n type: Literal[\"ip\", \"tunnel\"] | None = None\n \"\"\"Type of the tunnel. If None, then it is not tested. Default: None\"\"\"\n interface: Interface | None = None\n \"\"\"Interface of the tunnel. If None, then it is not tested. Default: None\"\"\"\n tunnel_id: Literal[\"TI-LFA\", \"ti-lfa\", \"unset\"] | None = None\n \"\"\"Computation method of the tunnel. If None, then it is not tested. Default: None\"\"\"\n\n def _eos_entry_lookup(self, search_value: IPv4Network, entries: dict[str, Any], search_key: str = \"endpoint\") -> dict[str, Any] | None:\n return next(\n (entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == str(search_value)),\n None,\n )\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingTunnels.\n\n This method performs the main test logic for verifying ISIS Segment Routing tunnels.\n It checks the command output, initiates defaults, and performs various checks on the tunnels.\n \"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n # initiate defaults\n failure_message = []\n\n if len(command_output[\"entries\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n for input_entry in self.inputs.entries:\n eos_entry = self._eos_entry_lookup(search_value=input_entry.endpoint, entries=command_output[\"entries\"])\n if eos_entry is None:\n failure_message.append(f\"Tunnel to {input_entry} is not found.\")\n elif input_entry.vias is not None:\n failure_src = []\n for via_input in input_entry.vias:\n if not self._check_tunnel_type(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel type\")\n if not self._check_tunnel_nexthop(via_input, eos_entry):\n failure_src.append(\"incorrect nexthop\")\n if not self._check_tunnel_interface(via_input, eos_entry):\n failure_src.append(\"incorrect interface\")\n if not self._check_tunnel_id(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel ID\")\n\n if failure_src:\n failure_message.append(f\"Tunnel to {input_entry.endpoint!s} is incorrect: {', '.join(failure_src)}\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n\n def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input tunnel type to check.\n eos_entry : dict[str, Any]\n The EOS entry containing the tunnel types.\n\n Returns\n -------\n bool\n True if the tunnel type matches any of the tunnel types in `eos_entry`, False otherwise.\n \"\"\"\n if via_input.type is not None:\n return any(\n via_input.type\n == get_value(\n dictionary=eos_via,\n key=\"type\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel nexthop matches the given input.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel nexthop matches, False otherwise.\n \"\"\"\n if via_input.nexthop is not None:\n return any(\n str(via_input.nexthop)\n == get_value(\n dictionary=eos_via,\n key=\"nexthop\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel interface exists in the given EOS entry.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel interface exists, False otherwise.\n \"\"\"\n if via_input.interface is not None:\n return any(\n via_input.interface\n == get_value(\n dictionary=eos_via,\n key=\"interface\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input vias to check.\n eos_entry : dict[str, Any])\n The EOS entry to compare against.\n\n Returns\n -------\n bool\n True if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias, False otherwise.\n \"\"\"\n if via_input.tunnel_id is not None:\n return any(\n via_input.tunnel_id.upper()\n == get_value(\n dictionary=eos_via,\n key=\"tunnelId.type\",\n default=\"undefined\",\n ).upper()\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Inputs","text":"Name Type Description Default entries list[Entry] List of tunnels to check on device. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Entry","text":"Name Type Description Default endpoint IPv4Network Endpoint IP of the tunnel. - vias list[Vias] | None Optional list of path to reach endpoint. None"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Vias","text":"Name Type Description Default nexthop IPv4Address | None Nexthop of the tunnel. If None, then it is not tested. Default: None None type Literal['ip', 'tunnel'] | None Type of the tunnel. If None, then it is not tested. Default: None None interface Interface | None Interface of the tunnel. If None, then it is not tested. Default: None None tunnel_id Literal['TI-LFA', 'ti-lfa', 'unset'] | None Computation method of the tunnel. If None, then it is not tested. Default: None None"},{"location":"api/tests.routing.ospf/","title":"OSPF","text":""},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFMaxLSA","title":"VerifyOSPFMaxLSA","text":"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold. Expected Results Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold. Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold. Skipped: The test will be skipped if no OSPF instance is found. Examples anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n Source code in anta/tests/routing/ospf.py class VerifyOSPFMaxLSA(AntaTest):\n \"\"\"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold.\n * Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold.\n * Skipped: The test will be skipped if no OSPF instance is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n ```\n \"\"\"\n\n description = \"Verifies all OSPF instances did not cross the maximum LSA threshold.\"\n categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyOSPFMaxLSA.\"\"\"\n command_output = self.instance_commands[0].json_output\n ospf_instance_info = _get_ospf_max_lsa_info(command_output)\n if not ospf_instance_info:\n self.result.is_skipped(\"No OSPF instance found.\")\n return\n all_instances_within_threshold = all(instance[\"numLsa\"] <= instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100) for instance in ospf_instance_info)\n if all_instances_within_threshold:\n self.result.is_success()\n else:\n exceeded_instances = [\n instance[\"instance\"] for instance in ospf_instance_info if instance[\"numLsa\"] > instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100)\n ]\n self.result.is_failure(f\"OSPF Instances {exceeded_instances} crossed the maximum LSA threshold.\")\n"},{"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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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.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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ip access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ipv6 access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show management security ssl certificate\", revision=1),\n AntaCommand(command=\"show clock\", revision=1),\n ]\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) -> Self:\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 get_args(RsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for RSA encryption. Allowed sizes are {get_args(RsaKeySize)}.\"\n raise ValueError(msg)\n\n if self.encryption_algorithm == \"ECDSA\" and self.key_size not in get_args(EcdsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for ECDSA encryption. Allowed sizes are {get_args(EcdsaKeySize)}.\"\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner login\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner motd\", revision=1)]\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.VerifyHardwareEntropy","title":"VerifyHardwareEntropy","text":"Verifies hardware entropy generation is enabled on device. Expected Results Success: The test will pass if hardware entropy generation is enabled. Failure: The test will fail if hardware entropy generation is not enabled. Examples anta.tests.security:\n - VerifyHardwareEntropy:\n Source code in anta/tests/security.py class VerifyHardwareEntropy(AntaTest):\n \"\"\"Verifies hardware entropy generation is enabled on device.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware entropy generation is enabled.\n * Failure: The test will fail if hardware entropy generation is not enabled.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyHardwareEntropy:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management security\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareEntropy.\"\"\"\n command_output = self.instance_commands[0].json_output\n\n # Check if hardware entropy generation is enabled.\n if not command_output.get(\"hardwareEntropyEnabled\"):\n self.result.is_failure(\"Hardware entropy generation is disabled.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.security/#anta.tests.security.VerifyIPSecConnHealth","title":"VerifyIPSecConnHealth","text":"Verifies all IPv4 security connections. Expected Results Success: The test will pass if all the IPv4 security connections are established in all vrf. Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf. Examples anta.tests.security:\n - VerifyIPSecConnHealth:\n Source code in anta/tests/security.py class VerifyIPSecConnHealth(AntaTest):\n \"\"\"Verifies all IPv4 security connections.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the IPv4 security connections are established in all vrf.\n * Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyIPSecConnHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip security connection vrf all\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyIPSecConnHealth.\"\"\"\n self.result.is_success()\n failure_conn = []\n command_output = self.instance_commands[0].json_output[\"connections\"]\n\n # Check if IP security connection is configured\n if not command_output:\n self.result.is_failure(\"No IPv4 security connection configured.\")\n return\n\n # Iterate over all ipsec connections\n for conn_data in command_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n failure_conn.append(f\"source:{source} destination:{destination} vrf:{vrf}\")\n if failure_conn:\n failure_msg = \"\\n\".join(failure_conn)\n self.result.is_failure(f\"The following IPv4 security connections are not established:\\n{failure_msg}.\")\n"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip access-lists {acl}\", revision=1)]\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) 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, acl in zip(self.instance_commands, self.inputs.ipv4_access_lists):\n # Collecting input ACL details\n acl_name = command_output.params.acl\n # Retrieve the expected entries from the inputs\n acl_entries = acl.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 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\", revision=1)]\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 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\", revision=1)]\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 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 try:\n line = next(line for line in command_output.split(\"\\n\") if line.startswith(\"SSHD status\"))\n except StopIteration:\n self.result.is_failure(\"Could not find SSH status in returned output.\")\n return\n status = line.split()[-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.VerifySpecificIPSecConn","title":"VerifySpecificIPSecConn","text":"Verifies the state of IPv4 security connections for a specified peer. It optionally allows for the verification of a specific path for a peer by providing source and destination addresses. If these addresses are not provided, it will verify all paths for the specified peer. Expected Results Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF. Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF. Examples anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n Source code in anta/tests/security.py class VerifySpecificIPSecConn(AntaTest):\n \"\"\"Verifies the state of IPv4 security connections for a specified peer.\n\n It optionally allows for the verification of a specific path for a peer by providing source and destination addresses.\n If these addresses are not provided, it will verify all paths for the specified peer.\n\n Expected Results\n ----------------\n * Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF.\n * Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n ```\n \"\"\"\n\n description = \"Verifies IPv4 security connections for a peer.\"\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip security connection vrf {vrf} path peer {peer}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificIPSecConn test.\"\"\"\n\n ip_security_connections: list[IPSecPeers]\n \"\"\"List of IP4v security peers.\"\"\"\n\n class IPSecPeers(BaseModel):\n \"\"\"Details of IPv4 security peers.\"\"\"\n\n peer: IPv4Address\n \"\"\"IPv4 address of the peer.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"Optional VRF for the IP security peer.\"\"\"\n\n connections: list[IPSecConn] | None = None\n \"\"\"Optional list of IPv4 security connections of a peer.\"\"\"\n\n class IPSecConn(BaseModel):\n \"\"\"Details of IPv4 security connections for a peer.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of the connection.\"\"\"\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of the connection.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input IP Sec connection.\"\"\"\n return [template.render(peer=conn.peer, vrf=conn.vrf) for conn in self.inputs.ip_security_connections]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificIPSecConn.\"\"\"\n self.result.is_success()\n for command_output, input_peer in zip(self.instance_commands, self.inputs.ip_security_connections):\n conn_output = command_output.json_output[\"connections\"]\n peer = command_output.params.peer\n vrf = command_output.params.vrf\n conn_input = input_peer.connections\n\n # Check if IPv4 security connection is configured\n if not conn_output:\n self.result.is_failure(f\"No IPv4 security connection configured for peer `{peer}`.\")\n continue\n\n # If connection details are not provided then check all connections of a peer\n if conn_input is None:\n for conn_data in conn_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source} destination:{destination} vrf:{vrf}` for peer `{peer}` is `Established` \"\n f\"but found `{state}` instead.\"\n )\n continue\n\n # Create a dictionary of existing connections for faster lookup\n existing_connections = {\n (conn_data.get(\"saddr\"), conn_data.get(\"daddr\"), conn_data.get(\"tunnelNs\")): next(iter(conn_data[\"pathDict\"].values()))\n for conn_data in conn_output.values()\n }\n for connection in conn_input:\n source_input = str(connection.source_address)\n destination_input = str(connection.destination_address)\n\n if (source_input, destination_input, vrf) in existing_connections:\n existing_state = existing_connections[(source_input, destination_input, vrf)]\n if existing_state != \"Established\":\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` \"\n f\"for peer `{peer}` is `Established` but found `{existing_state}` instead.\"\n )\n else:\n self.result.is_failure(\n f\"IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` for peer `{peer}` is not found.\"\n )\n"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"Inputs","text":"Name Type Description Default ip_security_connections list[IPSecPeers] List of IP4v security peers. -"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecPeers","text":"Name Type Description Default peer IPv4Address IPv4 address of the peer. - vrf str Optional VRF for the IP security peer. 'default' connections list[IPSecConn] | None Optional list of IPv4 security connections of a peer. None"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecConn","text":"Name Type Description Default source_address IPv4Address Source IPv4 address of the connection. - destination_address IPv4Address Destination IPv4 address of the connection. -"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management telnet\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=1)]\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. This test performs the following checks for each specified DNS Server: Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF. Ensuring an appropriate priority level. 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 any of the following conditions are met: The provided DNS server is not configured. The provided DNS server with designated VRF and priority does not match the expected information. 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 This test performs the following checks for each specified DNS Server:\n\n 1. Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF.\n 2. Ensuring an appropriate priority level.\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 any of the following conditions are met:\n - The provided DNS server is not configured.\n - The provided DNS server with designated VRF and priority does not match the expected information.\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 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\", revision=1)]\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 DnsServer: ClassVar[type[DnsServer]] = DnsServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyDNSServers.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"nameServerConfigs\"]\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 # Check if the DNS server is configured with specified VRF.\n if (output := get_dict_superset(command_output, input_dict)) is None:\n self.result.is_failure(f\"{server} - Not configured\")\n continue\n\n # Check if the DNS server priority matches with expected.\n if output[\"priority\"] != priority:\n self.result.is_failure(f\"{server} - Incorrect priority; Priority: {output['priority']}\")\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.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 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 categories: ClassVar[list[str]] = [\"services\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hostname\", revision=1)]\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.services/#input-models","title":"Input models","text":""},{"location":"api/tests.services/#anta.input_models.services.DnsServer","title":"DnsServer","text":"Model for a DNS server configuration. Name Type Description Default server_address IPv4Address | IPv6Address The IPv4 or IPv6 address of the DNS server. - vrf str The VRF instance in which the DNS server resides. Defaults to 'default'. 'default' priority int The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest. Field(ge=0, le=4) Source code in anta/input_models/services.py class DnsServer(BaseModel):\n \"\"\"Model for a DNS server configuration.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: IPv4Address | IPv6Address\n \"\"\"The IPv4 or IPv6 address of the DNS server.\"\"\"\n vrf: str = \"default\"\n \"\"\"The VRF instance in which the DNS server resides. Defaults to 'default'.\"\"\"\n priority: int = Field(ge=0, le=4)\n \"\"\"The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the DnsServer for reporting.\n\n Examples\n --------\n Server 10.0.0.1 (VRF: default, Priority: 1)\n \"\"\"\n return f\"Server {self.server_address} (VRF: {self.vrf}, Priority: {self.priority})\"\n"},{"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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP contact is configured.\n if not (contact := get_value(self.instance_commands[0].json_output, \"contact.contact\")):\n self.result.is_failure(\"SNMP contact is not configured.\")\n return\n\n # Verifies the expected SNMP contact.\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.VerifySnmpErrorCounters","title":"VerifySnmpErrorCounters","text":"Verifies the SNMP error counters. By default, all error counters will be checked for any non-zero values. An optional list of specific error counters can be provided for granular testing. Expected Results Success: The test will pass if the SNMP error counter(s) are zero/None. Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured. Examples ```yaml anta.tests.snmp: - VerifySnmpErrorCounters: error_counters: - inVersionErrs - inBadCommunityNames Source code in anta/tests/snmp.py class VerifySnmpErrorCounters(AntaTest):\n \"\"\"Verifies the SNMP error counters.\n\n By default, all error counters will be checked for any non-zero values.\n An optional list of specific error counters can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP error counter(s) are zero/None.\n * Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpErrorCounters:\n error_counters:\n - inVersionErrs\n - inBadCommunityNames\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpErrorCounters test.\"\"\"\n\n error_counters: list[SnmpErrorCounter] | None = None\n \"\"\"Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpErrorCounters.\"\"\"\n error_counters = self.inputs.error_counters\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (snmp_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP error counters not provided, It will check all the error counters.\n if not error_counters:\n error_counters = list(get_args(SnmpErrorCounter))\n\n error_counters_not_ok = {counter: value for counter in error_counters if (value := snmp_counters.get(counter))}\n\n # Check if any failures\n if not error_counters_not_ok:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP error counters are not found or have non-zero error counters:\\n{error_counters_not_ok}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpErrorCounters-attributes","title":"Inputs","text":"Name Type Description Default error_counters list[SnmpErrorCounter] | None Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters. None"},{"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 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\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP location is configured.\n if not (location := get_value(self.instance_commands[0].json_output, \"location.location\")):\n self.result.is_failure(\"SNMP location is not configured.\")\n return\n\n # Verifies the expected SNMP location.\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.VerifySnmpPDUCounters","title":"VerifySnmpPDUCounters","text":"Verifies the SNMP PDU counters. By default, all SNMP PDU counters will be checked for any non-zero values. An optional list of specific SNMP PDU(s) can be provided for granular testing. Expected Results Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero. Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found. Examples anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n Source code in anta/tests/snmp.py class VerifySnmpPDUCounters(AntaTest):\n \"\"\"Verifies the SNMP PDU counters.\n\n By default, all SNMP PDU counters will be checked for any non-zero values.\n An optional list of specific SNMP PDU(s) can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero.\n * Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpPDUCounters test.\"\"\"\n\n pdus: list[SnmpPdu] | None = None\n \"\"\"Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpPDUCounters.\"\"\"\n snmp_pdus = self.inputs.pdus\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (pdu_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP PDUs not provided, It will check all the update error counters.\n if not snmp_pdus:\n snmp_pdus = list(get_args(SnmpPdu))\n\n failures = {pdu: value for pdu in snmp_pdus if (value := pdu_counters.get(pdu, \"Not Found\")) == \"Not Found\" or value == 0}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP PDU counters are not found or have zero PDU counters:\\n{failures}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpPDUCounters-attributes","title":"Inputs","text":"Name Type Description Default pdus list[SnmpPdu] | None Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters. None"},{"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 description = \"Verifies if the SNMP agent is enabled.\"\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show extensions\", revision=2),\n AntaCommand(command=\"show boot-extensions\", revision=1),\n ]\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 description = \"Verifies the EOS version of the device.\"\n categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree blockedports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree counters\", revision=1)]\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 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\", revision=1)]\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 vlan_id = command.params.vlan\n if not (topologies := get_value(command.json_output, \"topologies\")):\n not_configured.append(vlan_id)\n else:\n interfaces_not_forwarding = []\n for value in topologies.values():\n if vlan_id and 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 forwarding 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show spanning-tree vlan {vlan}\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree root detail\", revision=1)]\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.stp/#anta.tests.stp.VerifyStpTopologyChanges","title":"VerifyStpTopologyChanges","text":"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold. Expected Results Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold. Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold, indicating potential instability in the topology. Examples anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n Source code in anta/tests/stp.py class VerifyStpTopologyChanges(AntaTest):\n \"\"\"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold.\n * Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold,\n indicating potential instability in the topology.\n\n Examples\n --------\n ```yaml\n anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree topology status detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStpTopologyChanges test.\"\"\"\n\n threshold: int\n \"\"\"The threshold number of changes in the STP topology.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStpTopologyChanges.\"\"\"\n failures: dict[str, Any] = {\"topologies\": {}}\n\n command_output = self.instance_commands[0].json_output\n stp_topologies = command_output.get(\"topologies\", {})\n\n # verifies all available topologies except the \"NoStp\" topology.\n stp_topologies.pop(\"NoStp\", None)\n\n # Verify the STP topology(s).\n if not stp_topologies:\n self.result.is_failure(\"STP is not configured.\")\n return\n\n # Verifies the number of changes across all interfaces\n for topology, topology_details in stp_topologies.items():\n interfaces = {\n interface: {\"Number of changes\": num_of_changes}\n for interface, details in topology_details.get(\"interfaces\", {}).items()\n if (num_of_changes := details.get(\"numChanges\")) > self.inputs.threshold\n }\n if interfaces:\n failures[\"topologies\"][topology] = interfaces\n\n if failures[\"topologies\"]:\n self.result.is_failure(f\"The following STP topologies are not configured or number of changes not within the threshold:\\n{failures}\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.stp/#anta.tests.stp.VerifyStpTopologyChanges-attributes","title":"Inputs","text":"Name Type Description Default threshold int The threshold number of changes in the STP topology. -"},{"location":"api/tests.stun/","title":"STUN","text":""},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient","title":"VerifyStunClient","text":"Verifies STUN client settings, including local IP/port and optionally public IP/port. Expected Results Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port. Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect. Examples anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n Source code in anta/tests/stun.py class VerifyStunClient(AntaTest):\n \"\"\"Verifies STUN client settings, including local IP/port and optionally public IP/port.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port.\n * Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show stun client translations {source_address} {source_port}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStunClient test.\"\"\"\n\n stun_clients: list[ClientAddress]\n\n class ClientAddress(BaseModel):\n \"\"\"Source and public address/port details of STUN client.\"\"\"\n\n source_address: IPv4Address\n \"\"\"IPv4 source address of STUN client.\"\"\"\n source_port: Port = 4500\n \"\"\"Source port number for STUN client.\"\"\"\n public_address: IPv4Address | None = None\n \"\"\"Optional IPv4 public address of STUN client.\"\"\"\n public_port: Port | None = None\n \"\"\"Optional public port number for STUN client.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each STUN translation.\"\"\"\n return [template.render(source_address=client.source_address, source_port=client.source_port) for client in self.inputs.stun_clients]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunClient.\"\"\"\n self.result.is_success()\n\n # Iterate over each command output and corresponding client input\n for command, client_input in zip(self.instance_commands, self.inputs.stun_clients):\n bindings = command.json_output[\"bindings\"]\n source_address = str(command.params.source_address)\n source_port = command.params.source_port\n\n # If no bindings are found for the STUN client, mark the test as a failure and continue with the next client\n if not bindings:\n self.result.is_failure(f\"STUN client transaction for source `{source_address}:{source_port}` is not found.\")\n continue\n\n # Extract the public address and port from the client input\n public_address = client_input.public_address\n public_port = client_input.public_port\n\n # Extract the transaction ID from the bindings\n transaction_id = next(iter(bindings.keys()))\n\n # Prepare the actual and expected STUN data for comparison\n actual_stun_data = {\n \"source ip\": get_value(bindings, f\"{transaction_id}.sourceAddress.ip\"),\n \"source port\": get_value(bindings, f\"{transaction_id}.sourceAddress.port\"),\n }\n expected_stun_data = {\"source ip\": source_address, \"source port\": source_port}\n\n # If public address is provided, add it to the actual and expected STUN data\n if public_address is not None:\n actual_stun_data[\"public ip\"] = get_value(bindings, f\"{transaction_id}.publicAddress.ip\")\n expected_stun_data[\"public ip\"] = str(public_address)\n\n # If public port is provided, add it to the actual and expected STUN data\n if public_port is not None:\n actual_stun_data[\"public port\"] = get_value(bindings, f\"{transaction_id}.publicAddress.port\")\n expected_stun_data[\"public port\"] = public_port\n\n # If the actual STUN data does not match the expected STUN data, mark the test as failure\n if actual_stun_data != expected_stun_data:\n failed_log = get_failed_logs(expected_stun_data, actual_stun_data)\n self.result.is_failure(f\"For STUN source `{source_address}:{source_port}`:{failed_log}\")\n"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"ClientAddress","text":"Name Type Description Default source_address IPv4Address IPv4 source address of STUN client. - source_port Port Source port number for STUN client. 4500 public_address IPv4Address | None Optional IPv4 public address of STUN client. None public_port Port | None Optional public port number for STUN client. None"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunServer","title":"VerifyStunServer","text":"Verifies the STUN server status is enabled and running. Expected Results Success: The test will pass if the STUN server status is enabled and running. Failure: The test will fail if the STUN server is disabled or not running. Examples anta.tests.stun:\n - VerifyStunServer:\n Source code in anta/tests/stun.py class VerifyStunServer(AntaTest):\n \"\"\"Verifies the STUN server status is enabled and running.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN server status is enabled and running.\n * Failure: The test will fail if the STUN server is disabled or not running.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunServer:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show stun server status\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunServer.\"\"\"\n command_output = self.instance_commands[0].json_output\n status_disabled = not command_output.get(\"enabled\")\n not_running = command_output.get(\"pid\") == 0\n\n if status_disabled and not_running:\n self.result.is_failure(\"STUN server status is disabled and not running.\")\n elif status_disabled:\n self.result.is_failure(\"STUN server status is disabled.\")\n elif not_running:\n self.result.is_failure(\"STUN server is not running.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.system/","title":"System","text":""},{"location":"api/tests.system/#tests","title":"Tests","text":""},{"location":"api/tests.system/#anta.tests.system.VerifyAgentLogs","title":"VerifyAgentLogs","text":"Verifies there are no agent crash reports. 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 there are no agent crash reports.\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show processes top once\", revision=1)]\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 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\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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.VerifyNTPAssociations","title":"VerifyNTPAssociations","text":"Verifies the Network Time Protocol (NTP) associations. Expected Results Success: The test will pass if the Primary NTP server (marked as preferred) has the condition \u2018sys.peer\u2019 and all other NTP servers have the condition \u2018candidate\u2019. Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition \u2018sys.peer\u2019 or if any other NTP server does not have the condition \u2018candidate\u2019. Examples anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n Source code in anta/tests/system.py class VerifyNTPAssociations(AntaTest):\n \"\"\"Verifies the Network Time Protocol (NTP) associations.\n\n Expected Results\n ----------------\n * Success: The test will pass if the Primary NTP server (marked as preferred) has the condition 'sys.peer' and\n all other NTP servers have the condition 'candidate'.\n * Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition 'sys.peer' or\n if any other NTP server does not have the condition 'candidate'.\n\n Examples\n --------\n ```yaml\n anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ntp associations\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyNTPAssociations test.\"\"\"\n\n ntp_servers: list[NTPServer]\n \"\"\"List of NTP servers.\"\"\"\n NTPServer: ClassVar[type[NTPServer]] = NTPServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyNTPAssociations.\"\"\"\n self.result.is_success()\n\n if not (peers := get_value(self.instance_commands[0].json_output, \"peers\")):\n self.result.is_failure(\"No NTP peers configured\")\n return\n\n # Iterate over each NTP server.\n for ntp_server in self.inputs.ntp_servers:\n server_address = str(ntp_server.server_address)\n\n # We check `peerIpAddr` in the peer details - covering IPv4Address input, or the peer key - covering Hostname input.\n matching_peer = next((peer for peer, peer_details in peers.items() if (server_address in {peer_details[\"peerIpAddr\"], peer})), None)\n\n if not matching_peer:\n self.result.is_failure(f\"{ntp_server} - Not configured\")\n continue\n\n # Collecting the expected/actual NTP peer details.\n exp_condition = \"sys.peer\" if ntp_server.preferred else \"candidate\"\n exp_stratum = ntp_server.stratum\n act_condition = get_value(peers[matching_peer], \"condition\")\n act_stratum = get_value(peers[matching_peer], \"stratumLevel\")\n\n if act_condition != exp_condition or act_stratum != exp_stratum:\n self.result.is_failure(f\"{ntp_server} - Bad association; Condition: {act_condition}, Stratum: {act_stratum}\")\n"},{"location":"api/tests.system/#anta.tests.system.VerifyNTPAssociations-attributes","title":"Inputs","text":"Name Type Description Default ntp_servers list[NTPServer] List of NTP servers. -"},{"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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show reload cause\", revision=1)]\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 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 description = \"Verifies the device uptime.\"\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show uptime\", revision=1)]\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.system/#input-models","title":"Input models","text":""},{"location":"api/tests.system/#anta.input_models.system.NTPServer","title":"NTPServer","text":"Model for a NTP server. Name Type Description Default server_address Hostname | IPv4Address The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name. For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output. - preferred bool Optional preferred for NTP server. If not provided, it defaults to `False`. False stratum int NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized. Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state. Field(ge=0, le=16) Source code in anta/input_models/system.py class NTPServer(BaseModel):\n \"\"\"Model for a NTP server.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: Hostname | IPv4Address\n \"\"\"The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration\n of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name.\n For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output.\"\"\"\n preferred: bool = False\n \"\"\"Optional preferred for NTP server. If not provided, it defaults to `False`.\"\"\"\n stratum: int = Field(ge=0, le=16)\n \"\"\"NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized.\n Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Representation of the NTPServer model.\"\"\"\n return f\"{self.server_address} (Preferred: {self.preferred}, Stratum: {self.stratum})\"\n"},{"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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\", revision=1)]\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 description = \"Verifies the Vxlan1 interface status.\"\n categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 there are no VXLAN config-sanity inconsistencies. 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 there are no VXLAN config-sanity inconsistencies.\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan config-sanity\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vni\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vtep\", revision=1)]\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[\n str, AfterValidator(aaa_group_prefix)\n]\n"},{"location":"api/types/#anta.custom_types.Afi","title":"Afi module-attribute","text":"Afi = Literal[\n \"ipv4\",\n \"ipv6\",\n \"vpn-ipv4\",\n \"vpn-ipv6\",\n \"evpn\",\n \"rt-membership\",\n \"path-selection\",\n \"link-state\",\n]\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.BfdProtocol","title":"BfdProtocol module-attribute","text":"BfdProtocol = Literal[\n \"bgp\",\n \"isis\",\n \"lag\",\n \"ospf\",\n \"ospfv3\",\n \"pim\",\n \"route-input\",\n \"static-bfd\",\n \"static-route\",\n \"vrrp\",\n \"vxlan\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpDropStats","title":"BgpDropStats module-attribute","text":"BgpDropStats = Literal[\n \"inDropAsloop\",\n \"inDropClusterIdLoop\",\n \"inDropMalformedMpbgp\",\n \"inDropOrigId\",\n \"inDropNhLocal\",\n \"inDropNhAfV6\",\n \"prefixDroppedMartianV4\",\n \"prefixDroppedMaxRouteLimitViolatedV4\",\n \"prefixDroppedMartianV6\",\n \"prefixDroppedMaxRouteLimitViolatedV6\",\n \"prefixLuDroppedV4\",\n \"prefixLuDroppedMartianV4\",\n \"prefixLuDroppedMaxRouteLimitViolatedV4\",\n \"prefixLuDroppedV6\",\n \"prefixLuDroppedMartianV6\",\n \"prefixLuDroppedMaxRouteLimitViolatedV6\",\n \"prefixEvpnDroppedUnsupportedRouteType\",\n \"prefixBgpLsDroppedReceptionUnsupported\",\n \"outDropV4LocalAddr\",\n \"outDropV6LocalAddr\",\n \"prefixVpnIpv4DroppedImportMatchFailure\",\n \"prefixVpnIpv4DroppedMaxRouteLimitViolated\",\n \"prefixVpnIpv6DroppedImportMatchFailure\",\n \"prefixVpnIpv6DroppedMaxRouteLimitViolated\",\n \"prefixEvpnDroppedImportMatchFailure\",\n \"prefixEvpnDroppedMaxRouteLimitViolated\",\n \"prefixRtMembershipDroppedLocalAsReject\",\n \"prefixRtMembershipDroppedMaxRouteLimitViolated\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpUpdateError","title":"BgpUpdateError module-attribute","text":"BgpUpdateError = Literal[\n \"inUpdErrWithdraw\",\n \"inUpdErrIgnore\",\n \"inUpdErrDisableAfiSafi\",\n \"disabledAfiSafi\",\n \"lastUpdErrTime\",\n]\n"},{"location":"api/types/#anta.custom_types.EcdsaKeySize","title":"EcdsaKeySize module-attribute","text":"EcdsaKeySize = Literal[256, 384, 512]\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[\n \"acl\",\n \"arp-inspection\",\n \"bpduguard\",\n \"dot1x-session-replace\",\n \"hitless-reload-down\",\n \"lacp-rate-limit\",\n \"link-flap\",\n \"no-internal-vlan\",\n \"portchannelguard\",\n \"portsec\",\n \"tapagg\",\n \"uplink-failure-detection\",\n]\n"},{"location":"api/types/#anta.custom_types.EthernetInterface","title":"EthernetInterface module-attribute","text":"EthernetInterface = Annotated[\n str,\n Field(pattern=\"^Ethernet[0-9]+(\\\\/[0-9]+)*$\"),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\n"},{"location":"api/types/#anta.custom_types.Hostname","title":"Hostname module-attribute","text":"Hostname = Annotated[\n str, Field(pattern=REGEXP_TYPE_HOSTNAME)\n]\n"},{"location":"api/types/#anta.custom_types.Interface","title":"Interface module-attribute","text":"Interface = Annotated[\n str,\n Field(pattern=REGEXP_TYPE_EOS_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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[\n str,\n BeforeValidator(\n bgp_multiprotocol_capabilities_abbreviations\n ),\n]\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.PortChannelInterface","title":"PortChannelInterface module-attribute","text":"PortChannelInterface = Annotated[\n str,\n Field(pattern=REGEX_TYPE_PORTCHANNEL),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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.REGEXP_BGP_IPV4_MPLS_LABELS","title":"REGEXP_BGP_IPV4_MPLS_LABELS module-attribute","text":"REGEXP_BGP_IPV4_MPLS_LABELS = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?label(s)?)\\\\b\"\n)\n Match IPv4 MPLS Labels."},{"location":"api/types/#anta.custom_types.REGEXP_BGP_L2VPN_AFI","title":"REGEXP_BGP_L2VPN_AFI module-attribute","text":"REGEXP_BGP_L2VPN_AFI = \"\\\\b(l2[\\\\s\\\\-]?vpn[\\\\s\\\\-]?evpn)\\\\b\"\n Match L2VPN EVPN AFI."},{"location":"api/types/#anta.custom_types.REGEXP_EOS_BLACKLIST_CMDS","title":"REGEXP_EOS_BLACKLIST_CMDS module-attribute","text":"REGEXP_EOS_BLACKLIST_CMDS = [\n \"^reload.*\",\n \"^conf\\\\w*\\\\s*(terminal|session)*\",\n \"^wr\\\\w*\\\\s*\\\\w+\",\n]\n List of regular expressions to blacklist from eos commands."},{"location":"api/types/#anta.custom_types.REGEXP_INTERFACE_ID","title":"REGEXP_INTERFACE_ID module-attribute","text":"REGEXP_INTERFACE_ID = '\\\\d+(\\\\/\\\\d+)*(\\\\.\\\\d+)?'\n Match Interface ID lilke 1/1.1."},{"location":"api/types/#anta.custom_types.REGEXP_PATH_MARKERS","title":"REGEXP_PATH_MARKERS module-attribute","text":"REGEXP_PATH_MARKERS = '[\\\\\\\\\\\\/\\\\s]'\n Match directory path from string."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_EOS_INTERFACE","title":"REGEXP_TYPE_EOS_INTERFACE module-attribute","text":"REGEXP_TYPE_EOS_INTERFACE = \"^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\\\\/[0-9]+)*(\\\\.[0-9]+)?$\"\n Match EOS interface types like Ethernet1/1, Vlan1, Loopback1, etc."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_HOSTNAME","title":"REGEXP_TYPE_HOSTNAME module-attribute","text":"REGEXP_TYPE_HOSTNAME = \"^(([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 Match hostname like my-hostname, my-hostname-1, my-hostname-1-2."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_VXLAN_SRC_INTERFACE","title":"REGEXP_TYPE_VXLAN_SRC_INTERFACE module-attribute","text":"REGEXP_TYPE_VXLAN_SRC_INTERFACE = \"^(Loopback)([0-9]|[1-9][0-9]{1,2}|[1-7][0-9]{3}|8[01][0-9]{2}|819[01])$\"\n Match Vxlan source interface like Loopback10."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_MPLS_VPN","title":"REGEX_BGP_IPV4_MPLS_VPN module-attribute","text":"REGEX_BGP_IPV4_MPLS_VPN = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?vpn)\\\\b\"\n)\n Match IPv4 MPLS VPN."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_UNICAST","title":"REGEX_BGP_IPV4_UNICAST module-attribute","text":"REGEX_BGP_IPV4_UNICAST = (\n \"\\\\b(ipv4[\\\\s\\\\-]?uni[\\\\s\\\\-]?cast)\\\\b\"\n)\n Match IPv4 Unicast."},{"location":"api/types/#anta.custom_types.REGEX_TYPE_PORTCHANNEL","title":"REGEX_TYPE_PORTCHANNEL module-attribute","text":"REGEX_TYPE_PORTCHANNEL = '^Port-Channel[0-9]{1,6}$'\n Match Port Channel interface like Port-Channel5."},{"location":"api/types/#anta.custom_types.RegexString","title":"RegexString module-attribute","text":"RegexString = Annotated[str, AfterValidator(validate_regex)]\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[\n \"unicast\", \"multicast\", \"labeled-unicast\", \"sr-te\"\n]\n"},{"location":"api/types/#anta.custom_types.SnmpErrorCounter","title":"SnmpErrorCounter module-attribute","text":"SnmpErrorCounter = Literal[\n \"inVersionErrs\",\n \"inBadCommunityNames\",\n \"inBadCommunityUses\",\n \"inParseErrs\",\n \"outTooBigErrs\",\n \"outNoSuchNameErrs\",\n \"outBadValueErrs\",\n \"outGeneralErrs\",\n]\n"},{"location":"api/types/#anta.custom_types.SnmpPdu","title":"SnmpPdu module-attribute","text":"SnmpPdu = Literal[\n \"inGetPdus\",\n \"inGetNextPdus\",\n \"inSetPdus\",\n \"outGetResponsePdus\",\n \"outTrapPdus\",\n]\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[\n str,\n Field(pattern=REGEXP_TYPE_VXLAN_SRC_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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(\n value: str,\n) -> str\n Abbreviations for different BGP multiprotocol capabilities. Examples IPv4 Unicast L2vpnEVPN ipv4 MPLS Labels ipv4Mplsvpn 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 REGEXP_BGP_L2VPN_AFI: \"l2VpnEvpn\",\n REGEXP_BGP_IPV4_MPLS_LABELS: \"ipv4MplsLabels\",\n REGEX_BGP_IPV4_MPLS_VPN: \"ipv4MplsVpn\",\n REGEX_BGP_IPV4_UNICAST: \"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(REGEXP_INTERFACE_ID)\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 return next((f\"{full_name}{intf_id}\" for alias, full_name in alias_map.items() if v.lower().startswith(alias)), 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 vlan -> Vlan loopback -> Loopback 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 v != \"\" and not v[0].isupper():\n return f\"{v[0].upper()}{v[1:]}\"\n return v\n"},{"location":"api/types/#anta.custom_types.validate_regex","title":"validate_regex","text":"validate_regex(value: str) -> str\n Validate that the input value is a valid regex format. Source code in anta/custom_types.py def validate_regex(value: str) -> str:\n \"\"\"Validate that the input value is a valid regex format.\"\"\"\n try:\n re.compile(value)\n except re.error as e:\n msg = f\"Invalid regex: {e}\"\n raise ValueError(msg) from e\n return value\n"},{"location":"cli/check/","title":"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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n --help Show this message and exit.\n"},{"location":"cli/debug/","title":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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":"Usage: 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.\n\n 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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"},{"location":"cli/debug/#example-of-multiple-arguments","title":"Example of multiple arguments","text":"Warning If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters. 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":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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_2024-04-09_15_56_19]\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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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 [DEPRECATED] Ensure devices have 'aaa authorization\n exec default local' configured (required for SCP on\n EOS). THIS WILL CHANGE THE CONFIGURATION OF YOUR\n 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. Warning ANTA can automatically configure aaa authorization exec default local using the anta exec collect-tech-support --configure option but this option is deprecated and will be removed in ANTA 2.0.0. 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":"The ANTA CLI offers multiple commands to access data from your local inventory."},{"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 (visit this page to learn more about tags). The --connected option allows to display only the devices where a connection has been established."},{"location":"cli/get-inventory-information/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 By default, anta get inventory only provides information that doesn\u2019t rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, use the --connected option."},{"location":"cli/get-inventory-information/#example","title":"Example","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 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={'DC1-SPINE1', 'DC1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC1-SPINE2': AsyncEOSDevice(\n name='DC1-SPINE2',\n tags={'DC1', 'SPINE', 'DC1-SPINE2'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE1': AsyncEOSDevice(\n name='DC2-SPINE1',\n tags={'DC2', 'DC2-SPINE1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE2': AsyncEOSDevice(\n name='DC2-SPINE2',\n tags={'DC2', 'DC2-SPINE2', 'SPINE'},\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=False,\n insecure=False\n )\n}\n"},{"location":"cli/inv-from-ansible/","title":"Inventory from Ansible","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\n NOTE: This command does not support inline vaulted variables. Make sure to\n comment them out.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var:\n ANTA_INVENTORY; required]\n --overwrite Do not prompt when overriding current inventory\n [env var: ANTA_GET_FROM_ANSIBLE_OVERWRITE]\n -g, --ansible-group TEXT Ansible group to filter\n --ansible-inventory FILE Path to your ansible inventory file to read\n [required]\n --help Show this message and exit.\n Warnings anta get from-ansible does not support inline vaulted variables, comment them out to generate your inventory. If the vaulted variable is necessary to build the inventory (e.g. ansible_host), it needs to be unvaulted for from-ansible command to work.\u201d The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritance 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: ---\nall:\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 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"},{"location":"cli/inv-from-cvp/","title":"Inventory from CVP","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. Info The current implementation only works with on-premises CloudVision instances, not with CloudVision as a Service (CVaaS)."},{"location":"cli/inv-from-cvp/#command-overview","title":"Command overview","text":"Usage: anta get from-cvp [OPTIONS]\n\n Build ANTA inventory from CloudVision.\n\n NOTE: Only username/password authentication is supported for on-premises CloudVision instances.\n Token authentication for both on-premises and CloudVision as a Service (CVaaS) is not supported.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var: ANTA_INVENTORY;\n required]\n --overwrite Do not prompt when overriding current inventory [env\n var: ANTA_GET_FROM_CVP_OVERWRITE]\n -host, --host TEXT CloudVision instance FQDN or IP [required]\n -u, --username TEXT CloudVision username [required]\n -p, --password TEXT CloudVision password [required]\n -c, --container TEXT CloudVision container where devices are configured\n --ignore-cert By default connection to CV will use HTTPS\n certificate, set this flag to disable it [env var:\n ANTA_GET_FROM_CVP_IGNORE_CERT]\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":"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 report Table report JSON report Custom template report CSV report Markdown report "},{"location":"cli/nrfu/#nrfu-command-overview","title":"NRFU Command overview","text":"Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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. Refer to the dedicated page for more information."},{"location":"cli/nrfu/#device-and-test-filtering","title":"Device and test filtering","text":"Options --device and --test can be used to target one or multiple devices and/or tests to run in your environment. The options can be repeated. Example: anta nrfu --device leaf1a --device leaf1b --test VerifyUptime --test VerifyReloadCause."},{"location":"cli/nrfu/#hide-results","title":"Hide results","text":"Option --hide can be used to hide test results in the output or report file based on their status. The option can be repeated. Example: anta nrfu --hide error --hide skipped."},{"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":"Usage: anta nrfu text [OPTIONS]\n\n ANTA command to check network states with text result.\n\nOptions:\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example","title":"Example","text":"anta nrfu --device DC1-LEAF1A text\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":"Usage: anta nrfu table [OPTIONS]\n\n ANTA command to check network states with table result.\n\nOptions:\n --group-by [device|test] Group result by test or device.\n --help Show this message and exit.\n 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 --device spine1 table\n anta nrfu --test VerifyZeroTouch table\n "},{"location":"cli/nrfu/#performing-nrfu-with-json-rendering","title":"Performing NRFU with JSON rendering","text":"The JSON rendering command in NRFU testing will generate an output of all test results in JSON format."},{"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 JSON 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. If specified, no output will be displayed in the terminal. This is useful for further processing or integration with other tools."},{"location":"cli/nrfu/#example_1","title":"Example","text":"anta nrfu --tags LEAF json\n"},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-csv-file","title":"Performing NRFU and saving results in a CSV file","text":"The csv command in NRFU testing is useful for generating a CSV file with all tests result. This file can be easily analyzed and filtered by operator for reporting purposes."},{"location":"cli/nrfu/#command-overview_3","title":"Command overview","text":"anta nrfu csv --help\nUsage: anta nrfu csv [OPTIONS]\n\n ANTA command to check network states with CSV result.\n\nOptions:\n --csv-output FILE Path to save report as a CSV file [env var:\n ANTA_NRFU_CSV_CSV_OUTPUT]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_2","title":"Example","text":""},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-markdown-file","title":"Performing NRFU and saving results in a Markdown file","text":"The md-report command in NRFU testing generates a comprehensive Markdown report containing various sections, including detailed statistics for devices and test categories."},{"location":"cli/nrfu/#command-overview_4","title":"Command overview","text":"anta nrfu md-report --help\n\nUsage: anta nrfu md-report [OPTIONS]\n\n ANTA command to check network state with Markdown report.\n\nOptions:\n --md-output FILE Path to save the report as a Markdown file [env var:\n ANTA_NRFU_MD_REPORT_MD_OUTPUT; required]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_3","title":"Example","text":""},{"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_5","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_4","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/nrfu/#dry-run-mode","title":"Dry-run mode","text":"It is possible to run anta nrfu --dry-run to execute ANTA up to the point where it should communicate with the network to execute the tests. When using --dry-run, all inventory devices are assumed to be online. This can be useful to check how many tests would be run using the catalog and inventory. "},{"location":"cli/overview/","title":"Overview","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."},{"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 selected inventory devices.\n"},{"location":"cli/overview/#anta-environment-variables","title":"ANTA environment 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_CATALOG=tests.yml\n Then, run the CLI without options: anta nrfu\n Note All environment variables may not be needed for every commands. Refer to <command> --help for the comprehensive environment variables names. Below are the environment 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":"ANTA uses tags to define test-to-device mappings (tests run on devices with matching tags) and the --tags CLI option acts as a filter to execute specific test/device combinations."},{"location":"cli/tag-management/#defining-tags","title":"Defining tags","text":""},{"location":"cli/tag-management/#device-tags","title":"Device tags","text":"Device tags can be defined in the inventory: anta_inventory:\n hosts:\n - name: leaf1\n host: leaf1.anta.arista.com\n tags: [\"leaf\"]\n - name: leaf2\n host: leaf2.anta.arista.com\n tags: [\"leaf\"]\n - name: spine1\n host: spine1.anta.arista.com\n tags: [\"spine\"]\n Each device also has its own name automatically added as a tag: $ anta get inventory\nCurrent inventory content is:\n{\n 'leaf1': AsyncEOSDevice(\n name='leaf1',\n tags={'leaf', 'leaf1'}, <--\n [...]\n host='leaf1.anta.arista.com',\n [...]\n ),\n 'leaf2': AsyncEOSDevice(\n name='leaf2',\n tags={'leaf', 'leaf2'}, <--\n [...]\n host='leaf2.anta.arista.com',\n [...]\n ),\n 'spine1': AsyncEOSDevice(\n name='spine1',\n tags={'spine1', 'spine'}, <--\n [...]\n host='spine1.anta.arista.com',\n [...]\n )\n}\n"},{"location":"cli/tag-management/#test-tags","title":"Test tags","text":"Tags can be defined in the test catalog to restrict tests to tagged devices: anta.tests.system:\n - VerifyUptime:\n minimum: 10\n filters:\n tags: ['spine']\n - VerifyUptime:\n minimum: 9\n filters:\n tags: ['leaf']\n - VerifyReloadCause:\n filters:\n tags: ['spine', 'leaf']\n - VerifyCoredump:\n - VerifyAgentLogs:\n - VerifyCPUUtilization:\n - VerifyMemoryUtilization:\n - VerifyFileSystemUtilization:\n - VerifyNTP:\n\nanta.tests.mlag:\n - VerifyMlagStatus:\n filters:\n tags: ['leaf']\n\nanta.tests.interfaces:\n - VerifyL3MTU:\n mtu: 1500\n filters:\n tags: ['spine']\n A tag used to filter a test can also be a device name Use different input values for a specific test Leverage tags to define different input values for a specific test. See the VerifyUptime example above."},{"location":"cli/tag-management/#using-tags","title":"Using tags","text":"Command Description No --tags option Run all tests on all devices according to the tag definitions in your inventory and test catalog. Tests without tags are executed on all devices. --tags leaf Run all tests marked with the leaf tag on all devices configured with the leaf tag. All other tests are ignored. --tags leaf,spine Run all tests marked with the leaf tag on all devices configured with the leaf tag.Run all tests marked with the spine tag on all devices configured with the spine tag. All other tests are ignored."},{"location":"cli/tag-management/#examples","title":"Examples","text":"The following examples use the inventory and test catalog defined above."},{"location":"cli/tag-management/#no-tags-option","title":"No --tags option","text":"Tests without tags are run on all devices. Tests with tags will only run on devices with matching tags. $ anta nrfu table --group-by device\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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 27\n---------------------------------\n Summary per device\n\u250f\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\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 leaf1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 leaf2 \u2502 7 \u2502 1 \u2502 1 \u2502 0 \u2502 VerifyAgentLogs \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 spine1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u2514\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/#single-tag","title":"Single tag","text":"With a tag specified, only tests matching this tag will be run on matching devices. $ anta nrfu --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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (2 established)\nTotal number of selected tests: 6\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\n In this case, only leaf devices defined in the inventory are used to run tests marked with the leaf in the test catalog."},{"location":"cli/tag-management/#multiple-tags","title":"Multiple tags","text":"It is possible to use multiple tags using the --tags tag1,tag2 syntax. $ anta nrfu --tags leaf,spine 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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 15\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyL3MTU :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyL3MTU :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyReloadCause :: SUCCESS\nspine1 :: VerifyMlagStatus :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyL3MTU :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\n"},{"location":"cli/tag-management/#obtaining-all-configured-tags","title":"Obtaining all configured tags","text":"As most ANTA commands accommodate tag filtering, this command is useful for enumerating all tags configured in the inventory. Running the anta get tags command will return a list of all tags configured in the inventory."},{"location":"cli/tag-management/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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/tag-management/#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 \"leaf\",\n \"leaf1\",\n \"leaf2\",\n \"spine\",\n \"spine1\"\n]\n"}]} \ No newline at end of file diff --git a/main/sitemap.xml.gz b/main/sitemap.xml.gz index cf5ee0d0e..74b57bcd3 100644 Binary files a/main/sitemap.xml.gz and b/main/sitemap.xml.gz differ diff --git a/main/troubleshooting/index.html b/main/troubleshooting/index.html index d8d155e45..d8db962e8 100644 --- a/main/troubleshooting/index.html +++ b/main/troubleshooting/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2418,7 +2418,7 @@ Troubleshooting on EOS - + @@ -2429,7 +2429,7 @@ Troubleshooting on EOS - + @@ -2440,7 +2440,7 @@ Troubleshooting on EOS - + diff --git a/main/usage-inventory-catalog/index.html b/main/usage-inventory-catalog/index.html index e066d66c8..272bea518 100644 --- a/main/usage-inventory-catalog/index.html +++ b/main/usage-inventory-catalog/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2879,7 +2879,7 @@ Example script to merge catalogs - + @@ -2890,7 +2890,7 @@ Example script to merge catalogs - + @@ -2901,7 +2901,7 @@ Example script to merge catalogs - +
class VerifyFieldNotice44Resolution(AntaTest): +120
class VerifyFieldNotice44Resolution(AntaTest): """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. @@ -2525,15 +2521,11 @@ self.result.is_success() incorrect_aboot_version = ( - aboot_version.startswith("4.0.") - and int(aboot_version.split(".")[2]) < 7 - or aboot_version.startswith("4.1.") - and int(aboot_version.split(".")[2]) < 1 + (aboot_version.startswith("4.0.") and int(aboot_version.split(".")[2]) < 7) + or (aboot_version.startswith("4.1.") and int(aboot_version.split(".")[2]) < 1) or ( - aboot_version.startswith("6.0.") - and int(aboot_version.split(".")[2]) < 9 - or aboot_version.startswith("6.1.") - and int(aboot_version.split(".")[2]) < 7 + (aboot_version.startswith("6.0.") and int(aboot_version.split(".")[2]) < 9) + or (aboot_version.startswith("6.1.") and int(aboot_version.split(".")[2]) < 7) ) ) if incorrect_aboot_version: @@ -2600,7 +2592,11 @@ Source code in anta/tests/field_notices.py - 127 + 123 +124 +125 +126 +127 128 129 130 @@ -2666,11 +2662,7 @@ 190 191 192 -193 -194 -195 -196 -197class VerifyFieldNotice72Resolution(AntaTest): +193class VerifyFieldNotice72Resolution(AntaTest): """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 @@ -2834,7 +2826,7 @@ - + @@ -2845,7 +2837,7 @@ - + @@ -2856,7 +2848,7 @@ - + diff --git a/main/api/tests.flow_tracking/index.html b/main/api/tests.flow_tracking/index.html index 0a196333b..97726c936 100644 --- a/main/api/tests.flow_tracking/index.html +++ b/main/api/tests.flow_tracking/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3322,7 +3322,7 @@ - + @@ -3333,7 +3333,7 @@ - + @@ -3344,7 +3344,7 @@ - + diff --git a/main/api/tests.greent/index.html b/main/api/tests.greent/index.html index b1ea4a557..6b86338ef 100644 --- a/main/api/tests.greent/index.html +++ b/main/api/tests.greent/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2593,7 +2593,7 @@ - + @@ -2604,7 +2604,7 @@ - + @@ -2615,7 +2615,7 @@ - + diff --git a/main/api/tests.hardware/index.html b/main/api/tests.hardware/index.html index 3762debb9..1ce9ab109 100644 --- a/main/api/tests.hardware/index.html +++ b/main/api/tests.hardware/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3611,7 +3611,7 @@ - + @@ -3622,7 +3622,7 @@ - + @@ -3633,7 +3633,7 @@ - + diff --git a/main/api/tests.interfaces/index.html b/main/api/tests.interfaces/index.html index 2d767ea9f..d2162dfa9 100644 --- a/main/api/tests.interfaces/index.html +++ b/main/api/tests.interfaces/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -6967,7 +6967,7 @@ - + @@ -6978,7 +6978,7 @@ - + @@ -6989,7 +6989,7 @@ - + diff --git a/main/api/tests.lanz/index.html b/main/api/tests.lanz/index.html index 63ed4a717..bd63b81b9 100644 --- a/main/api/tests.lanz/index.html +++ b/main/api/tests.lanz/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2449,7 +2449,7 @@ - + @@ -2460,7 +2460,7 @@ - + @@ -2471,7 +2471,7 @@ - + diff --git a/main/api/tests.logging/index.html b/main/api/tests.logging/index.html index 307bf9315..5efda247a 100644 --- a/main/api/tests.logging/index.html +++ b/main/api/tests.logging/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4005,7 +4005,7 @@ - + @@ -4016,7 +4016,7 @@ - + @@ -4027,7 +4027,7 @@ - + diff --git a/main/api/tests.mlag/index.html b/main/api/tests.mlag/index.html index d8987f555..f2a748e95 100644 --- a/main/api/tests.mlag/index.html +++ b/main/api/tests.mlag/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3607,7 +3607,7 @@ - + @@ -3618,7 +3618,7 @@ - + @@ -3629,7 +3629,7 @@ - + diff --git a/main/api/tests.multicast/index.html b/main/api/tests.multicast/index.html index 84d512a14..ec75770d8 100644 --- a/main/api/tests.multicast/index.html +++ b/main/api/tests.multicast/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2767,7 +2767,7 @@ Inputs - + @@ -2778,7 +2778,7 @@ Inputs - + @@ -2789,7 +2789,7 @@ Inputs - + diff --git a/main/api/tests.path_selection/index.html b/main/api/tests.path_selection/index.html index 10781ddf6..1e996847e 100644 --- a/main/api/tests.path_selection/index.html +++ b/main/api/tests.path_selection/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2944,7 +2944,7 @@ RouterPath - + @@ -2955,7 +2955,7 @@ RouterPath - + @@ -2966,7 +2966,7 @@ RouterPath - + diff --git a/main/api/tests.profiles/index.html b/main/api/tests.profiles/index.html index ad7134d4f..d2e832279 100644 --- a/main/api/tests.profiles/index.html +++ b/main/api/tests.profiles/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2759,7 +2759,7 @@ Inputs< - + @@ -2770,7 +2770,7 @@ Inputs< - + @@ -2781,7 +2781,7 @@ Inputs< - + diff --git a/main/api/tests.ptp/index.html b/main/api/tests.ptp/index.html index 85ef5a5ff..73c2d28af 100644 --- a/main/api/tests.ptp/index.html +++ b/main/api/tests.ptp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3210,7 +3210,7 @@ - + @@ -3221,7 +3221,7 @@ - + @@ -3232,7 +3232,7 @@ - + diff --git a/main/api/tests.routing.bgp/index.html b/main/api/tests.routing.bgp/index.html index dca68a1fc..64de1e0b6 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -9227,7 +9227,7 @@ - + @@ -9238,7 +9238,7 @@ - + @@ -9249,7 +9249,7 @@ - + diff --git a/main/api/tests.routing.generic/index.html b/main/api/tests.routing.generic/index.html index a97dfb4fb..d80640fbf 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3190,7 +3190,7 @@ Inputs - + @@ -3201,7 +3201,7 @@ Inputs - + @@ -3212,7 +3212,7 @@ Inputs - + diff --git a/main/api/tests.routing.isis/index.html b/main/api/tests.routing.isis/index.html index 817e5f85b..1e2e5cf07 100644 --- a/main/api/tests.routing.isis/index.html +++ b/main/api/tests.routing.isis/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -5077,7 +5077,7 @@ Vias - + @@ -5088,7 +5088,7 @@ Vias - + @@ -5099,7 +5099,7 @@ Vias - + diff --git a/main/api/tests.routing.ospf/index.html b/main/api/tests.routing.ospf/index.html index e29e71dc0..db70676e7 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2858,7 +2858,7 @@ - + @@ -2869,7 +2869,7 @@ - + @@ -2880,7 +2880,7 @@ - + diff --git a/main/api/tests.security/index.html b/main/api/tests.security/index.html index a51964fbb..747288d15 100644 --- a/main/api/tests.security/index.html +++ b/main/api/tests.security/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -6422,7 +6422,7 @@ - + @@ -6433,7 +6433,7 @@ - + @@ -6444,7 +6444,7 @@ - + diff --git a/main/api/tests.services/index.html b/main/api/tests.services/index.html index 7b20d6acd..cc27eb5bb 100644 --- a/main/api/tests.services/index.html +++ b/main/api/tests.services/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3749,7 +3749,7 @@ - + @@ -3760,7 +3760,7 @@ - + @@ -3771,7 +3771,7 @@ - + diff --git a/main/api/tests.snmp/index.html b/main/api/tests.snmp/index.html index 3e6e32650..dc4d855b3 100644 --- a/main/api/tests.snmp/index.html +++ b/main/api/tests.snmp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4048,7 +4048,7 @@ Inputs - + @@ -4059,7 +4059,7 @@ Inputs - + @@ -4070,7 +4070,7 @@ Inputs - + diff --git a/main/api/tests.software/index.html b/main/api/tests.software/index.html index 549116e1b..8f7935376 100644 --- a/main/api/tests.software/index.html +++ b/main/api/tests.software/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2935,7 +2935,7 @@ Inputs - + @@ -2946,7 +2946,7 @@ Inputs - + @@ -2957,7 +2957,7 @@ Inputs - + diff --git a/main/api/tests.stp/index.html b/main/api/tests.stp/index.html index e5d902b6f..1024cd4a6 100644 --- a/main/api/tests.stp/index.html +++ b/main/api/tests.stp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3696,7 +3696,7 @@ Inputs - + @@ -3707,7 +3707,7 @@ Inputs - + @@ -3718,7 +3718,7 @@ Inputs - + diff --git a/main/api/tests.stun/index.html b/main/api/tests.stun/index.html index 747f2d3a3..2ea88167b 100644 --- a/main/api/tests.stun/index.html +++ b/main/api/tests.stun/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2906,7 +2906,7 @@ - + @@ -2917,7 +2917,7 @@ - + @@ -2928,7 +2928,7 @@ - + diff --git a/main/api/tests.system/index.html b/main/api/tests.system/index.html index 406a5497b..6bfaa38b6 100644 --- a/main/api/tests.system/index.html +++ b/main/api/tests.system/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4154,7 +4154,7 @@ - + @@ -4165,7 +4165,7 @@ - + @@ -4176,7 +4176,7 @@ - + diff --git a/main/api/tests.vlan/index.html b/main/api/tests.vlan/index.html index 00feb26d7..0fd71d0dd 100644 --- a/main/api/tests.vlan/index.html +++ b/main/api/tests.vlan/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2594,7 +2594,7 @@ Inputs - + @@ -2605,7 +2605,7 @@ Inputs - + @@ -2616,7 +2616,7 @@ Inputs - + diff --git a/main/api/tests.vxlan/index.html b/main/api/tests.vxlan/index.html index b55df5d03..eccd46cb2 100644 --- a/main/api/tests.vxlan/index.html +++ b/main/api/tests.vxlan/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3435,7 +3435,7 @@ Inputs - + @@ -3446,7 +3446,7 @@ Inputs - + @@ -3457,7 +3457,7 @@ Inputs - + diff --git a/main/api/tests/index.html b/main/api/tests/index.html index 837937a96..486addd84 100644 --- a/main/api/tests/index.html +++ b/main/api/tests/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2356,7 +2356,7 @@ Using the Tests - + @@ -2367,7 +2367,7 @@ Using the Tests - + @@ -2378,7 +2378,7 @@ Using the Tests - + diff --git a/main/api/types/index.html b/main/api/types/index.html index 3fa39ceb0..ec45d8303 100644 --- a/main/api/types/index.html +++ b/main/api/types/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4867,7 +4867,7 @@ - + @@ -4878,7 +4878,7 @@ - + @@ -4889,7 +4889,7 @@ - + diff --git a/main/assets/stylesheets/main.0253249f.min.css b/main/assets/stylesheets/main.0253249f.min.css deleted file mode 100644 index 9a7a6982d..000000000 --- a/main/assets/stylesheets/main.0253249f.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}.md-typeset ul[type]{list-style-type:revert-layer}[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}.md-typeset ol ol ol ol,.md-typeset ul ol ol ol{list-style-type:upper-alpha}.md-typeset ol ol ol ol ol,.md-typeset ul ol ol ol ol{list-style-type:upper-roman}.md-typeset ol[type],.md-typeset ul[type]{list-style-type:revert-layer}[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:-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{border-radius:100%;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)}.md-typeset .md-code__content{display:grid}@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)}@media print{.md-feedback{display:none}}.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;position:relative}.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{-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-style:normal;font-weight:400;outline:none;text-align:initial;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}:root{--md-tooltip-width:20rem;--md-tooltip-tail:0.3rem}.md-tooltip2{-webkit-backface-visibility:hidden;backface-visibility:hidden;color:var(--md-default-fg-color);font-family:var(--md-text-font-family);opacity:0;pointer-events:none;position:absolute;top:calc(var(--md-tooltip-host-y) + var(--md-tooltip-y));transform:translateY(-.4rem);transform-origin:calc(var(--md-tooltip-host-x) + var(--md-tooltip-x)) 0;transition:transform 0ms .25s,opacity .25s,z-index .25s;width:100%;z-index:0}.md-tooltip2:before{border-left:var(--md-tooltip-tail) solid #0000;border-right:var(--md-tooltip-tail) solid #0000;content:"";display:block;left:clamp(1.5 * .8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-tail),100vw - 2 * var(--md-tooltip-tail) - 1.5 * .8rem);position:absolute;z-index:1}.md-tooltip2--top:before{border-top:var(--md-tooltip-tail) solid var(--md-default-bg-color);bottom:calc(var(--md-tooltip-tail)*-1 + .025rem);filter:drop-shadow(0 1px 0 hsla(0,0%,0%,.05))}.md-tooltip2--bottom:before{border-bottom:var(--md-tooltip-tail) solid var(--md-default-bg-color);filter:drop-shadow(0 -1px 0 hsla(0,0%,0%,.05));top:calc(var(--md-tooltip-tail)*-1 + .025rem)}.md-tooltip2--active{opacity:1;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.5,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip2__inner{scrollbar-gutter:stable;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);max-height:40vh;max-width:calc(100vw - 1.6rem);position:relative;scrollbar-width:thin}.md-tooltip2__inner::-webkit-scrollbar{height:.2rem;width:.2rem}.md-tooltip2__inner::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-tooltip2__inner::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}[role=tooltip]>.md-tooltip2__inner{font-size:.5rem;font-weight:700;left:clamp(.8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-width)/2,100vw - var(--md-tooltip-width) - .8rem);max-width:min(100vw - 2 * .8rem,400px);padding:.2rem .4rem;-webkit-user-select:none;user-select:none;width:-moz-fit-content;width:fit-content}.md-tooltip2__inner.md-typeset>:first-child{margin-top:0}.md-tooltip2__inner.md-typeset>:last-child{margin-bottom:0}[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__alias{margin-left:.3rem;opacity:.7}.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: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]{display:initial}.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)}@media print{.giscus,[id=__comments]{display:none}}: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.0253249f.min.css.map b/main/assets/stylesheets/main.0253249f.min.css.map deleted file mode 100644 index 748194709..000000000 --- a/main/assets/stylesheets/main.0253249f.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/_code.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/_tooltip2.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/_giscus.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,gBCyyCF,CCvzCA,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,wNAAA,CACA,gNAAA,CACA,iNJgIF,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,qBAWE,kCAAA,CAAA,0BAAA,CADA,eAAA,CATA,aAAA,CAEA,QAAA,CAMA,uCAAA,CALA,aAAA,CAFA,oCAAA,CAKA,yDAAA,CACA,oBAAA,CAFA,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,CIxDI,qBACE,4BJ0DN,CIrDE,kDAGE,kBJuDJ,CI1DE,kDAGE,mBJuDJ,CI1DE,8BAEE,SJwDJ,CIpDI,0DACE,iBJuDN,CInDI,oCACE,2BJsDN,CInDM,0CACE,2BJsDR,CInDQ,gDACE,2BJsDV,CInDU,sDACE,2BJsDZ,CI9CI,0CACE,4BJiDN,CI7CI,wDACE,kBJiDN,CIlDI,wDACE,mBJiDN,CIlDI,oCAEE,kBJgDN,CI7CM,kGAEE,aJiDR,CI7CM,0DACE,eJgDR,CI5CM,4HAEE,kBJ+CR,CIjDM,4HAEE,mBJ+CR,CIjDM,oFACE,kBAAA,CAAA,eJgDR,CIzCE,yBAEE,mBJ2CJ,CI7CE,yBAEE,oBJ2CJ,CI7CE,eACE,mBAAA,CAAA,cJ4CJ,CIvCE,kDAIE,WAAA,CADA,cJ0CJ,CIlCI,4BAEE,oBJoCN,CIhCI,6BAEE,oBJkCN,CI9BI,kCACE,YJgCN,CI3BE,mBACE,iBAAA,CAGA,eAAA,CADA,cAAA,CAEA,iBAAA,CAHA,sBAAA,CAAA,iBJgCJ,CI1BI,uBACE,aAAA,CACA,aJ4BN,CIvBE,uBAGE,iBAAA,CADA,eAAA,CADA,eJ2BJ,CIrBE,mBACE,cJuBJ,CInBE,+BAME,2CAAA,CACA,iDAAA,CACA,mBAAA,CAPA,oBAAA,CAGA,gBAAA,CAFA,cAAA,CACA,aAAA,CAEA,iBJwBJ,CIlBI,aAXF,+BAYI,aJqBJ,CACF,CIhBI,iCACE,gBJkBN,CIXM,8FACE,YJaR,CITM,4FACE,eJWR,CINI,8FACE,eJQN,CILM,kHACE,gBJOR,CIFI,kCAGE,eAAA,CAFA,cAAA,CACA,sBAAA,CAEA,kBJIN,CIAI,kCAGE,qDAAA,CAFA,sBAAA,CACA,kBJGN,CIEI,wCACE,iCJAN,CIGM,8CACE,qDAAA,CACA,sDJDR,CIMI,iCACE,iBJJN,CISE,wCACE,cJPJ,CIUI,wDAIE,gBJFN,CIFI,wDAIE,iBJFN,CIFI,8CAME,UAAA,CALA,oBAAA,CAEA,YAAA,CAIA,oDAAA,CAAA,4CAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CALA,0BAAA,CAHA,WJAN,CIYI,oDACE,oDJVN,CIcI,mEACE,kDAAA,CACA,yDAAA,CAAA,iDJZN,CIgBI,oEACE,kDAAA,CACA,0DAAA,CAAA,kDJdN,CImBE,wBACE,iBAAA,CACA,eAAA,CACA,iBJjBJ,CIqBE,mBACE,oBAAA,CAEA,kBAAA,CADA,eJlBJ,CIsBI,aANF,mBAOI,aJnBJ,CACF,CIsBI,8BACE,aAAA,CAEA,QAAA,CACA,eAAA,CAFA,UJlBN,CKlWI,0CDmYF,uBACE,iBJ7BF,CIgCE,4BACE,eJ9BJ,CACF,CMjiBE,uBAOE,kBAAA,CALA,aAAA,CACA,aAAA,CAEA,aAAA,CACA,eAAA,CALA,iBAAA,CAOA,sCACE,CALF,YNuiBJ,CM9hBI,2BACE,aNgiBN,CM5hBI,6BAME,+CAAA,CAFA,yCAAA,CAHA,eAAA,CACA,eAAA,CACA,kBAAA,CAEA,iBN+hBN,CM1hBI,6BAEE,aAAA,CADA,YN6hBN,CMvhBE,wBACE,kBNyhBJ,CMthBI,4BAIE,kBAAA,CAHA,mCAAA,CAIA,uBNshBN,CMlhBI,4DAEE,oBAAA,CADA,SNqhBN,CMjhBM,oEACE,mBNmhBR,CO5kBA,WAGE,0CAAA,CADA,+BAAA,CADA,aPilBF,CO5kBE,aANF,WAOI,YP+kBF,CACF,CO5kBE,oBAEE,2CAAA,CADA,gCP+kBJ,CO1kBE,kBAGE,eAAA,CADA,iBAAA,CADA,eP8kBJ,COxkBE,6BACE,WP6kBJ,CO9kBE,6BACE,UP6kBJ,CO9kBE,mBAEE,aAAA,CACA,cAAA,CACA,uBP0kBJ,COvkBI,0BACE,YPykBN,COrkBI,yBACE,UPukBN,CQ5mBA,KASE,cAAA,CARA,WAAA,CACA,iBRgnBF,CK5cI,oCGtKJ,KAaI,gBRymBF,CACF,CKjdI,oCGtKJ,KAkBI,cRymBF,CACF,CQpmBA,KASE,2CAAA,CAPA,YAAA,CACA,qBAAA,CAKA,eAAA,CAHA,eAAA,CAJA,iBAAA,CAGA,UR0mBF,CQlmBE,aAZF,KAaI,aRqmBF,CACF,CKldI,0CGhJF,yBAII,cRkmBJ,CACF,CQzlBA,SAEE,gBAAA,CAAA,iBAAA,CADA,eR6lBF,CQxlBA,cACE,YAAA,CAEA,qBAAA,CADA,WR4lBF,CQxlBE,aANF,cAOI,aR2lBF,CACF,CQvlBA,SACE,WR0lBF,CQvlBE,gBACE,YAAA,CACA,WAAA,CACA,iBRylBJ,CQplBA,aACE,eAAA,CACA,sBRulBF,CQ9kBA,WACE,YRilBF,CQ5kBA,WAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,ORilBF,CQ5kBE,uCACE,aR8kBJ,CQ1kBE,+BAEE,uCAAA,CADA,kBR6kBJ,CQvkBA,SASE,2CAAA,CACA,mBAAA,CAFA,gCAAA,CADA,gBAAA,CADA,YAAA,CAMA,SAAA,CADA,uCAAA,CANA,mBAAA,CAJA,cAAA,CAYA,2BAAA,CATA,URilBF,CQrkBE,eAEE,SAAA,CAIA,uBAAA,CAHA,oEACE,CAHF,UR0kBJ,CQ5jBA,MACE,WR+jBF,CSxtBA,MACE,6PT0tBF,CSptBA,cASE,mBAAA,CAFA,0CAAA,CACA,cAAA,CAFA,YAAA,CAIA,uCAAA,CACA,oBAAA,CAVA,iBAAA,CAEA,UAAA,CADA,QAAA,CAUA,qBAAA,CAPA,WAAA,CADA,ST+tBF,CSptBE,aAfF,cAgBI,YTutBF,CACF,CSptBE,kCAEE,uCAAA,CADA,YTutBJ,CSltBE,qBACE,uCTotBJ,CShtBE,wCACE,+BTktBJ,CS7sBE,oBAME,6BAAA,CADA,UAAA,CAJA,aAAA,CAEA,cAAA,CACA,aAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,aTutBJ,CS3sBE,sBACE,cT6sBJ,CS1sBI,2BACE,2CT4sBN,CStsBI,kEAEE,uDAAA,CADA,+BTysBN,CU3wBE,8BACE,YV8wBJ,CWnxBA,mBACE,GACE,SAAA,CACA,0BXsxBF,CWnxBA,GACE,SAAA,CACA,uBXqxBF,CACF,CWjxBA,mBACE,GACE,SXmxBF,CWhxBA,GACE,SXkxBF,CACF,CWvwBE,qBASE,2BAAA,CAFA,mCAAA,CAAA,2BAAA,CADA,0BAAA,CADA,WAAA,CAGA,SAAA,CAPA,cAAA,CACA,KAAA,CAEA,UAAA,CADA,SX+wBJ,CWrwBE,mBAcE,mDAAA,CANA,2CAAA,CACA,QAAA,CACA,mBAAA,CARA,QAAA,CASA,kDACE,CAPF,eAAA,CAEA,aAAA,CADA,SAAA,CALA,cAAA,CAGA,UAAA,CADA,SXgxBJ,CWjwBE,kBACE,aXmwBJ,CW/vBE,sBACE,YAAA,CACA,YXiwBJ,CW9vBI,oCACE,aXgwBN,CW3vBE,sBACE,mBX6vBJ,CW1vBI,6CACE,cX4vBN,CKtpBI,0CMvGA,6CAKI,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,UX8vBN,CACF,CWvvBE,kBACE,cXyvBJ,CY11BA,YACE,WAAA,CAIA,WZ01BF,CYv1BE,mBAEE,qBAAA,CADA,iBZ01BJ,CK7rBI,sCOtJE,4EACE,kBZs1BN,CYl1BI,0JACE,mBZo1BN,CYr1BI,8EACE,kBZo1BN,CACF,CY/0BI,0BAGE,UAAA,CAFA,aAAA,CACA,YZk1BN,CY70BI,+BACE,eZ+0BN,CYz0BE,8BACE,WZ80BJ,CY/0BE,8BACE,UZ80BJ,CY/0BE,8BAIE,iBZ20BJ,CY/0BE,8BAIE,kBZ20BJ,CY/0BE,oBAGE,cAAA,CADA,SZ60BJ,CYx0BI,aAPF,oBAQI,YZ20BJ,CACF,CYx0BI,gCACE,yCZ00BN,CYt0BI,wBACE,cAAA,CACA,kBZw0BN,CYr0BM,kCACE,oBZu0BR,Cax4BA,qBAEE,Wbs5BF,Cax5BA,qBAEE,Ubs5BF,Cax5BA,WAQE,2CAAA,CACA,mBAAA,CANA,YAAA,CAOA,8BAAA,CALA,iBAAA,CAMA,SAAA,CALA,mBAAA,CACA,mBAAA,CANA,cAAA,CAcA,0BAAA,CAHA,wCACE,CATF,Sbo5BF,Cat4BE,aAlBF,WAmBI,Yby4BF,CACF,Cat4BE,mBAEE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,kEby4BJ,Cal4BE,kBAEE,gCAAA,CADA,ebq4BJ,Ccv6BA,aACE,gBAAA,CACA,iBd06BF,Ccv6BE,sBAGE,WAAA,CADA,QAAA,CADA,Sd26BJ,Ccr6BE,oBAEE,eAAA,CADA,edw6BJ,Ccn6BE,oBACE,iBdq6BJ,Ccj6BE,mBAEE,YAAA,CACA,cAAA,CACA,6BAAA,CAHA,iBds6BJ,Cch6BI,iDACE,yCdk6BN,Cc95BI,6BACE,iBdg6BN,Cc35BE,mBAGE,uCAAA,CACA,cAAA,CAHA,aAAA,CACA,cAAA,CAGA,sBd65BJ,Cc15BI,gDACE,+Bd45BN,Ccx5BI,4BACE,0CAAA,CACA,mBd05BN,Ccr5BE,mBAEE,SAAA,CADA,iBAAA,CAKA,2BAAA,CAHA,8Ddw5BJ,Ccl5BI,qBAEE,aAAA,CADA,edq5BN,Cch5BI,6BACE,SAAA,CACA,uBdk5BN,Cc74BE,aAnFF,aAoFI,Ydg5BF,CACF,Cer+BA,WAEE,0CAAA,CADA,+Bfy+BF,Cer+BE,aALF,WAMI,Yfw+BF,CACF,Cer+BE,kBACE,6BAAA,CAEA,aAAA,CADA,afw+BJ,Cep+BI,gCACE,Yfs+BN,Cej+BE,iBAOE,eAAA,CANA,YAAA,CAKA,cAAA,CAGA,mBAAA,CAAA,eAAA,CADA,cAAA,CAGA,uCAAA,CADA,eAAA,CAEA,uBf+9BJ,Ce59BI,8CACE,Uf89BN,Ce19BI,+BACE,oBf49BN,CK90BI,0CUvIE,uBACE,afw9BN,Cer9BM,yCACE,Yfu9BR,CACF,Cel9BI,iCACE,gBfq9BN,Cet9BI,iCACE,iBfq9BN,Cet9BI,uBAEE,gBfo9BN,Cej9BM,iCACE,efm9BR,Ce78BE,kBACE,WAAA,CAIA,eAAA,CADA,mBAAA,CAFA,6BAAA,CACA,cAAA,CAGA,kBf+8BJ,Ce38BE,mBAEE,YAAA,CADA,af88BJ,Cez8BE,sBACE,gBAAA,CACA,Uf28BJ,Cet8BA,gBACE,gDfy8BF,Cet8BE,uBACE,YAAA,CACA,cAAA,CACA,6BAAA,CACA,afw8BJ,Cep8BE,kCACE,sCfs8BJ,Cen8BI,gFACE,+Bfq8BN,Ce77BA,cAKE,wCAAA,CADA,gBAAA,CADA,iBAAA,CADA,eAAA,CADA,Ufo8BF,CKx5BI,mCU7CJ,cASI,Ufg8BF,CACF,Ce57BE,yBACE,sCf87BJ,Cev7BA,WACE,mBAAA,CACA,SAAA,CAEA,cAAA,CADA,qBf27BF,CKv6BI,mCUvBJ,WAQI,ef07BF,CACF,Cev7BE,iBACE,oBAAA,CAEA,aAAA,CACA,iBAAA,CAFA,Yf27BJ,Cet7BI,wBACE,efw7BN,Cep7BI,qBAGE,iBAAA,CAFA,gBAAA,CACA,mBfu7BN,CgB7lCE,uBAME,kBAAA,CACA,mBAAA,CAHA,gCAAA,CACA,cAAA,CAJA,oBAAA,CAEA,eAAA,CADA,kBAAA,CAMA,gEhBgmCJ,CgB1lCI,gCAEE,2CAAA,CACA,uCAAA,CAFA,gChB8lCN,CgBxlCI,0DAEE,0CAAA,CACA,sCAAA,CAFA,+BhB4lCN,CgBrlCE,gCAKE,4BhB0lCJ,CgB/lCE,gEAME,6BhBylCJ,CgB/lCE,gCAME,4BhBylCJ,CgB/lCE,sBAIE,6DAAA,CAGA,8BAAA,CAJA,eAAA,CAFA,aAAA,CACA,eAAA,CAMA,sChBulCJ,CgBllCI,wDACE,6CAAA,CACA,8BhBolCN,CgBhlCI,+BACE,UhBklCN,CiBroCA,WAOE,2CAAA,CAGA,8CACE,CALF,gCAAA,CADA,aAAA,CAHA,MAAA,CADA,eAAA,CACA,OAAA,CACA,KAAA,CACA,SjB4oCF,CiBjoCE,aAfF,WAgBI,YjBooCF,CACF,CiBjoCE,mBAIE,2BAAA,CAHA,iEjBooCJ,CiB7nCE,mBACE,kDACE,CAEF,kEjB6nCJ,CiBvnCE,kBAEE,kBAAA,CADA,YAAA,CAEA,ejBynCJ,CiBrnCE,mBAKE,kBAAA,CAEA,cAAA,CAHA,YAAA,CAIA,uCAAA,CALA,aAAA,CAFA,iBAAA,CAQA,uBAAA,CAHA,qBAAA,CAJA,SjB8nCJ,CiBpnCI,yBACE,UjBsnCN,CiBlnCI,iCACE,oBjBonCN,CiBhnCI,uCAEE,uCAAA,CADA,YjBmnCN,CiB9mCI,2BAEE,YAAA,CADA,ajBinCN,CKngCI,0CY/GA,2BAMI,YjBgnCN,CACF,CiB7mCM,8DAIE,iBAAA,CAHA,aAAA,CAEA,aAAA,CADA,UjBinCR,CKjiCI,mCYzEA,iCAII,YjB0mCN,CACF,CiBvmCM,wCACE,YjBymCR,CiBrmCM,+CACE,oBjBumCR,CK5iCI,sCYtDA,iCAII,YjBkmCN,CACF,CiB7lCE,kBAEE,YAAA,CACA,cAAA,CAFA,iBAAA,CAIA,8DACE,CAFF,kBjBgmCJ,CiB1lCI,oCAGE,SAAA,CADA,mBAAA,CAKA,6BAAA,CAHA,8DACE,CAJF,UjBgmCN,CiBvlCM,8CACE,8BjBylCR,CiBplCI,8BACE,ejBslCN,CiBjlCE,4BAGE,gBAAA,CAAA,kBjBqlCJ,CiBxlCE,4BAGE,iBAAA,CAAA,iBjBqlCJ,CiBxlCE,kBACE,WAAA,CAGA,eAAA,CAFA,aAAA,CAGA,kBjBmlCJ,CiBhlCI,4CAGE,SAAA,CADA,mBAAA,CAKA,8BAAA,CAHA,8DACE,CAJF,UjBslCN,CiB7kCM,sDACE,6BjB+kCR,CiB3kCM,8DAGE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,8DACE,CAJF,SjBilCR,CiBtkCI,uCAGE,WAAA,CAFA,iBAAA,CACA,UjBykCN,CiBnkCE,mBACE,YAAA,CACA,aAAA,CACA,cAAA,CAEA,+CACE,CAFF,kBjBskCJ,CiBhkCI,8DACE,WAAA,CACA,SAAA,CACA,oCjBkkCN,CiBzjCI,yBACE,QjB2jCN,CiBtjCE,mBACE,YjBwjCJ,CKpnCI,mCY2DF,6BAQI,gBjBwjCJ,CiBhkCA,6BAQI,iBjBwjCJ,CiBhkCA,mBAKI,aAAA,CAEA,iBAAA,CADA,ajB0jCJ,CACF,CK5nCI,sCY2DF,6BAaI,kBjBwjCJ,CiBrkCA,6BAaI,mBjBwjCJ,CACF,CDvyCA,SAGE,uCAAA,CAFA,eAAA,CACA,eC2yCF,CDvyCE,eACE,mBAAA,CACA,cAAA,CAGA,eAAA,CADA,QAAA,CADA,SC2yCJ,CDryCE,sCAEE,WAAA,CADA,iBAAA,CAAA,kBCwyCJ,CDnyCE,eACE,+BCqyCJ,CDlyCI,0CACE,+BCoyCN,CD9xCA,UAKE,wBmBaa,CnBZb,oBAAA,CAFA,UAAA,CAHA,oBAAA,CAEA,eAAA,CADA,0BAAA,CAAA,2BCqyCF,CmBv0CA,MACE,uMAAA,CACA,sLAAA,CACA,iNnB00CF,CmBp0CA,QACE,eAAA,CACA,enBu0CF,CmBp0CE,eAKE,uCAAA,CAJA,aAAA,CAGA,eAAA,CADA,eAAA,CADA,eAAA,CAIA,sBnBs0CJ,CmBn0CI,+BACE,YnBq0CN,CmBl0CM,mCAEE,WAAA,CADA,UnBq0CR,CmB7zCQ,sFAME,iBAAA,CALA,aAAA,CAGA,aAAA,CADA,cAAA,CAEA,kBAAA,CAHA,UnBm0CV,CmBxzCE,cAGE,eAAA,CADA,QAAA,CADA,SnB4zCJ,CmBtzCE,cAGE,sBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBAAA,CACA,uBAAA,CACA,sBnBwzCJ,CmBrzCI,sBACE,uCnBuzCN,CmBhzCM,6EAEE,+BnBkzCR,CmB7yCI,2BAIE,iBnB4yCN,CmBxyCI,4CACE,gBnB0yCN,CmB3yCI,4CACE,iBnB0yCN,CmBtyCI,kBAME,iBAAA,CAFA,aAAA,CACA,YAAA,CAFA,iBnByyCN,CmBlyCI,sGACE,+BAAA,CACA,cnBoyCN,CmBhyCI,4BACE,uCAAA,CACA,oBnBkyCN,CmB9xCI,0CACE,YnBgyCN,CmB7xCM,yDAIE,6BAAA,CAHA,aAAA,CAEA,WAAA,CAEA,qCAAA,CAAA,6BAAA,CAHA,UnBkyCR,CmB3xCM,kDACE,YnB6xCR,CmBvxCE,iCACE,YnByxCJ,CmBtxCI,6CACE,WAAA,CAGA,WnBsxCN,CmBjxCE,cACE,anBmxCJ,CmB/wCE,gBACE,YnBixCJ,CKlvCI,0CcxBA,0CASE,2CAAA,CAHA,YAAA,CACA,qBAAA,CACA,WAAA,CALA,MAAA,CADA,iBAAA,CACA,OAAA,CACA,KAAA,CACA,SnBgxCJ,CmBrwCI,+DACE,eAAA,CACA,enBuwCN,CmBnwCI,gCAQE,qDAAA,CAHA,uCAAA,CAEA,cAAA,CALA,aAAA,CAEA,kBAAA,CADA,wBAAA,CAFA,iBAAA,CAKA,kBnBuwCN,CmBlwCM,wDAEE,UnBywCR,CmB3wCM,wDAEE,WnBywCR,CmB3wCM,8CAIE,aAAA,CAEA,aAAA,CACA,YAAA,CANA,iBAAA,CAEA,SAAA,CAEA,YnBswCR,CmBjwCQ,oDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnB0wCV,CmB9vCM,8CAIE,2CAAA,CACA,gEACE,CALF,eAAA,CAEA,4BAAA,CADA,kBnBmwCR,CmB5vCQ,2DACE,YnB8vCV,CmBzvCM,8CAGE,2CAAA,CADA,gCAAA,CADA,enB6vCR,CmBvvCM,yCAIE,aAAA,CAFA,UAAA,CAIA,YAAA,CADA,aAAA,CAJA,iBAAA,CACA,WAAA,CACA,SnB4vCR,CmBpvCI,+BACE,MnBsvCN,CmBlvCI,+BACE,4DnBovCN,CmBjvCM,qDACE,+BnBmvCR,CmBhvCQ,sHACE,+BnBkvCV,CmB5uCI,+BAEE,YAAA,CADA,mBnB+uCN,CmB3uCM,mCACE,enB6uCR,CmBzuCM,6CACE,SnB2uCR,CmBvuCM,uDAGE,mBnB0uCR,CmB7uCM,uDAGE,kBnB0uCR,CmB7uCM,6CAIE,gBAAA,CAFA,aAAA,CADA,YnB4uCR,CmBtuCQ,mDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnB+uCV,CmB/tCM,+CACE,mBnBiuCR,CmBztCM,4CAEE,wBAAA,CADA,enB4tCR,CmBxtCQ,oEACE,mBnB0tCV,CmB3tCQ,oEACE,oBnB0tCV,CmBttCQ,4EACE,iBnBwtCV,CmBztCQ,4EACE,kBnBwtCV,CmBptCQ,oFACE,mBnBstCV,CmBvtCQ,oFACE,oBnBstCV,CmBltCQ,4FACE,mBnBotCV,CmBrtCQ,4FACE,oBnBotCV,CmB7sCE,mBACE,wBnB+sCJ,CmB3sCE,wBACE,YAAA,CACA,SAAA,CAIA,0BAAA,CAHA,oEnB8sCJ,CmBxsCI,kCACE,2BnB0sCN,CmBrsCE,gCACE,SAAA,CAIA,uBAAA,CAHA,qEnBwsCJ,CmBlsCI,8CAEE,kCAAA,CAAA,0BnBmsCN,CACF,CKr4CI,0Cc0MA,0CACE,YnB8rCJ,CmB3rCI,yDACE,UnB6rCN,CmBzrCI,wDACE,YnB2rCN,CmBvrCI,kDACE,YnByrCN,CmBprCE,gBAIE,iDAAA,CADA,gCAAA,CAFA,aAAA,CACA,enBwrCJ,CACF,CKl8CM,+DcmRF,6CACE,YnBkrCJ,CmB/qCI,4DACE,UnBirCN,CmB7qCI,2DACE,YnB+qCN,CmB3qCI,qDACE,YnB6qCN,CACF,CK17CI,mCc7JJ,QAgbI,oBnB2qCF,CmBrqCI,kCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnBuqCN,CmBlqCM,6CACE,uBnBoqCR,CmBhqCM,gDACE,YnBkqCR,CmB7pCI,2CACE,kBnBgqCN,CmBjqCI,2CACE,mBnBgqCN,CmBjqCI,iCAEE,oBnB+pCN,CmBxpCI,yDACE,kBnB0pCN,CmB3pCI,yDACE,iBnB0pCN,CACF,CKn9CI,sCc7JJ,QA4dI,oBAAA,CACA,oDnBwpCF,CmBlpCI,gCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnBopCN,CmB/oCM,8CACE,uBnBipCR,CmB7oCM,8CACE,YnB+oCR,CmB1oCI,yCACE,kBnB6oCN,CmB9oCI,yCACE,mBnB6oCN,CmB9oCI,+BAEE,oBnB4oCN,CmBroCI,uDACE,kBnBuoCN,CmBxoCI,uDACE,iBnBuoCN,CmBloCE,wBACE,YAAA,CACA,sBAAA,CAEA,SAAA,CACA,6FACE,CAHF,mBnBsoCJ,CmB9nCI,sCACE,enBgoCN,CmB3nCE,iFACE,sBAAA,CAEA,SAAA,CACA,4FACE,CAHF,kBnB+nCJ,CmBtnCE,iDACE,enBwnCJ,CmBpnCE,6CACE,YnBsnCJ,CmBlnCE,uBACE,aAAA,CACA,enBonCJ,CmBjnCI,kCACE,enBmnCN,CmB/mCI,qCACE,enBinCN,CmB9mCM,0CACE,uCnBgnCR,CmB5mCM,6DACE,mBnB8mCR,CmB1mCM,yFAEE,YnB4mCR,CmBvmCI,yCAEE,kBnB2mCN,CmB7mCI,yCAEE,mBnB2mCN,CmB7mCI,+BACE,aAAA,CAGA,SAAA,CADA,kBnB0mCN,CmBtmCM,2DACE,SnBwmCR,CmBlmCE,cAGE,kBAAA,CADA,YAAA,CAEA,gCAAA,CAHA,WnBumCJ,CmBjmCI,oBACE,uDnBmmCN,CmB/lCI,oBAME,6BAAA,CACA,kBAAA,CAFA,UAAA,CAJA,oBAAA,CAEA,WAAA,CAKA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,yBAAA,CARA,qBAAA,CAFA,UnB2mCN,CmB9lCM,8BACE,wBnBgmCR,CmB5lCM,kKAEE,uBnB6lCR,CmB/kCI,2EACE,YnBolCN,CmBjlCM,oDACE,anBmlCR,CmBhlCQ,kEAKE,qCAAA,CACA,qDAAA,CAFA,YAAA,CAHA,eAAA,CACA,KAAA,CACA,SnBqlCV,CmB/kCU,0FACE,mBnBilCZ,CmB5kCQ,0EACE,QnB8kCV,CmBzkCM,sFACE,kBnB2kCR,CmB5kCM,sFACE,mBnB2kCR,CmBvkCM,kDACE,uCnBykCR,CmBnkCI,2CACE,sBAAA,CAEA,SAAA,CADA,kBnBskCN,CmB7jCI,qFAIE,mDnBgkCN,CmBpkCI,qFAIE,oDnBgkCN,CmBpkCI,2EACE,aAAA,CACA,oBAAA,CAGA,SAAA,CAFA,kBnBikCN,CmB5jCM,yFAEE,gBAAA,CADA,gBnB+jCR,CmB1jCM,0FACE,YnB4jCR,CACF,CoBnxDA,eAKE,eAAA,CACA,eAAA,CAJA,SpB0xDF,CoBnxDE,gCANA,kBAAA,CAFA,YAAA,CAGA,sBpBiyDF,CoB5xDE,iBAOE,mBAAA,CAFA,aAAA,CADA,gBAAA,CAEA,iBpBsxDJ,CoBjxDE,wBAEE,qDAAA,CADA,uCpBoxDJ,CoB/wDE,qBACE,6CpBixDJ,CoB5wDI,sDAEE,uDAAA,CADA,+BpB+wDN,CoB3wDM,8DACE,+BpB6wDR,CoBxwDI,mCACE,uCAAA,CACA,oBpB0wDN,CoBtwDI,yBAKE,iBAAA,CADA,yCAAA,CAHA,aAAA,CAEA,eAAA,CADA,YpB2wDN,CqB3zDE,eAGE,+DAAA,CADA,oBAAA,CADA,qBrBg0DJ,CK3oDI,0CgBtLF,eAOI,YrB8zDJ,CACF,CqBxzDM,6BACE,oBrB0zDR,CqBpzDE,kBACE,YAAA,CACA,qBAAA,CACA,SAAA,CACA,qBrBszDJ,CqB/yDI,0BACE,sBrBizDN,CqB9yDM,gEACE,+BrBgzDR,CqB1yDE,gBAEE,uCAAA,CADA,erB6yDJ,CqBxyDE,kBACE,oBrB0yDJ,CqBvyDI,mCAGE,kBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBrByyDN,CqBryDI,oCAIE,kBAAA,CAHA,mBAAA,CACA,kBAAA,CACA,SAAA,CAGA,QAAA,CADA,iBrBwyDN,CqBnyDI,0DACE,kBrBqyDN,CqBtyDI,0DACE,iBrBqyDN,CqBjyDI,iDACE,uBAAA,CAEA,YrBkyDN,CqB7xDE,4BACE,YrB+xDJ,CqBxxDA,YAGE,kBAAA,CAFA,YAAA,CAIA,eAAA,CAHA,SAAA,CAIA,eAAA,CAFA,UrB6xDF,CqBxxDE,yBACE,WrB0xDJ,CqBnxDA,kBACE,YrBsxDF,CK9sDI,0CgBzEJ,kBAKI,wBrBsxDF,CACF,CqBnxDE,qCACE,WrBqxDJ,CKzuDI,sCgB7CF,+CAKI,kBrBqxDJ,CqB1xDA,+CAKI,mBrBqxDJ,CACF,CK3tDI,0CgBrDJ,6BAMI,SAAA,CAFA,eAAA,CACA,UrBkxDF,CqB/wDE,qDACE,gBrBixDJ,CqB9wDE,gDACE,SrBgxDJ,CqB7wDE,4CACE,iBAAA,CAAA,kBrB+wDJ,CqB5wDE,2CAEE,WAAA,CADA,crB+wDJ,CqB3wDE,2CACE,mBAAA,CACA,cAAA,CACA,SAAA,CACA,oBAAA,CAAA,iBrB6wDJ,CqB1wDE,2CACE,SrB4wDJ,CqBzwDE,qCAEE,WAAA,CACA,eAAA,CAFA,erB6wDJ,CACF,CsBv7DA,MACE,qBAAA,CACA,yBtB07DF,CsBp7DA,aAME,qCAAA,CADA,cAAA,CAEA,0FACE,CAPF,cAAA,CACA,KAAA,CAaA,mDAAA,CACA,qBAAA,CAJA,wFACE,CATF,UAAA,CADA,StB87DF,CuBz8DA,MACE,mfvB48DF,CuBt8DA,WACE,iBvBy8DF,CK3yDI,mCkB/JJ,WAKI,evBy8DF,CACF,CuBt8DE,kBACE,YvBw8DJ,CuBp8DE,oBAEE,SAAA,CADA,SvBu8DJ,CKpyDI,0CkBpKF,8BAOI,YvB+8DJ,CuBt9DA,8BAOI,avB+8DJ,CuBt9DA,oBAaI,2CAAA,CACA,kBAAA,CAJA,WAAA,CACA,eAAA,CACA,mBAAA,CANA,iBAAA,CAEA,SAAA,CAUA,uBAAA,CAHA,4CACE,CAPF,UvB68DJ,CuBj8DI,+DACE,SAAA,CACA,oCvBm8DN,CACF,CK10DI,mCkBjJF,8BAgCI,MvBs8DJ,CuBt+DA,8BAgCI,OvBs8DJ,CuBt+DA,oBAqCI,0BAAA,CADA,cAAA,CADA,QAAA,CAJA,cAAA,CAEA,KAAA,CAKA,sDACE,CALF,OvBo8DJ,CuB17DI,+DAME,YAAA,CACA,SAAA,CACA,4CACE,CARF,UvB+7DN,CACF,CKz0DI,0CkBxGA,+DAII,mBvBi7DN,CACF,CKv3DM,+DkB/DF,+DASI,mBvBi7DN,CACF,CK53DM,+DkB/DF,+DAcI,mBvBi7DN,CACF,CuB56DE,kBAEE,kCAAA,CAAA,0BvB66DJ,CK31DI,0CkBpFF,4BAOI,MvBq7DJ,CuB57DA,4BAOI,OvBq7DJ,CuB57DA,kBAWI,QAAA,CAEA,SAAA,CADA,eAAA,CANA,cAAA,CAEA,KAAA,CAWA,wBAAA,CALA,qGACE,CALF,OAAA,CADA,SvBm7DJ,CuBt6DI,4BACE,yBvBw6DN,CuBp6DI,6DAEE,WAAA,CACA,SAAA,CAMA,uBAAA,CALA,sGACE,CAJF,UvB06DN,CACF,CKt4DI,mCkBjEF,4BA2CI,WvBo6DJ,CuB/8DA,4BA2CI,UvBo6DJ,CuB/8DA,kBA6CI,eAAA,CAHA,iBAAA,CAIA,8CAAA,CAFA,avBm6DJ,CACF,CKr6DM,+DkBOF,6DAII,avB85DN,CACF,CKp5DI,sCkBfA,6DASI,avB85DN,CACF,CuBz5DE,iBAIE,2CAAA,CACA,0BAAA,CAFA,aAAA,CAFA,iBAAA,CAKA,2CACE,CALF,SvB+5DJ,CKj6DI,mCkBAF,iBAaI,0BAAA,CACA,mBAAA,CAFA,avB25DJ,CuBt5DI,uBACE,0BvBw5DN,CACF,CuBp5DI,4DAEE,2CAAA,CACA,6BAAA,CACA,8BAAA,CAHA,gCvBy5DN,CuBj5DE,4BAKE,mBAAA,CAAA,oBvBs5DJ,CuB35DE,4BAKE,mBAAA,CAAA,oBvBs5DJ,CuB35DE,kBAQE,gBAAA,CAFA,eAAA,CAFA,WAAA,CAHA,iBAAA,CAMA,sBAAA,CAJA,UAAA,CADA,SvBy5DJ,CuBh5DI,+BACE,qBvBk5DN,CuB94DI,kEAEE,uCvB+4DN,CuB34DI,6BACE,YvB64DN,CKj7DI,0CkBaF,kBA8BI,eAAA,CADA,aAAA,CADA,UvB84DJ,CACF,CK38DI,mCkBgCF,4BAmCI,mBvB84DJ,CuBj7DA,4BAmCI,oBvB84DJ,CuBj7DA,kBAqCI,aAAA,CADA,evB64DJ,CuBz4DI,+BACE,uCvB24DN,CuBv4DI,mCACE,gCvBy4DN,CuBr4DI,6DACE,kBvBu4DN,CuBp4DM,8EACE,uCvBs4DR,CuBl4DM,0EACE,WvBo4DR,CACF,CuB93DE,iBAIE,cAAA,CAHA,oBAAA,CAEA,aAAA,CAEA,kCACE,CAJF,YvBm4DJ,CuB33DI,uBACE,UvB63DN,CuBz3DI,yCAEE,UvB63DN,CuB/3DI,yCAEE,WvB63DN,CuB/3DI,+BACE,iBAAA,CAEA,SAAA,CACA,SvB23DN,CuBx3DM,6CACE,oBvB03DR,CKj+DI,0CkB+FA,yCAaI,UvB03DN,CuBv4DE,yCAaI,WvB03DN,CuBv4DE,+BAcI,SvBy3DN,CuBt3DM,+CACE,YvBw3DR,CACF,CK7/DI,mCkBkHA,+BAwBI,mBvBu3DN,CuBp3DM,8CACE,YvBs3DR,CACF,CuBh3DE,8BAEE,WvBq3DJ,CuBv3DE,8BAEE,UvBq3DJ,CuBv3DE,oBAKE,mBAAA,CAJA,iBAAA,CAEA,SAAA,CACA,SvBm3DJ,CKz/DI,0CkBkIF,8BASI,WvBm3DJ,CuB53DA,8BASI,UvBm3DJ,CuB53DA,oBAUI,SvBk3DJ,CACF,CuB/2DI,uCACE,iBvBq3DN,CuBt3DI,uCACE,kBvBq3DN,CuBt3DI,6BAEE,uCAAA,CACA,SAAA,CAIA,oBAAA,CAHA,+DvBk3DN,CuB52DM,iDAEE,uCAAA,CADA,YvB+2DR,CuB12DM,gGAGE,SAAA,CADA,mBAAA,CAEA,kBvB22DR,CuBx2DQ,sGACE,UvB02DV,CuBn2DE,8BAOE,mBAAA,CAAA,oBvB02DJ,CuBj3DE,8BAOE,mBAAA,CAAA,oBvB02DJ,CuBj3DE,oBAIE,kBAAA,CAKA,yCAAA,CANA,YAAA,CAKA,eAAA,CAFA,WAAA,CAKA,SAAA,CAVA,iBAAA,CACA,KAAA,CAUA,uBAAA,CAFA,kBAAA,CALA,UvB42DJ,CKnjEI,mCkBkMF,8BAgBI,mBvBs2DJ,CuBt3DA,8BAgBI,oBvBs2DJ,CuBt3DA,oBAiBI,evBq2DJ,CACF,CuBl2DI,+DACE,SAAA,CACA,0BvBo2DN,CuB/1DE,6BAKE,+BvBk2DJ,CuBv2DE,0DAME,gCvBi2DJ,CuBv2DE,6BAME,+BvBi2DJ,CuBv2DE,mBAIE,eAAA,CAHA,iBAAA,CAEA,UAAA,CADA,SvBq2DJ,CKljEI,0CkB2MF,mBAWI,QAAA,CADA,UvBk2DJ,CACF,CK3kEI,mCkB8NF,mBAiBI,SAAA,CADA,UAAA,CAEA,sBvBi2DJ,CuB91DI,8DACE,8BAAA,CACA,SvBg2DN,CACF,CuB31DE,uBASE,kCAAA,CAAA,0BAAA,CAFA,2CAAA,CANA,WAAA,CACA,eAAA,CAIA,kBvB41DJ,CuBt1DI,iEAZF,uBAaI,uBvBy1DJ,CACF,CKxnEM,+DkBiRJ,uBAkBI,avBy1DJ,CACF,CKvmEI,sCkB2PF,uBAuBI,avBy1DJ,CACF,CK5mEI,mCkB2PF,uBA4BI,YAAA,CACA,yDAAA,CACA,oBvBy1DJ,CuBt1DI,kEACE,evBw1DN,CuBp1DI,6BACE,+CvBs1DN,CuBl1DI,0CAEE,YAAA,CADA,WvBq1DN,CuBh1DI,gDACE,oDvBk1DN,CuB/0DM,sDACE,0CvBi1DR,CACF,CuB10DA,kBACE,gCAAA,CACA,qBvB60DF,CuB10DE,wBAME,qDAAA,CAFA,uCAAA,CAFA,gBAAA,CACA,kBAAA,CAFA,eAAA,CAIA,uBvB60DJ,CKhpEI,mCkB8TF,kCAUI,mBvB40DJ,CuBt1DA,kCAUI,oBvB40DJ,CACF,CuBx0DE,wBAGE,eAAA,CADA,QAAA,CADA,SAAA,CAIA,wBAAA,CAAA,gBvBy0DJ,CuBr0DE,wBACE,yDvBu0DJ,CuBp0DI,oCACE,evBs0DN,CuBj0DE,wBACE,aAAA,CAEA,YAAA,CADA,uBAAA,CAEA,gCvBm0DJ,CuBh0DI,4DACE,uDvBk0DN,CuB9zDI,gDACE,mBvBg0DN,CuB3zDE,gCAKE,cAAA,CADA,aAAA,CAGA,YAAA,CANA,eAAA,CAKA,uBAAA,CAJA,KAAA,CACA,SvBi0DJ,CuB1zDI,wCACE,YvB4zDN,CuBvzDI,wDACE,YvByzDN,CuBrzDI,oCAGE,+BAAA,CADA,gBAAA,CADA,mBAAA,CAGA,2CvBuzDN,CKlsEI,mCkBuYA,8CAUI,mBvBqzDN,CuB/zDE,8CAUI,oBvBqzDN,CACF,CuBjzDI,oFAEE,uDAAA,CADA,+BvBozDN,CuB9yDE,sCACE,2CvBgzDJ,CuB3yDE,2BAGE,eAAA,CADA,eAAA,CADA,iBvB+yDJ,CKntEI,mCkBmaF,qCAOI,mBvB6yDJ,CuBpzDA,qCAOI,oBvB6yDJ,CACF,CuBzyDE,kCAEE,MvB+yDJ,CuBjzDE,kCAEE,OvB+yDJ,CuBjzDE,wBAME,uCAAA,CAFA,aAAA,CACA,YAAA,CAJA,iBAAA,CAEA,YvB8yDJ,CK7sEI,0CkB4ZF,wBAUI,YvB2yDJ,CACF,CuBxyDI,8BAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,+CAAA,CAAA,uCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UvBizDN,CuBvyDM,wCACE,oBvByyDR,CuBnyDE,8BAGE,uCAAA,CAFA,gBAAA,CACA,evBsyDJ,CuBlyDI,iCAKE,gCAAA,CAHA,eAAA,CACA,eAAA,CACA,eAAA,CAHA,evBwyDN,CuBjyDM,sCACE,oBvBmyDR,CuB9xDI,iCAKE,gCAAA,CAHA,gBAAA,CACA,eAAA,CACA,eAAA,CAHA,avBoyDN,CuB7xDM,sCACE,oBvB+xDR,CuBzxDE,yBAKE,gCAAA,CAJA,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,avB8xDJ,CuBvxDE,uBAGE,wBAAA,CAFA,+BAAA,CACA,yBvB0xDJ,CwB97EA,WACE,iBAAA,CACA,SxBi8EF,CwB97EE,kBAOE,2CAAA,CACA,mBAAA,CACA,8BAAA,CAHA,gCAAA,CAHA,QAAA,CAEA,gBAAA,CADA,YAAA,CAMA,SAAA,CATA,iBAAA,CACA,sBAAA,CAaA,mCAAA,CAJA,oExBi8EJ,CwB17EI,6EACE,gBAAA,CACA,SAAA,CAKA,+BAAA,CAJA,8ExB67EN,CwBr7EI,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,OxB87EN,CwBl7EE,iBAOE,mBAAA,CAFA,eAAA,CACA,oBAAA,CAHA,QAAA,CAFA,kBAAA,CAGA,aAAA,CAFA,SxBy7EJ,CwBh7EE,iBACE,kBxBk7EJ,CwB96EE,2BAGE,kBAAA,CAAA,oBxBo7EJ,CwBv7EE,2BAGE,mBAAA,CAAA,mBxBo7EJ,CwBv7EE,iBAIE,cAAA,CAHA,aAAA,CAKA,YAAA,CADA,uBAAA,CAEA,2CACE,CANF,UxBq7EJ,CwB36EI,8CACE,+BxB66EN,CwBz6EI,uBACE,qDxB26EN,CyB//EA,YAIE,qBAAA,CADA,aAAA,CAGA,gBAAA,CALA,eAAA,CACA,UAAA,CAGA,azBmgFF,CyB//EE,aATF,YAUI,YzBkgFF,CACF,CKp1EI,0CoB3KF,+BAKI,azBugFJ,CyB5gFA,+BAKI,czBugFJ,CyB5gFA,qBAWI,2CAAA,CAHA,aAAA,CAEA,WAAA,CANA,cAAA,CAEA,KAAA,CASA,uBAAA,CAHA,iEACE,CAJF,aAAA,CAFA,SzBqgFJ,CyB1/EI,mEACE,8BAAA,CACA,6BzB4/EN,CyBz/EM,6EACE,8BzB2/ER,CyBt/EI,6CAEE,QAAA,CAAA,MAAA,CACA,QAAA,CACA,eAAA,CAHA,iBAAA,CACA,OAAA,CAGA,qBAAA,CAHA,KzB2/EN,CACF,CKn4EI,sCoBtKJ,YAuDI,QzBs/EF,CyBn/EE,mBACE,WzBq/EJ,CyBj/EE,6CACE,UzBm/EJ,CACF,CyB/+EE,uBACE,YAAA,CACA,OzBi/EJ,CKl5EI,mCoBjGF,uBAMI,QzBi/EJ,CyB9+EI,8BACE,WzBg/EN,CyB5+EI,qCACE,azB8+EN,CyB1+EI,+CACE,kBzB4+EN,CACF,CyBv+EE,wBAIE,uBAAA,CAOA,kCAAA,CAAA,0BAAA,CAVA,cAAA,CACA,eAAA,CACA,yDAAA,CAMA,oBzBs+EJ,CyBj+EI,2CAEE,YAAA,CADA,WzBo+EN,CyB/9EI,mEACE,+CzBi+EN,CyB99EM,qHACE,oDzBg+ER,CyB79EQ,iIACE,0CzB+9EV,CyBh9EE,wCAGE,wBACE,qBzBg9EJ,CyB58EE,6BACE,kCzB88EJ,CyB/8EE,6BACE,iCzB88EJ,CACF,CK16EI,0CoB5BF,YAME,0BAAA,CADA,QAAA,CAEA,SAAA,CANA,cAAA,CACA,KAAA,CAMA,sDACE,CALF,OAAA,CADA,SzB+8EF,CyBp8EE,4CAEE,WAAA,CACA,SAAA,CACA,4CACE,CAJF,UzBy8EJ,CACF,C0BtnFA,iBACE,GACE,Q1BwnFF,C0BrnFA,GACE,a1BunFF,CACF,C0BnnFA,gBACE,GACE,SAAA,CACA,0B1BqnFF,C0BlnFA,IACE,S1BonFF,C0BjnFA,GACE,SAAA,CACA,uB1BmnFF,CACF,C0B3mFA,MACE,2eAAA,CACA,+fAAA,CACA,0lBAAA,CACA,kf1B6mFF,C0BvmFA,WAOE,kCAAA,CAAA,0BAAA,CANA,aAAA,CACA,gBAAA,CACA,eAAA,CAEA,uCAAA,CAGA,uBAAA,CAJA,kB1B6mFF,C0BtmFE,iBACE,U1BwmFJ,C0BpmFE,iBACE,oBAAA,CAEA,aAAA,CACA,qBAAA,CAFA,U1BwmFJ,C0BnmFI,+BACE,iB1BsmFN,C0BvmFI,+BACE,kB1BsmFN,C0BvmFI,qBAEE,gB1BqmFN,C0BjmFI,kDACE,iB1BomFN,C0BrmFI,kDACE,kB1BomFN,C0BrmFI,kDAEE,iB1BmmFN,C0BrmFI,kDAEE,kB1BmmFN,C0B9lFE,iCAGE,iB1BmmFJ,C0BtmFE,iCAGE,kB1BmmFJ,C0BtmFE,uBACE,oBAAA,CACA,6BAAA,CAEA,eAAA,CACA,sBAAA,CACA,qB1BgmFJ,C0B5lFE,kBACE,YAAA,CAMA,gBAAA,CALA,SAAA,CAMA,oBAAA,CAHA,gBAAA,CAIA,WAAA,CAHA,eAAA,CAFA,SAAA,CADA,U1BomFJ,C0B3lFI,iDACE,4B1B6lFN,C0BxlFE,iBACE,eAAA,CACA,sB1B0lFJ,C0BvlFI,gDACE,2B1BylFN,C0BrlFI,kCAIE,kB1B6lFN,C0BjmFI,kCAIE,iB1B6lFN,C0BjmFI,wBAOE,6BAAA,CADA,UAAA,CALA,oBAAA,CAEA,YAAA,CAMA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CALA,uBAAA,CAHA,W1B+lFN,C0BnlFI,iCACE,a1BqlFN,C0BjlFI,iCACE,gDAAA,CAAA,wC1BmlFN,C0B/kFI,+BACE,8CAAA,CAAA,sC1BilFN,C0B7kFI,+BACE,8CAAA,CAAA,sC1B+kFN,C0B3kFI,sCACE,qDAAA,CAAA,6C1B6kFN,C0BvkFA,gBACE,Y1B0kFF,C0BvkFE,gCAIE,kB1B2kFJ,C0B/kFE,gCAIE,iB1B2kFJ,C0B/kFE,sBAGE,kBAAA,CAGA,uCAAA,CALA,mBAAA,CAIA,gBAAA,CAHA,S1B6kFJ,C0BtkFI,+BACE,aAAA,CACA,oB1BwkFN,C0BpkFI,2CACE,U1BukFN,C0BxkFI,2CACE,W1BukFN,C0BxkFI,iCAEE,kB1BskFN,C0BlkFI,0BACE,W1BokFN,C2B3vFA,MACE,iSAAA,CACA,4UAAA,CACA,+NAAA,CACA,gZ3B8vFF,C2BrvFE,iBAME,kDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,cAAA,CAIA,mCAAA,CAAA,2BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CANA,0BAAA,CAFA,a3BgwFJ,C2BpvFE,uBACE,6B3BsvFJ,C2BlvFE,sBACE,wCAAA,CAAA,gC3BovFJ,C2BhvFE,6BACE,+CAAA,CAAA,uC3BkvFJ,C2B9uFE,4BACE,8CAAA,CAAA,sC3BgvFJ,C4B3xFA,SASE,2CAAA,CADA,gCAAA,CAJA,aAAA,CAGA,eAAA,CADA,aAAA,CADA,UAAA,CAFA,S5BkyFF,C4BzxFE,aAZF,SAaI,Y5B4xFF,CACF,CKjnFI,0CuBzLJ,SAkBI,Y5B4xFF,CACF,C4BzxFE,iBACE,mB5B2xFJ,C4BvxFE,yBAIE,iB5B8xFJ,C4BlyFE,yBAIE,kB5B8xFJ,C4BlyFE,eAQE,eAAA,CAPA,YAAA,CAMA,eAAA,CAJA,QAAA,CAEA,aAAA,CAHA,SAAA,CAWA,oBAAA,CAPA,kB5B4xFJ,C4BlxFI,kCACE,Y5BoxFN,C4B/wFE,eACE,aAAA,CACA,kBAAA,CAAA,mB5BixFJ,C4B9wFI,sCACE,aAAA,CACA,S5BgxFN,C4B1wFE,eAOE,kCAAA,CAAA,0BAAA,CANA,YAAA,CAEA,eAAA,CADA,gBAAA,CAMA,UAAA,CAJA,uCAAA,CACA,oBAAA,CAIA,8D5B2wFJ,C4BtwFI,0CACE,aAAA,CACA,S5BwwFN,C4BpwFI,6BAEE,kB5BuwFN,C4BzwFI,6BAEE,iB5BuwFN,C4BzwFI,mBAGE,iBAAA,CAFA,Y5BwwFN,C4BjwFM,2CACE,qB5BmwFR,C4BpwFM,2CACE,qB5BswFR,C4BvwFM,2CACE,qB5BywFR,C4B1wFM,2CACE,qB5B4wFR,C4B7wFM,2CACE,oB5B+wFR,C4BhxFM,2CACE,qB5BkxFR,C4BnxFM,2CACE,qB5BqxFR,C4BtxFM,2CACE,qB5BwxFR,C4BzxFM,4CACE,qB5B2xFR,C4B5xFM,4CACE,oB5B8xFR,C4B/xFM,4CACE,qB5BiyFR,C4BlyFM,4CACE,qB5BoyFR,C4BryFM,4CACE,qB5BuyFR,C4BxyFM,4CACE,qB5B0yFR,C4B3yFM,4CACE,oB5B6yFR,C4BvyFI,gCACE,SAAA,CAIA,yBAAA,CAHA,wC5B0yFN,C6B74FA,MACE,mS7Bg5FF,C6Bv4FE,mCACE,mBAAA,CACA,cAAA,CACA,QAAA,CAEA,mBAAA,CADA,kB7B24FJ,C6Bt4FE,oBAGE,kBAAA,CAOA,+CAAA,CACA,oBAAA,CAVA,mBAAA,CAIA,gBAAA,CACA,0BAAA,CACA,eAAA,CALA,QAAA,CAOA,qBAAA,CADA,eAAA,CAJA,wB7B+4FJ,C6Br4FI,0BAGE,uCAAA,CAFA,aAAA,CACA,YAAA,CAEA,6C7Bu4FN,C6Bl4FM,gEAEE,0CAAA,CADA,+B7Bq4FR,C6B/3FI,yBACE,uB7Bi4FN,C6Bz3FI,gCAME,oDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,qCAAA,CAAA,6BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CAPA,0BAAA,CAFA,W7Bo4FN,C6Bv3FI,wFACE,0C7By3FN,C8Bn8FA,iBACE,GACE,oB9Bs8FF,C8Bn8FA,IACE,kB9Bq8FF,C8Bl8FA,GACE,oB9Bo8FF,CACF,C8B57FA,MACE,yNAAA,CACA,sP9B+7FF,C8Bx7FA,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,S9B47FF,C8B16FE,oBAEE,SAAA,CAKA,uBAAA,CAJA,2EACE,CAHF,S9B+6FJ,C8Br6FE,oBAEE,eAAA,CACA,wBAAA,CAAA,gBAAA,CAFA,U9By6FJ,C8Bp6FI,6CACE,qC9Bs6FN,C8Bl6FI,uCAEE,eAAA,CADA,mB9Bq6FN,C8B/5FI,6BACE,Y9Bi6FN,C8B55FE,8CACE,sC9B85FJ,C8B15FE,mBAEE,gBAAA,CADA,a9B65FJ,C8Bz5FI,2CACE,Y9B25FN,C8Bv5FI,0CACE,e9By5FN,C8Bj5FA,eACE,iBAAA,CACA,eAAA,CAIA,YAAA,CAHA,kBAAA,CAEA,0BAAA,CADA,kB9Bs5FF,C8Bj5FE,yBACE,a9Bm5FJ,C8B/4FE,oBACE,sCAAA,CACA,iB9Bi5FJ,C8B74FE,6BACE,oBAAA,CAGA,gB9B64FJ,C8Bz4FE,sBAYE,mBAAA,CANA,cAAA,CAHA,oBAAA,CACA,gBAAA,CAAA,iBAAA,CAIA,YAAA,CAGA,eAAA,CAVA,iBAAA,CAMA,wBAAA,CAAA,gBAAA,CAFA,uBAAA,CAHA,S9Bm5FJ,C8Br4FI,qCACE,uB9Bu4FN,C8Bn4FI,cArBF,sBAsBI,W9Bs4FJ,C8Bn4FI,wCACE,2B9Bq4FN,C8Bj4FI,6BAOE,qCAAA,CACA,+CAAA,CAAA,uC9Bs4FN,C8B53FI,yDAZE,UAAA,CADA,YAAA,CAKA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CACA,SAAA,CAEA,WAAA,CADA,U9B05FN,C8B34FI,4BAOE,oDAAA,CACA,4CAAA,CAAA,oCAAA,CAQA,uBAAA,CAJA,+C9B+3FN,C8Bx3FM,gDACE,uB9B03FR,C8Bt3FM,mFACE,0C9Bw3FR,CACF,C8Bn3FI,0CAGE,2BAAA,CADA,uBAAA,CADA,S9Bu3FN,C8Bj3FI,8CACE,oB9Bm3FN,C8Bh3FM,aAJF,8CASI,8CAAA,CACA,iBAAA,CAHA,gCAAA,CADA,eAAA,CADA,cAAA,CAGA,kB9Bq3FN,C8Bh3FM,oDACE,mC9Bk3FR,CACF,C8Bt2FE,gCAEE,iBAAA,CADA,e9B02FJ,C8Bt2FI,mCACE,iB9Bw2FN,C8Br2FM,oDAEE,a9Bo3FR,C8Bt3FM,oDAEE,c9Bo3FR,C8Bt3FM,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,CARA,S9Bm3FR,C+BnoGA,MACE,wBAAA,CACA,wB/BsoGF,C+BhoGA,aA+BE,kCAAA,CAAA,0BAAA,CAjBA,gCAAA,CADA,sCAAA,CAGA,SAAA,CADA,mBAAA,CAdA,iBAAA,CAGA,wDACE,CAgBF,4BAAA,CAGA,uEACE,CARF,uDACE,CANF,UAAA,CADA,S/BooGF,C+B7mGE,oBAuBE,8CAAA,CAAA,+CAAA,CADA,UAAA,CADA,aAAA,CAfA,gJACE,CANF,iBAAA,CAmBA,S/BimGJ,C+B1lGE,yBAGE,kEAAA,CAFA,gDAAA,CACA,6C/B6lGJ,C+BxlGE,4BAGE,qEAAA,CADA,8CAAA,CADA,6C/B4lGJ,C+BtlGE,qBAEE,SAAA,CAKA,uBAAA,CAJA,wEACE,CAHF,S/B2lGJ,C+BjlGE,oBAqBE,uBAAA,CAEA,2CAAA,CACA,mBAAA,CACA,8BAAA,CAnBA,0FACE,CAaF,eAAA,CADA,8BAAA,CAlBA,iBAAA,CAqBA,oB/BskGJ,C+BhkGI,uCAEE,YAAA,CADA,W/BmkGN,C+B9jGI,6CACE,oD/BgkGN,C+B7jGM,mDACE,0C/B+jGR,C+BvjGI,mCAwBE,eAAA,CACA,eAAA,CAxBA,oIACE,CAgBF,sCACE,CAIF,mBAAA,CAKA,wBAAA,CAAA,gBAAA,CAbA,sBAAA,CAAA,iB/BijGN,C+BhiGI,4CACE,Y/BkiGN,C+B9hGI,2CACE,e/BgiGN,CgCntGA,kBAME,ehC+tGF,CgCruGA,kBAME,gBhC+tGF,CgCruGA,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,ShCkuGF,CgC/sGE,aAtBF,QAuBI,YhCktGF,CACF,CgC/sGE,kBACE,wBhCitGJ,CgC7sGE,gBAEE,SAAA,CADA,mBAAA,CAGA,+BAAA,CADA,uBhCgtGJ,CgC5sGI,0BACE,8BhC8sGN,CgCzsGE,4BAEE,0CAAA,CADA,+BhC4sGJ,CgCvsGE,YACE,oBAAA,CACA,oBhCysGJ,CiC9vGA,oBACE,GACE,mBjCiwGF,CACF,CiCzvGA,MACE,wfjC2vGF,CiCrvGA,YACE,aAAA,CAEA,eAAA,CADA,ajCyvGF,CiCrvGE,+BAOE,kBAAA,CAAA,kBjCsvGJ,CiC7vGE,+BAOE,iBAAA,CAAA,mBjCsvGJ,CiC7vGE,qBAQE,aAAA,CACA,cAAA,CACA,YAAA,CATA,iBAAA,CAKA,UjCuvGJ,CiChvGI,qCAIE,iBjCwvGN,CiC5vGI,qCAIE,kBjCwvGN,CiC5vGI,2BAME,6BAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,WjC0vGN,CiC7uGE,mBACE,iBAAA,CACA,UjC+uGJ,CiC3uGE,kBAWE,2CAAA,CACA,mBAAA,CACA,8BAAA,CALA,gCAAA,CACA,oBAAA,CAHA,kBAAA,CAFA,YAAA,CAUA,SAAA,CAPA,aAAA,CAFA,SAAA,CAJA,iBAAA,CASA,4BAAA,CARA,UAAA,CAaA,+CACE,CAbF,SjCyvGJ,CiCxuGI,+EACE,gBAAA,CACA,SAAA,CACA,sCjC0uGN,CiCpuGI,qCAEE,oCACE,gCjCquGN,CiCjuGI,2CACE,cjCmuGN,CACF,CiC9tGE,kBACE,kBjCguGJ,CiC5tGE,4BAGE,kBAAA,CAAA,oBjCmuGJ,CiCtuGE,4BAGE,mBAAA,CAAA,mBjCmuGJ,CiCtuGE,kBAKE,cAAA,CAJA,aAAA,CAMA,YAAA,CADA,uBAAA,CAEA,2CACE,CALF,kBAAA,CAFA,UjCouGJ,CiCztGI,gDACE,+BjC2tGN,CiCvtGI,wBACE,qDjCytGN,CkC/zGA,MAEI,6VAAA,CAAA,uWAAA,CAAA,qPAAA,CAAA,2xBAAA,CAAA,qMAAA,CAAA,+aAAA,CAAA,2LAAA,CAAA,yPAAA,CAAA,2TAAA,CAAA,oaAAA,CAAA,2SAAA,CAAA,2LlCw1GJ,CkC50GE,4CAME,8CAAA,CACA,4BAAA,CACA,mBAAA,CACA,8BAAA,CAJA,mCAAA,CAJA,iBAAA,CAGA,gBAAA,CADA,iBAAA,CADA,eAAA,CASA,uBAAA,CADA,2BlCg1GJ,CkC50GI,aAdF,4CAeI,elC+0GJ,CACF,CkC50GI,sEACE,gClC80GN,CkCz0GI,gDACE,qBlC20GN,CkCv0GI,gIAEE,iBAAA,CADA,clC00GN,CkCr0GI,4FACE,iBlCu0GN,CkCn0GI,kFACE,elCq0GN,CkCj0GI,0FACE,YlCm0GN,CkC/zGI,8EACE,mBlCi0GN,CkC5zGE,sEAGE,iBAAA,CAAA,mBlCs0GJ,CkCz0GE,sEAGE,kBAAA,CAAA,kBlCs0GJ,CkCz0GE,sEASE,uBlCg0GJ,CkCz0GE,sEASE,wBlCg0GJ,CkCz0GE,sEAUE,4BlC+zGJ,CkCz0GE,4IAWE,6BlC8zGJ,CkCz0GE,sEAWE,4BlC8zGJ,CkCz0GE,kDAOE,0BAAA,CACA,WAAA,CAFA,eAAA,CADA,eAAA,CAHA,oBAAA,CAAA,iBAAA,CADA,iBlCw0GJ,CkC3zGI,kFACE,elC6zGN,CkCzzGI,oFAEE,UlCo0GN,CkCt0GI,oFAEE,WlCo0GN,CkCt0GI,gEAOE,wBhBiIU,CgBlIV,UAAA,CADA,WAAA,CAGA,kDAAA,CAAA,0CAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CACA,UlCk0GN,CkCvzGI,4DACE,4DlCyzGN,CkC3yGE,sDACE,oBlC8yGJ,CkC3yGI,gFACE,gClC6yGN,CkCxyGE,8DACE,0BlC2yGJ,CkCxyGI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClC0yGN,CkCtyGI,0EACE,alCwyGN,CkC7zGE,8DACE,oBlCg0GJ,CkC7zGI,wFACE,gClC+zGN,CkC1zGE,sEACE,0BlC6zGJ,CkC1zGI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClC4zGN,CkCxzGI,kFACE,alC0zGN,CkC/0GE,sDACE,oBlCk1GJ,CkC/0GI,gFACE,gClCi1GN,CkC50GE,8DACE,0BlC+0GJ,CkC50GI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClC80GN,CkC10GI,0EACE,alC40GN,CkCj2GE,oDACE,oBlCo2GJ,CkCj2GI,8EACE,gClCm2GN,CkC91GE,4DACE,0BlCi2GJ,CkC91GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClCg2GN,CkC51GI,wEACE,alC81GN,CkCn3GE,4DACE,oBlCs3GJ,CkCn3GI,sFACE,gClCq3GN,CkCh3GE,oEACE,0BlCm3GJ,CkCh3GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCk3GN,CkC92GI,gFACE,alCg3GN,CkCr4GE,8DACE,oBlCw4GJ,CkCr4GI,wFACE,gClCu4GN,CkCl4GE,sEACE,0BlCq4GJ,CkCl4GI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClCo4GN,CkCh4GI,kFACE,alCk4GN,CkCv5GE,4DACE,oBlC05GJ,CkCv5GI,sFACE,gClCy5GN,CkCp5GE,oEACE,0BlCu5GJ,CkCp5GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCs5GN,CkCl5GI,gFACE,alCo5GN,CkCz6GE,4DACE,oBlC46GJ,CkCz6GI,sFACE,gClC26GN,CkCt6GE,oEACE,0BlCy6GJ,CkCt6GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCw6GN,CkCp6GI,gFACE,alCs6GN,CkC37GE,0DACE,oBlC87GJ,CkC37GI,oFACE,gClC67GN,CkCx7GE,kEACE,0BlC27GJ,CkCx7GI,gFACE,wBAlBG,CAmBH,oDAAA,CAAA,4ClC07GN,CkCt7GI,8EACE,alCw7GN,CkC78GE,oDACE,oBlCg9GJ,CkC78GI,8EACE,gClC+8GN,CkC18GE,4DACE,0BlC68GJ,CkC18GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClC48GN,CkCx8GI,wEACE,alC08GN,CkC/9GE,4DACE,oBlCk+GJ,CkC/9GI,sFACE,gClCi+GN,CkC59GE,oEACE,0BlC+9GJ,CkC59GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClC89GN,CkC19GI,gFACE,alC49GN,CkCj/GE,wDACE,oBlCo/GJ,CkCj/GI,kFACE,gClCm/GN,CkC9+GE,gEACE,0BlCi/GJ,CkC9+GI,8EACE,wBAlBG,CAmBH,mDAAA,CAAA,2ClCg/GN,CkC5+GI,4EACE,alC8+GN,CmClpHA,MACE,qMnCqpHF,CmC5oHE,sBAEE,uCAAA,CADA,gBnCgpHJ,CmC5oHI,mCACE,anC8oHN,CmC/oHI,mCACE,cnC8oHN,CmC1oHM,4BACE,sBnC4oHR,CmCzoHQ,mCACE,gCnC2oHV,CmCvoHQ,2DACE,SAAA,CAEA,uBAAA,CADA,enC0oHV,CmCroHQ,yGACE,SAAA,CACA,uBnCuoHV,CmCnoHQ,yCACE,YnCqoHV,CmC9nHE,0BACE,eAAA,CACA,enCgoHJ,CmC7nHI,+BACE,oBnC+nHN,CmC1nHE,gDACE,YnC4nHJ,CmCxnHE,8BAIE,+BAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,SAAA,CAKA,4BAAA,CAJA,4DACE,CAHF,0BnC4nHJ,CmCnnHI,aAdF,8BAeI,+BAAA,CACA,SAAA,CACA,uBnCsnHJ,CACF,CmCnnHI,wCACE,6BnCqnHN,CmCjnHI,oCACE,+BnCmnHN,CmC/mHI,qCAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,YAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,WnCwnHN,CmC3mHQ,mDACE,oBnC6mHV,CoC3tHE,kCAEE,iBpCiuHJ,CoCnuHE,kCAEE,kBpCiuHJ,CoCnuHE,wBAGE,yCAAA,CAFA,oBAAA,CAGA,SAAA,CACA,mCpC8tHJ,CoCztHI,aAVF,wBAWI,YpC4tHJ,CACF,CoCxtHE,6FAEE,SAAA,CACA,mCpC0tHJ,CoCptHE,4FAEE,+BpCstHJ,CoCltHE,oBACE,yBAAA,CACA,uBAAA,CAGA,yEpCktHJ,CKnlHI,sC+BrHE,qDACE,uBpC2sHN,CACF,CoCtsHE,kEACE,yBpCwsHJ,CoCpsHE,sBACE,0BpCssHJ,CqCjwHE,2BACE,arCowHJ,CK/kHI,0CgCtLF,2BAKI,erCowHJ,CqCjwHI,6BACE,iBrCmwHN,CACF,CqC/vHI,6BAEE,0BAAA,CAAA,2BAAA,CADA,eAAA,CAEA,iBrCiwHN,CqC9vHM,2CACE,kBrCgwHR,CqC1vHI,6CACE,QrC4vHN,CsCxxHE,uBACE,4CtC4xHJ,CsCvxHE,8CAJE,kCAAA,CAAA,0BtC+xHJ,CsC3xHE,uBACE,4CtC0xHJ,CsCrxHE,4BAEE,kCAAA,CAAA,0BAAA,CADA,qCtCwxHJ,CsCpxHI,mCACE,atCsxHN,CsClxHI,kCACE,atCoxHN,CsC/wHE,0BAKE,eAAA,CAJA,aAAA,CAEA,YAAA,CACA,aAAA,CAFA,kBAAA,CAAA,mBtCoxHJ,CsC9wHI,uCACE,etCgxHN,CsC5wHI,sCACE,kBtC8wHN,CuC3zHA,MACE,oLvC8zHF,CuCrzHE,oBAGE,iBAAA,CAEA,gBAAA,CADA,avCuzHJ,CuCnzHI,wCACE,uBvCqzHN,CuCjzHI,gCAEE,eAAA,CADA,gBvCozHN,CuC7yHM,wCACE,mBvC+yHR,CuCzyHE,8BAKE,oBvC6yHJ,CuClzHE,8BAKE,mBvC6yHJ,CuClzHE,8BAUE,4BvCwyHJ,CuClzHE,4DAWE,6BvCuyHJ,CuClzHE,8BAWE,4BvCuyHJ,CuClzHE,oBASE,cAAA,CANA,aAAA,CACA,eAAA,CAIA,evC0yHJ,CuCpyHI,kCACE,uCAAA,CACA,oBvCsyHN,CuClyHI,wCAEE,uCAAA,CADA,YvCqyHN,CuChyHI,oCAEE,WvC6yHN,CuC/yHI,oCAEE,UvC6yHN,CuC/yHI,0BAOE,6BAAA,CADA,UAAA,CADA,WAAA,CAGA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CAUA,sBAAA,CADA,yBAAA,CARA,UvC2yHN,CuC/xHM,oCACE,wBvCiyHR,CuC5xHI,4BACE,YvC8xHN,CuCzxHI,4CACE,YvC2xHN,CwCr3HE,+DACE,sBAAA,CAEA,mBAAA,CACA,0BAAA,CACA,uBxCu3HJ,CwCp3HI,2EAGE,iBAAA,CADA,eAAA,CADA,yBxCw3HN,CwCj3HE,mEACE,0BxCm3HJ,CwC/2HE,oBACE,qBxCi3HJ,CwC72HE,gBACE,oBxC+2HJ,CwC32HE,gBACE,qBxC62HJ,CwCz2HE,iBACE,kBxC22HJ,CwCv2HE,kBACE,kBxCy2HJ,CyCl5HE,6BACE,sCzCq5HJ,CyCl5HE,cACE,yCzCo5HJ,CyCx4HE,sIACE,oCzC04HJ,CyCl4HE,2EACE,qCzCo4HJ,CyC13HE,wGACE,oCzC43HJ,CyCn3HE,yFACE,qCzCq3HJ,CyCh3HE,6BACE,kCzCk3HJ,CyC52HE,6CACE,sCzC82HJ,CyCv2HE,4DACE,sCzCy2HJ,CyCl2HE,4DACE,qCzCo2HJ,CyC31HE,yFACE,qCzC61HJ,CyCr1HE,2EACE,sCzCu1HJ,CyC50HE,wHACE,qCzC80HJ,CyCz0HE,8BAGE,mBAAA,CADA,gBAAA,CADA,gBzC60HJ,CyCx0HE,eACE,4CzC00HJ,CyCv0HE,eACE,4CzCy0HJ,CyCr0HE,gBAIE,+CAAA,CACA,kDAAA,CAJA,aAAA,CAEA,wBAAA,CADA,wBzC00HJ,CyCn0HE,yBAOE,wCAAA,CACA,+DAAA,CACA,4BAAA,CACA,6BAAA,CARA,iBAAA,CAGA,eAAA,CACA,eAAA,CAFA,cAAA,CADA,oCAAA,CAFA,iBzC80HJ,CyCl0HI,6BACE,YzCo0HN,CyCj0HM,kCACE,wBAAA,CACA,yBzCm0HR,CyC7zHE,iCAaE,wCAAA,CACA,+DAAA,CAJA,uCAAA,CACA,0BAAA,CALA,UAAA,CAJA,oBAAA,CAOA,2BAAA,CADA,2BAAA,CADA,2BAAA,CANA,eAAA,CAWA,wBAAA,CAAA,gBAAA,CAPA,SzCs0HJ,CyCpzHE,sBACE,iBAAA,CACA,iBzCszHJ,CyCjzHE,iCAKE,ezC+yHJ,CyC5yHI,sCACE,gBzC8yHN,CyC1yHI,gDACE,YzC4yHN,CyClyHA,gBACE,iBzCqyHF,CyCjyHE,yCACE,aAAA,CACA,SzCmyHJ,CyC9xHE,mBACE,YzCgyHJ,CyC3xHE,oBACE,QzC6xHJ,CyCzxHE,4BACE,WAAA,CACA,SAAA,CACA,ezC2xHJ,CyCxxHI,0CACE,YzC0xHN,CyCpxHE,yBAKE,wCAAA,CAEA,+BAAA,CADA,4BAAA,CAHA,eAAA,CADA,oDAAA,CAEA,wBAAA,CAAA,gBzCyxHJ,CyClxHE,2BAEE,+DAAA,CADA,2BzCqxHJ,CyCjxHI,+BACE,uCAAA,CACA,gBzCmxHN,CyC9wHE,sBACE,MAAA,CACA,WzCgxHJ,CyC3wHA,aACE,azC8wHF,CyCpwHE,4BAEE,aAAA,CADA,YzCwwHJ,CyCpwHI,wDAEE,2BAAA,CADA,wBzCuwHN,CyCjwHE,+BAKE,2CAAA,CAEA,+BAAA,CADA,gCAAA,CADA,sBAAA,CAHA,mBAAA,CACA,gBAAA,CAFA,azCywHJ,CyChwHI,qCAEE,UAAA,CACA,UAAA,CAFA,azCowHN,CK34HI,0CoCsJF,8BACE,iBzCyvHF,CyC/uHE,wSAGE,ezCqvHJ,CyCjvHE,sCAEE,mBAAA,CACA,eAAA,CADA,oBAAA,CADA,kBAAA,CAAA,mBzCqvHJ,CACF,C0CllII,yDAIE,+BAAA,CACA,8BAAA,CAFA,aAAA,CADA,QAAA,CADA,iB1CwlIN,C0ChlII,uBAEE,uCAAA,CADA,c1CmlIN,C0C9hIM,iHAEE,WAlDkB,CAiDlB,kB1CyiIR,C0C1iIM,6HAEE,WAlDkB,CAiDlB,kB1CqjIR,C0CtjIM,6HAEE,WAlDkB,CAiDlB,kB1CikIR,C0ClkIM,oHAEE,WAlDkB,CAiDlB,kB1C6kIR,C0C9kIM,0HAEE,WAlDkB,CAiDlB,kB1CylIR,C0C1lIM,uHAEE,WAlDkB,CAiDlB,kB1CqmIR,C0CtmIM,uHAEE,WAlDkB,CAiDlB,kB1CinIR,C0ClnIM,6HAEE,WAlDkB,CAiDlB,kB1C6nIR,C0C9nIM,yCAEE,WAlDkB,CAiDlB,kB1CioIR,C0CloIM,yCAEE,WAlDkB,CAiDlB,kB1CqoIR,C0CtoIM,0CAEE,WAlDkB,CAiDlB,kB1CyoIR,C0C1oIM,uCAEE,WAlDkB,CAiDlB,kB1C6oIR,C0C9oIM,wCAEE,WAlDkB,CAiDlB,kB1CipIR,C0ClpIM,sCAEE,WAlDkB,CAiDlB,kB1CqpIR,C0CtpIM,wCAEE,WAlDkB,CAiDlB,kB1CypIR,C0C1pIM,oCAEE,WAlDkB,CAiDlB,kB1C6pIR,C0C9pIM,2CAEE,WAlDkB,CAiDlB,kB1CiqIR,C0ClqIM,qCAEE,WAlDkB,CAiDlB,kB1CqqIR,C0CtqIM,oCAEE,WAlDkB,CAiDlB,kB1CyqIR,C0C1qIM,kCAEE,WAlDkB,CAiDlB,kB1C6qIR,C0C9qIM,qCAEE,WAlDkB,CAiDlB,kB1CirIR,C0ClrIM,mCAEE,WAlDkB,CAiDlB,kB1CqrIR,C0CtrIM,qCAEE,WAlDkB,CAiDlB,kB1CyrIR,C0C1rIM,wCAEE,WAlDkB,CAiDlB,kB1C6rIR,C0C9rIM,sCAEE,WAlDkB,CAiDlB,kB1CisIR,C0ClsIM,2CAEE,WAlDkB,CAiDlB,kB1CqsIR,C0C1rIM,iCAEE,WAPkB,CAMlB,iB1C6rIR,C0C9rIM,uCAEE,WAPkB,CAMlB,iB1CisIR,C0ClsIM,mCAEE,WAPkB,CAMlB,iB1CqsIR,C2CvxIA,MACE,2LAAA,CACA,yL3C0xIF,C2CjxIE,wBAKE,mBAAA,CAHA,YAAA,CACA,qBAAA,CACA,YAAA,CAHA,iB3CwxIJ,C2C9wII,8BAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,O3CkxIN,C2C7wIM,qCACE,0B3C+wIR,C2ClvIM,kEACE,0C3CovIR,C2C9uIE,2BAME,uBAAA,CADA,+DAAA,CAJA,YAAA,CACA,cAAA,CACA,aAAA,CACA,oB3CkvIJ,C2C7uII,aATF,2BAUI,gB3CgvIJ,CACF,C2C7uII,cAGE,+BACE,iB3C6uIN,C2C1uIM,sCAQE,qCAAA,CANA,QAAA,CAKA,UAAA,CAHA,aAAA,CAEA,UAAA,CAHA,MAAA,CAFA,iBAAA,CAaA,2CAAA,CALA,2DACE,CAGF,kDAAA,CARA,+B3CkvIR,CACF,C2CpuII,8CACE,Y3CsuIN,C2CluII,iCAUE,+BAAA,CACA,6BAAA,CALA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,gBAAA,CACA,eAAA,CAFA,8BAAA,CAMA,+BAAA,CAGA,2CACE,CANF,kBAAA,CALA,U3C8uIN,C2C/tIM,aAII,6CACE,O3C8tIV,C2C/tIQ,8CACE,O3CiuIV,C2CluIQ,8CACE,O3CouIV,C2CruIQ,8CACE,O3CuuIV,C2CxuIQ,8CACE,O3C0uIV,C2C3uIQ,8CACE,O3C6uIV,C2C9uIQ,8CACE,O3CgvIV,C2CjvIQ,8CACE,O3CmvIV,C2CpvIQ,8CACE,O3CsvIV,C2CvvIQ,+CACE,Q3CyvIV,C2C1vIQ,+CACE,Q3C4vIV,C2C7vIQ,+CACE,Q3C+vIV,C2ChwIQ,+CACE,Q3CkwIV,C2CnwIQ,+CACE,Q3CqwIV,C2CtwIQ,+CACE,Q3CwwIV,C2CzwIQ,+CACE,Q3C2wIV,C2C5wIQ,+CACE,Q3C8wIV,C2C/wIQ,+CACE,Q3CixIV,C2ClxIQ,+CACE,Q3CoxIV,C2CrxIQ,+CACE,Q3CuxIV,CACF,C2ClxIM,uCACE,gC3CoxIR,C2ChxIM,oDACE,a3CkxIR,C2C7wII,yCACE,S3C+wIN,C2C3wIM,2CACE,aAAA,CACA,8B3C6wIR,C2CvwIE,4BACE,U3CywIJ,C2CtwII,aAJF,4BAKI,gB3CywIJ,CACF,C2CrwIE,0BACE,Y3CuwIJ,C2CpwII,aAJF,0BAKI,a3CuwIJ,C2CnwIM,sCACE,O3CqwIR,C2CtwIM,uCACE,O3CwwIR,C2CzwIM,uCACE,O3C2wIR,C2C5wIM,uCACE,O3C8wIR,C2C/wIM,uCACE,O3CixIR,C2ClxIM,uCACE,O3CoxIR,C2CrxIM,uCACE,O3CuxIR,C2CxxIM,uCACE,O3C0xIR,C2C3xIM,uCACE,O3C6xIR,C2C9xIM,wCACE,Q3CgyIR,C2CjyIM,wCACE,Q3CmyIR,C2CpyIM,wCACE,Q3CsyIR,C2CvyIM,wCACE,Q3CyyIR,C2C1yIM,wCACE,Q3C4yIR,C2C7yIM,wCACE,Q3C+yIR,C2ChzIM,wCACE,Q3CkzIR,C2CnzIM,wCACE,Q3CqzIR,C2CtzIM,wCACE,Q3CwzIR,C2CzzIM,wCACE,Q3C2zIR,C2C5zIM,wCACE,Q3C8zIR,CACF,C2CxzII,+FAEE,Q3C0zIN,C2CvzIM,yGACE,wBAAA,CACA,yB3C0zIR,C2CjzIM,2DAEE,wBAAA,CACA,yBAAA,CAFA,Q3CqzIR,C2C9yIM,iEACE,Q3CgzIR,C2C7yIQ,qLAGE,wBAAA,CACA,yBAAA,CAFA,Q3CizIV,C2C3yIQ,6FACE,wBAAA,CACA,yB3C6yIV,C2CxyIM,yDACE,kB3C0yIR,C2CryII,sCACE,Q3CuyIN,C2ClyIE,2BAEE,iBAAA,CAOA,kBAAA,CAHA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,YAAA,CACA,gBAAA,CAEA,mBAAA,CAGA,gCAAA,CAPA,W3C2yIJ,C2CjyII,iCAEE,uDAAA,CADA,+B3CoyIN,C2C/xII,iCAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,8CAAA,CAAA,sCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,+CACE,CATF,U3CyyIN,C2C1xIE,4BAOE,yEACE,CANF,YAAA,CAGA,aAAA,CAFA,qBAAA,CAGA,mBAAA,CALA,iBAAA,CAYA,wBAAA,CATA,Y3CgyIJ,C2CpxII,sCACE,wB3CsxIN,C2ClxII,oCACE,S3CoxIN,C2ChxII,kCAGE,wEACE,CAFF,mBAAA,CADA,O3CoxIN,C2C1wIM,uDACE,8CAAA,CAAA,sC3C4wIR,CKn5II,0CsCqJF,wDAEE,kB3CowIF,C2CtwIA,wDAEE,mB3CowIF,C2CtwIA,8CAGE,eAAA,CAFA,eAAA,CAGA,iC3CkwIF,C2C9vIE,8DACE,mB3CiwIJ,C2ClwIE,8DACE,kB3CiwIJ,C2ClwIE,oDAEE,U3CgwIJ,C2C5vIE,8EAEE,kB3C+vIJ,C2CjwIE,8EAEE,mB3C+vIJ,C2CjwIE,8EAGE,kB3C8vIJ,C2CjwIE,8EAGE,mB3C8vIJ,C2CjwIE,oEACE,U3CgwIJ,C2C1vIE,8EAEE,mB3C6vIJ,C2C/vIE,8EAEE,kB3C6vIJ,C2C/vIE,8EAGE,mB3C4vIJ,C2C/vIE,8EAGE,kB3C4vIJ,C2C/vIE,oEACE,U3C8vIJ,CACF,C2ChvIE,cAHF,olDAII,gC3CmvIF,C2ChvIE,g8GACE,uC3CkvIJ,CACF,C2C7uIA,4sDACE,+B3CgvIF,C2C5uIA,wmDACE,a3C+uIF,C4CnnJA,MACE,qWAAA,CACA,8W5CsnJF,C4C7mJE,4BAEE,oBAAA,CADA,iB5CinJJ,C4C5mJI,sDAEE,S5C+mJN,C4CjnJI,sDAEE,U5C+mJN,C4CjnJI,4CACE,iBAAA,CAEA,S5C8mJN,C4CzmJE,+CAEE,SAAA,CADA,U5C4mJJ,C4CvmJE,kDAEE,W5CknJJ,C4CpnJE,kDAEE,Y5CknJJ,C4CpnJE,wCAOE,qDAAA,CADA,UAAA,CADA,aAAA,CAGA,0CAAA,CAAA,kCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,SAAA,CACA,Y5CgnJJ,C4CrmJE,gEACE,wB1B2Wa,C0B1Wb,mDAAA,CAAA,2C5CumJJ,C6CvpJA,aAQE,wBACE,Y7CspJF,CACF,C8ChqJA,QACE,8DAAA,CAGA,+CAAA,CACA,iEAAA,CACA,oDAAA,CACA,sDAAA,CACA,mDAAA,CAGA,qEAAA,CACA,qEAAA,CACA,wEAAA,CACA,0EAAA,CACA,wEAAA,CACA,yEAAA,CACA,kEAAA,CACA,+DAAA,CACA,oEAAA,CACA,oEAAA,CACA,mEAAA,CACA,gEAAA,CACA,uEAAA,CACA,mEAAA,CACA,qEAAA,CACA,oEAAA,CACA,gEAAA,CACA,wEAAA,CACA,qEAAA,CACA,+D9C8pJF,C8CxpJA,SAEE,kBAAA,CADA,Y9C4pJF,C+C9rJE,kBAUE,cAAA,CATA,YAAA,CACA,kEACE,CAQF,Y/C0rJJ,C+CtrJI,sDACE,gB/CwrJN,C+ClrJI,oFAKE,wDAAA,CACA,mBAAA,CAJA,aAAA,CAEA,QAAA,CADA,aAAA,CAIA,sC/CorJN,C+C/qJM,iOACE,kBAAA,CACA,8B/CkrJR,C+C9qJM,6FACE,iBAAA,CAAA,c/CirJR,C+C7qJM,2HACE,Y/CgrJR,C+C5qJM,wHACE,e/C+qJR,C+ChqJI,yMAGE,eAAA,CAAA,Y/CwqJN,C+C1pJI,ybAOE,W/CgqJN,C+C5pJI,8BACE,eAAA,CAAA,Y/C8pJN,CK1lJI,mC2ChKA,8BACE,UhDkwJJ,CgDnwJE,8BACE,WhDkwJJ,CgDnwJE,8BAGE,kBhDgwJJ,CgDnwJE,8BAGE,iBhDgwJJ,CgDnwJE,oBAKE,mBAAA,CADA,YAAA,CAFA,ahDiwJJ,CgD3vJI,kCACE,WhD8vJN,CgD/vJI,kCACE,UhD8vJN,CgD/vJI,kCAEE,iBAAA,CAAA,chD6vJN,CgD/vJI,kCAEE,aAAA,CAAA,kBhD6vJN,CACF","file":"main.css"} \ No newline at end of file diff --git a/main/assets/stylesheets/main.6f8fc17f.min.css b/main/assets/stylesheets/main.6f8fc17f.min.css new file mode 100644 index 000000000..a0d06b0eb --- /dev/null +++ b/main/assets/stylesheets/main.6f8fc17f.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 h5 code{text-transform:none}.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}.md-typeset ul[type]{list-style-type:revert-layer}[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}.md-typeset ol ol ol ol,.md-typeset ul ol ol ol{list-style-type:upper-alpha}.md-typeset ol ol ol ol ol,.md-typeset ul ol ol ol ol{list-style-type:upper-roman}.md-typeset ol[type],.md-typeset ul[type]{list-style-type:revert-layer}[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:-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{border-radius:100%;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)}.md-typeset .md-code__content{display:grid}@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)}@media print{.md-feedback{display:none}}.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;position:relative}.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{-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-style:normal;font-weight:400;outline:none;text-align:initial;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}:root{--md-tooltip-width:20rem;--md-tooltip-tail:0.3rem}.md-tooltip2{-webkit-backface-visibility:hidden;backface-visibility:hidden;color:var(--md-default-fg-color);font-family:var(--md-text-font-family);opacity:0;pointer-events:none;position:absolute;top:calc(var(--md-tooltip-host-y) + var(--md-tooltip-y));transform:translateY(-.4rem);transform-origin:calc(var(--md-tooltip-host-x) + var(--md-tooltip-x)) 0;transition:transform 0ms .25s,opacity .25s,z-index .25s;width:100%;z-index:0}.md-tooltip2:before{border-left:var(--md-tooltip-tail) solid #0000;border-right:var(--md-tooltip-tail) solid #0000;content:"";display:block;left:clamp(1.5 * .8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-tail),100vw - 2 * var(--md-tooltip-tail) - 1.5 * .8rem);position:absolute;z-index:1}.md-tooltip2--top:before{border-top:var(--md-tooltip-tail) solid var(--md-default-bg-color);bottom:calc(var(--md-tooltip-tail)*-1 + .025rem);filter:drop-shadow(0 1px 0 hsla(0,0%,0%,.05))}.md-tooltip2--bottom:before{border-bottom:var(--md-tooltip-tail) solid var(--md-default-bg-color);filter:drop-shadow(0 -1px 0 hsla(0,0%,0%,.05));top:calc(var(--md-tooltip-tail)*-1 + .025rem)}.md-tooltip2--active{opacity:1;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.5,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip2__inner{scrollbar-gutter:stable;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);max-height:40vh;max-width:calc(100vw - 1.6rem);position:relative;scrollbar-width:thin}.md-tooltip2__inner::-webkit-scrollbar{height:.2rem;width:.2rem}.md-tooltip2__inner::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-tooltip2__inner::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}[role=tooltip]>.md-tooltip2__inner{font-size:.5rem;font-weight:700;left:clamp(.8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-width)/2,100vw - var(--md-tooltip-width) - .8rem);max-width:min(100vw - 2 * .8rem,400px);padding:.2rem .4rem;-webkit-user-select:none;user-select:none;width:-moz-fit-content;width:fit-content}.md-tooltip2__inner.md-typeset>:first-child{margin-top:0}.md-tooltip2__inner.md-typeset>:last-child{margin-bottom:0}[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__alias{margin-left:.3rem;opacity:.7}.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: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]{display:initial}.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)}@media print{.giscus,[id=__comments]{display:none}}: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.6f8fc17f.min.css.map b/main/assets/stylesheets/main.6f8fc17f.min.css.map new file mode 100644 index 000000000..8ba5ce39e --- /dev/null +++ b/main/assets/stylesheets/main.6f8fc17f.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/_code.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/_tooltip2.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/_giscus.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,gBC4yCF,CC1zCA,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,wNAAA,CACA,gNAAA,CACA,iNJgIF,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,CIzGI,oBACE,mBJ2GN,CItGE,eAGE,+DAAA,CAFA,iBAAA,CACA,cJyGJ,CIpGE,cACE,+BAAA,CACA,qBJsGJ,CInGI,mCAEE,sBJoGN,CIhGI,wCACE,+BJkGN,CI/FM,kDACE,uDJiGR,CI5FI,mBACE,kBAAA,CACA,iCJ8FN,CI1FI,4BACE,uCAAA,CACA,oBJ4FN,CIvFE,iDAIE,6BAAA,CACA,aAAA,CAFA,2BJ2FJ,CItFI,aARF,iDASI,oBJ2FJ,CACF,CIvFE,iBAIE,wCAAA,CACA,mBAAA,CACA,kCAAA,CAAA,0BAAA,CAJA,eAAA,CADA,uBAAA,CAEA,qBJ4FJ,CItFI,qCAEE,uCAAA,CADA,YJyFN,CInFE,gBAEE,iBAAA,CACA,eAAA,CAFA,iBJuFJ,CIlFI,qBAWE,kCAAA,CAAA,0BAAA,CADA,eAAA,CATA,aAAA,CAEA,QAAA,CAMA,uCAAA,CALA,aAAA,CAFA,oCAAA,CAKA,yDAAA,CACA,oBAAA,CAFA,iBAAA,CADA,iBJ0FN,CIjFM,2BACE,+CJmFR,CI/EM,wCAEE,YAAA,CADA,WJkFR,CI7EM,8CACE,oDJ+ER,CI5EQ,oDACE,0CJ8EV,CIvEE,gBAOE,4CAAA,CACA,mBAAA,CACA,mKACE,CANF,gCAAA,CAHA,oBAAA,CAEA,eAAA,CADA,uBAAA,CAIA,uBAAA,CADA,qBJ6EJ,CIlEE,iBAGE,6CAAA,CACA,kCAAA,CAAA,0BAAA,CAHA,aAAA,CACA,qBJsEJ,CIhEE,iBAGE,6DAAA,CADA,WAAA,CADA,oBJoEJ,CI9DE,kBACE,WJgEJ,CI5DE,oDAEE,qBJ8DJ,CIhEE,oDAEE,sBJ8DJ,CI1DE,iCACE,kBJ+DJ,CIhEE,iCACE,mBJ+DJ,CIhEE,iCAIE,2DJ4DJ,CIhEE,iCAIE,4DJ4DJ,CIhEE,uBAGE,uCAAA,CADA,aAAA,CAAA,cJ8DJ,CIxDE,eACE,oBJ0DJ,CItDI,qBACE,4BJwDN,CInDE,kDAGE,kBJqDJ,CIxDE,kDAGE,mBJqDJ,CIxDE,8BAEE,SJsDJ,CIlDI,0DACE,iBJqDN,CIjDI,oCACE,2BJoDN,CIjDM,0CACE,2BJoDR,CIjDQ,gDACE,2BJoDV,CIjDU,sDACE,2BJoDZ,CI5CI,0CACE,4BJ+CN,CI3CI,wDACE,kBJ+CN,CIhDI,wDACE,mBJ+CN,CIhDI,oCAEE,kBJ8CN,CI3CM,kGAEE,aJ+CR,CI3CM,0DACE,eJ8CR,CI1CM,4HAEE,kBJ6CR,CI/CM,4HAEE,mBJ6CR,CI/CM,oFACE,kBAAA,CAAA,eJ8CR,CIvCE,yBAEE,mBJyCJ,CI3CE,yBAEE,oBJyCJ,CI3CE,eACE,mBAAA,CAAA,cJ0CJ,CIrCE,kDAIE,WAAA,CADA,cJwCJ,CIhCI,4BAEE,oBJkCN,CI9BI,6BAEE,oBJgCN,CI5BI,kCACE,YJ8BN,CIzBE,mBACE,iBAAA,CAGA,eAAA,CADA,cAAA,CAEA,iBAAA,CAHA,sBAAA,CAAA,iBJ8BJ,CIxBI,uBACE,aAAA,CACA,aJ0BN,CIrBE,uBAGE,iBAAA,CADA,eAAA,CADA,eJyBJ,CInBE,mBACE,cJqBJ,CIjBE,+BAME,2CAAA,CACA,iDAAA,CACA,mBAAA,CAPA,oBAAA,CAGA,gBAAA,CAFA,cAAA,CACA,aAAA,CAEA,iBJsBJ,CIhBI,aAXF,+BAYI,aJmBJ,CACF,CIdI,iCACE,gBJgBN,CITM,8FACE,YJWR,CIPM,4FACE,eJSR,CIJI,8FACE,eJMN,CIHM,kHACE,gBJKR,CIAI,kCAGE,eAAA,CAFA,cAAA,CACA,sBAAA,CAEA,kBJEN,CIEI,kCAGE,qDAAA,CAFA,sBAAA,CACA,kBJCN,CIII,wCACE,iCJFN,CIKM,8CACE,qDAAA,CACA,sDJHR,CIQI,iCACE,iBJNN,CIWE,wCACE,cJTJ,CIYI,wDAIE,gBJJN,CIAI,wDAIE,iBJJN,CIAI,8CAME,UAAA,CALA,oBAAA,CAEA,YAAA,CAIA,oDAAA,CAAA,4CAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CALA,0BAAA,CAHA,WJFN,CIcI,oDACE,oDJZN,CIgBI,mEACE,kDAAA,CACA,yDAAA,CAAA,iDJdN,CIkBI,oEACE,kDAAA,CACA,0DAAA,CAAA,kDJhBN,CIqBE,wBACE,iBAAA,CACA,eAAA,CACA,iBJnBJ,CIuBE,mBACE,oBAAA,CAEA,kBAAA,CADA,eJpBJ,CIwBI,aANF,mBAOI,aJrBJ,CACF,CIwBI,8BACE,aAAA,CAEA,QAAA,CACA,eAAA,CAFA,UJpBN,CKrWI,0CDwYF,uBACE,iBJ/BF,CIkCE,4BACE,eJhCJ,CACF,CMpiBE,uBAOE,kBAAA,CALA,aAAA,CACA,aAAA,CAEA,aAAA,CACA,eAAA,CALA,iBAAA,CAOA,sCACE,CALF,YN0iBJ,CMjiBI,2BACE,aNmiBN,CM/hBI,6BAME,+CAAA,CAFA,yCAAA,CAHA,eAAA,CACA,eAAA,CACA,kBAAA,CAEA,iBNkiBN,CM7hBI,6BAEE,aAAA,CADA,YNgiBN,CM1hBE,wBACE,kBN4hBJ,CMzhBI,4BAIE,kBAAA,CAHA,mCAAA,CAIA,uBNyhBN,CMrhBI,4DAEE,oBAAA,CADA,SNwhBN,CMphBM,oEACE,mBNshBR,CO/kBA,WAGE,0CAAA,CADA,+BAAA,CADA,aPolBF,CO/kBE,aANF,WAOI,YPklBF,CACF,CO/kBE,oBAEE,2CAAA,CADA,gCPklBJ,CO7kBE,kBAGE,eAAA,CADA,iBAAA,CADA,ePilBJ,CO3kBE,6BACE,WPglBJ,COjlBE,6BACE,UPglBJ,COjlBE,mBAEE,aAAA,CACA,cAAA,CACA,uBP6kBJ,CO1kBI,0BACE,YP4kBN,COxkBI,yBACE,UP0kBN,CQ/mBA,KASE,cAAA,CARA,WAAA,CACA,iBRmnBF,CK/cI,oCGtKJ,KAaI,gBR4mBF,CACF,CKpdI,oCGtKJ,KAkBI,cR4mBF,CACF,CQvmBA,KASE,2CAAA,CAPA,YAAA,CACA,qBAAA,CAKA,eAAA,CAHA,eAAA,CAJA,iBAAA,CAGA,UR6mBF,CQrmBE,aAZF,KAaI,aRwmBF,CACF,CKrdI,0CGhJF,yBAII,cRqmBJ,CACF,CQ5lBA,SAEE,gBAAA,CAAA,iBAAA,CADA,eRgmBF,CQ3lBA,cACE,YAAA,CAEA,qBAAA,CADA,WR+lBF,CQ3lBE,aANF,cAOI,aR8lBF,CACF,CQ1lBA,SACE,WR6lBF,CQ1lBE,gBACE,YAAA,CACA,WAAA,CACA,iBR4lBJ,CQvlBA,aACE,eAAA,CACA,sBR0lBF,CQjlBA,WACE,YRolBF,CQ/kBA,WAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,ORolBF,CQ/kBE,uCACE,aRilBJ,CQ7kBE,+BAEE,uCAAA,CADA,kBRglBJ,CQ1kBA,SASE,2CAAA,CACA,mBAAA,CAFA,gCAAA,CADA,gBAAA,CADA,YAAA,CAMA,SAAA,CADA,uCAAA,CANA,mBAAA,CAJA,cAAA,CAYA,2BAAA,CATA,URolBF,CQxkBE,eAEE,SAAA,CAIA,uBAAA,CAHA,oEACE,CAHF,UR6kBJ,CQ/jBA,MACE,WRkkBF,CS3tBA,MACE,6PT6tBF,CSvtBA,cASE,mBAAA,CAFA,0CAAA,CACA,cAAA,CAFA,YAAA,CAIA,uCAAA,CACA,oBAAA,CAVA,iBAAA,CAEA,UAAA,CADA,QAAA,CAUA,qBAAA,CAPA,WAAA,CADA,STkuBF,CSvtBE,aAfF,cAgBI,YT0tBF,CACF,CSvtBE,kCAEE,uCAAA,CADA,YT0tBJ,CSrtBE,qBACE,uCTutBJ,CSntBE,wCACE,+BTqtBJ,CShtBE,oBAME,6BAAA,CADA,UAAA,CAJA,aAAA,CAEA,cAAA,CACA,aAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,aT0tBJ,CS9sBE,sBACE,cTgtBJ,CS7sBI,2BACE,2CT+sBN,CSzsBI,kEAEE,uDAAA,CADA,+BT4sBN,CU9wBE,8BACE,YVixBJ,CWtxBA,mBACE,GACE,SAAA,CACA,0BXyxBF,CWtxBA,GACE,SAAA,CACA,uBXwxBF,CACF,CWpxBA,mBACE,GACE,SXsxBF,CWnxBA,GACE,SXqxBF,CACF,CW1wBE,qBASE,2BAAA,CAFA,mCAAA,CAAA,2BAAA,CADA,0BAAA,CADA,WAAA,CAGA,SAAA,CAPA,cAAA,CACA,KAAA,CAEA,UAAA,CADA,SXkxBJ,CWxwBE,mBAcE,mDAAA,CANA,2CAAA,CACA,QAAA,CACA,mBAAA,CARA,QAAA,CASA,kDACE,CAPF,eAAA,CAEA,aAAA,CADA,SAAA,CALA,cAAA,CAGA,UAAA,CADA,SXmxBJ,CWpwBE,kBACE,aXswBJ,CWlwBE,sBACE,YAAA,CACA,YXowBJ,CWjwBI,oCACE,aXmwBN,CW9vBE,sBACE,mBXgwBJ,CW7vBI,6CACE,cX+vBN,CKzpBI,0CMvGA,6CAKI,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,UXiwBN,CACF,CW1vBE,kBACE,cX4vBJ,CY71BA,YACE,WAAA,CAIA,WZ61BF,CY11BE,mBAEE,qBAAA,CADA,iBZ61BJ,CKhsBI,sCOtJE,4EACE,kBZy1BN,CYr1BI,0JACE,mBZu1BN,CYx1BI,8EACE,kBZu1BN,CACF,CYl1BI,0BAGE,UAAA,CAFA,aAAA,CACA,YZq1BN,CYh1BI,+BACE,eZk1BN,CY50BE,8BACE,WZi1BJ,CYl1BE,8BACE,UZi1BJ,CYl1BE,8BAIE,iBZ80BJ,CYl1BE,8BAIE,kBZ80BJ,CYl1BE,oBAGE,cAAA,CADA,SZg1BJ,CY30BI,aAPF,oBAQI,YZ80BJ,CACF,CY30BI,gCACE,yCZ60BN,CYz0BI,wBACE,cAAA,CACA,kBZ20BN,CYx0BM,kCACE,oBZ00BR,Ca34BA,qBAEE,Wby5BF,Ca35BA,qBAEE,Uby5BF,Ca35BA,WAQE,2CAAA,CACA,mBAAA,CANA,YAAA,CAOA,8BAAA,CALA,iBAAA,CAMA,SAAA,CALA,mBAAA,CACA,mBAAA,CANA,cAAA,CAcA,0BAAA,CAHA,wCACE,CATF,Sbu5BF,Caz4BE,aAlBF,WAmBI,Yb44BF,CACF,Caz4BE,mBAEE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,kEb44BJ,Car4BE,kBAEE,gCAAA,CADA,ebw4BJ,Cc16BA,aACE,gBAAA,CACA,iBd66BF,Cc16BE,sBAGE,WAAA,CADA,QAAA,CADA,Sd86BJ,Ccx6BE,oBAEE,eAAA,CADA,ed26BJ,Cct6BE,oBACE,iBdw6BJ,Ccp6BE,mBAEE,YAAA,CACA,cAAA,CACA,6BAAA,CAHA,iBdy6BJ,Ccn6BI,iDACE,yCdq6BN,Ccj6BI,6BACE,iBdm6BN,Cc95BE,mBAGE,uCAAA,CACA,cAAA,CAHA,aAAA,CACA,cAAA,CAGA,sBdg6BJ,Cc75BI,gDACE,+Bd+5BN,Cc35BI,4BACE,0CAAA,CACA,mBd65BN,Ccx5BE,mBAEE,SAAA,CADA,iBAAA,CAKA,2BAAA,CAHA,8Dd25BJ,Ccr5BI,qBAEE,aAAA,CADA,edw5BN,Ccn5BI,6BACE,SAAA,CACA,uBdq5BN,Cch5BE,aAnFF,aAoFI,Ydm5BF,CACF,Cex+BA,WAEE,0CAAA,CADA,+Bf4+BF,Cex+BE,aALF,WAMI,Yf2+BF,CACF,Cex+BE,kBACE,6BAAA,CAEA,aAAA,CADA,af2+BJ,Cev+BI,gCACE,Yfy+BN,Cep+BE,iBAOE,eAAA,CANA,YAAA,CAKA,cAAA,CAGA,mBAAA,CAAA,eAAA,CADA,cAAA,CAGA,uCAAA,CADA,eAAA,CAEA,uBfk+BJ,Ce/9BI,8CACE,Ufi+BN,Ce79BI,+BACE,oBf+9BN,CKj1BI,0CUvIE,uBACE,af29BN,Cex9BM,yCACE,Yf09BR,CACF,Cer9BI,iCACE,gBfw9BN,Cez9BI,iCACE,iBfw9BN,Cez9BI,uBAEE,gBfu9BN,Cep9BM,iCACE,efs9BR,Ceh9BE,kBACE,WAAA,CAIA,eAAA,CADA,mBAAA,CAFA,6BAAA,CACA,cAAA,CAGA,kBfk9BJ,Ce98BE,mBAEE,YAAA,CADA,afi9BJ,Ce58BE,sBACE,gBAAA,CACA,Uf88BJ,Cez8BA,gBACE,gDf48BF,Cez8BE,uBACE,YAAA,CACA,cAAA,CACA,6BAAA,CACA,af28BJ,Cev8BE,kCACE,sCfy8BJ,Cet8BI,gFACE,+Bfw8BN,Ceh8BA,cAKE,wCAAA,CADA,gBAAA,CADA,iBAAA,CADA,eAAA,CADA,Ufu8BF,CK35BI,mCU7CJ,cASI,Ufm8BF,CACF,Ce/7BE,yBACE,sCfi8BJ,Ce17BA,WACE,mBAAA,CACA,SAAA,CAEA,cAAA,CADA,qBf87BF,CK16BI,mCUvBJ,WAQI,ef67BF,CACF,Ce17BE,iBACE,oBAAA,CAEA,aAAA,CACA,iBAAA,CAFA,Yf87BJ,Cez7BI,wBACE,ef27BN,Cev7BI,qBAGE,iBAAA,CAFA,gBAAA,CACA,mBf07BN,CgBhmCE,uBAME,kBAAA,CACA,mBAAA,CAHA,gCAAA,CACA,cAAA,CAJA,oBAAA,CAEA,eAAA,CADA,kBAAA,CAMA,gEhBmmCJ,CgB7lCI,gCAEE,2CAAA,CACA,uCAAA,CAFA,gChBimCN,CgB3lCI,0DAEE,0CAAA,CACA,sCAAA,CAFA,+BhB+lCN,CgBxlCE,gCAKE,4BhB6lCJ,CgBlmCE,gEAME,6BhB4lCJ,CgBlmCE,gCAME,4BhB4lCJ,CgBlmCE,sBAIE,6DAAA,CAGA,8BAAA,CAJA,eAAA,CAFA,aAAA,CACA,eAAA,CAMA,sChB0lCJ,CgBrlCI,wDACE,6CAAA,CACA,8BhBulCN,CgBnlCI,+BACE,UhBqlCN,CiBxoCA,WAOE,2CAAA,CAGA,8CACE,CALF,gCAAA,CADA,aAAA,CAHA,MAAA,CADA,eAAA,CACA,OAAA,CACA,KAAA,CACA,SjB+oCF,CiBpoCE,aAfF,WAgBI,YjBuoCF,CACF,CiBpoCE,mBAIE,2BAAA,CAHA,iEjBuoCJ,CiBhoCE,mBACE,kDACE,CAEF,kEjBgoCJ,CiB1nCE,kBAEE,kBAAA,CADA,YAAA,CAEA,ejB4nCJ,CiBxnCE,mBAKE,kBAAA,CAEA,cAAA,CAHA,YAAA,CAIA,uCAAA,CALA,aAAA,CAFA,iBAAA,CAQA,uBAAA,CAHA,qBAAA,CAJA,SjBioCJ,CiBvnCI,yBACE,UjBynCN,CiBrnCI,iCACE,oBjBunCN,CiBnnCI,uCAEE,uCAAA,CADA,YjBsnCN,CiBjnCI,2BAEE,YAAA,CADA,ajBonCN,CKtgCI,0CY/GA,2BAMI,YjBmnCN,CACF,CiBhnCM,8DAIE,iBAAA,CAHA,aAAA,CAEA,aAAA,CADA,UjBonCR,CKpiCI,mCYzEA,iCAII,YjB6mCN,CACF,CiB1mCM,wCACE,YjB4mCR,CiBxmCM,+CACE,oBjB0mCR,CK/iCI,sCYtDA,iCAII,YjBqmCN,CACF,CiBhmCE,kBAEE,YAAA,CACA,cAAA,CAFA,iBAAA,CAIA,8DACE,CAFF,kBjBmmCJ,CiB7lCI,oCAGE,SAAA,CADA,mBAAA,CAKA,6BAAA,CAHA,8DACE,CAJF,UjBmmCN,CiB1lCM,8CACE,8BjB4lCR,CiBvlCI,8BACE,ejBylCN,CiBplCE,4BAGE,gBAAA,CAAA,kBjBwlCJ,CiB3lCE,4BAGE,iBAAA,CAAA,iBjBwlCJ,CiB3lCE,kBACE,WAAA,CAGA,eAAA,CAFA,aAAA,CAGA,kBjBslCJ,CiBnlCI,4CAGE,SAAA,CADA,mBAAA,CAKA,8BAAA,CAHA,8DACE,CAJF,UjBylCN,CiBhlCM,sDACE,6BjBklCR,CiB9kCM,8DAGE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,8DACE,CAJF,SjBolCR,CiBzkCI,uCAGE,WAAA,CAFA,iBAAA,CACA,UjB4kCN,CiBtkCE,mBACE,YAAA,CACA,aAAA,CACA,cAAA,CAEA,+CACE,CAFF,kBjBykCJ,CiBnkCI,8DACE,WAAA,CACA,SAAA,CACA,oCjBqkCN,CiB5jCI,yBACE,QjB8jCN,CiBzjCE,mBACE,YjB2jCJ,CKvnCI,mCY2DF,6BAQI,gBjB2jCJ,CiBnkCA,6BAQI,iBjB2jCJ,CiBnkCA,mBAKI,aAAA,CAEA,iBAAA,CADA,ajB6jCJ,CACF,CK/nCI,sCY2DF,6BAaI,kBjB2jCJ,CiBxkCA,6BAaI,mBjB2jCJ,CACF,CD1yCA,SAGE,uCAAA,CAFA,eAAA,CACA,eC8yCF,CD1yCE,eACE,mBAAA,CACA,cAAA,CAGA,eAAA,CADA,QAAA,CADA,SC8yCJ,CDxyCE,sCAEE,WAAA,CADA,iBAAA,CAAA,kBC2yCJ,CDtyCE,eACE,+BCwyCJ,CDryCI,0CACE,+BCuyCN,CDjyCA,UAKE,wBmBaa,CnBZb,oBAAA,CAFA,UAAA,CAHA,oBAAA,CAEA,eAAA,CADA,0BAAA,CAAA,2BCwyCF,CmB10CA,MACE,uMAAA,CACA,sLAAA,CACA,iNnB60CF,CmBv0CA,QACE,eAAA,CACA,enB00CF,CmBv0CE,eAKE,uCAAA,CAJA,aAAA,CAGA,eAAA,CADA,eAAA,CADA,eAAA,CAIA,sBnBy0CJ,CmBt0CI,+BACE,YnBw0CN,CmBr0CM,mCAEE,WAAA,CADA,UnBw0CR,CmBh0CQ,sFAME,iBAAA,CALA,aAAA,CAGA,aAAA,CADA,cAAA,CAEA,kBAAA,CAHA,UnBs0CV,CmB3zCE,cAGE,eAAA,CADA,QAAA,CADA,SnB+zCJ,CmBzzCE,cAGE,sBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBAAA,CACA,uBAAA,CACA,sBnB2zCJ,CmBxzCI,sBACE,uCnB0zCN,CmBnzCM,6EAEE,+BnBqzCR,CmBhzCI,2BAIE,iBnB+yCN,CmB3yCI,4CACE,gBnB6yCN,CmB9yCI,4CACE,iBnB6yCN,CmBzyCI,kBAME,iBAAA,CAFA,aAAA,CACA,YAAA,CAFA,iBnB4yCN,CmBryCI,sGACE,+BAAA,CACA,cnBuyCN,CmBnyCI,4BACE,uCAAA,CACA,oBnBqyCN,CmBjyCI,0CACE,YnBmyCN,CmBhyCM,yDAIE,6BAAA,CAHA,aAAA,CAEA,WAAA,CAEA,qCAAA,CAAA,6BAAA,CAHA,UnBqyCR,CmB9xCM,kDACE,YnBgyCR,CmB1xCE,iCACE,YnB4xCJ,CmBzxCI,6CACE,WAAA,CAGA,WnByxCN,CmBpxCE,cACE,anBsxCJ,CmBlxCE,gBACE,YnBoxCJ,CKrvCI,0CcxBA,0CASE,2CAAA,CAHA,YAAA,CACA,qBAAA,CACA,WAAA,CALA,MAAA,CADA,iBAAA,CACA,OAAA,CACA,KAAA,CACA,SnBmxCJ,CmBxwCI,+DACE,eAAA,CACA,enB0wCN,CmBtwCI,gCAQE,qDAAA,CAHA,uCAAA,CAEA,cAAA,CALA,aAAA,CAEA,kBAAA,CADA,wBAAA,CAFA,iBAAA,CAKA,kBnB0wCN,CmBrwCM,wDAEE,UnB4wCR,CmB9wCM,wDAEE,WnB4wCR,CmB9wCM,8CAIE,aAAA,CAEA,aAAA,CACA,YAAA,CANA,iBAAA,CAEA,SAAA,CAEA,YnBywCR,CmBpwCQ,oDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnB6wCV,CmBjwCM,8CAIE,2CAAA,CACA,gEACE,CALF,eAAA,CAEA,4BAAA,CADA,kBnBswCR,CmB/vCQ,2DACE,YnBiwCV,CmB5vCM,8CAGE,2CAAA,CADA,gCAAA,CADA,enBgwCR,CmB1vCM,yCAIE,aAAA,CAFA,UAAA,CAIA,YAAA,CADA,aAAA,CAJA,iBAAA,CACA,WAAA,CACA,SnB+vCR,CmBvvCI,+BACE,MnByvCN,CmBrvCI,+BACE,4DnBuvCN,CmBpvCM,qDACE,+BnBsvCR,CmBnvCQ,sHACE,+BnBqvCV,CmB/uCI,+BAEE,YAAA,CADA,mBnBkvCN,CmB9uCM,mCACE,enBgvCR,CmB5uCM,6CACE,SnB8uCR,CmB1uCM,uDAGE,mBnB6uCR,CmBhvCM,uDAGE,kBnB6uCR,CmBhvCM,6CAIE,gBAAA,CAFA,aAAA,CADA,YnB+uCR,CmBzuCQ,mDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnBkvCV,CmBluCM,+CACE,mBnBouCR,CmB5tCM,4CAEE,wBAAA,CADA,enB+tCR,CmB3tCQ,oEACE,mBnB6tCV,CmB9tCQ,oEACE,oBnB6tCV,CmBztCQ,4EACE,iBnB2tCV,CmB5tCQ,4EACE,kBnB2tCV,CmBvtCQ,oFACE,mBnBytCV,CmB1tCQ,oFACE,oBnBytCV,CmBrtCQ,4FACE,mBnButCV,CmBxtCQ,4FACE,oBnButCV,CmBhtCE,mBACE,wBnBktCJ,CmB9sCE,wBACE,YAAA,CACA,SAAA,CAIA,0BAAA,CAHA,oEnBitCJ,CmB3sCI,kCACE,2BnB6sCN,CmBxsCE,gCACE,SAAA,CAIA,uBAAA,CAHA,qEnB2sCJ,CmBrsCI,8CAEE,kCAAA,CAAA,0BnBssCN,CACF,CKx4CI,0Cc0MA,0CACE,YnBisCJ,CmB9rCI,yDACE,UnBgsCN,CmB5rCI,wDACE,YnB8rCN,CmB1rCI,kDACE,YnB4rCN,CmBvrCE,gBAIE,iDAAA,CADA,gCAAA,CAFA,aAAA,CACA,enB2rCJ,CACF,CKr8CM,+DcmRF,6CACE,YnBqrCJ,CmBlrCI,4DACE,UnBorCN,CmBhrCI,2DACE,YnBkrCN,CmB9qCI,qDACE,YnBgrCN,CACF,CK77CI,mCc7JJ,QAgbI,oBnB8qCF,CmBxqCI,kCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnB0qCN,CmBrqCM,6CACE,uBnBuqCR,CmBnqCM,gDACE,YnBqqCR,CmBhqCI,2CACE,kBnBmqCN,CmBpqCI,2CACE,mBnBmqCN,CmBpqCI,iCAEE,oBnBkqCN,CmB3pCI,yDACE,kBnB6pCN,CmB9pCI,yDACE,iBnB6pCN,CACF,CKt9CI,sCc7JJ,QA4dI,oBAAA,CACA,oDnB2pCF,CmBrpCI,gCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnBupCN,CmBlpCM,8CACE,uBnBopCR,CmBhpCM,8CACE,YnBkpCR,CmB7oCI,yCACE,kBnBgpCN,CmBjpCI,yCACE,mBnBgpCN,CmBjpCI,+BAEE,oBnB+oCN,CmBxoCI,uDACE,kBnB0oCN,CmB3oCI,uDACE,iBnB0oCN,CmBroCE,wBACE,YAAA,CACA,sBAAA,CAEA,SAAA,CACA,6FACE,CAHF,mBnByoCJ,CmBjoCI,sCACE,enBmoCN,CmB9nCE,iFACE,sBAAA,CAEA,SAAA,CACA,4FACE,CAHF,kBnBkoCJ,CmBznCE,iDACE,enB2nCJ,CmBvnCE,6CACE,YnBynCJ,CmBrnCE,uBACE,aAAA,CACA,enBunCJ,CmBpnCI,kCACE,enBsnCN,CmBlnCI,qCACE,enBonCN,CmBjnCM,0CACE,uCnBmnCR,CmB/mCM,6DACE,mBnBinCR,CmB7mCM,yFAEE,YnB+mCR,CmB1mCI,yCAEE,kBnB8mCN,CmBhnCI,yCAEE,mBnB8mCN,CmBhnCI,+BACE,aAAA,CAGA,SAAA,CADA,kBnB6mCN,CmBzmCM,2DACE,SnB2mCR,CmBrmCE,cAGE,kBAAA,CADA,YAAA,CAEA,gCAAA,CAHA,WnB0mCJ,CmBpmCI,oBACE,uDnBsmCN,CmBlmCI,oBAME,6BAAA,CACA,kBAAA,CAFA,UAAA,CAJA,oBAAA,CAEA,WAAA,CAKA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,yBAAA,CARA,qBAAA,CAFA,UnB8mCN,CmBjmCM,8BACE,wBnBmmCR,CmB/lCM,kKAEE,uBnBgmCR,CmBllCI,2EACE,YnBulCN,CmBplCM,oDACE,anBslCR,CmBnlCQ,kEAKE,qCAAA,CACA,qDAAA,CAFA,YAAA,CAHA,eAAA,CACA,KAAA,CACA,SnBwlCV,CmBllCU,0FACE,mBnBolCZ,CmB/kCQ,0EACE,QnBilCV,CmB5kCM,sFACE,kBnB8kCR,CmB/kCM,sFACE,mBnB8kCR,CmB1kCM,kDACE,uCnB4kCR,CmBtkCI,2CACE,sBAAA,CAEA,SAAA,CADA,kBnBykCN,CmBhkCI,qFAIE,mDnBmkCN,CmBvkCI,qFAIE,oDnBmkCN,CmBvkCI,2EACE,aAAA,CACA,oBAAA,CAGA,SAAA,CAFA,kBnBokCN,CmB/jCM,yFAEE,gBAAA,CADA,gBnBkkCR,CmB7jCM,0FACE,YnB+jCR,CACF,CoBtxDA,eAKE,eAAA,CACA,eAAA,CAJA,SpB6xDF,CoBtxDE,gCANA,kBAAA,CAFA,YAAA,CAGA,sBpBoyDF,CoB/xDE,iBAOE,mBAAA,CAFA,aAAA,CADA,gBAAA,CAEA,iBpByxDJ,CoBpxDE,wBAEE,qDAAA,CADA,uCpBuxDJ,CoBlxDE,qBACE,6CpBoxDJ,CoB/wDI,sDAEE,uDAAA,CADA,+BpBkxDN,CoB9wDM,8DACE,+BpBgxDR,CoB3wDI,mCACE,uCAAA,CACA,oBpB6wDN,CoBzwDI,yBAKE,iBAAA,CADA,yCAAA,CAHA,aAAA,CAEA,eAAA,CADA,YpB8wDN,CqB9zDE,eAGE,+DAAA,CADA,oBAAA,CADA,qBrBm0DJ,CK9oDI,0CgBtLF,eAOI,YrBi0DJ,CACF,CqB3zDM,6BACE,oBrB6zDR,CqBvzDE,kBACE,YAAA,CACA,qBAAA,CACA,SAAA,CACA,qBrByzDJ,CqBlzDI,0BACE,sBrBozDN,CqBjzDM,gEACE,+BrBmzDR,CqB7yDE,gBAEE,uCAAA,CADA,erBgzDJ,CqB3yDE,kBACE,oBrB6yDJ,CqB1yDI,mCAGE,kBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBrB4yDN,CqBxyDI,oCAIE,kBAAA,CAHA,mBAAA,CACA,kBAAA,CACA,SAAA,CAGA,QAAA,CADA,iBrB2yDN,CqBtyDI,0DACE,kBrBwyDN,CqBzyDI,0DACE,iBrBwyDN,CqBpyDI,iDACE,uBAAA,CAEA,YrBqyDN,CqBhyDE,4BACE,YrBkyDJ,CqB3xDA,YAGE,kBAAA,CAFA,YAAA,CAIA,eAAA,CAHA,SAAA,CAIA,eAAA,CAFA,UrBgyDF,CqB3xDE,yBACE,WrB6xDJ,CqBtxDA,kBACE,YrByxDF,CKjtDI,0CgBzEJ,kBAKI,wBrByxDF,CACF,CqBtxDE,qCACE,WrBwxDJ,CK5uDI,sCgB7CF,+CAKI,kBrBwxDJ,CqB7xDA,+CAKI,mBrBwxDJ,CACF,CK9tDI,0CgBrDJ,6BAMI,SAAA,CAFA,eAAA,CACA,UrBqxDF,CqBlxDE,qDACE,gBrBoxDJ,CqBjxDE,gDACE,SrBmxDJ,CqBhxDE,4CACE,iBAAA,CAAA,kBrBkxDJ,CqB/wDE,2CAEE,WAAA,CADA,crBkxDJ,CqB9wDE,2CACE,mBAAA,CACA,cAAA,CACA,SAAA,CACA,oBAAA,CAAA,iBrBgxDJ,CqB7wDE,2CACE,SrB+wDJ,CqB5wDE,qCAEE,WAAA,CACA,eAAA,CAFA,erBgxDJ,CACF,CsB17DA,MACE,qBAAA,CACA,yBtB67DF,CsBv7DA,aAME,qCAAA,CADA,cAAA,CAEA,0FACE,CAPF,cAAA,CACA,KAAA,CAaA,mDAAA,CACA,qBAAA,CAJA,wFACE,CATF,UAAA,CADA,StBi8DF,CuB58DA,MACE,mfvB+8DF,CuBz8DA,WACE,iBvB48DF,CK9yDI,mCkB/JJ,WAKI,evB48DF,CACF,CuBz8DE,kBACE,YvB28DJ,CuBv8DE,oBAEE,SAAA,CADA,SvB08DJ,CKvyDI,0CkBpKF,8BAOI,YvBk9DJ,CuBz9DA,8BAOI,avBk9DJ,CuBz9DA,oBAaI,2CAAA,CACA,kBAAA,CAJA,WAAA,CACA,eAAA,CACA,mBAAA,CANA,iBAAA,CAEA,SAAA,CAUA,uBAAA,CAHA,4CACE,CAPF,UvBg9DJ,CuBp8DI,+DACE,SAAA,CACA,oCvBs8DN,CACF,CK70DI,mCkBjJF,8BAgCI,MvBy8DJ,CuBz+DA,8BAgCI,OvBy8DJ,CuBz+DA,oBAqCI,0BAAA,CADA,cAAA,CADA,QAAA,CAJA,cAAA,CAEA,KAAA,CAKA,sDACE,CALF,OvBu8DJ,CuB77DI,+DAME,YAAA,CACA,SAAA,CACA,4CACE,CARF,UvBk8DN,CACF,CK50DI,0CkBxGA,+DAII,mBvBo7DN,CACF,CK13DM,+DkB/DF,+DASI,mBvBo7DN,CACF,CK/3DM,+DkB/DF,+DAcI,mBvBo7DN,CACF,CuB/6DE,kBAEE,kCAAA,CAAA,0BvBg7DJ,CK91DI,0CkBpFF,4BAOI,MvBw7DJ,CuB/7DA,4BAOI,OvBw7DJ,CuB/7DA,kBAWI,QAAA,CAEA,SAAA,CADA,eAAA,CANA,cAAA,CAEA,KAAA,CAWA,wBAAA,CALA,qGACE,CALF,OAAA,CADA,SvBs7DJ,CuBz6DI,4BACE,yBvB26DN,CuBv6DI,6DAEE,WAAA,CACA,SAAA,CAMA,uBAAA,CALA,sGACE,CAJF,UvB66DN,CACF,CKz4DI,mCkBjEF,4BA2CI,WvBu6DJ,CuBl9DA,4BA2CI,UvBu6DJ,CuBl9DA,kBA6CI,eAAA,CAHA,iBAAA,CAIA,8CAAA,CAFA,avBs6DJ,CACF,CKx6DM,+DkBOF,6DAII,avBi6DN,CACF,CKv5DI,sCkBfA,6DASI,avBi6DN,CACF,CuB55DE,iBAIE,2CAAA,CACA,0BAAA,CAFA,aAAA,CAFA,iBAAA,CAKA,2CACE,CALF,SvBk6DJ,CKp6DI,mCkBAF,iBAaI,0BAAA,CACA,mBAAA,CAFA,avB85DJ,CuBz5DI,uBACE,0BvB25DN,CACF,CuBv5DI,4DAEE,2CAAA,CACA,6BAAA,CACA,8BAAA,CAHA,gCvB45DN,CuBp5DE,4BAKE,mBAAA,CAAA,oBvBy5DJ,CuB95DE,4BAKE,mBAAA,CAAA,oBvBy5DJ,CuB95DE,kBAQE,gBAAA,CAFA,eAAA,CAFA,WAAA,CAHA,iBAAA,CAMA,sBAAA,CAJA,UAAA,CADA,SvB45DJ,CuBn5DI,+BACE,qBvBq5DN,CuBj5DI,kEAEE,uCvBk5DN,CuB94DI,6BACE,YvBg5DN,CKp7DI,0CkBaF,kBA8BI,eAAA,CADA,aAAA,CADA,UvBi5DJ,CACF,CK98DI,mCkBgCF,4BAmCI,mBvBi5DJ,CuBp7DA,4BAmCI,oBvBi5DJ,CuBp7DA,kBAqCI,aAAA,CADA,evBg5DJ,CuB54DI,+BACE,uCvB84DN,CuB14DI,mCACE,gCvB44DN,CuBx4DI,6DACE,kBvB04DN,CuBv4DM,8EACE,uCvBy4DR,CuBr4DM,0EACE,WvBu4DR,CACF,CuBj4DE,iBAIE,cAAA,CAHA,oBAAA,CAEA,aAAA,CAEA,kCACE,CAJF,YvBs4DJ,CuB93DI,uBACE,UvBg4DN,CuB53DI,yCAEE,UvBg4DN,CuBl4DI,yCAEE,WvBg4DN,CuBl4DI,+BACE,iBAAA,CAEA,SAAA,CACA,SvB83DN,CuB33DM,6CACE,oBvB63DR,CKp+DI,0CkB+FA,yCAaI,UvB63DN,CuB14DE,yCAaI,WvB63DN,CuB14DE,+BAcI,SvB43DN,CuBz3DM,+CACE,YvB23DR,CACF,CKhgEI,mCkBkHA,+BAwBI,mBvB03DN,CuBv3DM,8CACE,YvBy3DR,CACF,CuBn3DE,8BAEE,WvBw3DJ,CuB13DE,8BAEE,UvBw3DJ,CuB13DE,oBAKE,mBAAA,CAJA,iBAAA,CAEA,SAAA,CACA,SvBs3DJ,CK5/DI,0CkBkIF,8BASI,WvBs3DJ,CuB/3DA,8BASI,UvBs3DJ,CuB/3DA,oBAUI,SvBq3DJ,CACF,CuBl3DI,uCACE,iBvBw3DN,CuBz3DI,uCACE,kBvBw3DN,CuBz3DI,6BAEE,uCAAA,CACA,SAAA,CAIA,oBAAA,CAHA,+DvBq3DN,CuB/2DM,iDAEE,uCAAA,CADA,YvBk3DR,CuB72DM,gGAGE,SAAA,CADA,mBAAA,CAEA,kBvB82DR,CuB32DQ,sGACE,UvB62DV,CuBt2DE,8BAOE,mBAAA,CAAA,oBvB62DJ,CuBp3DE,8BAOE,mBAAA,CAAA,oBvB62DJ,CuBp3DE,oBAIE,kBAAA,CAKA,yCAAA,CANA,YAAA,CAKA,eAAA,CAFA,WAAA,CAKA,SAAA,CAVA,iBAAA,CACA,KAAA,CAUA,uBAAA,CAFA,kBAAA,CALA,UvB+2DJ,CKtjEI,mCkBkMF,8BAgBI,mBvBy2DJ,CuBz3DA,8BAgBI,oBvBy2DJ,CuBz3DA,oBAiBI,evBw2DJ,CACF,CuBr2DI,+DACE,SAAA,CACA,0BvBu2DN,CuBl2DE,6BAKE,+BvBq2DJ,CuB12DE,0DAME,gCvBo2DJ,CuB12DE,6BAME,+BvBo2DJ,CuB12DE,mBAIE,eAAA,CAHA,iBAAA,CAEA,UAAA,CADA,SvBw2DJ,CKrjEI,0CkB2MF,mBAWI,QAAA,CADA,UvBq2DJ,CACF,CK9kEI,mCkB8NF,mBAiBI,SAAA,CADA,UAAA,CAEA,sBvBo2DJ,CuBj2DI,8DACE,8BAAA,CACA,SvBm2DN,CACF,CuB91DE,uBASE,kCAAA,CAAA,0BAAA,CAFA,2CAAA,CANA,WAAA,CACA,eAAA,CAIA,kBvB+1DJ,CuBz1DI,iEAZF,uBAaI,uBvB41DJ,CACF,CK3nEM,+DkBiRJ,uBAkBI,avB41DJ,CACF,CK1mEI,sCkB2PF,uBAuBI,avB41DJ,CACF,CK/mEI,mCkB2PF,uBA4BI,YAAA,CACA,yDAAA,CACA,oBvB41DJ,CuBz1DI,kEACE,evB21DN,CuBv1DI,6BACE,+CvBy1DN,CuBr1DI,0CAEE,YAAA,CADA,WvBw1DN,CuBn1DI,gDACE,oDvBq1DN,CuBl1DM,sDACE,0CvBo1DR,CACF,CuB70DA,kBACE,gCAAA,CACA,qBvBg1DF,CuB70DE,wBAME,qDAAA,CAFA,uCAAA,CAFA,gBAAA,CACA,kBAAA,CAFA,eAAA,CAIA,uBvBg1DJ,CKnpEI,mCkB8TF,kCAUI,mBvB+0DJ,CuBz1DA,kCAUI,oBvB+0DJ,CACF,CuB30DE,wBAGE,eAAA,CADA,QAAA,CADA,SAAA,CAIA,wBAAA,CAAA,gBvB40DJ,CuBx0DE,wBACE,yDvB00DJ,CuBv0DI,oCACE,evBy0DN,CuBp0DE,wBACE,aAAA,CAEA,YAAA,CADA,uBAAA,CAEA,gCvBs0DJ,CuBn0DI,4DACE,uDvBq0DN,CuBj0DI,gDACE,mBvBm0DN,CuB9zDE,gCAKE,cAAA,CADA,aAAA,CAGA,YAAA,CANA,eAAA,CAKA,uBAAA,CAJA,KAAA,CACA,SvBo0DJ,CuB7zDI,wCACE,YvB+zDN,CuB1zDI,wDACE,YvB4zDN,CuBxzDI,oCAGE,+BAAA,CADA,gBAAA,CADA,mBAAA,CAGA,2CvB0zDN,CKrsEI,mCkBuYA,8CAUI,mBvBwzDN,CuBl0DE,8CAUI,oBvBwzDN,CACF,CuBpzDI,oFAEE,uDAAA,CADA,+BvBuzDN,CuBjzDE,sCACE,2CvBmzDJ,CuB9yDE,2BAGE,eAAA,CADA,eAAA,CADA,iBvBkzDJ,CKttEI,mCkBmaF,qCAOI,mBvBgzDJ,CuBvzDA,qCAOI,oBvBgzDJ,CACF,CuB5yDE,kCAEE,MvBkzDJ,CuBpzDE,kCAEE,OvBkzDJ,CuBpzDE,wBAME,uCAAA,CAFA,aAAA,CACA,YAAA,CAJA,iBAAA,CAEA,YvBizDJ,CKhtEI,0CkB4ZF,wBAUI,YvB8yDJ,CACF,CuB3yDI,8BAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,+CAAA,CAAA,uCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UvBozDN,CuB1yDM,wCACE,oBvB4yDR,CuBtyDE,8BAGE,uCAAA,CAFA,gBAAA,CACA,evByyDJ,CuBryDI,iCAKE,gCAAA,CAHA,eAAA,CACA,eAAA,CACA,eAAA,CAHA,evB2yDN,CuBpyDM,sCACE,oBvBsyDR,CuBjyDI,iCAKE,gCAAA,CAHA,gBAAA,CACA,eAAA,CACA,eAAA,CAHA,avBuyDN,CuBhyDM,sCACE,oBvBkyDR,CuB5xDE,yBAKE,gCAAA,CAJA,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,avBiyDJ,CuB1xDE,uBAGE,wBAAA,CAFA,+BAAA,CACA,yBvB6xDJ,CwBj8EA,WACE,iBAAA,CACA,SxBo8EF,CwBj8EE,kBAOE,2CAAA,CACA,mBAAA,CACA,8BAAA,CAHA,gCAAA,CAHA,QAAA,CAEA,gBAAA,CADA,YAAA,CAMA,SAAA,CATA,iBAAA,CACA,sBAAA,CAaA,mCAAA,CAJA,oExBo8EJ,CwB77EI,6EACE,gBAAA,CACA,SAAA,CAKA,+BAAA,CAJA,8ExBg8EN,CwBx7EI,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,OxBi8EN,CwBr7EE,iBAOE,mBAAA,CAFA,eAAA,CACA,oBAAA,CAHA,QAAA,CAFA,kBAAA,CAGA,aAAA,CAFA,SxB47EJ,CwBn7EE,iBACE,kBxBq7EJ,CwBj7EE,2BAGE,kBAAA,CAAA,oBxBu7EJ,CwB17EE,2BAGE,mBAAA,CAAA,mBxBu7EJ,CwB17EE,iBAIE,cAAA,CAHA,aAAA,CAKA,YAAA,CADA,uBAAA,CAEA,2CACE,CANF,UxBw7EJ,CwB96EI,8CACE,+BxBg7EN,CwB56EI,uBACE,qDxB86EN,CyBlgFA,YAIE,qBAAA,CADA,aAAA,CAGA,gBAAA,CALA,eAAA,CACA,UAAA,CAGA,azBsgFF,CyBlgFE,aATF,YAUI,YzBqgFF,CACF,CKv1EI,0CoB3KF,+BAKI,azB0gFJ,CyB/gFA,+BAKI,czB0gFJ,CyB/gFA,qBAWI,2CAAA,CAHA,aAAA,CAEA,WAAA,CANA,cAAA,CAEA,KAAA,CASA,uBAAA,CAHA,iEACE,CAJF,aAAA,CAFA,SzBwgFJ,CyB7/EI,mEACE,8BAAA,CACA,6BzB+/EN,CyB5/EM,6EACE,8BzB8/ER,CyBz/EI,6CAEE,QAAA,CAAA,MAAA,CACA,QAAA,CACA,eAAA,CAHA,iBAAA,CACA,OAAA,CAGA,qBAAA,CAHA,KzB8/EN,CACF,CKt4EI,sCoBtKJ,YAuDI,QzBy/EF,CyBt/EE,mBACE,WzBw/EJ,CyBp/EE,6CACE,UzBs/EJ,CACF,CyBl/EE,uBACE,YAAA,CACA,OzBo/EJ,CKr5EI,mCoBjGF,uBAMI,QzBo/EJ,CyBj/EI,8BACE,WzBm/EN,CyB/+EI,qCACE,azBi/EN,CyB7+EI,+CACE,kBzB++EN,CACF,CyB1+EE,wBAIE,uBAAA,CAOA,kCAAA,CAAA,0BAAA,CAVA,cAAA,CACA,eAAA,CACA,yDAAA,CAMA,oBzBy+EJ,CyBp+EI,2CAEE,YAAA,CADA,WzBu+EN,CyBl+EI,mEACE,+CzBo+EN,CyBj+EM,qHACE,oDzBm+ER,CyBh+EQ,iIACE,0CzBk+EV,CyBn9EE,wCAGE,wBACE,qBzBm9EJ,CyB/8EE,6BACE,kCzBi9EJ,CyBl9EE,6BACE,iCzBi9EJ,CACF,CK76EI,0CoB5BF,YAME,0BAAA,CADA,QAAA,CAEA,SAAA,CANA,cAAA,CACA,KAAA,CAMA,sDACE,CALF,OAAA,CADA,SzBk9EF,CyBv8EE,4CAEE,WAAA,CACA,SAAA,CACA,4CACE,CAJF,UzB48EJ,CACF,C0BznFA,iBACE,GACE,Q1B2nFF,C0BxnFA,GACE,a1B0nFF,CACF,C0BtnFA,gBACE,GACE,SAAA,CACA,0B1BwnFF,C0BrnFA,IACE,S1BunFF,C0BpnFA,GACE,SAAA,CACA,uB1BsnFF,CACF,C0B9mFA,MACE,2eAAA,CACA,+fAAA,CACA,0lBAAA,CACA,kf1BgnFF,C0B1mFA,WAOE,kCAAA,CAAA,0BAAA,CANA,aAAA,CACA,gBAAA,CACA,eAAA,CAEA,uCAAA,CAGA,uBAAA,CAJA,kB1BgnFF,C0BzmFE,iBACE,U1B2mFJ,C0BvmFE,iBACE,oBAAA,CAEA,aAAA,CACA,qBAAA,CAFA,U1B2mFJ,C0BtmFI,+BACE,iB1BymFN,C0B1mFI,+BACE,kB1BymFN,C0B1mFI,qBAEE,gB1BwmFN,C0BpmFI,kDACE,iB1BumFN,C0BxmFI,kDACE,kB1BumFN,C0BxmFI,kDAEE,iB1BsmFN,C0BxmFI,kDAEE,kB1BsmFN,C0BjmFE,iCAGE,iB1BsmFJ,C0BzmFE,iCAGE,kB1BsmFJ,C0BzmFE,uBACE,oBAAA,CACA,6BAAA,CAEA,eAAA,CACA,sBAAA,CACA,qB1BmmFJ,C0B/lFE,kBACE,YAAA,CAMA,gBAAA,CALA,SAAA,CAMA,oBAAA,CAHA,gBAAA,CAIA,WAAA,CAHA,eAAA,CAFA,SAAA,CADA,U1BumFJ,C0B9lFI,iDACE,4B1BgmFN,C0B3lFE,iBACE,eAAA,CACA,sB1B6lFJ,C0B1lFI,gDACE,2B1B4lFN,C0BxlFI,kCAIE,kB1BgmFN,C0BpmFI,kCAIE,iB1BgmFN,C0BpmFI,wBAOE,6BAAA,CADA,UAAA,CALA,oBAAA,CAEA,YAAA,CAMA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CALA,uBAAA,CAHA,W1BkmFN,C0BtlFI,iCACE,a1BwlFN,C0BplFI,iCACE,gDAAA,CAAA,wC1BslFN,C0BllFI,+BACE,8CAAA,CAAA,sC1BolFN,C0BhlFI,+BACE,8CAAA,CAAA,sC1BklFN,C0B9kFI,sCACE,qDAAA,CAAA,6C1BglFN,C0B1kFA,gBACE,Y1B6kFF,C0B1kFE,gCAIE,kB1B8kFJ,C0BllFE,gCAIE,iB1B8kFJ,C0BllFE,sBAGE,kBAAA,CAGA,uCAAA,CALA,mBAAA,CAIA,gBAAA,CAHA,S1BglFJ,C0BzkFI,+BACE,aAAA,CACA,oB1B2kFN,C0BvkFI,2CACE,U1B0kFN,C0B3kFI,2CACE,W1B0kFN,C0B3kFI,iCAEE,kB1BykFN,C0BrkFI,0BACE,W1BukFN,C2B9vFA,MACE,iSAAA,CACA,4UAAA,CACA,+NAAA,CACA,gZ3BiwFF,C2BxvFE,iBAME,kDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,cAAA,CAIA,mCAAA,CAAA,2BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CANA,0BAAA,CAFA,a3BmwFJ,C2BvvFE,uBACE,6B3ByvFJ,C2BrvFE,sBACE,wCAAA,CAAA,gC3BuvFJ,C2BnvFE,6BACE,+CAAA,CAAA,uC3BqvFJ,C2BjvFE,4BACE,8CAAA,CAAA,sC3BmvFJ,C4B9xFA,SASE,2CAAA,CADA,gCAAA,CAJA,aAAA,CAGA,eAAA,CADA,aAAA,CADA,UAAA,CAFA,S5BqyFF,C4B5xFE,aAZF,SAaI,Y5B+xFF,CACF,CKpnFI,0CuBzLJ,SAkBI,Y5B+xFF,CACF,C4B5xFE,iBACE,mB5B8xFJ,C4B1xFE,yBAIE,iB5BiyFJ,C4BryFE,yBAIE,kB5BiyFJ,C4BryFE,eAQE,eAAA,CAPA,YAAA,CAMA,eAAA,CAJA,QAAA,CAEA,aAAA,CAHA,SAAA,CAWA,oBAAA,CAPA,kB5B+xFJ,C4BrxFI,kCACE,Y5BuxFN,C4BlxFE,eACE,aAAA,CACA,kBAAA,CAAA,mB5BoxFJ,C4BjxFI,sCACE,aAAA,CACA,S5BmxFN,C4B7wFE,eAOE,kCAAA,CAAA,0BAAA,CANA,YAAA,CAEA,eAAA,CADA,gBAAA,CAMA,UAAA,CAJA,uCAAA,CACA,oBAAA,CAIA,8D5B8wFJ,C4BzwFI,0CACE,aAAA,CACA,S5B2wFN,C4BvwFI,6BAEE,kB5B0wFN,C4B5wFI,6BAEE,iB5B0wFN,C4B5wFI,mBAGE,iBAAA,CAFA,Y5B2wFN,C4BpwFM,2CACE,qB5BswFR,C4BvwFM,2CACE,qB5BywFR,C4B1wFM,2CACE,qB5B4wFR,C4B7wFM,2CACE,qB5B+wFR,C4BhxFM,2CACE,oB5BkxFR,C4BnxFM,2CACE,qB5BqxFR,C4BtxFM,2CACE,qB5BwxFR,C4BzxFM,2CACE,qB5B2xFR,C4B5xFM,4CACE,qB5B8xFR,C4B/xFM,4CACE,oB5BiyFR,C4BlyFM,4CACE,qB5BoyFR,C4BryFM,4CACE,qB5BuyFR,C4BxyFM,4CACE,qB5B0yFR,C4B3yFM,4CACE,qB5B6yFR,C4B9yFM,4CACE,oB5BgzFR,C4B1yFI,gCACE,SAAA,CAIA,yBAAA,CAHA,wC5B6yFN,C6Bh5FA,MACE,mS7Bm5FF,C6B14FE,mCACE,mBAAA,CACA,cAAA,CACA,QAAA,CAEA,mBAAA,CADA,kB7B84FJ,C6Bz4FE,oBAGE,kBAAA,CAOA,+CAAA,CACA,oBAAA,CAVA,mBAAA,CAIA,gBAAA,CACA,0BAAA,CACA,eAAA,CALA,QAAA,CAOA,qBAAA,CADA,eAAA,CAJA,wB7Bk5FJ,C6Bx4FI,0BAGE,uCAAA,CAFA,aAAA,CACA,YAAA,CAEA,6C7B04FN,C6Br4FM,gEAEE,0CAAA,CADA,+B7Bw4FR,C6Bl4FI,yBACE,uB7Bo4FN,C6B53FI,gCAME,oDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,qCAAA,CAAA,6BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CAPA,0BAAA,CAFA,W7Bu4FN,C6B13FI,wFACE,0C7B43FN,C8Bt8FA,iBACE,GACE,oB9By8FF,C8Bt8FA,IACE,kB9Bw8FF,C8Br8FA,GACE,oB9Bu8FF,CACF,C8B/7FA,MACE,yNAAA,CACA,sP9Bk8FF,C8B37FA,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,S9B+7FF,C8B76FE,oBAEE,SAAA,CAKA,uBAAA,CAJA,2EACE,CAHF,S9Bk7FJ,C8Bx6FE,oBAEE,eAAA,CACA,wBAAA,CAAA,gBAAA,CAFA,U9B46FJ,C8Bv6FI,6CACE,qC9By6FN,C8Br6FI,uCAEE,eAAA,CADA,mB9Bw6FN,C8Bl6FI,6BACE,Y9Bo6FN,C8B/5FE,8CACE,sC9Bi6FJ,C8B75FE,mBAEE,gBAAA,CADA,a9Bg6FJ,C8B55FI,2CACE,Y9B85FN,C8B15FI,0CACE,e9B45FN,C8Bp5FA,eACE,iBAAA,CACA,eAAA,CAIA,YAAA,CAHA,kBAAA,CAEA,0BAAA,CADA,kB9By5FF,C8Bp5FE,yBACE,a9Bs5FJ,C8Bl5FE,oBACE,sCAAA,CACA,iB9Bo5FJ,C8Bh5FE,6BACE,oBAAA,CAGA,gB9Bg5FJ,C8B54FE,sBAYE,mBAAA,CANA,cAAA,CAHA,oBAAA,CACA,gBAAA,CAAA,iBAAA,CAIA,YAAA,CAGA,eAAA,CAVA,iBAAA,CAMA,wBAAA,CAAA,gBAAA,CAFA,uBAAA,CAHA,S9Bs5FJ,C8Bx4FI,qCACE,uB9B04FN,C8Bt4FI,cArBF,sBAsBI,W9By4FJ,C8Bt4FI,wCACE,2B9Bw4FN,C8Bp4FI,6BAOE,qCAAA,CACA,+CAAA,CAAA,uC9By4FN,C8B/3FI,yDAZE,UAAA,CADA,YAAA,CAKA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CACA,SAAA,CAEA,WAAA,CADA,U9B65FN,C8B94FI,4BAOE,oDAAA,CACA,4CAAA,CAAA,oCAAA,CAQA,uBAAA,CAJA,+C9Bk4FN,C8B33FM,gDACE,uB9B63FR,C8Bz3FM,mFACE,0C9B23FR,CACF,C8Bt3FI,0CAGE,2BAAA,CADA,uBAAA,CADA,S9B03FN,C8Bp3FI,8CACE,oB9Bs3FN,C8Bn3FM,aAJF,8CASI,8CAAA,CACA,iBAAA,CAHA,gCAAA,CADA,eAAA,CADA,cAAA,CAGA,kB9Bw3FN,C8Bn3FM,oDACE,mC9Bq3FR,CACF,C8Bz2FE,gCAEE,iBAAA,CADA,e9B62FJ,C8Bz2FI,mCACE,iB9B22FN,C8Bx2FM,oDAEE,a9Bu3FR,C8Bz3FM,oDAEE,c9Bu3FR,C8Bz3FM,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,CARA,S9Bs3FR,C+BtoGA,MACE,wBAAA,CACA,wB/ByoGF,C+BnoGA,aA+BE,kCAAA,CAAA,0BAAA,CAjBA,gCAAA,CADA,sCAAA,CAGA,SAAA,CADA,mBAAA,CAdA,iBAAA,CAGA,wDACE,CAgBF,4BAAA,CAGA,uEACE,CARF,uDACE,CANF,UAAA,CADA,S/BuoGF,C+BhnGE,oBAuBE,8CAAA,CAAA,+CAAA,CADA,UAAA,CADA,aAAA,CAfA,gJACE,CANF,iBAAA,CAmBA,S/BomGJ,C+B7lGE,yBAGE,kEAAA,CAFA,gDAAA,CACA,6C/BgmGJ,C+B3lGE,4BAGE,qEAAA,CADA,8CAAA,CADA,6C/B+lGJ,C+BzlGE,qBAEE,SAAA,CAKA,uBAAA,CAJA,wEACE,CAHF,S/B8lGJ,C+BplGE,oBAqBE,uBAAA,CAEA,2CAAA,CACA,mBAAA,CACA,8BAAA,CAnBA,0FACE,CAaF,eAAA,CADA,8BAAA,CAlBA,iBAAA,CAqBA,oB/BykGJ,C+BnkGI,uCAEE,YAAA,CADA,W/BskGN,C+BjkGI,6CACE,oD/BmkGN,C+BhkGM,mDACE,0C/BkkGR,C+B1jGI,mCAwBE,eAAA,CACA,eAAA,CAxBA,oIACE,CAgBF,sCACE,CAIF,mBAAA,CAKA,wBAAA,CAAA,gBAAA,CAbA,sBAAA,CAAA,iB/BojGN,C+BniGI,4CACE,Y/BqiGN,C+BjiGI,2CACE,e/BmiGN,CgCttGA,kBAME,ehCkuGF,CgCxuGA,kBAME,gBhCkuGF,CgCxuGA,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,ShCquGF,CgCltGE,aAtBF,QAuBI,YhCqtGF,CACF,CgCltGE,kBACE,wBhCotGJ,CgChtGE,gBAEE,SAAA,CADA,mBAAA,CAGA,+BAAA,CADA,uBhCmtGJ,CgC/sGI,0BACE,8BhCitGN,CgC5sGE,4BAEE,0CAAA,CADA,+BhC+sGJ,CgC1sGE,YACE,oBAAA,CACA,oBhC4sGJ,CiCjwGA,oBACE,GACE,mBjCowGF,CACF,CiC5vGA,MACE,wfjC8vGF,CiCxvGA,YACE,aAAA,CAEA,eAAA,CADA,ajC4vGF,CiCxvGE,+BAOE,kBAAA,CAAA,kBjCyvGJ,CiChwGE,+BAOE,iBAAA,CAAA,mBjCyvGJ,CiChwGE,qBAQE,aAAA,CACA,cAAA,CACA,YAAA,CATA,iBAAA,CAKA,UjC0vGJ,CiCnvGI,qCAIE,iBjC2vGN,CiC/vGI,qCAIE,kBjC2vGN,CiC/vGI,2BAME,6BAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,WjC6vGN,CiChvGE,mBACE,iBAAA,CACA,UjCkvGJ,CiC9uGE,kBAWE,2CAAA,CACA,mBAAA,CACA,8BAAA,CALA,gCAAA,CACA,oBAAA,CAHA,kBAAA,CAFA,YAAA,CAUA,SAAA,CAPA,aAAA,CAFA,SAAA,CAJA,iBAAA,CASA,4BAAA,CARA,UAAA,CAaA,+CACE,CAbF,SjC4vGJ,CiC3uGI,+EACE,gBAAA,CACA,SAAA,CACA,sCjC6uGN,CiCvuGI,qCAEE,oCACE,gCjCwuGN,CiCpuGI,2CACE,cjCsuGN,CACF,CiCjuGE,kBACE,kBjCmuGJ,CiC/tGE,4BAGE,kBAAA,CAAA,oBjCsuGJ,CiCzuGE,4BAGE,mBAAA,CAAA,mBjCsuGJ,CiCzuGE,kBAKE,cAAA,CAJA,aAAA,CAMA,YAAA,CADA,uBAAA,CAEA,2CACE,CALF,kBAAA,CAFA,UjCuuGJ,CiC5tGI,gDACE,+BjC8tGN,CiC1tGI,wBACE,qDjC4tGN,CkCl0GA,MAEI,6VAAA,CAAA,uWAAA,CAAA,qPAAA,CAAA,2xBAAA,CAAA,qMAAA,CAAA,+aAAA,CAAA,2LAAA,CAAA,yPAAA,CAAA,2TAAA,CAAA,oaAAA,CAAA,2SAAA,CAAA,2LlC21GJ,CkC/0GE,4CAME,8CAAA,CACA,4BAAA,CACA,mBAAA,CACA,8BAAA,CAJA,mCAAA,CAJA,iBAAA,CAGA,gBAAA,CADA,iBAAA,CADA,eAAA,CASA,uBAAA,CADA,2BlCm1GJ,CkC/0GI,aAdF,4CAeI,elCk1GJ,CACF,CkC/0GI,sEACE,gClCi1GN,CkC50GI,gDACE,qBlC80GN,CkC10GI,gIAEE,iBAAA,CADA,clC60GN,CkCx0GI,4FACE,iBlC00GN,CkCt0GI,kFACE,elCw0GN,CkCp0GI,0FACE,YlCs0GN,CkCl0GI,8EACE,mBlCo0GN,CkC/zGE,sEAGE,iBAAA,CAAA,mBlCy0GJ,CkC50GE,sEAGE,kBAAA,CAAA,kBlCy0GJ,CkC50GE,sEASE,uBlCm0GJ,CkC50GE,sEASE,wBlCm0GJ,CkC50GE,sEAUE,4BlCk0GJ,CkC50GE,4IAWE,6BlCi0GJ,CkC50GE,sEAWE,4BlCi0GJ,CkC50GE,kDAOE,0BAAA,CACA,WAAA,CAFA,eAAA,CADA,eAAA,CAHA,oBAAA,CAAA,iBAAA,CADA,iBlC20GJ,CkC9zGI,kFACE,elCg0GN,CkC5zGI,oFAEE,UlCu0GN,CkCz0GI,oFAEE,WlCu0GN,CkCz0GI,gEAOE,wBhBiIU,CgBlIV,UAAA,CADA,WAAA,CAGA,kDAAA,CAAA,0CAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CACA,UlCq0GN,CkC1zGI,4DACE,4DlC4zGN,CkC9yGE,sDACE,oBlCizGJ,CkC9yGI,gFACE,gClCgzGN,CkC3yGE,8DACE,0BlC8yGJ,CkC3yGI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClC6yGN,CkCzyGI,0EACE,alC2yGN,CkCh0GE,8DACE,oBlCm0GJ,CkCh0GI,wFACE,gClCk0GN,CkC7zGE,sEACE,0BlCg0GJ,CkC7zGI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClC+zGN,CkC3zGI,kFACE,alC6zGN,CkCl1GE,sDACE,oBlCq1GJ,CkCl1GI,gFACE,gClCo1GN,CkC/0GE,8DACE,0BlCk1GJ,CkC/0GI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClCi1GN,CkC70GI,0EACE,alC+0GN,CkCp2GE,oDACE,oBlCu2GJ,CkCp2GI,8EACE,gClCs2GN,CkCj2GE,4DACE,0BlCo2GJ,CkCj2GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClCm2GN,CkC/1GI,wEACE,alCi2GN,CkCt3GE,4DACE,oBlCy3GJ,CkCt3GI,sFACE,gClCw3GN,CkCn3GE,oEACE,0BlCs3GJ,CkCn3GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCq3GN,CkCj3GI,gFACE,alCm3GN,CkCx4GE,8DACE,oBlC24GJ,CkCx4GI,wFACE,gClC04GN,CkCr4GE,sEACE,0BlCw4GJ,CkCr4GI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClCu4GN,CkCn4GI,kFACE,alCq4GN,CkC15GE,4DACE,oBlC65GJ,CkC15GI,sFACE,gClC45GN,CkCv5GE,oEACE,0BlC05GJ,CkCv5GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCy5GN,CkCr5GI,gFACE,alCu5GN,CkC56GE,4DACE,oBlC+6GJ,CkC56GI,sFACE,gClC86GN,CkCz6GE,oEACE,0BlC46GJ,CkCz6GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClC26GN,CkCv6GI,gFACE,alCy6GN,CkC97GE,0DACE,oBlCi8GJ,CkC97GI,oFACE,gClCg8GN,CkC37GE,kEACE,0BlC87GJ,CkC37GI,gFACE,wBAlBG,CAmBH,oDAAA,CAAA,4ClC67GN,CkCz7GI,8EACE,alC27GN,CkCh9GE,oDACE,oBlCm9GJ,CkCh9GI,8EACE,gClCk9GN,CkC78GE,4DACE,0BlCg9GJ,CkC78GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClC+8GN,CkC38GI,wEACE,alC68GN,CkCl+GE,4DACE,oBlCq+GJ,CkCl+GI,sFACE,gClCo+GN,CkC/9GE,oEACE,0BlCk+GJ,CkC/9GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCi+GN,CkC79GI,gFACE,alC+9GN,CkCp/GE,wDACE,oBlCu/GJ,CkCp/GI,kFACE,gClCs/GN,CkCj/GE,gEACE,0BlCo/GJ,CkCj/GI,8EACE,wBAlBG,CAmBH,mDAAA,CAAA,2ClCm/GN,CkC/+GI,4EACE,alCi/GN,CmCrpHA,MACE,qMnCwpHF,CmC/oHE,sBAEE,uCAAA,CADA,gBnCmpHJ,CmC/oHI,mCACE,anCipHN,CmClpHI,mCACE,cnCipHN,CmC7oHM,4BACE,sBnC+oHR,CmC5oHQ,mCACE,gCnC8oHV,CmC1oHQ,2DACE,SAAA,CAEA,uBAAA,CADA,enC6oHV,CmCxoHQ,yGACE,SAAA,CACA,uBnC0oHV,CmCtoHQ,yCACE,YnCwoHV,CmCjoHE,0BACE,eAAA,CACA,enCmoHJ,CmChoHI,+BACE,oBnCkoHN,CmC7nHE,gDACE,YnC+nHJ,CmC3nHE,8BAIE,+BAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,SAAA,CAKA,4BAAA,CAJA,4DACE,CAHF,0BnC+nHJ,CmCtnHI,aAdF,8BAeI,+BAAA,CACA,SAAA,CACA,uBnCynHJ,CACF,CmCtnHI,wCACE,6BnCwnHN,CmCpnHI,oCACE,+BnCsnHN,CmClnHI,qCAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,YAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,WnC2nHN,CmC9mHQ,mDACE,oBnCgnHV,CoC9tHE,kCAEE,iBpCouHJ,CoCtuHE,kCAEE,kBpCouHJ,CoCtuHE,wBAGE,yCAAA,CAFA,oBAAA,CAGA,SAAA,CACA,mCpCiuHJ,CoC5tHI,aAVF,wBAWI,YpC+tHJ,CACF,CoC3tHE,6FAEE,SAAA,CACA,mCpC6tHJ,CoCvtHE,4FAEE,+BpCytHJ,CoCrtHE,oBACE,yBAAA,CACA,uBAAA,CAGA,yEpCqtHJ,CKtlHI,sC+BrHE,qDACE,uBpC8sHN,CACF,CoCzsHE,kEACE,yBpC2sHJ,CoCvsHE,sBACE,0BpCysHJ,CqCpwHE,2BACE,arCuwHJ,CKllHI,0CgCtLF,2BAKI,erCuwHJ,CqCpwHI,6BACE,iBrCswHN,CACF,CqClwHI,6BAEE,0BAAA,CAAA,2BAAA,CADA,eAAA,CAEA,iBrCowHN,CqCjwHM,2CACE,kBrCmwHR,CqC7vHI,6CACE,QrC+vHN,CsC3xHE,uBACE,4CtC+xHJ,CsC1xHE,8CAJE,kCAAA,CAAA,0BtCkyHJ,CsC9xHE,uBACE,4CtC6xHJ,CsCxxHE,4BAEE,kCAAA,CAAA,0BAAA,CADA,qCtC2xHJ,CsCvxHI,mCACE,atCyxHN,CsCrxHI,kCACE,atCuxHN,CsClxHE,0BAKE,eAAA,CAJA,aAAA,CAEA,YAAA,CACA,aAAA,CAFA,kBAAA,CAAA,mBtCuxHJ,CsCjxHI,uCACE,etCmxHN,CsC/wHI,sCACE,kBtCixHN,CuC9zHA,MACE,oLvCi0HF,CuCxzHE,oBAGE,iBAAA,CAEA,gBAAA,CADA,avC0zHJ,CuCtzHI,wCACE,uBvCwzHN,CuCpzHI,gCAEE,eAAA,CADA,gBvCuzHN,CuChzHM,wCACE,mBvCkzHR,CuC5yHE,8BAKE,oBvCgzHJ,CuCrzHE,8BAKE,mBvCgzHJ,CuCrzHE,8BAUE,4BvC2yHJ,CuCrzHE,4DAWE,6BvC0yHJ,CuCrzHE,8BAWE,4BvC0yHJ,CuCrzHE,oBASE,cAAA,CANA,aAAA,CACA,eAAA,CAIA,evC6yHJ,CuCvyHI,kCACE,uCAAA,CACA,oBvCyyHN,CuCryHI,wCAEE,uCAAA,CADA,YvCwyHN,CuCnyHI,oCAEE,WvCgzHN,CuClzHI,oCAEE,UvCgzHN,CuClzHI,0BAOE,6BAAA,CADA,UAAA,CADA,WAAA,CAGA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CAUA,sBAAA,CADA,yBAAA,CARA,UvC8yHN,CuClyHM,oCACE,wBvCoyHR,CuC/xHI,4BACE,YvCiyHN,CuC5xHI,4CACE,YvC8xHN,CwCx3HE,+DACE,sBAAA,CAEA,mBAAA,CACA,0BAAA,CACA,uBxC03HJ,CwCv3HI,2EAGE,iBAAA,CADA,eAAA,CADA,yBxC23HN,CwCp3HE,mEACE,0BxCs3HJ,CwCl3HE,oBACE,qBxCo3HJ,CwCh3HE,gBACE,oBxCk3HJ,CwC92HE,gBACE,qBxCg3HJ,CwC52HE,iBACE,kBxC82HJ,CwC12HE,kBACE,kBxC42HJ,CyCr5HE,6BACE,sCzCw5HJ,CyCr5HE,cACE,yCzCu5HJ,CyC34HE,sIACE,oCzC64HJ,CyCr4HE,2EACE,qCzCu4HJ,CyC73HE,wGACE,oCzC+3HJ,CyCt3HE,yFACE,qCzCw3HJ,CyCn3HE,6BACE,kCzCq3HJ,CyC/2HE,6CACE,sCzCi3HJ,CyC12HE,4DACE,sCzC42HJ,CyCr2HE,4DACE,qCzCu2HJ,CyC91HE,yFACE,qCzCg2HJ,CyCx1HE,2EACE,sCzC01HJ,CyC/0HE,wHACE,qCzCi1HJ,CyC50HE,8BAGE,mBAAA,CADA,gBAAA,CADA,gBzCg1HJ,CyC30HE,eACE,4CzC60HJ,CyC10HE,eACE,4CzC40HJ,CyCx0HE,gBAIE,+CAAA,CACA,kDAAA,CAJA,aAAA,CAEA,wBAAA,CADA,wBzC60HJ,CyCt0HE,yBAOE,wCAAA,CACA,+DAAA,CACA,4BAAA,CACA,6BAAA,CARA,iBAAA,CAGA,eAAA,CACA,eAAA,CAFA,cAAA,CADA,oCAAA,CAFA,iBzCi1HJ,CyCr0HI,6BACE,YzCu0HN,CyCp0HM,kCACE,wBAAA,CACA,yBzCs0HR,CyCh0HE,iCAaE,wCAAA,CACA,+DAAA,CAJA,uCAAA,CACA,0BAAA,CALA,UAAA,CAJA,oBAAA,CAOA,2BAAA,CADA,2BAAA,CADA,2BAAA,CANA,eAAA,CAWA,wBAAA,CAAA,gBAAA,CAPA,SzCy0HJ,CyCvzHE,sBACE,iBAAA,CACA,iBzCyzHJ,CyCpzHE,iCAKE,ezCkzHJ,CyC/yHI,sCACE,gBzCizHN,CyC7yHI,gDACE,YzC+yHN,CyCryHA,gBACE,iBzCwyHF,CyCpyHE,yCACE,aAAA,CACA,SzCsyHJ,CyCjyHE,mBACE,YzCmyHJ,CyC9xHE,oBACE,QzCgyHJ,CyC5xHE,4BACE,WAAA,CACA,SAAA,CACA,ezC8xHJ,CyC3xHI,0CACE,YzC6xHN,CyCvxHE,yBAKE,wCAAA,CAEA,+BAAA,CADA,4BAAA,CAHA,eAAA,CADA,oDAAA,CAEA,wBAAA,CAAA,gBzC4xHJ,CyCrxHE,2BAEE,+DAAA,CADA,2BzCwxHJ,CyCpxHI,+BACE,uCAAA,CACA,gBzCsxHN,CyCjxHE,sBACE,MAAA,CACA,WzCmxHJ,CyC9wHA,aACE,azCixHF,CyCvwHE,4BAEE,aAAA,CADA,YzC2wHJ,CyCvwHI,wDAEE,2BAAA,CADA,wBzC0wHN,CyCpwHE,+BAKE,2CAAA,CAEA,+BAAA,CADA,gCAAA,CADA,sBAAA,CAHA,mBAAA,CACA,gBAAA,CAFA,azC4wHJ,CyCnwHI,qCAEE,UAAA,CACA,UAAA,CAFA,azCuwHN,CK94HI,0CoCsJF,8BACE,iBzC4vHF,CyClvHE,wSAGE,ezCwvHJ,CyCpvHE,sCAEE,mBAAA,CACA,eAAA,CADA,oBAAA,CADA,kBAAA,CAAA,mBzCwvHJ,CACF,C0CrlII,yDAIE,+BAAA,CACA,8BAAA,CAFA,aAAA,CADA,QAAA,CADA,iB1C2lIN,C0CnlII,uBAEE,uCAAA,CADA,c1CslIN,C0CjiIM,iHAEE,WAlDkB,CAiDlB,kB1C4iIR,C0C7iIM,6HAEE,WAlDkB,CAiDlB,kB1CwjIR,C0CzjIM,6HAEE,WAlDkB,CAiDlB,kB1CokIR,C0CrkIM,oHAEE,WAlDkB,CAiDlB,kB1CglIR,C0CjlIM,0HAEE,WAlDkB,CAiDlB,kB1C4lIR,C0C7lIM,uHAEE,WAlDkB,CAiDlB,kB1CwmIR,C0CzmIM,uHAEE,WAlDkB,CAiDlB,kB1ConIR,C0CrnIM,6HAEE,WAlDkB,CAiDlB,kB1CgoIR,C0CjoIM,yCAEE,WAlDkB,CAiDlB,kB1CooIR,C0CroIM,yCAEE,WAlDkB,CAiDlB,kB1CwoIR,C0CzoIM,0CAEE,WAlDkB,CAiDlB,kB1C4oIR,C0C7oIM,uCAEE,WAlDkB,CAiDlB,kB1CgpIR,C0CjpIM,wCAEE,WAlDkB,CAiDlB,kB1CopIR,C0CrpIM,sCAEE,WAlDkB,CAiDlB,kB1CwpIR,C0CzpIM,wCAEE,WAlDkB,CAiDlB,kB1C4pIR,C0C7pIM,oCAEE,WAlDkB,CAiDlB,kB1CgqIR,C0CjqIM,2CAEE,WAlDkB,CAiDlB,kB1CoqIR,C0CrqIM,qCAEE,WAlDkB,CAiDlB,kB1CwqIR,C0CzqIM,oCAEE,WAlDkB,CAiDlB,kB1C4qIR,C0C7qIM,kCAEE,WAlDkB,CAiDlB,kB1CgrIR,C0CjrIM,qCAEE,WAlDkB,CAiDlB,kB1CorIR,C0CrrIM,mCAEE,WAlDkB,CAiDlB,kB1CwrIR,C0CzrIM,qCAEE,WAlDkB,CAiDlB,kB1C4rIR,C0C7rIM,wCAEE,WAlDkB,CAiDlB,kB1CgsIR,C0CjsIM,sCAEE,WAlDkB,CAiDlB,kB1CosIR,C0CrsIM,2CAEE,WAlDkB,CAiDlB,kB1CwsIR,C0C7rIM,iCAEE,WAPkB,CAMlB,iB1CgsIR,C0CjsIM,uCAEE,WAPkB,CAMlB,iB1CosIR,C0CrsIM,mCAEE,WAPkB,CAMlB,iB1CwsIR,C2C1xIA,MACE,2LAAA,CACA,yL3C6xIF,C2CpxIE,wBAKE,mBAAA,CAHA,YAAA,CACA,qBAAA,CACA,YAAA,CAHA,iB3C2xIJ,C2CjxII,8BAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,O3CqxIN,C2ChxIM,qCACE,0B3CkxIR,C2CrvIM,kEACE,0C3CuvIR,C2CjvIE,2BAME,uBAAA,CADA,+DAAA,CAJA,YAAA,CACA,cAAA,CACA,aAAA,CACA,oB3CqvIJ,C2ChvII,aATF,2BAUI,gB3CmvIJ,CACF,C2ChvII,cAGE,+BACE,iB3CgvIN,C2C7uIM,sCAQE,qCAAA,CANA,QAAA,CAKA,UAAA,CAHA,aAAA,CAEA,UAAA,CAHA,MAAA,CAFA,iBAAA,CAaA,2CAAA,CALA,2DACE,CAGF,kDAAA,CARA,+B3CqvIR,CACF,C2CvuII,8CACE,Y3CyuIN,C2CruII,iCAUE,+BAAA,CACA,6BAAA,CALA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,gBAAA,CACA,eAAA,CAFA,8BAAA,CAMA,+BAAA,CAGA,2CACE,CANF,kBAAA,CALA,U3CivIN,C2CluIM,aAII,6CACE,O3CiuIV,C2CluIQ,8CACE,O3CouIV,C2CruIQ,8CACE,O3CuuIV,C2CxuIQ,8CACE,O3C0uIV,C2C3uIQ,8CACE,O3C6uIV,C2C9uIQ,8CACE,O3CgvIV,C2CjvIQ,8CACE,O3CmvIV,C2CpvIQ,8CACE,O3CsvIV,C2CvvIQ,8CACE,O3CyvIV,C2C1vIQ,+CACE,Q3C4vIV,C2C7vIQ,+CACE,Q3C+vIV,C2ChwIQ,+CACE,Q3CkwIV,C2CnwIQ,+CACE,Q3CqwIV,C2CtwIQ,+CACE,Q3CwwIV,C2CzwIQ,+CACE,Q3C2wIV,C2C5wIQ,+CACE,Q3C8wIV,C2C/wIQ,+CACE,Q3CixIV,C2ClxIQ,+CACE,Q3CoxIV,C2CrxIQ,+CACE,Q3CuxIV,C2CxxIQ,+CACE,Q3C0xIV,CACF,C2CrxIM,uCACE,gC3CuxIR,C2CnxIM,oDACE,a3CqxIR,C2ChxII,yCACE,S3CkxIN,C2C9wIM,2CACE,aAAA,CACA,8B3CgxIR,C2C1wIE,4BACE,U3C4wIJ,C2CzwII,aAJF,4BAKI,gB3C4wIJ,CACF,C2CxwIE,0BACE,Y3C0wIJ,C2CvwII,aAJF,0BAKI,a3C0wIJ,C2CtwIM,sCACE,O3CwwIR,C2CzwIM,uCACE,O3C2wIR,C2C5wIM,uCACE,O3C8wIR,C2C/wIM,uCACE,O3CixIR,C2ClxIM,uCACE,O3CoxIR,C2CrxIM,uCACE,O3CuxIR,C2CxxIM,uCACE,O3C0xIR,C2C3xIM,uCACE,O3C6xIR,C2C9xIM,uCACE,O3CgyIR,C2CjyIM,wCACE,Q3CmyIR,C2CpyIM,wCACE,Q3CsyIR,C2CvyIM,wCACE,Q3CyyIR,C2C1yIM,wCACE,Q3C4yIR,C2C7yIM,wCACE,Q3C+yIR,C2ChzIM,wCACE,Q3CkzIR,C2CnzIM,wCACE,Q3CqzIR,C2CtzIM,wCACE,Q3CwzIR,C2CzzIM,wCACE,Q3C2zIR,C2C5zIM,wCACE,Q3C8zIR,C2C/zIM,wCACE,Q3Ci0IR,CACF,C2C3zII,+FAEE,Q3C6zIN,C2C1zIM,yGACE,wBAAA,CACA,yB3C6zIR,C2CpzIM,2DAEE,wBAAA,CACA,yBAAA,CAFA,Q3CwzIR,C2CjzIM,iEACE,Q3CmzIR,C2ChzIQ,qLAGE,wBAAA,CACA,yBAAA,CAFA,Q3CozIV,C2C9yIQ,6FACE,wBAAA,CACA,yB3CgzIV,C2C3yIM,yDACE,kB3C6yIR,C2CxyII,sCACE,Q3C0yIN,C2CryIE,2BAEE,iBAAA,CAOA,kBAAA,CAHA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,YAAA,CACA,gBAAA,CAEA,mBAAA,CAGA,gCAAA,CAPA,W3C8yIJ,C2CpyII,iCAEE,uDAAA,CADA,+B3CuyIN,C2ClyII,iCAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,8CAAA,CAAA,sCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,+CACE,CATF,U3C4yIN,C2C7xIE,4BAOE,yEACE,CANF,YAAA,CAGA,aAAA,CAFA,qBAAA,CAGA,mBAAA,CALA,iBAAA,CAYA,wBAAA,CATA,Y3CmyIJ,C2CvxII,sCACE,wB3CyxIN,C2CrxII,oCACE,S3CuxIN,C2CnxII,kCAGE,wEACE,CAFF,mBAAA,CADA,O3CuxIN,C2C7wIM,uDACE,8CAAA,CAAA,sC3C+wIR,CKt5II,0CsCqJF,wDAEE,kB3CuwIF,C2CzwIA,wDAEE,mB3CuwIF,C2CzwIA,8CAGE,eAAA,CAFA,eAAA,CAGA,iC3CqwIF,C2CjwIE,8DACE,mB3CowIJ,C2CrwIE,8DACE,kB3CowIJ,C2CrwIE,oDAEE,U3CmwIJ,C2C/vIE,8EAEE,kB3CkwIJ,C2CpwIE,8EAEE,mB3CkwIJ,C2CpwIE,8EAGE,kB3CiwIJ,C2CpwIE,8EAGE,mB3CiwIJ,C2CpwIE,oEACE,U3CmwIJ,C2C7vIE,8EAEE,mB3CgwIJ,C2ClwIE,8EAEE,kB3CgwIJ,C2ClwIE,8EAGE,mB3C+vIJ,C2ClwIE,8EAGE,kB3C+vIJ,C2ClwIE,oEACE,U3CiwIJ,CACF,C2CnvIE,cAHF,olDAII,gC3CsvIF,C2CnvIE,g8GACE,uC3CqvIJ,CACF,C2ChvIA,4sDACE,+B3CmvIF,C2C/uIA,wmDACE,a3CkvIF,C4CtnJA,MACE,qWAAA,CACA,8W5CynJF,C4ChnJE,4BAEE,oBAAA,CADA,iB5ConJJ,C4C/mJI,sDAEE,S5CknJN,C4CpnJI,sDAEE,U5CknJN,C4CpnJI,4CACE,iBAAA,CAEA,S5CinJN,C4C5mJE,+CAEE,SAAA,CADA,U5C+mJJ,C4C1mJE,kDAEE,W5CqnJJ,C4CvnJE,kDAEE,Y5CqnJJ,C4CvnJE,wCAOE,qDAAA,CADA,UAAA,CADA,aAAA,CAGA,0CAAA,CAAA,kCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,SAAA,CACA,Y5CmnJJ,C4CxmJE,gEACE,wB1B2Wa,C0B1Wb,mDAAA,CAAA,2C5C0mJJ,C6C1pJA,aAQE,wBACE,Y7CypJF,CACF,C8CnqJA,QACE,8DAAA,CAGA,+CAAA,CACA,iEAAA,CACA,oDAAA,CACA,sDAAA,CACA,mDAAA,CAGA,qEAAA,CACA,qEAAA,CACA,wEAAA,CACA,0EAAA,CACA,wEAAA,CACA,yEAAA,CACA,kEAAA,CACA,+DAAA,CACA,oEAAA,CACA,oEAAA,CACA,mEAAA,CACA,gEAAA,CACA,uEAAA,CACA,mEAAA,CACA,qEAAA,CACA,oEAAA,CACA,gEAAA,CACA,wEAAA,CACA,qEAAA,CACA,+D9CiqJF,C8C3pJA,SAEE,kBAAA,CADA,Y9C+pJF,C+CjsJE,kBAUE,cAAA,CATA,YAAA,CACA,kEACE,CAQF,Y/C6rJJ,C+CzrJI,sDACE,gB/C2rJN,C+CrrJI,oFAKE,wDAAA,CACA,mBAAA,CAJA,aAAA,CAEA,QAAA,CADA,aAAA,CAIA,sC/CurJN,C+ClrJM,iOACE,kBAAA,CACA,8B/CqrJR,C+CjrJM,6FACE,iBAAA,CAAA,c/CorJR,C+ChrJM,2HACE,Y/CmrJR,C+C/qJM,wHACE,e/CkrJR,C+CnqJI,yMAGE,eAAA,CAAA,Y/C2qJN,C+C7pJI,ybAOE,W/CmqJN,C+C/pJI,8BACE,eAAA,CAAA,Y/CiqJN,CK7lJI,mC2ChKA,8BACE,UhDqwJJ,CgDtwJE,8BACE,WhDqwJJ,CgDtwJE,8BAGE,kBhDmwJJ,CgDtwJE,8BAGE,iBhDmwJJ,CgDtwJE,oBAKE,mBAAA,CADA,YAAA,CAFA,ahDowJJ,CgD9vJI,kCACE,WhDiwJN,CgDlwJI,kCACE,UhDiwJN,CgDlwJI,kCAEE,iBAAA,CAAA,chDgwJN,CgDlwJI,kCAEE,aAAA,CAAA,kBhDgwJN,CACF","file":"main.css"} \ No newline at end of file diff --git a/main/cli/check/index.html b/main/cli/check/index.html index 72225126a..5da64616d 100644 --- a/main/cli/check/index.html +++ b/main/cli/check/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2320,7 +2320,7 @@ Checking the catalog - + @@ -2331,7 +2331,7 @@ Checking the catalog - + @@ -2342,7 +2342,7 @@ Checking the catalog - + diff --git a/main/cli/debug/index.html b/main/cli/debug/index.html index ae66b40d0..c90e0d693 100644 --- a/main/cli/debug/index.html +++ b/main/cli/debug/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2632,7 +2632,7 @@ Example of multiple arguments - + @@ -2643,7 +2643,7 @@ Example of multiple arguments - + @@ -2654,7 +2654,7 @@ Example of multiple arguments - + diff --git a/main/cli/exec/index.html b/main/cli/exec/index.html index 6e5bb22f4..3302bc93a 100644 --- a/main/cli/exec/index.html +++ b/main/cli/exec/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2825,7 +2825,7 @@ Example - + @@ -2836,7 +2836,7 @@ Example - + @@ -2847,7 +2847,7 @@ Example - + diff --git a/main/cli/get-inventory-information/index.html b/main/cli/get-inventory-information/index.html index 66f981d71..8522e13c2 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2517,7 +2517,7 @@ Example - + @@ -2528,7 +2528,7 @@ Example - + @@ -2539,7 +2539,7 @@ Example - + diff --git a/main/cli/inv-from-ansible/index.html b/main/cli/inv-from-ansible/index.html index c36ec8a8b..c32b4d88e 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2385,7 +2385,7 @@ Command output - + @@ -2396,7 +2396,7 @@ Command output - + @@ -2407,7 +2407,7 @@ Command output - + diff --git a/main/cli/inv-from-cvp/index.html b/main/cli/inv-from-cvp/index.html index db12c0fee..58ea6abbe 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2386,7 +2386,7 @@ Creating an inventory fr - + @@ -2397,7 +2397,7 @@ Creating an inventory fr - + @@ -2408,7 +2408,7 @@ Creating an inventory fr - + diff --git a/main/cli/nrfu/index.html b/main/cli/nrfu/index.html index af83ed585..e4814d923 100644 --- a/main/cli/nrfu/index.html +++ b/main/cli/nrfu/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3185,7 +3185,7 @@ Dry-run mode - + @@ -3196,7 +3196,7 @@ Dry-run mode - + @@ -3207,7 +3207,7 @@ Dry-run mode - + diff --git a/main/cli/overview/index.html b/main/cli/overview/index.html index 7ae34083d..de7dfc32e 100644 --- a/main/cli/overview/index.html +++ b/main/cli/overview/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2500,7 +2500,7 @@ Shell Completion - + @@ -2511,7 +2511,7 @@ Shell Completion - + @@ -2522,7 +2522,7 @@ Shell Completion - + diff --git a/main/cli/tag-management/index.html b/main/cli/tag-management/index.html index 0bd503e15..e4c5b1edd 100644 --- a/main/cli/tag-management/index.html +++ b/main/cli/tag-management/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2841,7 +2841,7 @@ Example - + @@ -2852,7 +2852,7 @@ Example - + @@ -2863,7 +2863,7 @@ Example - + diff --git a/main/contribution/index.html b/main/contribution/index.html index 818cbe7a9..184709092 100644 --- a/main/contribution/index.html +++ b/main/contribution/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2812,7 +2812,7 @@ Continuous Integration - + @@ -2823,7 +2823,7 @@ Continuous Integration - + @@ -2834,7 +2834,7 @@ Continuous Integration - + diff --git a/main/faq/index.html b/main/faq/index.html index 9e540805e..45f0c4469 100644 --- a/main/faq/index.html +++ b/main/faq/index.html @@ -18,7 +18,7 @@ - + @@ -26,7 +26,7 @@ - + @@ -131,7 +131,7 @@ - + @@ -228,7 +228,7 @@ - + ANTA on Github @@ -264,7 +264,7 @@ - + Arista Network Test Automation - ANTA @@ -274,7 +274,7 @@ - + ANTA on Github @@ -2588,7 +2588,7 @@ Still facing issues? - + @@ -2599,7 +2599,7 @@ Still facing issues? - + @@ -2610,7 +2610,7 @@ Still facing issues? - + diff --git a/main/getting-started/index.html b/main/getting-started/index.html index 1d2129f24..3a3fe708b 100644 --- a/main/getting-started/index.html +++ b/main/getting-started/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2968,7 +2968,7 @@ Basic usage in a Python script - + @@ -2979,7 +2979,7 @@ Basic usage in a Python script - + @@ -2990,7 +2990,7 @@ Basic usage in a Python script - + diff --git a/main/index.html b/main/index.html index 673ccba42..29105cdbb 100644 --- a/main/index.html +++ b/main/index.html @@ -18,7 +18,7 @@ - + @@ -26,7 +26,7 @@ - + @@ -131,7 +131,7 @@ - + @@ -228,7 +228,7 @@ - + ANTA on Github @@ -264,7 +264,7 @@ - + Arista Network Test Automation - ANTA @@ -274,7 +274,7 @@ - + ANTA on Github @@ -2526,7 +2526,7 @@ Credits - + @@ -2537,7 +2537,7 @@ Credits - + @@ -2548,7 +2548,7 @@ Credits - + diff --git a/main/requirements-and-installation/index.html b/main/requirements-and-installation/index.html index 9705edfd0..1e081fc3c 100644 --- a/main/requirements-and-installation/index.html +++ b/main/requirements-and-installation/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2583,7 +2583,7 @@ EOS Requirements - + @@ -2594,7 +2594,7 @@ EOS Requirements - + @@ -2605,7 +2605,7 @@ EOS Requirements - + diff --git a/main/search/search_index.json b/main/search/search_index.json index 4ff562a6b..c53e00eff 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: As a Python library in your own application The ANTA CLI "},{"location":"#install-anta-library","title":"Install ANTA library","text":"The library will NOT install the necessary dependencies for the CLI. # Install ANTA as a library\npip install anta\n"},{"location":"#install-anta-cli","title":"Install ANTA CLI","text":"If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx is a tool to install and run python applications in isolated environments. Refer to pipx instructions to install on your system. pipx installs ANTA in an isolated python environment and makes it available globally. This is not recommended if you plan to contribute to ANTA # Install ANTA CLI with pipx\n$ pipx install anta[cli]\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 You can also still choose to install it with directly with pip: pip install anta[cli]\n"},{"location":"#documentation","title":"Documentation","text":"The documentation is published on ANTA package website."},{"location":"#contribution-guide","title":"Contribution guide","text":"Contributions are welcome. Please refer to the contribution guide"},{"location":"#credits","title":"Credits","text":"Thank you to Jeremy Schulman for aio-eapi. 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":"Contribution model is based on a fork-model. Don\u2019t push to aristanetworks/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-environment","title":"Create a development environment","text":"Run the following commands to create an ANTA development environment: # Clone repository\n$ git clone https://github.com/aristanetworks/anta.git\n$ cd anta\n\n# Install ANTA in editable mode and its development tools\n$ pip install -e .[dev]\n# To also install the CLI\n$ pip install -e .[dev,cli]\n\n# Verify installation\n$ pip list -e\nPackage Version Editable project location\n------- ------- -------------------------\nanta 1.1.0 /mnt/lab/projects/anta\n Then, tox is configured 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\npy39 -> Run pytest with py39\npy310 -> Run pytest with py310\npy311 -> Run pytest with py311\npy312 -> Run pytest with py312\nreport -> Generate coverage report\n"},{"location":"contribution/#code-linting","title":"Code linting","text":"tox -e lint\n[...]\nlint: commands[0]> ruff check .\nAll checks passed!\nlint: commands[1]> ruff format . --check\n158 files already formatted\nlint: commands[2]> pylint anta\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\nlint: commands[3]> pylint tests\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\n lint: OK (22.69=setup[2.19]+cmd[0.02,0.02,9.71,10.75] seconds)\n congratulations :) (22.72 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 68 source files\ntype: commands[1]> mypy --config-file=pyproject.toml tests\nSuccess: no issues found in 82 source files\n type: OK (31.15=setup[14.62]+cmd[6.05,10.48] seconds)\n congratulations :) (31.18 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.units.anta_tests 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.units.anta_tests import test\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 pre-commit\ntrim trailing whitespace.................................................Passed\nfix end of files.........................................................Passed\ncheck for added large files..............................................Passed\ncheck for merge conflicts................................................Passed\nCheck and insert license on Python files.................................Passed\nCheck and insert license on Markdown files...............................Passed\nRun Ruff linter..........................................................Passed\nRun Ruff formatter.......................................................Passed\nCheck code style with pylint.............................................Passed\nChecks for common misspellings in text files.............................Passed\nCheck typing with mypy...................................................Passed\nCheck Markdown files style...............................................Passed\n"},{"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. The results can be viewed here."},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#a-local-os-error-occurred-while-connecting-to-a-device","title":"A local OS error occurred while connecting to a device","text":"A local OS error occurred while connecting to a device When running ANTA, you can receive A local OS error occurred while connecting to <device> errors. The underlying OSError exception can have various reasons: [Errno 24] Too many open files or [Errno 16] Device or resource busy. This usually means that the operating system refused to open a new file descriptor (or socket) for the ANTA process. This might be due to the hard limit for open file descriptors currently set for the ANTA process. At startup, ANTA sets the soft limit of its process to the hard limit up to 16384. This is because the soft limit is usually 1024 and the hard limit is usually higher (depends on the system). If the hard limit of the ANTA process is still lower than the number of selected tests in ANTA, the ANTA process may request to the operating system too many file descriptors and get an error, a WARNING is displayed at startup if this is the case."},{"location":"faq/#solution","title":"Solution","text":"One solution could be to raise the hard limit for the user starting the ANTA process. You can get the current hard limit for a user using the command ulimit -n -H while logged in. Create the file /etc/security/limits.d/10-anta.conf with the following content: <user> hard nofile <value>\n The user is the one with which the ANTA process is started. The value is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again."},{"location":"faq/#timeout-error-in-the-logs","title":"Timeout error in the logs","text":"Timeout error in the logs When running ANTA, you can receive <Foo>Timeout errors in the logs (could be ReadTimeout, WriteTimeout, ConnectTimeout or PoolTimeout). More details on the timeouts of the underlying library are available here: https://www.python-httpx.org/advanced/timeouts. This might be due to the time the host on which ANTA is run takes to reach the target devices (for instance if going through firewalls, NATs, \u2026) or when a lot of tests are being run at the same time on a device (eAPI has a queue mechanism to avoid exhausting EOS resources because of a high number of simultaneous eAPI requests)."},{"location":"faq/#solution_1","title":"Solution","text":"Use the timeout option. As an example for the nrfu command: anta nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml --timeout 50 text\n The previous command set a couple of options for ANTA NRFU, one them being the timeout command, by default, when running ANTA from CLI, it is set to 30s. The timeout is increased to 50s to allow ANTA to wait for API calls a little longer."},{"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_2","title":"Solution","text":" 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 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_3","title":"Solution","text":" 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_4","title":"Solution","text":" 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 follow the troubleshooting instructions and report the issue in our GitHub repository."},{"location":"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 install ANTA package is to run Python (>=3.9) and its pip package to install: pip install anta[cli]\n For more details about how to install package, please see the requirements and installation 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: s1-spine1\n tags: ['fabric', 'spine']\n - host: 192.168.0.11\n name: s1-spine2\n tags: ['fabric', 'spine']\n - host: 192.168.0.12\n name: s1-leaf1\n tags: ['fabric', 'leaf']\n - host: 192.168.0.13\n name: s1-leaf2\n tags: ['fabric', 'leaf']\n - host: 192.168.0.14\n name: s1-leaf3\n tags: ['fabric', 'leaf']\n - host: 192.168.0.15\n name: s1-leaf3\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: ---\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\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":""},{"location":"getting-started/#cli","title":"CLI","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. Usage: 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 selected inventory devices.\n Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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 Note The following examples shows how to pass all the CLI options. See how to use environment variables instead in the CLI overview"},{"location":"getting-started/#default-report-using-table","title":"Default report using table","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n `# table is default if not provided` \\\n table\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:01] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.058. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.069. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:02] INFO Running ANTA tests completed in: 0:00:00.969. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 s1-spine1 \u2502 VerifyMlagConfigSanity \u2502 skipped \u2502 MLAG is disabled \u2502 Verifies there are no MLAG config-sanity \u2502 MLAG \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502 inconsistencies. \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-spine1 \u2502 VerifyEOSVersion \u2502 failure \u2502 device is running version \u2502 Verifies the EOS version of the device. \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 \"4.32.2F-38195967.4322F (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)\" not in expected versions: \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['4.25.4M', '4.26.1F', \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 '4.28.3M-28837868.4283M (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n[...]\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyTerminAttrVersion \u2502 failure \u2502 device is running TerminAttr version \u2502 Verifies the TerminAttr version of the \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 v1.34.0 and is not in the allowed list: \u2502 device. \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['v1.22.1'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyZeroTouch \u2502 success \u2502 \u2502 Verifies ZeroTouch is disabled \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\u2500\u2500\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\u2500\u2500\u2500\u2500\u2500\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 arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n 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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:52:39] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.057. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.068. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:52:40] INFO Running ANTA tests completed in: 0:00:00.863. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\n\ns1-spine1 :: VerifyEOSVersion :: FAILURE(device is running version \"4.32.2F-38195967.4322F (engineering build)\" not in expected versions: ['4.25.4M', '4.26.1F',\n'4.28.3M-28837868.4283M (engineering build)'])\ns1-spine1 :: VerifyTerminAttrVersion :: FAILURE(device is running TerminAttr version v1.34.0 and is not in the allowed list: ['v1.22.1'])\ns1-spine1 :: VerifyZeroTouch :: SUCCESS()\ns1-spine1 :: VerifyMlagConfigSanity :: SKIPPED(MLAG is disabled)\n"},{"location":"getting-started/#report-in-json-format","title":"Report in JSON format","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable `\\\n `# --enable-password <password> `\\\n --catalog ./catalog.yml \\\n json\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:11] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.053. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.065. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:12] INFO Running ANTA tests completed in: 0:00:00.857. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \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\": \"s1-spine1\",\n \"test\": \"VerifyNTP\",\n \"categories\": [\n \"system\"\n ],\n \"description\": \"Verifies if NTP is synchronised.\",\n \"result\": \"success\",\n \"messages\": [],\n \"custom_field\": null\n },\n {\n \"name\": \"s1-spine1\",\n \"test\": \"VerifyMlagConfigSanity\",\n \"categories\": [\n \"mlag\"\n ],\n \"description\": \"Verifies there are no MLAG config-sanity inconsistencies.\",\n \"result\": \"skipped\",\n \"messages\": [\n \"MLAG is disabled\"\n ],\n \"custom_field\": null\n },\n [...]\n"},{"location":"getting-started/#basic-usage-in-a-python-script","title":"Basic usage in a Python script","text":"# 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\"\"\"Example script for ANTA.\n\nusage:\n\npython anta_runner.py\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport sys\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.cli.nrfu.utils import anta_progress_bar\nfrom anta.inventory import AntaInventory\nfrom anta.logger import Log, setup_logging\nfrom anta.models import AntaTest\nfrom anta.result_manager import ResultManager\nfrom anta.runner import main as anta_runner\n\n# setup logging\nsetup_logging(Log.INFO, Path(\"/tmp/anta.log\"))\nLOGGER = logging.getLogger()\nSCRIPT_LOG_PREFIX = \"[bold magenta][ANTA RUNNER SCRIPT][/] \" # For convenience purpose - there are nicer way to do this.\n\n\n# NOTE: The inventory and catalog files are not delivered with this script\nUSERNAME = \"admin\"\nPASSWORD = \"admin\"\nCATALOG_PATH = Path(\"/tmp/anta_catalog.yml\")\nINVENTORY_PATH = Path(\"/tmp/anta_inventory.yml\")\n\n# Load catalog file\ntry:\n catalog = AntaCatalog.parse(CATALOG_PATH)\nexcept Exception:\n LOGGER.exception(\"%s Catalog failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Catalog loaded!\", SCRIPT_LOG_PREFIX)\n\n# Load inventory\ntry:\n inventory = AntaInventory.parse(INVENTORY_PATH, username=USERNAME, password=PASSWORD)\nexcept Exception:\n LOGGER.exception(\"%s Inventory failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Inventory loaded!\", SCRIPT_LOG_PREFIX)\n\n# Create result manager object\nmanager = ResultManager()\n\n# Launch ANTA\nLOGGER.info(\"%s Starting ANTA runner...\", SCRIPT_LOG_PREFIX)\nwith anta_progress_bar() as AntaTest.progress:\n # Set dry_run to True to avoid connecting to the devices\n asyncio.run(anta_runner(manager, inventory, catalog, dry_run=False))\n\nLOGGER.info(\"%s ANTA run completed!\", SCRIPT_LOG_PREFIX)\n\n# Manipulate the test result object\nfor test_result in manager.results:\n LOGGER.info(\"%s %s:%s:%s\", SCRIPT_LOG_PREFIX, test_result.name, test_result.test, test_result.result)\n"},{"location":"requirements-and-installation/","title":"Installation","text":""},{"location":"requirements-and-installation/#python-version","title":"Python version","text":"Python 3 (>=3.9) is required: python --version\nPython 3.11.8\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-library-from-pypi-server","title":"Install library from Pypi server","text":"pip install anta\n Warning This command alone will not install the ANTA CLI requirements. "},{"location":"requirements-and-installation/#install-anta-cli-as-an-application-with-pipx","title":"Install ANTA CLI as an application with pipx","text":"pipx is a tool to install and run python applications in isolated environments. If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx installs ANTA in an isolated python environment and makes it available globally. pipx install anta[cli]\n Info Please take the time to read through the installation instructions of pipx before getting started."},{"location":"requirements-and-installation/#install-cli-from-pypi-server","title":"Install CLI from Pypi server","text":"Alternatively, pip install with cli extra is enough to install the ANTA CLI. pip install anta[cli]\n"},{"location":"requirements-and-installation/#install-anta-from-github","title":"Install ANTA from github","text":"pip install git+https://github.com/aristanetworks/anta.git\npip install git+https://github.com/aristanetworks/anta.git#egg=anta[cli]\n\n# You can even specify the branch, tag or commit:\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>#egg=anta[cli]\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 v1.1.0\n"},{"location":"requirements-and-installation/#eos-requirements","title":"EOS Requirements","text":"To get ANTA working, the targeted Arista EOS devices must have eAPI enabled. They need to use 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":"troubleshooting/","title":"Troubleshooting ANTA","text":"A couple of things to check when hitting an issue with ANTA: flowchart LR\n A>Hitting an issue with ANTA] --> B{Is my issue <br >listed in the FAQ?}\n B -- Yes --> C{Does the FAQ solution<br />works for me?}\n C -- Yes --> V(((Victory)))\n B -->|No| E{Is my problem<br />mentioned in one<br />of the open issues?}\n C -->|No| E\n E -- Yes --> F{Has the issue been<br />fixed in a newer<br />release or in main?}\n F -- Yes --> U[Upgrade]\n E -- No ---> H((Follow the steps below<br />and open a Github issue))\n U --> I{Did it fix<br /> your problem}\n I -- Yes --> V\n I -- No --> H\n F -- No ----> G((Add a comment on the <br />issue indicating you<br >are hitting this and<br />describing your setup<br /> and adding your logs.))\n\n click B \"../faq\" \"FAQ\"\n click E \"https://github.com/aristanetworks/anta/issues\"\n click H \"https://github.com/aristanetworks/anta/issues\"\n style A stroke:#f00,stroke-width:2px"},{"location":"troubleshooting/#capturing-logs","title":"Capturing logs","text":"To help document the issue in Github, it is important to capture some logs so the developers can understand what is affecting your system. No logs mean that the first question asked on the issue will probably be \u201cCan you share some logs please?\u201d. ANTA provides very verbose logs when using the DEBUG level. When using DEBUG log level with a log file, the DEBUG logging level is not sent to stdout, but only to the file. Danger On real deployments, do not use DEBUG logging level without setting a log file at the same time. To save the logs to a file called anta.log, use the following flags: # Where ANTA_COMMAND is one of nrfu, debug, get, exec, check\nanta -l DEBUG \u2013log-file anta.log <ANTA_COMMAND>\n See anta --help for more information. These have to precede the nrfu cmd. Tip Remember that in ANTA, each level of command has its own options and they can only be set at this level. so the -l and --log-file MUST be between anta and the ANTA_COMMAND. similarly, all the nrfu options MUST be set between the nrfu and the ANTA_NRFU_SUBCOMMAND (json, text, table or tpl-report). As an example, for the nrfu command, it would look like: anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#anta_debug-environment-variable","title":"ANTA_DEBUG environment variable","text":"Warning Do not use this if you do not know why. This produces a lot of logs and can create confusion if you do not know what to look for. The environment variable ANTA_DEBUG=true enable ANTA Debug Mode. This flag is used by various functions in ANTA: when set to true, the function will display or log more information. In particular, when an Exception occurs in the code and this variable is set, the logging function used by ANTA is different to also produce the Python traceback for debugging. This typically needs to be done when opening a GitHub issue and an Exception is seen at runtime. Example: ANTA_DEBUG=true anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#troubleshooting-on-eos","title":"Troubleshooting on EOS","text":"ANTA is using a specific ID in eAPI requests towards EOS. This allows for easier eAPI requests debugging on the device using EOS configuration trace CapiApp setting UwsgiRequestContext/4,CapiUwsgiServer/4 to set up CapiApp agent logs. Then, you can view agent logs using: bash tail -f /var/log/agents/CapiApp-*\n\n2024-05-15 15:32:54.056166 1429 UwsgiRequestContext 4 request content b'{\"jsonrpc\": \"2.0\", \"method\": \"runCmds\", \"params\": {\"version\": \"latest\", \"cmds\": [{\"cmd\": \"show ip route vrf default 10.255.0.3\", \"revision\": 4}], \"format\": \"json\", \"autoComplete\": false, \"expandAliases\": false}, \"id\": \"ANTA-VerifyRoutingTableEntry-132366530677328\"}'\n"},{"location":"usage-inventory-catalog/","title":"Inventory and Test catalog","text":"The ANTA framework needs 2 important inputs from the user to run: A device inventory 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 accessible via eAPI ranges: scan a range for devices accessible 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 in either YAML or JSON: ---\n<Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\n {\n \"<Python module>\": [\n {\n \"<AntaTest subclass>\": <AntaTest.Input compliant dictionary>\n }\n ]\n}\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 or equivalent in JSON: {\n \"anta.tests.connectivity\": [\n {\n \"VerifyReachability\": {\n \"result_overwrite\": {\n \"description\": \"Test with overwritten description\",\n \"categories\": [\n \"Overwritten category 1\"\n ],\n \"custom_field\": \"Test run by John Doe\"\n },\n \"filters\": {\n \"tags\": [\n \"leaf\"\n ]\n },\n \"hosts\": [\n {\n \"destination\": \"1.1.1.1\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n },\n {\n \"destination\": \"8.8.8.8\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n }\n ]\n }\n }\n ]\n}\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 or JSON file: anta.tests.software:\n - VerifyEOSVersion:\n # List of allowed EOS versions.\n versions:\n - 4.25.4M\n - 4.26.1F\n {\n \"anta.tests.software\": [\n {\n \"VerifyEOSVersion\": {\n \"versions\": [\n \"4.25.4M\",\n \"4.31.1F\"\n ]\n }\n }\n ]\n}\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 custom.tests.system Python module, the test catalog will be: custom.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":"usage-inventory-catalog/#example-script-to-merge-catalogs","title":"Example script to merge catalogs","text":"The following script reads all the files in intended/test_catalogs/ with names <device_name>-catalog.yml and merge them together inside one big catalog anta-catalog.yml using the new AntaCatalog.merge_catalogs() class method. # Copyright (c) 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\"\"\"Script that merge a collection of catalogs into one AntaCatalog.\"\"\"\n\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.models import AntaTest\n\nCATALOG_SUFFIX = \"-catalog.yml\"\nCATALOG_DIR = \"intended/test_catalogs/\"\n\nif __name__ == \"__main__\":\n catalogs = []\n for file in Path(CATALOG_DIR).glob(\"*\" + CATALOG_SUFFIX):\n device = str(file).removesuffix(CATALOG_SUFFIX).removeprefix(CATALOG_DIR)\n print(f\"Loading test catalog for device {device}\")\n catalog = AntaCatalog.parse(file)\n # Add the device name as a tag to all tests in the catalog\n for test in catalog.tests:\n test.inputs.filters = AntaTest.Input.Filters(tags={device})\n catalogs.append(catalog)\n\n # Merge all catalogs\n merged_catalog = AntaCatalog.merge_catalogs(catalogs)\n\n # Save the merged catalog to a file\n with Path(\"anta-catalog.yml\").open(\"w\") as f:\n f.write(merged_catalog.dump().yaml())\n Warning The AntaCatalog.merge() method is deprecated and will be removed in ANTA v2.0. Please use the AntaCatalog.merge_catalogs() class method instead."},{"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. "},{"location":"advanced_usages/as-python-lib/#examples","title":"Examples","text":""},{"location":"advanced_usages/as-python-lib/#parse-an-anta-inventory-file","title":"Parse an ANTA inventory file","text":"# Copyright (c) 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\"\"\"Script that parses an ANTA inventory file, connects to devices and print their status.\"\"\"\n\nimport asyncio\n\nfrom anta.inventory import AntaInventory\n\n\nasync def main(inv: AntaInventory) -> None:\n \"\"\"Read an AntaInventory and try to connect to every device in the inventory.\n\n 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\n\nif __name__ == \"__main__\":\n # Create the AntaInventory instance\n inventory = AntaInventory.parse(\n filename=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\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."},{"location":"advanced_usages/as-python-lib/#run-eos-commands","title":"Run EOS commands","text":"# Copyright (c) 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\"\"\"Script that runs a list of EOS commands on reachable devices.\"\"\"\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 \"\"\"Run a list of commands against each valid device in the inventory.\n\n Take an AntaInventory and a list of commands as string\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 -------\n dict[str, list[AntaCommand]]\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=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\n )\n\n # Create a list of commands with json output\n command_list = [\"show version\", \"show ip bgp summary\"]\n\n # Run the main asyncio entry point\n res = asyncio.run(main(inventory, command_list))\n\n pprint(res)\n"},{"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: 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 Caching can be disabled per device, network or range by setting the disable_cache key to True when defining 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. For tests developers, 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: set = None) -> None:\n super().__init__(name, tags, disable_cache=True)\n"},{"location":"advanced_usages/custom-tests/","title":"Developing ANTA tests","text":"Info 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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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":"Full AntaTest API documentation is available in the API documentation section"},{"location":"advanced_usages/custom-tests/#class-attributes","title":"Class Attributes","text":" name (str, optional): Name of the test. Used during reporting. By default set to the Class name. description (str, optional): A human readable description of your test. By default set to the first line of the docstring. 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":"Attributes: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. 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":"Full Input model documentation is available in API documentation section Attributes: Name Type Description result_overwrite ResultOverwrite | None Define fields to overwrite in the TestResult object."},{"location":"advanced_usages/custom-tests/#resultoverwrite-model","title":"ResultOverwrite model","text":"Full ResultOverwrite model documentation is available in API documentation section Attributes: Name Type Description description str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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 occurrence 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: 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. 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. 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. 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. 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, the first line is used as description of the test by default>\n \"\"\"\n\n # name = <override> # uncomment to override default behavior of name=Class Name\n # description = <override> # uncomment to override default behavior of description=first line of docstring\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 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 values 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, integrations with existing tools are not broken. This is done by using by default version=1 in eAPI calls. By default, ANTA uses version=\"latest\" in AntaCommand, but when developing tests, the revision MUST be provided when the outformat of the command is json. As explained earlier, this is to ensure that the eAPI always returns the same output model and that the test remains always valid from the day it was created. For some commands, you may also want to run them with a different revision or version. For instance, the VerifyBFDPeersHealth 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/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: Parse the command outputs using the self.instance_commands instance attribute. If needed, access the test inputs using the self.inputs instance attribute and write your conditional logic. 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_custom and the test is defined in anta_custom.dc_project Python module, the test catalog would look like: anta_custom.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(\n tests: list[AntaTestDefinition] | None = None,\n filename: str | Path | None = None,\n)\n Class representing an ANTA Catalog. It can be instantiated using its constructor or one of the static methods: parse(), from_list() or from_dict() Parameters: Name Type Description Default tests list[AntaTestDefinition] | None A list of AntaTestDefinition instances. None filename str | Path | None The path from which the catalog is loaded. None"},{"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.build_indexes","title":"build_indexes","text":"build_indexes(\n filtered_tests: set[str] | None = None,\n) -> None\n Indexes tests by their tags for quick access during filtering operations. If a filtered_tests set is provided, only the tests in this set will be indexed. This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests. Once the indexes are built, the indexes_built attribute is set to True. Source code in anta/catalog.py def build_indexes(self, filtered_tests: set[str] | None = None) -> None:\n \"\"\"Indexes tests by their tags for quick access during filtering operations.\n\n If a `filtered_tests` set is provided, only the tests in this set will be indexed.\n\n This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests.\n\n Once the indexes are built, the `indexes_built` attribute is set to True.\n \"\"\"\n for test in self.tests:\n # Skip tests that are not in the specified filtered_tests set\n if filtered_tests and test.test.name not in filtered_tests:\n continue\n\n # Indexing by tag\n if test.inputs.filters and (test_tags := test.inputs.filters.tags):\n for tag in test_tags:\n self.tag_to_tests[tag].add(test)\n else:\n self.tag_to_tests[None].add(test)\n\n self.indexes_built = True\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.clear_indexes","title":"clear_indexes","text":"clear_indexes() -> None\n Clear this AntaCatalog instance indexes. Source code in anta/catalog.py def clear_indexes(self) -> None:\n \"\"\"Clear this AntaCatalog instance indexes.\"\"\"\n self._init_indexes()\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.dump","title":"dump","text":"dump() -> AntaCatalogFile\n Return an AntaCatalogFile instance from this AntaCatalog instance. Returns: Type Description AntaCatalogFile An AntaCatalogFile instance containing tests of this AntaCatalog instance. Source code in anta/catalog.py def dump(self) -> AntaCatalogFile:\n \"\"\"Return an AntaCatalogFile instance from this AntaCatalog instance.\n\n Returns\n -------\n AntaCatalogFile\n An AntaCatalogFile instance containing tests of this AntaCatalog instance.\n \"\"\"\n root: dict[ImportString[Any], list[AntaTestDefinition]] = {}\n for test in self.tests:\n # Cannot use AntaTest.module property as the class is not instantiated\n root.setdefault(test.test.__module__, []).append(test)\n return AntaCatalogFile(root=root)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.from_dict","title":"from_dict staticmethod","text":"from_dict(\n data: RawCatalogInput,\n filename: str | Path | None = None,\n) -> 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. Parameters: Name Type Description Default data RawCatalogInput Python dictionary used to instantiate the AntaCatalog instance. required filename str | Path | None value to be set as AntaCatalog instance attribute None Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 dictionary content. 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 Parameters\n ----------\n data\n Python dictionary used to instantiate the AntaCatalog instance.\n filename\n value to be set as AntaCatalog instance attribute\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' dictionary content.\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. Parameters: Name Type Description Default data ListAntaTestTuples Python list used to instantiate the AntaCatalog instance. required Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 list content. 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 Parameters\n ----------\n data\n Python list used to instantiate the AntaCatalog instance.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' list content.\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(\n tags: set[str], *, strict: bool = False\n) -> set[AntaTestDefinition]\n Return all tests that match a given set of tags, according to the specified strictness. Parameters: Name Type Description Default tags set[str] The tags to filter tests by. If empty, return all tests without tags. required strict bool If True, returns only tests that contain all specified tags (intersection). If False, returns tests that contain any of the specified tags (union). False Returns: Type Description set[AntaTestDefinition] A set of tests that match the given tags. Raises: Type Description ValueError If the indexes have not been built prior to method call. Source code in anta/catalog.py def get_tests_by_tags(self, tags: set[str], *, strict: bool = False) -> set[AntaTestDefinition]:\n \"\"\"Return all tests that match a given set of tags, according to the specified strictness.\n\n Parameters\n ----------\n tags\n The tags to filter tests by. If empty, return all tests without tags.\n strict\n If True, returns only tests that contain all specified tags (intersection).\n If False, returns tests that contain any of the specified tags (union).\n\n Returns\n -------\n set[AntaTestDefinition]\n A set of tests that match the given tags.\n\n Raises\n ------\n ValueError\n If the indexes have not been built prior to method call.\n \"\"\"\n if not self.indexes_built:\n msg = \"Indexes have not been built yet. Call build_indexes() first.\"\n raise ValueError(msg)\n if not tags:\n return self.tag_to_tests[None]\n\n filtered_sets = [self.tag_to_tests[tag] for tag in tags if tag in self.tag_to_tests]\n if not filtered_sets:\n return set()\n\n if strict:\n return set.intersection(*filtered_sets)\n return set.union(*filtered_sets)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge","title":"merge","text":"merge(catalog: AntaCatalog) -> AntaCatalog\n Merge two AntaCatalog instances. Warning This method is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead. Parameters: Name Type Description Default catalog AntaCatalog AntaCatalog instance to merge to this instance. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of the two instances. Source code in anta/catalog.py def merge(self, catalog: AntaCatalog) -> AntaCatalog:\n \"\"\"Merge two AntaCatalog instances.\n\n Warning\n -------\n This method is deprecated and will be removed in ANTA v2.0. Use `AntaCatalog.merge_catalogs()` instead.\n\n Parameters\n ----------\n catalog\n AntaCatalog instance to merge to this instance.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of the two instances.\n \"\"\"\n # TODO: Use a decorator to deprecate this method instead. See https://github.com/aristanetworks/anta/issues/754\n warn(\n message=\"AntaCatalog.merge() is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n return self.merge_catalogs([self, catalog])\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge_catalogs","title":"merge_catalogs classmethod","text":"merge_catalogs(catalogs: list[AntaCatalog]) -> AntaCatalog\n Merge multiple AntaCatalog instances. Parameters: Name Type Description Default catalogs list[AntaCatalog] A list of AntaCatalog instances to merge. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of all the input catalogs. Source code in anta/catalog.py @classmethod\ndef merge_catalogs(cls, catalogs: list[AntaCatalog]) -> AntaCatalog:\n \"\"\"Merge multiple AntaCatalog instances.\n\n Parameters\n ----------\n catalogs\n A list of AntaCatalog instances to merge.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of all the input catalogs.\n \"\"\"\n combined_tests = list(chain(*(catalog.tests for catalog in catalogs)))\n return cls(tests=combined_tests)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.parse","title":"parse staticmethod","text":"parse(\n filename: str | Path,\n file_format: Literal[\"yaml\", \"json\"] = \"yaml\",\n) -> AntaCatalog\n Create an AntaCatalog instance from a test catalog file. Parameters: Name Type Description Default filename str | Path Path to test catalog YAML or JSON file. required file_format Literal['yaml', 'json'] Format of the file, either \u2018yaml\u2019 or \u2018json\u2019. 'yaml' Returns: Type Description AntaCatalog An AntaCatalog populated with the file content. Source code in anta/catalog.py @staticmethod\ndef parse(filename: str | Path, file_format: Literal[\"yaml\", \"json\"] = \"yaml\") -> AntaCatalog:\n \"\"\"Create an AntaCatalog instance from a test catalog file.\n\n Parameters\n ----------\n filename\n Path to test catalog YAML or JSON file.\n file_format\n Format of the file, either 'yaml' or 'json'.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the file content.\n \"\"\"\n if file_format not in [\"yaml\", \"json\"]:\n message = f\"'{file_format}' is not a valid format for an AntaCatalog file. Only 'yaml' and 'json' are supported.\"\n raise ValueError(message)\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) if file_format == \"yaml\" else json_load(f)\n except (TypeError, YAMLError, OSError, ValueError) 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(\n **data: (\n type[AntaTest]\n | AntaTest.Input\n | dict[str, Any]\n | None\n )\n)\n Bases: BaseModel Define a test with its associated inputs. Attributes: Name Type Description test type[AntaTest] An AntaTest concrete subclass. inputs Input The associated AntaTest.Input subclass instance. https://docs.pydantic.dev/2.0/usage/validators/#using-validation-context-with-basemodel-initialization."},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.check_inputs","title":"check_inputs","text":"check_inputs() -> Self\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) -> Self:\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(\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. 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\"Could not instantiate inputs as type {type(data).__name__} is not valid\"\n raise ValueError(msg)\n"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.serialize_model","title":"serialize_model","text":"serialize_model() -> dict[str, AntaTest.Input]\n Serialize the AntaTestDefinition model. The dictionary representing the model will be look like: <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n Returns: Type Description dict A dictionary representing the model. Source code in anta/catalog.py @model_serializer()\ndef serialize_model(self) -> dict[str, AntaTest.Input]:\n \"\"\"Serialize the AntaTestDefinition model.\n\n The dictionary representing the model will be look like:\n ```\n <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n ```\n\n Returns\n -------\n dict\n A dictionary representing the model.\n \"\"\"\n return {self.test.__name__: self.inputs}\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: <Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\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 if not data:\n return data\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 isinstance(test_definition, AntaTestDefinition):\n test_definitions.append(test_definition)\n continue\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 return data\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.flatten_modules","title":"flatten_modules staticmethod","text":"flatten_modules(\n data: dict[str, Any], package: str | None = None\n) -> dict[ModuleType, list[Any]]\n Allow the user to provide a data structure with nested Python modules. Example anta.tests.routing:\n generic:\n - <AntaTestDefinition>\n bgp:\n - <AntaTestDefinition>\n anta.tests.routing.generic and anta.tests.routing.bgp are importable Python modules. 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:\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 elif isinstance(tests, list):\n # This is a list of AntaTestDefinition\n modules[module] = tests\n else:\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 return modules\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.to_json","title":"to_json","text":"to_json() -> str\n Return a JSON representation string of this model. Returns: Type Description str The JSON representation string of this model. Source code in anta/catalog.py def to_json(self) -> str:\n \"\"\"Return a JSON representation string of this model.\n\n Returns\n -------\n str\n The JSON representation string of this model.\n \"\"\"\n return self.model_dump_json(serialize_as_any=True, exclude_unset=True, indent=2)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/catalog.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return safe_dump(safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/csv_reporter/","title":"CSV reporter","text":"CSV Report management for ANTA."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv","title":"ReportCsv","text":"Build a CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_name: str = \"Test Name\",\n test_status: str = \"Test Status\",\n messages: str = \"Message(s)\",\n description: str = \"Test description\",\n categories: str = \"Test category\",\n)\n Headers for the CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.convert_to_list","title":"convert_to_list classmethod","text":"convert_to_list(result: TestResult) -> list[str]\n Convert a TestResult into a list of string for creating file content. Parameters: Name Type Description Default result TestResult A TestResult to convert into list. required Returns: Type Description list[str] TestResult converted into a list. Source code in anta/reporter/csv_reporter.py @classmethod\ndef convert_to_list(cls, result: TestResult) -> list[str]:\n \"\"\"Convert a TestResult into a list of string for creating file content.\n\n Parameters\n ----------\n result\n A TestResult to convert into list.\n\n Returns\n -------\n list[str]\n TestResult converted into a list.\n \"\"\"\n message = cls.split_list_to_txt_list(result.messages) if len(result.messages) > 0 else \"\"\n categories = cls.split_list_to_txt_list(convert_categories(result.categories)) if len(result.categories) > 0 else \"None\"\n return [\n str(result.name),\n result.test,\n result.result,\n message,\n result.description,\n categories,\n ]\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.generate","title":"generate classmethod","text":"generate(\n results: ResultManager, csv_filename: pathlib.Path\n) -> None\n Build CSV flle with tests results. Parameters: Name Type Description Default results ResultManager A ResultManager instance. required csv_filename Path File path where to save CSV data. required Raises: Type Description OSError if any is raised while writing the CSV file. Source code in anta/reporter/csv_reporter.py @classmethod\ndef generate(cls, results: ResultManager, csv_filename: pathlib.Path) -> None:\n \"\"\"Build CSV flle with tests results.\n\n Parameters\n ----------\n results\n A ResultManager instance.\n csv_filename\n File path where to save CSV data.\n\n Raises\n ------\n OSError\n if any is raised while writing the CSV file.\n \"\"\"\n headers = [\n cls.Headers.device,\n cls.Headers.test_name,\n cls.Headers.test_status,\n cls.Headers.messages,\n cls.Headers.description,\n cls.Headers.categories,\n ]\n\n try:\n with csv_filename.open(mode=\"w\", encoding=\"utf-8\") as csvfile:\n csvwriter = csv.writer(\n csvfile,\n delimiter=\",\",\n )\n csvwriter.writerow(headers)\n for entry in results.results:\n csvwriter.writerow(cls.convert_to_list(entry))\n except OSError as exc:\n message = f\"OSError caught while writing the CSV file '{csv_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.split_list_to_txt_list","title":"split_list_to_txt_list classmethod","text":"split_list_to_txt_list(\n usr_list: list[str], delimiter: str = \" - \"\n) -> str\n Split list to multi-lines string. Parameters: Name Type Description Default usr_list list[str] List of string to concatenate. required delimiter str A delimiter to use to start string. Defaults to None. ' - ' Returns: Type Description str Multi-lines string. Source code in anta/reporter/csv_reporter.py @classmethod\ndef split_list_to_txt_list(cls, usr_list: list[str], delimiter: str = \" - \") -> str:\n \"\"\"Split list to multi-lines string.\n\n Parameters\n ----------\n usr_list\n List of string to concatenate.\n delimiter\n A delimiter to use to start string. Defaults to None.\n\n Returns\n -------\n str\n Multi-lines string.\n\n \"\"\"\n return f\"{delimiter}\".join(f\"{line}\" for line in usr_list)\n"},{"location":"api/device/","title":"Device","text":""},{"location":"api/device/#antadevice-base-class","title":"AntaDevice base class","text":""},{"location":"api/device/#anta.device.AntaDevice","title":"AntaDevice","text":"AntaDevice(\n name: str,\n tags: set[str] | None = None,\n *,\n disable_cache: bool = False\n)\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 str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. cache Cache | None In-memory cache from aiocache library for this device (None if cache is disabled). cache_locks dict Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled. Parameters: Name Type Description Default name str Device name. required tags set[str] | None Tags for this device. None disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AntaDevice.cache_statistics","title":"cache_statistics property","text":"cache_statistics: dict[str, Any] | None\n Return 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.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AntaDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AntaDevice.\"\"\"\n return (\n f\"AntaDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AntaDevice._collect","title":"_collect abstractmethod async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output. This abstract coroutine can be used to implement any command collection method for a device in ANTA. The _collect() implementation needs to populate the output attribute of the AntaCommand object passed as argument. If a failure occurs, the _collect() implementation is expected to catch the exception and implement proper logging, the output attribute of the AntaCommand object passed as argument would be None in this case. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py @abstractmethod\nasync def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None:\n \"\"\"Collect device command output.\n\n This abstract coroutine can be used to implement any command collection method\n for a device in ANTA.\n\n The `_collect()` implementation needs to populate the `output` attribute\n of the `AntaCommand` object passed as argument.\n\n If a failure occurs, the `_collect()` implementation is expected to catch the\n exception and implement proper logging, the `output` attribute of the\n `AntaCommand` object passed as argument would be `None` in this case.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n"},{"location":"api/device/#anta.device.AntaDevice.collect","title":"collect async","text":"collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> 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. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect(self, command: AntaCommand, *, collection_id: str | None = None) -> 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 Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\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, collection_id=collection_id)\n await self.cache.set(command.uid, command.output) # pylint: disable=no-member\n else:\n await self._collect(command=command, collection_id=collection_id)\n"},{"location":"api/device/#anta.device.AntaDevice.collect_commands","title":"collect_commands async","text":"collect_commands(\n commands: list[AntaCommand],\n *,\n collection_id: str | None = None\n) -> None\n Collect multiple commands. Parameters: Name Type Description Default commands list[AntaCommand] The commands to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect_commands(self, commands: list[AntaCommand], *, collection_id: str | None = None) -> None:\n \"\"\"Collect multiple commands.\n\n Parameters\n ----------\n commands\n The commands to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n await asyncio.gather(*(self.collect(command=command, collection_id=collection_id) for command in commands))\n"},{"location":"api/device/#anta.device.AntaDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device, usually through SCP. It is not mandatory to implement this for a valid AntaDevice subclass. Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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\n - `is_online`: When the device IP is reachable and a port can be open.\n\n - `established`: When a command execution succeeds.\n\n - `hw_model`: The hardware model of the device.\n \"\"\"\n"},{"location":"api/device/#async-eos-device-class","title":"Async EOS device class","text":""},{"location":"api/device/#anta.device.AsyncEOSDevice","title":"AsyncEOSDevice","text":"AsyncEOSDevice(\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: set[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)\n Bases: AntaDevice Implementation of AntaDevice for EOS using aio-eapi. Attributes: Name Type Description name str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. Parameters: Name Type Description Default host str Device FQDN or IP. required username str Username to connect to eAPI and SSH. required password str Password to connect to eAPI and SSH. required name str | None Device name. None enable bool Collect commands using privileged mode. False enable_password str | None Password used to gain privileged access on EOS. None port int | None eAPI port. Defaults to 80 is proto is \u2018http\u2019 or 443 if proto is \u2018https\u2019. None ssh_port int | None SSH port. 22 tags set[str] | None Tags for this device. None timeout float | None Timeout value in seconds for outgoing API calls. None insecure bool Disable SSH Host Key validation. False proto Literal['http', 'https'] eAPI protocol. Value can be \u2018http\u2019 or \u2018https\u2019. 'https' disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AsyncEOSDevice.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AsyncEOSDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AsyncEOSDevice.\"\"\"\n return (\n f\"AsyncEOSDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r}, \"\n f\"host={self._session.host!r}, \"\n f\"eapi_port={self._session.port!r}, \"\n f\"username={self._ssh_opts.username!r}, \"\n f\"enable={self.enable!r}, \"\n f\"insecure={self._ssh_opts.known_hosts is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AsyncEOSDevice._collect","title":"_collect async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output from EOS using aio-eapi. Supports outformat json and text as output structure. Gain privileged access using the enable_password attribute of the AntaDevice instance if populated. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None: # noqa: C901 function is too complex - because of many required except blocks\n \"\"\"Collect device command output from EOS using aio-eapi.\n\n Supports outformat `json` and `text` as output structure.\n Gain privileged access using the `enable_password` attribute\n of the `AntaDevice` instance if populated.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n commands: list[dict[str, str | int]] = []\n if self.enable and self._enable_password is not None:\n commands.append(\n {\n \"cmd\": \"enable\",\n \"input\": str(self._enable_password),\n },\n )\n elif self.enable:\n # No password\n commands.append({\"cmd\": \"enable\"})\n commands += [{\"cmd\": command.command, \"revision\": command.revision}] if command.revision else [{\"cmd\": command.command}]\n try:\n response: list[dict[str, Any] | str] = await self._session.cli(\n commands=commands,\n ofmt=command.ofmt,\n version=command.version,\n req_id=f\"ANTA-{collection_id}-{id(command)}\" if collection_id else f\"ANTA-{id(command)}\",\n ) # type: ignore[assignment] # multiple commands returns a list\n # Do not keep response of 'enable' command\n command.output = response[-1]\n except asynceapi.EapiCommandError as e:\n # This block catches exceptions related to EOS issuing an error.\n command.errors = e.errors\n if command.requires_privileges:\n logger.error(\n \"Command '%s' requires privileged mode on %s. Verify user permissions and if the `enable` option is required.\", command.command, self.name\n )\n if command.supported:\n logger.error(\"Command '%s' failed on %s: %s\", command.command, self.name, e.errors[0] if len(e.errors) == 1 else e.errors)\n else:\n logger.debug(\"Command '%s' is not supported on '%s' (%s)\", command.command, self.name, self.hw_model)\n except TimeoutException as e:\n # This block catches Timeout exceptions.\n command.errors = [exc_to_str(e)]\n timeouts = self._session.timeout.as_dict()\n logger.error(\n \"%s occurred while sending a command to %s. Consider increasing the timeout.\\nCurrent timeouts: Connect: %s | Read: %s | Write: %s | Pool: %s\",\n exc_to_str(e),\n self.name,\n timeouts[\"connect\"],\n timeouts[\"read\"],\n timeouts[\"write\"],\n timeouts[\"pool\"],\n )\n except (ConnectError, OSError) as e:\n # This block catches OSError and socket issues related exceptions.\n command.errors = [exc_to_str(e)]\n if (isinstance(exc := e.__cause__, httpcore.ConnectError) and isinstance(os_error := exc.__context__, OSError)) or isinstance(os_error := e, OSError): # pylint: disable=no-member\n if isinstance(os_error.__cause__, OSError):\n os_error = os_error.__cause__\n logger.error(\"A local OS error occurred while connecting to %s: %s.\", self.name, os_error)\n else:\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n except HTTPError as e:\n # This block catches most of the httpx Exceptions and logs a general message.\n command.errors = [exc_to_str(e)]\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n logger.debug(\"%s: %s\", self.name, command)\n"},{"location":"api/device/#anta.device.AsyncEOSDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device using asyncssh.scp(). Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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() function 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 = AntaCommand(command=\"show version\")\n await self._collect(show_version)\n if not show_version.collected:\n logger.warning(\"Cannot get hardware information from device %s\", self.name)\n else:\n self.hw_model = show_version.json_output.get(\"modelName\", None)\n if self.hw_model is None:\n logger.critical(\"Cannot parse 'show version' returned by device %s\", self.name)\n # in some cases it is possible that 'modelName' comes back empty\n # and it is nice to get a meaninfule error message\n elif self.hw_model == \"\":\n logger.critical(\"Got an empty 'modelName' in the 'show version' returned by device %s\", self.name)\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.devices","title":"devices property","text":"devices: list[AntaDevice]\n List of AntaDevice in this inventory."},{"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. Parameters: Name Type Description Default device AntaDevice Device object to be added. required Source code in anta/inventory/__init__.py def add_device(self, device: AntaDevice) -> None:\n \"\"\"Add a device to final inventory.\n\n Parameters\n ----------\n device\n 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(\n *,\n established_only: bool = False,\n tags: set[str] | None = None,\n devices: set[str] | None = None\n) -> AntaInventory\n Return a filtered inventory. Parameters: Name Type Description Default established_only bool Whether or not to include only established devices. False tags set[str] | None Tags to filter devices. None devices set[str] | None Names to filter devices. None 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: set[str] | None = None, devices: set[str] | None = None) -> AntaInventory:\n \"\"\"Return a filtered inventory.\n\n Parameters\n ----------\n established_only\n Whether or not to include only established devices.\n tags\n Tags to filter devices.\n devices\n Names to filter devices.\n\n Returns\n -------\n AntaInventory\n An inventory with filtered AntaDevice objects.\n \"\"\"\n\n def _filter_devices(device: AntaDevice) -> bool:\n \"\"\"Select the devices based on the inputs `tags`, `devices` and `established_only`.\"\"\"\n if tags is not None and all(tag not in tags for tag in device.tags):\n return False\n if devices is None or device.name in devices:\n return bool(not established_only or device.established)\n return False\n\n filtered_devices: list[AntaDevice] = list(filter(_filter_devices, self.values()))\n result = AntaInventory()\n for device in filtered_devices:\n result.add_device(device)\n return result\n"},{"location":"api/inventory/#anta.inventory.AntaInventory.parse","title":"parse staticmethod","text":"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. The inventory devices are AsyncEOSDevice instances. Parameters: Name Type Description Default filename str | Path Path to device inventory YAML file. required username str Username to use to connect to devices. required password str Password to use to connect to devices. required enable_password str | None Enable password to use if required. None timeout float | None Timeout value in seconds for outgoing API calls. None enable bool Whether or not the commands need to be run in enable mode towards the devices. False insecure bool Disable SSH Host Key validation. False disable_cache bool Disable cache globally. False 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 Parameters\n ----------\n filename\n Path to device inventory YAML file.\n username\n Username to use to connect to devices.\n password\n Password to use to connect to devices.\n enable_password\n Enable password to use if required.\n timeout\n Timeout value in seconds for outgoing API calls.\n enable\n Whether or not the commands need to be run in enable mode towards the devices.\n insecure\n Disable SSH Host Key validation.\n disable_cache\n Disable cache globally.\n\n Raises\n ------\n InventoryRootKeyError\n Root key of inventory is missing.\n InventoryIncorrectSchemaError\n 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 Device inventory input model."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryInput.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/inventory/models.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return yaml.safe_dump(yaml.safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryHost","title":"AntaInventoryHost","text":" Bases: BaseModel Host entry of AntaInventoryInput. Attributes: Name Type Description host Hostname | IPvAnyAddress IP Address or FQDN of the device. port Port | None Custom eAPI port to use. name str | None Custom name of the device. tags set[str] Tags of the device. disable_cache bool Disable cache for this device."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryNetwork","title":"AntaInventoryNetwork","text":" Bases: BaseModel Network entry of AntaInventoryInput. Attributes: Name Type Description network IPvAnyNetwork Subnet to use for scanning. tags set[str] Tags of the devices in this network. disable_cache bool Disable cache for all devices in this network."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryRange","title":"AntaInventoryRange","text":" Bases: BaseModel IP Range entry of AntaInventoryInput. Attributes: Name Type Description start IPvAnyAddress IPv4 or IPv6 address for the beginning of the range. stop IPvAnyAddress IPv4 or IPv6 address for the end of the range. tags set[str] Tags of the devices in this IP range. disable_cache bool Disable cache for all devices in this IP range."},{"location":"api/md_reporter/","title":"Markdown reporter","text":"Markdown report generator for ANTA test results."},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport","title":"ANTAReport","text":"ANTAReport(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generate the # ANTA Report section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the # ANTA Report section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `# ANTA Report` section of the markdown report.\"\"\"\n self.write_heading(heading_level=1)\n toc = MD_REPORT_TOC\n self.mdfile.write(toc + \"\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase","title":"MDReportBase","text":"MDReportBase(mdfile: TextIOWrapper, results: ResultManager)\n Bases: ABC Base class for all sections subclasses. Every subclasses must implement the generate_section method that uses the ResultManager object to generate and write content to the provided markdown file. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_heading_name","title":"generate_heading_name","text":"generate_heading_name() -> str\n Generate a formatted heading name based on the class name. Returns: Type Description str Formatted header name. Example ANTAReport will become ANTA Report. TestResultsSummary will become Test Results Summary. Source code in anta/reporter/md_reporter.py def generate_heading_name(self) -> str:\n \"\"\"Generate a formatted heading name based on the class name.\n\n Returns\n -------\n str\n Formatted header name.\n\n Example\n -------\n - `ANTAReport` will become `ANTA Report`.\n - `TestResultsSummary` will become `Test Results Summary`.\n \"\"\"\n class_name = self.__class__.__name__\n\n # Split the class name into words, keeping acronyms together\n words = re.findall(r\"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\\d|\\W|$)|\\d+\", class_name)\n\n # Capitalize each word, but keep acronyms in all caps\n formatted_words = [word if word.isupper() else word.capitalize() for word in words]\n\n return \" \".join(formatted_words)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of a markdown table for a specific report section. Subclasses can implement this method to generate the content of the table rows. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of a markdown table for a specific report section.\n\n Subclasses can implement this method to generate the content of the table rows.\n \"\"\"\n msg = \"Subclasses should implement this method\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_section","title":"generate_section abstractmethod","text":"generate_section() -> None\n Abstract method to generate a specific section of the markdown report. Must be implemented by subclasses. Source code in anta/reporter/md_reporter.py @abstractmethod\ndef generate_section(self) -> None:\n \"\"\"Abstract method to generate a specific section of the markdown report.\n\n Must be implemented by subclasses.\n \"\"\"\n msg = \"Must be implemented by subclasses\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.safe_markdown","title":"safe_markdown","text":"safe_markdown(text: str | None) -> str\n Escape markdown characters in the text to prevent markdown rendering issues. Parameters: Name Type Description Default text str | None The text to escape markdown characters from. required Returns: Type Description str The text with escaped markdown characters. Source code in anta/reporter/md_reporter.py def safe_markdown(self, text: str | None) -> str:\n \"\"\"Escape markdown characters in the text to prevent markdown rendering issues.\n\n Parameters\n ----------\n text\n The text to escape markdown characters from.\n\n Returns\n -------\n str\n The text with escaped markdown characters.\n \"\"\"\n # Custom field from a TestResult object can be None\n if text is None:\n return \"\"\n\n # Replace newlines with spaces to keep content on one line\n text = text.replace(\"\\n\", \" \")\n\n # Replace backticks with single quotes\n return text.replace(\"`\", \"'\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_heading","title":"write_heading","text":"write_heading(heading_level: int) -> None\n Write a markdown heading to the markdown file. The heading name used is the class name. Parameters: Name Type Description Default heading_level int The level of the heading (1-6). required Example ## Test Results Summary Source code in anta/reporter/md_reporter.py def write_heading(self, heading_level: int) -> None:\n \"\"\"Write a markdown heading to the markdown file.\n\n The heading name used is the class name.\n\n Parameters\n ----------\n heading_level\n The level of the heading (1-6).\n\n Example\n -------\n `## Test Results Summary`\n \"\"\"\n # Ensure the heading level is within the valid range of 1 to 6\n heading_level = max(1, min(heading_level, 6))\n heading_name = self.generate_heading_name()\n heading = \"#\" * heading_level + \" \" + heading_name\n self.mdfile.write(f\"{heading}\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_table","title":"write_table","text":"write_table(\n table_heading: list[str], *, last_table: bool = False\n) -> None\n Write a markdown table with a table heading and multiple rows to the markdown file. Parameters: Name Type Description Default table_heading list[str] List of strings to join for the table heading. required last_table bool Flag to determine if it\u2019s the last table of the markdown file to avoid unnecessary new line. Defaults to False. False Source code in anta/reporter/md_reporter.py def write_table(self, table_heading: list[str], *, last_table: bool = False) -> None:\n \"\"\"Write a markdown table with a table heading and multiple rows to the markdown file.\n\n Parameters\n ----------\n table_heading\n List of strings to join for the table heading.\n last_table\n Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.\n \"\"\"\n self.mdfile.write(\"\\n\".join(table_heading) + \"\\n\")\n for row in self.generate_rows():\n self.mdfile.write(row)\n if not last_table:\n self.mdfile.write(\"\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator","title":"MDReportGenerator","text":"Class responsible for generating a Markdown report based on the provided ResultManager object. It aggregates different report sections, each represented by a subclass of MDReportBase, and sequentially generates their content into a markdown file. The generate class method will loop over all the section subclasses and call their generate_section method. The final report will be generated in the same order as the sections list of the method."},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator.generate","title":"generate classmethod","text":"generate(results: ResultManager, md_filename: Path) -> None\n Generate and write the various sections of the markdown report. Parameters: Name Type Description Default results ResultManager The ResultsManager instance containing all test results. required md_filename Path The path to the markdown file to write the report into. required Source code in anta/reporter/md_reporter.py @classmethod\ndef generate(cls, results: ResultManager, md_filename: Path) -> None:\n \"\"\"Generate and write the various sections of the markdown report.\n\n Parameters\n ----------\n results\n The ResultsManager instance containing all test results.\n md_filename\n The path to the markdown file to write the report into.\n \"\"\"\n try:\n with md_filename.open(\"w\", encoding=\"utf-8\") as mdfile:\n sections: list[MDReportBase] = [\n ANTAReport(mdfile, results),\n TestResultsSummary(mdfile, results),\n SummaryTotals(mdfile, results),\n SummaryTotalsDeviceUnderTest(mdfile, results),\n SummaryTotalsPerCategory(mdfile, results),\n TestResults(mdfile, results),\n ]\n for section in sections:\n section.generate_section()\n except OSError as exc:\n message = f\"OSError caught while writing the Markdown file '{md_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals","title":"SummaryTotals","text":"SummaryTotals(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals table.\"\"\"\n yield (\n f\"| {self.results.get_total_results()} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SUCCESS})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SKIPPED})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.FAILURE})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.ERROR})} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest","title":"SummaryTotalsDeviceUnderTest","text":"SummaryTotalsDeviceUnderTest(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Devices Under Tests section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals device under test table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals device under test table.\"\"\"\n for device, stat in self.results.device_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n categories_skipped = \", \".join(sorted(convert_categories(list(stat.categories_skipped))))\n categories_failed = \", \".join(sorted(convert_categories(list(stat.categories_failed))))\n yield (\n f\"| {device} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} | {stat.tests_error_count} \"\n f\"| {categories_skipped or '-'} | {categories_failed or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Devices Under Tests section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Devices Under Tests` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory","title":"SummaryTotalsPerCategory","text":"SummaryTotalsPerCategory(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Per Category section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals per category table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals per category table.\"\"\"\n for category, stat in self.results.sorted_category_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n yield (\n f\"| {category} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} \"\n f\"| {stat.tests_error_count} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Per Category section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Per Category` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults","title":"TestResults","text":"TestResults(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generates the ## Test Results section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the all test results table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the all test results table.\"\"\"\n for result in self.results.get_results(sort_by=[\"name\", \"test\"]):\n messages = self.safe_markdown(\", \".join(result.messages))\n categories = \", \".join(convert_categories(result.categories))\n yield (\n f\"| {result.name or '-'} | {categories or '-'} | {result.test or '-'} \"\n f\"| {result.description or '-'} | {self.safe_markdown(result.custom_field) or '-'} | {result.result or '-'} | {messages or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n self.write_table(table_heading=self.TABLE_HEADING, last_table=True)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary","title":"TestResultsSummary","text":"TestResultsSummary(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ## Test Results Summary section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results Summary section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results Summary` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n"},{"location":"api/models/","title":"Test models","text":""},{"location":"api/models/#test-definition","title":"Test definition","text":""},{"location":"api/models/#anta.models.AntaTest","title":"AntaTest","text":"AntaTest(\n device: AntaDevice,\n inputs: dict[str, Any] | AntaTest.Input | None = None,\n eos_data: list[dict[Any, Any] | str] | None = None,\n)\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 '''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 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: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. Parameters: Name Type Description Default device AntaDevice AntaDevice instance on which the test will be run. required inputs dict[str, Any] | Input | None Dictionary of attributes used to instantiate the AntaTest.Input instance. None eos_data list[dict[Any, Any] | str] | None Populate outputs of the test commands instead of collecting from devices. This list must have the same length and order than the instance_commands instance attribute. None"},{"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 Return 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 Return a list of all the commands that have failed."},{"location":"api/models/#anta.models.AntaTest.module","title":"module property","text":"module: str\n Return the Python module in which this AntaTest class is defined."},{"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: Name Type Description result_overwrite ResultOverwrite | None 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 set[str] | None Tag of devices on which to run 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 str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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(\n function: F,\n) -> Callable[..., Coroutine[Any, Any, TestResult]]\n Decorate the test() method in child classes. This decorator implements (in this order): Instantiate the command outputs if eos_data is provided to the test() method Collect the commands from the device Run the test() method 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 Parameters\n ----------\n self\n The test instance.\n eos_data\n 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\n Any keyword argument to pass to the test.\n\n Returns\n -------\n TestResult\n The TestResult instance attribute populated with error status if any.\n\n \"\"\"\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 AntaTest.update_progress()\n return self.result\n\n if cmds := self.failed_commands:\n unsupported_commands = [f\"'{c.command}' is not supported on {self.device.hw_model}\" for c in cmds if not c.supported]\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 else:\n self.result.is_error(message=\"\\n\".join([f\"{c.command} has failed: {', '.join(c.errors)}\" for c in cmds]))\n AntaTest.update_progress()\n return self.result\n\n try:\n function(self, **kwargs)\n except Exception as e: # noqa: BLE001\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 # TODO: find a correct way to time test execution\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, collection_id=self.name)\n except Exception as e: # noqa: BLE001\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]:\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 _ = template\n msg = f\"AntaTemplate are provided but render() method has not been implemented for {self.module}.{self.__class__.__name__}\"\n raise NotImplementedError(msg)\n"},{"location":"api/models/#anta.models.AntaTest.save_commands_data","title":"save_commands_data","text":"save_commands_data(\n eos_data: list[dict[str, Any] | str]\n) -> 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 reason\")\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 reason\")\n ```\n\n \"\"\"\n"},{"location":"api/models/#command-definition","title":"Command definition","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 str Device command. version Literal[1, 'latest'] eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision | None eAPI revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt Literal['json', 'text'] eAPI output - json or text. output dict[str, Any] | str | None Output of the command. Only defined if there was no errors. template AntaTemplate | None AntaTemplate object used to render this command. errors list[str] If the command execution fails, eAPI returns a list of strings detailing the error(s). params AntaParamsBaseModel Pydantic Model containing the variables values used to render the template. use_cache bool Enable or disable caching for this AntaCommand if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaCommand.collected","title":"collected property","text":"collected: bool\n Return True if the command has been collected, False otherwise. A command that has not been collected could have returned an error. See error property."},{"location":"api/models/#anta.models.AntaCommand.error","title":"error property","text":"error: bool\n Return True if the command returned an error, False otherwise."},{"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.requires_privileges","title":"requires_privileges property","text":"requires_privileges: bool\n Return True if the command requires privileged mode, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"location":"api/models/#anta.models.AntaCommand.supported","title":"supported property","text":"supported: bool\n Return True if the command is supported on the device hardware platform, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"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/#anta.models.AntaTemplate","title":"AntaTemplate","text":"AntaTemplate(\n template: str,\n version: Literal[1, \"latest\"] = \"latest\",\n revision: Revision | None = None,\n ofmt: Literal[\"json\", \"text\"] = \"json\",\n *,\n use_cache: bool = True\n)\n 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: \u2018show vlan {vlan_id}\u2019. version eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt eAPI output - json or text. use_cache Enable or disable caching for this AntaTemplate if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaTemplate.__repr__","title":"__repr__","text":"__repr__() -> str\n Return the representation of the class. Copying pydantic model style, excluding params_schema Source code in anta/models.py def __repr__(self) -> str:\n \"\"\"Return the representation of the class.\n\n Copying pydantic model style, excluding `params_schema`\n \"\"\"\n return \" \".join(f\"{a}={v!r}\" for a, v in vars(self).items() if a != \"params_schema\")\n"},{"location":"api/models/#anta.models.AntaTemplate.render","title":"render","text":"render(**params: str | int | bool) -> AntaCommand\n Render an AntaCommand from an AntaTemplate instance. Keep the parameters used in the AntaTemplate instance. Parameters: Name Type Description Default params str | int | bool Dictionary of variables with string values to render the Python f-string. {} Returns: Type Description AntaCommand The rendered AntaCommand. This AntaCommand instance have a template attribute that references this AntaTemplate instance. Raises: Type Description AntaTemplateRenderError If a parameter is missing to render the AntaTemplate instance. Source code in anta/models.py def render(self, **params: str | int | bool) -> AntaCommand:\n \"\"\"Render an AntaCommand from an AntaTemplate instance.\n\n Keep the parameters used in the AntaTemplate instance.\n\n Parameters\n ----------\n params\n Dictionary of variables with string values to render the Python f-string.\n\n Returns\n -------\n AntaCommand\n The rendered AntaCommand.\n This AntaCommand instance have a template attribute that references this\n AntaTemplate instance.\n\n Raises\n ------\n AntaTemplateRenderError\n If a parameter is missing to render the AntaTemplate instance.\n \"\"\"\n try:\n command = self.template.format(**params)\n except (KeyError, SyntaxError) as e:\n raise AntaTemplateRenderError(self, e.args[0]) from e\n return AntaCommand(\n command=command,\n ofmt=self.ofmt,\n version=self.version,\n revision=self.revision,\n template=self,\n params=self.params_schema(**params),\n use_cache=self.use_cache,\n )\n"},{"location":"api/reporters/","title":"Other reporters","text":"Report management for ANTA."},{"location":"api/reporters/#anta.reporter.ReportJinja","title":"ReportJinja","text":"ReportJinja(template_path: pathlib.Path)\n Report builder based on a Jinja2 template."},{"location":"api/reporters/#anta.reporter.ReportJinja.render","title":"render","text":"render(\n data: list[dict[str, Any]],\n *,\n trim_blocks: bool = True,\n lstrip_blocks: bool = True\n) -> str\n Build a report based on a Jinja2 template. Report is built based on a J2 template provided by user. Data structure sent to template is: Example >>> print(ResultManager.json)\n[\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n]\n Parameters: Name Type Description Default data list[dict[str, Any]] List of results from ResultManager.results. required trim_blocks bool enable trim_blocks for J2 rendering. True lstrip_blocks bool enable lstrip_blocks for J2 rendering. True Returns: Type Description str Rendered template Source code in anta/reporter/__init__.py def render(self, data: list[dict[str, Any]], *, trim_blocks: bool = True, lstrip_blocks: bool = True) -> str:\n \"\"\"Build a report based on a Jinja2 template.\n\n Report is built based on a J2 template provided by user.\n Data structure sent to template is:\n\n Example\n -------\n ```\n >>> print(ResultManager.json)\n [\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n ]\n ```\n\n Parameters\n ----------\n data\n List of results from `ResultManager.results`.\n trim_blocks\n enable trim_blocks for J2 rendering.\n lstrip_blocks\n enable lstrip_blocks for J2 rendering.\n\n Returns\n -------\n str\n Rendered template\n\n \"\"\"\n with self.template_path.open(encoding=\"utf-8\") as file_:\n template = Template(file_.read(), trim_blocks=trim_blocks, lstrip_blocks=lstrip_blocks)\n\n return template.render({\"data\": data})\n"},{"location":"api/reporters/#anta.reporter.ReportTable","title":"ReportTable","text":"TableReport Generate a Table based on TestResult."},{"location":"api/reporters/#anta.reporter.ReportTable.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_case: str = \"Test Name\",\n number_of_success: str = \"# of success\",\n number_of_failure: str = \"# of failure\",\n number_of_skipped: str = \"# of skipped\",\n number_of_errors: str = \"# of errors\",\n list_of_error_nodes: str = \"List of failed or error nodes\",\n list_of_error_tests: str = \"List of failed or error test cases\",\n)\n Headers for the table report."},{"location":"api/reporters/#anta.reporter.ReportTable.report_all","title":"report_all","text":"report_all(\n manager: ResultManager, title: str = \"All tests results\"\n) -> Table\n Create a table report with all tests for one or all devices. Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required title str Title for the report. Defaults to \u2018All tests results\u2019. 'All tests results' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_all(self, manager: ResultManager, title: str = \"All tests results\") -> Table:\n \"\"\"Create a table report with all tests for one or all devices.\n\n Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n title\n Title for the report. Defaults to 'All tests results'.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\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 def add_line(result: TestResult) -> None:\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(convert_categories(result.categories))\n table.add_row(str(result.name), result.test, state, message, result.description, categories)\n\n for result in manager.results:\n add_line(result)\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_devices","title":"report_summary_devices","text":"report_summary_devices(\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table\n Create a table report with result aggregated per device. Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required devices list[str] | None List of device names to include. None to select all devices. None title str Title of the report. 'Summary per device' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_devices(\n self,\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per device.\n\n Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n devices\n List of device names to include. None to select all devices.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.device,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_tests,\n ]\n table = self._build_headers(headers=headers, table=table)\n for device, stats in sorted(manager.device_stats.items()):\n if devices is None or device in devices:\n table.add_row(\n device,\n str(stats.tests_success_count),\n str(stats.tests_skipped_count),\n str(stats.tests_failure_count),\n str(stats.tests_error_count),\n \", \".join(stats.tests_failure),\n )\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_tests","title":"report_summary_tests","text":"report_summary_tests(\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table\n Create a table report with result aggregated per test. Create table with full output: Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required tests list[str] | None List of test names to include. None to select all tests. None title str Title of the report. 'Summary per test' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_tests(\n self,\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per test.\n\n Create table with full output:\n Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n tests\n List of test names to include. None to select all tests.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.test_case,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_nodes,\n ]\n table = self._build_headers(headers=headers, table=table)\n for test, stats in sorted(manager.test_stats.items()):\n if tests is None or test in tests:\n table.add_row(\n test,\n str(stats.devices_success_count),\n str(stats.devices_skipped_count),\n str(stats.devices_failure_count),\n str(stats.devices_error_count),\n \", \".join(stats.devices_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":" options:\n filters: [\"!^_[^_]\", \"!^__len__\"]\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager","title":"ResultManager","text":"ResultManager()\n Helper to manage Test Results and generate reports. Examples Create Inventory: inventory_anta = AntaInventory.parse(\n filename='examples/inventory.yml',\n username='ansible',\n password='ansible',\n)\n Create Result Manager: manager = ResultManager()\n Run tests for all connected devices: for device in inventory_anta.get_inventory().devices:\n manager.add(\n VerifyNTP(device=device).test()\n )\n manager.add(\n VerifyEOSVersion(device=device).test(version='4.28.3M')\n )\n Print result in native format: manager.results\n[\n TestResult(\n name=\"pf1\",\n test=\"VerifyZeroTouch\",\n categories=[\"configuration\"],\n description=\"Verifies ZeroTouch is disabled\",\n result=\"success\",\n messages=[],\n custom_field=None,\n ),\n TestResult(\n name=\"pf1\",\n test='VerifyNTP',\n categories=[\"software\"],\n categories=['system'],\n description='Verifies if NTP is synchronised.',\n result='failure',\n messages=[\"The device is not synchronized with the configured NTP server(s): 'NTP is disabled.'\"],\n custom_field=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."},{"location":"api/result_manager/#anta.result_manager.ResultManager.json","title":"json property","text":"json: str\n Get a JSON representation of the results."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results","title":"results property writable","text":"results: list[TestResult]\n Get the list of TestResult."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results_by_status","title":"results_by_status cached property","text":"results_by_status: dict[AntaTestStatus, list[TestResult]]\n A cached property that returns the results grouped by status."},{"location":"api/result_manager/#anta.result_manager.ResultManager.sorted_category_stats","title":"sorted_category_stats property","text":"sorted_category_stats: dict[str, CategoryStats]\n A property that returns the category_stats dictionary sorted by key name."},{"location":"api/result_manager/#anta.result_manager.ResultManager.__len__","title":"__len__","text":"__len__() -> int\n Implement len method to count number of results. Source code in anta/result_manager/__init__.py def __len__(self) -> int:\n \"\"\"Implement __len__ method to count number of results.\"\"\"\n return len(self._result_entries)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.add","title":"add","text":"add(result: TestResult) -> None\n Add a result to the ResultManager instance. The result is added to the internal list of results and the overall status of the ResultManager instance is updated based on the added test status. Parameters: Name Type Description Default result TestResult TestResult to add to the ResultManager instance. required Source code in anta/result_manager/__init__.py def add(self, result: TestResult) -> None:\n \"\"\"Add a result to the ResultManager instance.\n\n The result is added to the internal list of results and the overall status\n of the ResultManager instance is updated based on the added test status.\n\n Parameters\n ----------\n result\n TestResult to add to the ResultManager instance.\n \"\"\"\n self._result_entries.append(result)\n self._update_status(result.result)\n self._update_stats(result)\n\n # Every time a new result is added, we need to clear the cached property\n self.__dict__.pop(\"results_by_status\", None)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter","title":"filter","text":"filter(hide: set[AntaTestStatus]) -> ResultManager\n Get a filtered ResultManager based on test status. Parameters: Name Type Description Default hide set[AntaTestStatus] Set of AntaTestStatus enum members to select tests to hide based on their status. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter(self, hide: set[AntaTestStatus]) -> ResultManager:\n \"\"\"Get a filtered ResultManager based on test status.\n\n Parameters\n ----------\n hide\n Set of AntaTestStatus enum members to select tests to hide based on their status.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n possible_statuses = set(AntaTestStatus)\n manager = ResultManager()\n manager.results = self.get_results(possible_statuses - hide)\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_devices","title":"filter_by_devices","text":"filter_by_devices(devices: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific devices. Parameters: Name Type Description Default devices set[str] Set of device names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_devices(self, devices: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific devices.\n\n Parameters\n ----------\n devices\n Set of device names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.name in devices]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_tests","title":"filter_by_tests","text":"filter_by_tests(tests: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific tests. Parameters: Name Type Description Default tests set[str] Set of test names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_tests(self, tests: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific tests.\n\n Parameters\n ----------\n tests\n Set of test names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.test in tests]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_devices","title":"get_devices","text":"get_devices() -> set[str]\n Get the set of all the device names. Returns: Type Description set[str] Set of device names. Source code in anta/result_manager/__init__.py def get_devices(self) -> set[str]:\n \"\"\"Get the set of all the device names.\n\n Returns\n -------\n set[str]\n Set of device names.\n \"\"\"\n return {str(result.name) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_results","title":"get_results","text":"get_results(\n status: set[AntaTestStatus] | None = None,\n sort_by: list[str] | None = None,\n) -> list[TestResult]\n Get the results, optionally filtered by status and sorted by TestResult fields. If no status is provided, all results are returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None sort_by list[str] | None Optional list of TestResult fields to sort the results. None Returns: Type Description list[TestResult] List of results. Source code in anta/result_manager/__init__.py def get_results(self, status: set[AntaTestStatus] | None = None, sort_by: list[str] | None = None) -> list[TestResult]:\n \"\"\"Get the results, optionally filtered by status and sorted by TestResult fields.\n\n If no status is provided, all results are returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n sort_by\n Optional list of TestResult fields to sort the results.\n\n Returns\n -------\n list[TestResult]\n List of results.\n \"\"\"\n # Return all results if no status is provided, otherwise return results for multiple statuses\n results = self._result_entries if status is None else list(chain.from_iterable(self.results_by_status.get(status, []) for status in status))\n\n if sort_by:\n accepted_fields = TestResult.model_fields.keys()\n if not set(sort_by).issubset(set(accepted_fields)):\n msg = f\"Invalid sort_by fields: {sort_by}. Accepted fields are: {list(accepted_fields)}\"\n raise ValueError(msg)\n results = sorted(results, key=lambda result: [getattr(result, field) for field in sort_by])\n\n return results\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_tests","title":"get_tests","text":"get_tests() -> set[str]\n Get the set of all the test names. Returns: Type Description set[str] Set of test names. Source code in anta/result_manager/__init__.py def get_tests(self) -> set[str]:\n \"\"\"Get the set of all the test names.\n\n Returns\n -------\n set[str]\n Set of test names.\n \"\"\"\n return {str(result.test) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_total_results","title":"get_total_results","text":"get_total_results(\n status: set[AntaTestStatus] | None = None,\n) -> int\n Get the total number of results, optionally filtered by status. If no status is provided, the total number of results is returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None Returns: Type Description int Total number of results. Source code in anta/result_manager/__init__.py def get_total_results(self, status: set[AntaTestStatus] | None = None) -> int:\n \"\"\"Get the total number of results, optionally filtered by status.\n\n If no status is provided, the total number of results is returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n\n Returns\n -------\n int\n Total number of results.\n \"\"\"\n if status is None:\n # Return the total number of results\n return sum(len(results) for results in self.results_by_status.values())\n\n # Return the total number of results for multiple statuses\n return sum(len(self.results_by_status.get(status, [])) for status in status)\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/#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 str Name of the device where the test was run. test str Name of the test run on the device. categories list[str] List of categories the TestResult belongs to. Defaults to the AntaTest categories. description str Description of the TestResult. Defaults to the AntaTest description. result AntaTestStatus Result of the test. Must be one of the AntaTestStatus Enum values: unset, success, failure, error or skipped. messages list[str] Messages to report after the test, if any. custom_field str | None 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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_error(self, message: str | None = None) -> None:\n \"\"\"Set status to error.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_failure(self, message: str | None = None) -> None:\n \"\"\"Set status to failure.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_skipped(self, message: str | None = None) -> None:\n \"\"\"Set status to skipped.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_success(self, message: str | None = None) -> None:\n \"\"\"Set status to success.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.SUCCESS, message)\n"},{"location":"api/runner/","title":"Runner","text":""},{"location":"api/runner/#anta.runner","title":"runner","text":"ANTA runner function."},{"location":"api/runner/#anta.runner.adjust_rlimit_nofile","title":"adjust_rlimit_nofile","text":"adjust_rlimit_nofile() -> tuple[int, int]\n Adjust the maximum number of open file descriptors for the ANTA process. The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable. If the ANTA_NOFILE environment variable is not set or is invalid, DEFAULT_NOFILE is used. Returns: Type Description tuple[int, int] The new soft and hard limits for open file descriptors. Source code in anta/runner.py def adjust_rlimit_nofile() -> tuple[int, int]:\n \"\"\"Adjust the maximum number of open file descriptors for the ANTA process.\n\n The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable.\n\n If the `ANTA_NOFILE` environment variable is not set or is invalid, `DEFAULT_NOFILE` is used.\n\n Returns\n -------\n tuple[int, int]\n The new soft and hard limits for open file descriptors.\n \"\"\"\n try:\n nofile = int(os.environ.get(\"ANTA_NOFILE\", DEFAULT_NOFILE))\n except ValueError as exception:\n logger.warning(\"The ANTA_NOFILE environment variable value is invalid: %s\\nDefault to %s.\", exc_to_str(exception), DEFAULT_NOFILE)\n nofile = DEFAULT_NOFILE\n\n limits = resource.getrlimit(resource.RLIMIT_NOFILE)\n logger.debug(\"Initial limit numbers for open file descriptors for the current ANTA process: Soft Limit: %s | Hard Limit: %s\", limits[0], limits[1])\n nofile = min(limits[1], nofile)\n logger.debug(\"Setting soft limit for open file descriptors for the current ANTA process to %s\", nofile)\n resource.setrlimit(resource.RLIMIT_NOFILE, (nofile, limits[1]))\n return resource.getrlimit(resource.RLIMIT_NOFILE)\n"},{"location":"api/runner/#anta.runner.get_coroutines","title":"get_coroutines","text":"get_coroutines(\n selected_tests: defaultdict[\n AntaDevice, set[AntaTestDefinition]\n ],\n manager: ResultManager,\n) -> list[Coroutine[Any, Any, TestResult]]\n Get the coroutines for the ANTA run. Parameters: Name Type Description Default selected_tests defaultdict[AntaDevice, set[AntaTestDefinition]] A mapping of devices to the tests to run. The selected tests are generated by the prepare_tests function. required manager ResultManager A ResultManager required Returns: Type Description list[Coroutine[Any, Any, TestResult]] The list of coroutines to run. Source code in anta/runner.py def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]], manager: ResultManager) -> list[Coroutine[Any, Any, TestResult]]:\n \"\"\"Get the coroutines for the ANTA run.\n\n Parameters\n ----------\n selected_tests\n A mapping of devices to the tests to run. The selected tests are generated by the `prepare_tests` function.\n manager\n A ResultManager\n\n Returns\n -------\n list[Coroutine[Any, Any, TestResult]]\n The list of coroutines to run.\n \"\"\"\n coros = []\n for device, test_definitions in selected_tests.items():\n for test in test_definitions:\n try:\n test_instance = test.test(device=device, inputs=test.inputs)\n manager.add(test_instance.result)\n coros.append(test_instance.test())\n except Exception as e: # noqa: PERF203, BLE001\n # An AntaTest instance is potentially user-defined code.\n # We need to catch everything and exit gracefully with an error message.\n message = \"\\n\".join(\n [\n f\"There is an error when creating test {test.test.__module__}.{test.test.__name__}.\",\n f\"If this is not a custom test implementation: {GITHUB_SUGGESTION}\",\n ],\n )\n anta_log_exception(e, message, logger)\n return coros\n"},{"location":"api/runner/#anta.runner.log_cache_statistics","title":"log_cache_statistics","text":"log_cache_statistics(devices: list[AntaDevice]) -> None\n Log cache statistics for each device in the inventory. Parameters: Name Type Description Default devices list[AntaDevice] List of devices in the inventory. required Source code in anta/runner.py def log_cache_statistics(devices: list[AntaDevice]) -> None:\n \"\"\"Log cache statistics for each device in the inventory.\n\n Parameters\n ----------\n devices\n List of devices in the inventory.\n \"\"\"\n for device in devices:\n if device.cache_statistics is not None:\n msg = (\n f\"Cache statistics for '{device.name}': \"\n f\"{device.cache_statistics['cache_hits']} hits / {device.cache_statistics['total_commands_sent']} \"\n f\"command(s) ({device.cache_statistics['cache_hit_ratio']})\"\n )\n logger.info(msg)\n else:\n logger.info(\"Caching is not enabled on %s\", device.name)\n"},{"location":"api/runner/#anta.runner.main","title":"main async","text":"main(\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False\n) -> None\n Run ANTA. Use this as an entrypoint to the test framework in your script. ResultManager object gets updated with the test results. Parameters: Name Type Description Default manager ResultManager ResultManager object to populate with the test results. required inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required devices set[str] | None Devices on which to run tests. None means all devices. These may come from the --device / -d CLI option in NRFU. None tests set[str] | None Tests to run against devices. None means all tests. These may come from the --test / -t CLI option in NRFU. None tags set[str] | None Tags to filter devices from the inventory. These may come from the --tags CLI option in NRFU. None established_only bool Include only established device(s). True dry_run bool Build the list of coroutine to run and stop before test execution. False Source code in anta/runner.py @cprofile()\nasync def main( # noqa: PLR0913\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False,\n) -> None:\n \"\"\"Run ANTA.\n\n Use this as an entrypoint to the test framework in your script.\n ResultManager object gets updated with the test results.\n\n Parameters\n ----------\n manager\n ResultManager object to populate with the test results.\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n devices\n Devices on which to run tests. None means all devices. These may come from the `--device / -d` CLI option in NRFU.\n tests\n Tests to run against devices. None means all tests. These may come from the `--test / -t` CLI option in NRFU.\n tags\n Tags to filter devices from the inventory. These may come from the `--tags` CLI option in NRFU.\n established_only\n Include only established device(s).\n dry_run\n Build the list of coroutine to run and stop before test execution.\n \"\"\"\n # Adjust the maximum number of open file descriptors for the ANTA process\n limits = adjust_rlimit_nofile()\n\n if not catalog.tests:\n logger.info(\"The list of tests is empty, exiting\")\n return\n\n with Catchtime(logger=logger, message=\"Preparing ANTA NRFU Run\"):\n # Setup the inventory\n selected_inventory = inventory if dry_run else await setup_inventory(inventory, tags, devices, established_only=established_only)\n if selected_inventory is None:\n return\n\n with Catchtime(logger=logger, message=\"Preparing the tests\"):\n selected_tests = prepare_tests(selected_inventory, catalog, tests, tags)\n if selected_tests is None:\n return\n final_tests_count = sum(len(tests) for tests in selected_tests.values())\n\n run_info = (\n \"--- ANTA NRFU Run Information ---\\n\"\n f\"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\\n\"\n f\"Total number of selected tests: {final_tests_count}\\n\"\n f\"Maximum number of open file descriptors for the current ANTA process: {limits[0]}\\n\"\n \"---------------------------------\"\n )\n\n logger.info(run_info)\n\n if final_tests_count > limits[0]:\n logger.warning(\n \"The number of concurrent tests is higher than the open file descriptors limit for this ANTA process.\\n\"\n \"Errors may occur while running the tests.\\n\"\n \"Please consult the ANTA FAQ.\"\n )\n\n coroutines = get_coroutines(selected_tests, manager)\n\n if dry_run:\n logger.info(\"Dry-run mode, exiting before running the tests.\")\n for coro in coroutines:\n coro.close()\n return\n\n if AntaTest.progress is not None:\n AntaTest.nrfu_task = AntaTest.progress.add_task(\"Running NRFU Tests...\", total=len(coroutines))\n\n with Catchtime(logger=logger, message=\"Running ANTA tests\"):\n await asyncio.gather(*coroutines)\n\n log_cache_statistics(selected_inventory.devices)\n"},{"location":"api/runner/#anta.runner.prepare_tests","title":"prepare_tests","text":"prepare_tests(\n inventory: AntaInventory,\n catalog: AntaCatalog,\n tests: set[str] | None,\n tags: set[str] | None,\n) -> (\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n)\n Prepare the tests to run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required tests set[str] | None Tests to run against devices. None means all tests. required tags set[str] | None Tags to filter devices from the inventory. required Returns: Type Description defaultdict[AntaDevice, set[AntaTestDefinition]] | None A mapping of devices to the tests to run or None if there are no tests to run. Source code in anta/runner.py def prepare_tests(\n inventory: AntaInventory, catalog: AntaCatalog, tests: set[str] | None, tags: set[str] | None\n) -> defaultdict[AntaDevice, set[AntaTestDefinition]] | None:\n \"\"\"Prepare the tests to run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n tests\n Tests to run against devices. None means all tests.\n tags\n Tags to filter devices from the inventory.\n\n Returns\n -------\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n A mapping of devices to the tests to run or None if there are no tests to run.\n \"\"\"\n # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests\n catalog.build_indexes(filtered_tests=tests)\n\n # Using a set to avoid inserting duplicate tests\n device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)\n\n total_test_count = 0\n\n # Create the device to tests mapping from the tags\n for device in inventory.devices:\n if tags:\n # If there are CLI tags, execute tests with matching tags for this device\n if not (matching_tags := tags.intersection(device.tags)):\n # The device does not have any selected tag, skipping\n continue\n device_to_tests[device].update(catalog.get_tests_by_tags(matching_tags))\n else:\n # If there is no CLI tags, execute all tests that do not have any tags\n device_to_tests[device].update(catalog.tag_to_tests[None])\n\n # Then add the tests with matching tags from device tags\n device_to_tests[device].update(catalog.get_tests_by_tags(device.tags))\n\n total_test_count += len(device_to_tests[device])\n\n if total_test_count == 0:\n msg = (\n f\"There are no tests{f' matching the tags {tags} ' if tags else ' '}to run in the current test catalog and device inventory, please verify your inputs.\"\n )\n logger.warning(msg)\n return None\n\n return device_to_tests\n"},{"location":"api/runner/#anta.runner.setup_inventory","title":"setup_inventory async","text":"setup_inventory(\n inventory: AntaInventory,\n tags: set[str] | None,\n devices: set[str] | None,\n *,\n established_only: bool\n) -> AntaInventory | None\n Set up the inventory for the ANTA run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required tags set[str] | None Tags to filter devices from the inventory. required devices set[str] | None Devices on which to run tests. None means all devices. required established_only bool If True use return only devices where a connection is established. required Returns: Type Description AntaInventory | None The filtered inventory or None if there are no devices to run tests on. Source code in anta/runner.py async def setup_inventory(inventory: AntaInventory, tags: set[str] | None, devices: set[str] | None, *, established_only: bool) -> AntaInventory | None:\n \"\"\"Set up the inventory for the ANTA run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n tags\n Tags to filter devices from the inventory.\n devices\n Devices on which to run tests. None means all devices.\n established_only\n If True use return only devices where a connection is established.\n\n Returns\n -------\n AntaInventory | None\n The filtered inventory or None if there are no devices to run tests on.\n \"\"\"\n if len(inventory) == 0:\n logger.info(\"The inventory is empty, exiting\")\n return None\n\n # Filter the inventory based on the CLI provided tags and devices if any\n selected_inventory = inventory.get_inventory(tags=tags, devices=devices) if tags or devices else inventory\n\n with Catchtime(logger=logger, message=\"Connecting to devices\"):\n # Connect to the devices\n await selected_inventory.connect_inventory()\n\n # Remove devices that are unreachable\n selected_inventory = selected_inventory.get_inventory(established_only=established_only)\n\n # If there are no devices in the inventory after filtering, exit\n if not selected_inventory.devices:\n msg = f'No reachable device {f\"matching the tags {tags} \" if tags else \"\"}was found.{f\" Selected devices: {devices} \" if devices is not None else \"\"}'\n logger.warning(msg)\n return None\n\n return selected_inventory\n"},{"location":"api/test.cvx/","title":"Test.cvx","text":""},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX","title":"VerifyManagementCVX","text":"Verifies the management CVX global status. Expected Results Success: The test will pass if the management CVX global status matches the expected status. Failure: The test will fail if the management CVX global status does not match the expected status. Examples anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n Source code in anta/tests/cvx.py class VerifyManagementCVX(AntaTest):\n \"\"\"Verifies the management CVX global status.\n\n Expected Results\n ----------------\n * Success: The test will pass if the management CVX global status matches the expected status.\n * Failure: The test will fail if the management CVX global status does not match the expected status.\n\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyManagementCVX test.\"\"\"\n\n enabled: bool\n \"\"\"Whether management CVX must be enabled (True) or disabled (False).\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyManagementCVX.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n cluster_status = command_output[\"clusterStatus\"]\n if (cluster_state := cluster_status.get(\"enabled\")) != self.inputs.enabled:\n self.result.is_failure(f\"Management CVX status is not valid: {cluster_state}\")\n"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX-attributes","title":"Inputs","text":"Name Type Description Default enabled bool Whether management CVX must be enabled (True) or disabled (False). -"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyMcsClientMounts","title":"VerifyMcsClientMounts","text":"Verify if all MCS client mounts are in mountStateMountComplete. Expected Results Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete. Failure: The test will fail even if one switch\u2019s MCS client mount status is not mountStateMountComplete. Examples anta.tests.cvx:\n- VerifyMcsClientMounts:\n Source code in anta/tests/cvx.py class VerifyMcsClientMounts(AntaTest):\n \"\"\"Verify if all MCS client mounts are in mountStateMountComplete.\n\n Expected Results\n ----------------\n * Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete.\n * Failure: The test will fail even if one switch's MCS client mount status is not mountStateMountComplete.\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyMcsClientMounts:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx mounts\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyMcsClientMounts.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n mount_states = command_output[\"mountStates\"]\n mcs_mount_state_detected = False\n for mount_state in mount_states:\n if not mount_state[\"type\"].startswith(\"Mcs\"):\n continue\n mcs_mount_state_detected = True\n if (state := mount_state[\"state\"]) != \"mountStateMountComplete\":\n self.result.is_failure(f\"MCS Client mount states are not valid: {state}\")\n\n if not mcs_mount_state_detected:\n self.result.is_failure(\"MCS Client mount states are not present\")\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authentication\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authorization\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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.avt/","title":"Adaptive Virtual Topology","text":""},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTPathHealth","title":"VerifyAVTPathHealth","text":"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs. Expected Results Success: The test will pass if all AVT paths for all VRFs are active and valid. Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid. Examples anta.tests.avt:\n - VerifyAVTPathHealth:\n Source code in anta/tests/avt.py class VerifyAVTPathHealth(AntaTest):\n \"\"\"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for all VRFs are active and valid.\n * Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTPathHealth:\n ```\n \"\"\"\n\n description = \"Verifies the status of all AVT paths for all VRFs.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTPathHealth.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output.get(\"vrfs\", {})\n\n # Check if AVT is configured\n if not command_output:\n self.result.is_failure(\"Adaptive virtual topology paths are not configured.\")\n return\n\n # Iterate over each VRF\n for vrf, vrf_data in command_output.items():\n # Iterate over each AVT path\n for profile, avt_path in vrf_data.get(\"avts\", {}).items():\n for path, flags in avt_path.get(\"avtPaths\", {}).items():\n # Get the status of the AVT path\n valid = flags[\"flags\"][\"valid\"]\n active = flags[\"flags\"][\"active\"]\n\n # Check the status of the AVT path\n if not valid and not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid and not active.\")\n elif not valid:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid.\")\n elif not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is not active.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole","title":"VerifyAVTRole","text":"Verifies the Adaptive Virtual Topology (AVT) role of a device. Expected Results Success: The test will pass if the AVT role of the device matches the expected role. Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role. Examples anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n Source code in anta/tests/avt.py class VerifyAVTRole(AntaTest):\n \"\"\"Verifies the Adaptive Virtual Topology (AVT) role of a device.\n\n Expected Results\n ----------------\n * Success: The test will pass if the AVT role of the device matches the expected role.\n * Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n ```\n \"\"\"\n\n description = \"Verifies the AVT role of a device.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTRole test.\"\"\"\n\n role: str\n \"\"\"Expected AVT role of the device.\"\"\"\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTRole.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output\n\n # Check if the AVT role matches the expected role\n if self.inputs.role != command_output.get(\"role\"):\n self.result.is_failure(f\"Expected AVT role as `{self.inputs.role}`, but found `{command_output.get('role')}` instead.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole-attributes","title":"Inputs","text":"Name Type Description Default role str Expected AVT role of the device. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath","title":"VerifyAVTSpecificPath","text":"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF. Expected Results Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided. If multiple paths are configured, the test will pass only if all the paths are valid and active. Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid, or does not match the specified type. Examples anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n Source code in anta/tests/avt.py class VerifyAVTSpecificPath(AntaTest):\n \"\"\"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided.\n If multiple paths are configured, the test will pass only if all the paths are valid and active.\n * Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid,\n or does not match the specified type.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n ```\n \"\"\"\n\n description = \"Verifies the status and type of an AVT path for a specified VRF.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show adaptive-virtual-topology path vrf {vrf} avt {avt_name} destination {destination}\")\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTSpecificPath test.\"\"\"\n\n avt_paths: list[AVTPaths]\n \"\"\"List of AVT paths to verify.\"\"\"\n\n class AVTPaths(BaseModel):\n \"\"\"Model for the details of AVT paths.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"The VRF for the AVT path. Defaults to 'default' if not provided.\"\"\"\n avt_name: str\n \"\"\"Name of the adaptive virtual topology.\"\"\"\n destination: IPv4Address\n \"\"\"The IPv4 address of the AVT peer.\"\"\"\n next_hop: IPv4Address\n \"\"\"The IPv4 address of the next hop for the AVT peer.\"\"\"\n path_type: str | None = None\n \"\"\"The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input AVT path/peer.\"\"\"\n return [template.render(vrf=path.vrf, avt_name=path.avt_name, destination=path.destination) for path in self.inputs.avt_paths]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTSpecificPath.\"\"\"\n # Assume the test is successful until a failure is detected\n self.result.is_success()\n\n # Process each command in the instance\n for command, input_avt in zip(self.instance_commands, self.inputs.avt_paths):\n # Extract the command output and parameters\n vrf = command.params.vrf\n avt_name = command.params.avt_name\n peer = str(command.params.destination)\n\n command_output = command.json_output.get(\"vrfs\", {})\n\n # If no AVT is configured, mark the test as failed and skip to the next command\n if not command_output:\n self.result.is_failure(f\"AVT configuration for peer '{peer}' under topology '{avt_name}' in VRF '{vrf}' is not found.\")\n continue\n\n # Extract the AVT paths\n avt_paths = get_value(command_output, f\"{vrf}.avts.{avt_name}.avtPaths\")\n next_hop, input_path_type = str(input_avt.next_hop), input_avt.path_type\n\n nexthop_path_found = path_type_found = False\n\n # Check each AVT path\n for path, path_data in avt_paths.items():\n # If the path does not match the expected next hop, skip to the next path\n if path_data.get(\"nexthopAddr\") != next_hop:\n continue\n\n nexthop_path_found = True\n path_type = \"direct\" if get_value(path_data, \"flags.directPath\") else \"multihop\"\n\n # If the path type does not match the expected path type, skip to the next path\n if input_path_type and path_type != input_path_type:\n continue\n\n path_type_found = True\n valid = get_value(path_data, \"flags.valid\")\n active = get_value(path_data, \"flags.active\")\n\n # Check the path status and type against the expected values\n if not all([valid, active]):\n failure_reasons = []\n if not get_value(path_data, \"flags.active\"):\n failure_reasons.append(\"inactive\")\n if not get_value(path_data, \"flags.valid\"):\n failure_reasons.append(\"invalid\")\n # Construct the failure message prefix\n failed_log = f\"AVT path '{path}' for topology '{avt_name}' in VRF '{vrf}'\"\n self.result.is_failure(f\"{failed_log} is {', '.join(failure_reasons)}.\")\n\n # If no matching next hop or path type was found, mark the test as failed\n if not nexthop_path_found or not path_type_found:\n self.result.is_failure(\n f\"No '{input_path_type}' path found with next-hop address '{next_hop}' for AVT peer '{peer}' under topology '{avt_name}' in VRF '{vrf}'.\"\n )\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"Inputs","text":"Name Type Description Default avt_paths list[AVTPaths] List of AVT paths to verify. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"AVTPaths","text":"Name Type Description Default vrf str The VRF for the AVT path. Defaults to 'default' if not provided. 'default' avt_name str Name of the adaptive virtual topology. - destination IPv4Address The IPv4 address of the AVT peer. - next_hop IPv4Address The IPv4 address of the next hop for the AVT peer. - path_type str | None The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered. None"},{"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 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\", revision=1),\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 categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\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 tx_interval = bfd_peers.tx_interval\n rx_interval = bfd_peers.rx_interval\n multiplier = bfd_peers.multiplier\n\n # Check if BFD peer configured\n bfd_output = get_value(\n self.instance_commands[0].json_output,\n f\"vrfs..{vrf}..ipv4Neighbors..{peer}..peerStats..\",\n separator=\"..\",\n )\n if not bfd_output:\n failures[peer] = {vrf: \"Not Configured\"}\n continue\n\n # Convert interval timer(s) into milliseconds to be consistent with the inputs.\n bfd_details = bfd_output.get(\"peerStatsDetail\", {})\n op_tx_interval = bfd_details.get(\"operTxInterval\") // 1000\n op_rx_interval = bfd_details.get(\"operRxInterval\") // 1000\n detect_multiplier = bfd_details.get(\"detectMult\")\n intervals_ok = op_tx_interval == tx_interval and op_rx_interval == rx_interval and detect_multiplier == multiplier\n\n # Check timers of BFD peer\n if not intervals_ok:\n failures[peer] = {\n vrf: {\n \"tx_interval\": op_tx_interval,\n \"rx_interval\": op_rx_interval,\n \"multiplier\": detect_multiplier,\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.VerifyBFDPeersRegProtocols","title":"VerifyBFDPeersRegProtocols","text":"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered. Expected Results Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s). Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s). Examples anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n Source code in anta/tests/bfd.py class VerifyBFDPeersRegProtocols(AntaTest):\n \"\"\"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered.\n\n Expected Results\n ----------------\n * Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s).\n * Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s).\n\n Examples\n --------\n ```yaml\n anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBFDPeersRegProtocols 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 protocols: list[BfdProtocol]\n \"\"\"List of protocols to be verified.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBFDPeersRegProtocols.\"\"\"\n # Initialize failure messages\n failures: dict[Any, Any] = {}\n\n # Iterating over BFD peers, extract the parameters and command output\n for bfd_peer in self.inputs.bfd_peers:\n peer = str(bfd_peer.peer_address)\n vrf = bfd_peer.vrf\n protocols = bfd_peer.protocols\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 registered protocols\n difference = set(protocols) - set(get_value(bfd_output, \"peerStatsDetail.apps\"))\n\n if difference:\n failures[peer] = {vrf: sorted(difference)}\n\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BFD peers are not configured or have non-registered protocol(s):\\n{failures}\")\n"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-attributes","title":"Inputs","text":"Name Type Description Default bfd_peers list[BFDPeer] List of IPv4 BFD peers. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-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' protocols list[BfdProtocol] List of protocols to be verified. -"},{"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 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\", revision=1)]\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 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.VerifyRunningConfigLines","title":"VerifyRunningConfigLines","text":"Verifies the given regular expression patterns are present in the running-config. Warning Since this uses regular expression searches on the whole running-config, it can drastically impact performance and should only be used if no other test is available. If possible, try using another ANTA test that is more specific. Expected Results Success: The test will pass if all the patterns are found in the running-config. Failure: The test will fail if any of the patterns are NOT found in the running-config. Examples anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n Source code in anta/tests/configuration.py class VerifyRunningConfigLines(AntaTest):\n \"\"\"Verifies the given regular expression patterns are present in the running-config.\n\n !!! warning\n Since this uses regular expression searches on the whole running-config, it can\n drastically impact performance and should only be used if no other test is available.\n\n If possible, try using another ANTA test that is more specific.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the patterns are found in the running-config.\n * Failure: The test will fail if any of the patterns are NOT found in the running-config.\n\n Examples\n --------\n ```yaml\n anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n ```\n \"\"\"\n\n description = \"Search the Running-Config for the given RegEx patterns.\"\n categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show running-config\", ofmt=\"text\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyRunningConfigLines test.\"\"\"\n\n regex_patterns: list[RegexString]\n \"\"\"List of regular expressions.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRunningConfigLines.\"\"\"\n failure_msgs = []\n command_output = self.instance_commands[0].text_output\n\n for pattern in self.inputs.regex_patterns:\n re_search = re.compile(pattern, flags=re.MULTILINE)\n\n if not re_search.search(command_output):\n failure_msgs.append(f\"'{pattern}'\")\n\n if not failure_msgs:\n self.result.is_success()\n else:\n self.result.is_failure(\"Following patterns were not found: \" + \",\".join(failure_msgs))\n"},{"location":"api/tests.configuration/#anta.tests.configuration.VerifyRunningConfigLines-attributes","title":"Inputs","text":"Name Type Description Default regex_patterns list[RegexString] List of regular expressions. -"},{"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 categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show zerotouch\", revision=1)]\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/#tests","title":"Tests","text":""},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors","title":"VerifyLLDPNeighbors","text":"Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. This test performs the following checks for each specified LLDP neighbor: Confirming matching ports on both local and neighboring devices. Ensuring compatibility of device names and interface identifiers. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored. Expected Results Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table. The system name or port of the LLDP neighbor does not match the expected 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 the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.\n\n This test performs the following checks for each specified LLDP neighbor:\n\n 1. Confirming matching ports on both local and neighboring devices.\n 2. Ensuring compatibility of device names and interface identifiers.\n 3. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table.\n - The system name or port of the LLDP neighbor does not match the expected 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 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\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLLDPNeighbors test.\"\"\"\n\n neighbors: list[LLDPNeighbor]\n \"\"\"List of LLDP neighbors.\"\"\"\n Neighbor: ClassVar[type[Neighbor]] = Neighbor\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLLDPNeighbors.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output[\"lldpNeighbors\"]\n for neighbor in self.inputs.neighbors:\n if neighbor.port not in output:\n self.result.is_failure(f\"{neighbor} - Port not found\")\n continue\n\n if len(lldp_neighbor_info := output[neighbor.port][\"lldpNeighborInfo\"]) == 0:\n self.result.is_failure(f\"{neighbor} - No LLDP neighbors\")\n continue\n\n # Check if the system name and neighbor port matches\n match_found = any(\n info[\"systemName\"] == neighbor.neighbor_device and info[\"neighborInterfaceInfo\"][\"interfaceId_v2\"] == neighbor.neighbor_port\n for info in lldp_neighbor_info\n )\n if not match_found:\n failure_msg = [f\"{info['systemName']}/{info['neighborInterfaceInfo']['interfaceId_v2']}\" for info in lldp_neighbor_info]\n self.result.is_failure(f\"{neighbor} - Wrong LLDP neighbors: {', '.join(failure_msg)}\")\n"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors-attributes","title":"Inputs","text":"Name Type Description Default neighbors list[LLDPNeighbor] List of LLDP neighbors. -"},{"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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"connectivity\"]\n # Template uses '{size}{df_bit}' without space since df_bit includes leading space when enabled\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"ping vrf {vrf} {destination} source {source} size {size}{df_bit} repeat {repeat}\", revision=1)\n ]\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 Host: ClassVar[type[Host]] = Host\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each host in the input list.\"\"\"\n commands = []\n for host in self.inputs.hosts:\n # df_bit includes leading space when enabled, empty string when disabled\n df_bit = \" df-bit\" if host.df_bit else \"\"\n command = template.render(destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat, size=host.size, df_bit=df_bit)\n commands.append(command)\n return commands\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyReachability.\"\"\"\n self.result.is_success()\n\n for command, host in zip(self.instance_commands, self.inputs.hosts):\n if f\"{host.repeat} received\" not in command.json_output[\"messages\"][0]:\n self.result.is_failure(f\"{host} - Unreachable\")\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/#input-models","title":"Input models","text":""},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Host","title":"Host","text":"Model for a remote host to ping. 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 size int Specify datagram size. Defaults to 100. 100 df_bit bool Enable do not fragment bit in IP header. Defaults to False. False Source code in anta/input_models/connectivity.py class Host(BaseModel):\n \"\"\"Model for a remote host to ping.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\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 size: int = 100\n \"\"\"Specify datagram size. Defaults to 100.\"\"\"\n df_bit: bool = False\n \"\"\"Enable do not fragment bit in IP header. Defaults to False.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the Host for reporting.\n\n Examples\n --------\n Host 10.1.1.1 (src: 10.2.2.2, vrf: mgmt, size: 100B, repeat: 2)\n\n \"\"\"\n df_status = \", df-bit: enabled\" if self.df_bit else \"\"\n return f\"Host {self.destination} (src: {self.source}, vrf: {self.vrf}, size: {self.size}B, repeat: {self.repeat}{df_status})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.LLDPNeighbor","title":"LLDPNeighbor","text":"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information. Name Type Description Default port Interface The LLDP port for the local device. - neighbor_device str The system name of the LLDP neighbor device. - neighbor_port Interface The LLDP port on the neighboring device. - Source code in anta/input_models/connectivity.py class LLDPNeighbor(BaseModel):\n \"\"\"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n port: Interface\n \"\"\"The LLDP port for the local device.\"\"\"\n neighbor_device: str\n \"\"\"The system name of the LLDP neighbor device.\"\"\"\n neighbor_port: Interface\n \"\"\"The LLDP port on the neighboring device.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the LLDPNeighbor for reporting.\n\n Examples\n --------\n Port Ethernet1 (Neighbor: DC1-SPINE2, Neighbor Port: Ethernet2)\n\n \"\"\"\n return f\"Port {self.port} (Neighbor: {self.neighbor_device}, Neighbor Port: {self.neighbor_port})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor","title":"Neighbor","text":"Alias for the LLDPNeighbor model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the LLDPNeighbor model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/connectivity.py class Neighbor(LLDPNeighbor): # pragma: no cover\n \"\"\"Alias for the LLDPNeighbor model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the LLDPNeighbor model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor.__init__","title":"__init__","text":"__init__(**data: Any) -> None\n Source code in anta/input_models/connectivity.py def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"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 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\", revision=1)]\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 break\n else:\n self.result.is_failure(\"Aboot component not found\")\n return\n\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 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\", revision=1)]\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_failure(\"Error in running test - Component FixedSystemvrm1 not found in 'show version'\")\n"},{"location":"api/tests.flow_tracking/","title":"Flow Tracking","text":""},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus","title":"VerifyHardwareFlowTrackerStatus","text":"Verifies if hardware flow tracking is running and an input tracker is active. This test optionally verifies the tracker interval/timeout and exporter configuration. Expected Results Success: The test will pass if hardware flow tracking is running and an input tracker is active. Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active, or the tracker interval/timeout and exporter configuration does not match the expected values. Examples anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n Source code in anta/tests/flow_tracking.py class VerifyHardwareFlowTrackerStatus(AntaTest):\n \"\"\"Verifies if hardware flow tracking is running and an input tracker is active.\n\n This test optionally verifies the tracker interval/timeout and exporter configuration.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware flow tracking is running and an input tracker is active.\n * Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active,\n or the tracker interval/timeout and exporter configuration does not match the expected values.\n\n Examples\n --------\n ```yaml\n anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n ```\n \"\"\"\n\n description = (\n \"Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration.\"\n )\n categories: ClassVar[list[str]] = [\"flow tracking\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show flow tracking hardware tracker {name}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyHardwareFlowTrackerStatus test.\"\"\"\n\n trackers: list[FlowTracker]\n \"\"\"List of flow trackers to verify.\"\"\"\n\n class FlowTracker(BaseModel):\n \"\"\"Detail of a flow tracker.\"\"\"\n\n name: str\n \"\"\"Name of the flow tracker.\"\"\"\n\n record_export: RecordExport | None = None\n \"\"\"Record export configuration for the flow tracker.\"\"\"\n\n exporters: list[Exporter] | None = None\n \"\"\"List of exporters for the flow tracker.\"\"\"\n\n class RecordExport(BaseModel):\n \"\"\"Record export configuration.\"\"\"\n\n on_inactive_timeout: int\n \"\"\"Timeout in milliseconds for exporting records when inactive.\"\"\"\n\n on_interval: int\n \"\"\"Interval in milliseconds for exporting records.\"\"\"\n\n class Exporter(BaseModel):\n \"\"\"Detail of an exporter.\"\"\"\n\n name: str\n \"\"\"Name of the exporter.\"\"\"\n\n local_interface: str\n \"\"\"Local interface used by the exporter.\"\"\"\n\n template_interval: int\n \"\"\"Template interval in milliseconds for the exporter.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each hardware tracker.\"\"\"\n return [template.render(name=tracker.name) for tracker in self.inputs.trackers]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareFlowTrackerStatus.\"\"\"\n self.result.is_success()\n for command, tracker_input in zip(self.instance_commands, self.inputs.trackers):\n hardware_tracker_name = command.params.name\n record_export = tracker_input.record_export.model_dump() if tracker_input.record_export else None\n exporters = [exporter.model_dump() for exporter in tracker_input.exporters] if tracker_input.exporters else None\n command_output = command.json_output\n\n # Check if hardware flow tracking is configured\n if not command_output.get(\"running\"):\n self.result.is_failure(\"Hardware flow tracking is not running.\")\n return\n\n # Check if the input hardware tracker is configured\n tracker_info = command_output[\"trackers\"].get(hardware_tracker_name)\n if not tracker_info:\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not configured.\")\n continue\n\n # Check if the input hardware tracker is active\n if not tracker_info.get(\"active\"):\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not active.\")\n continue\n\n # Check the input hardware tracker timeouts\n failure_msg = \"\"\n if record_export:\n record_export_failure = validate_record_export(record_export, tracker_info)\n if record_export_failure:\n failure_msg += record_export_failure\n\n # Check the input hardware tracker exporters' configuration\n if exporters:\n exporters_failure = validate_exporters(exporters, tracker_info)\n if exporters_failure:\n failure_msg += exporters_failure\n\n if failure_msg:\n self.result.is_failure(f\"{hardware_tracker_name}: {failure_msg}\\n\")\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Inputs","text":"Name Type Description Default trackers list[FlowTracker] List of flow trackers to verify. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"FlowTracker","text":"Name Type Description Default name str Name of the flow tracker. - record_export RecordExport | None Record export configuration for the flow tracker. None exporters list[Exporter] | None List of exporters for the flow tracker. None"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"RecordExport","text":"Name Type Description Default on_inactive_timeout int Timeout in milliseconds for exporting records when inactive. - on_interval int Interval in milliseconds for exporting records. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Exporter","text":"Name Type Description Default name str Name of the exporter. - local_interface str Local interface used by the exporter. - template_interval int Template interval in milliseconds for the exporter. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_exporters","title":"validate_exporters","text":"validate_exporters(\n exporters: list[dict[str, str]],\n tracker_info: dict[str, str],\n) -> str\n Validate the exporter configurations against the tracker info. Parameters: Name Type Description Default exporters list[dict[str, str]] The list of expected exporter configurations. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str Failure message if any exporter configuration does not match. Source code in anta/tests/flow_tracking.py def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the exporter configurations against the tracker info.\n\n Parameters\n ----------\n exporters\n The list of expected exporter configurations.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n Failure message if any exporter configuration does not match.\n \"\"\"\n failed_log = \"\"\n for exporter in exporters:\n exporter_name = exporter[\"name\"]\n actual_exporter_info = tracker_info[\"exporters\"].get(exporter_name)\n if not actual_exporter_info:\n failed_log += f\"\\nExporter `{exporter_name}` is not configured.\"\n continue\n\n expected_exporter_data = {\"local interface\": exporter[\"local_interface\"], \"template interval\": exporter[\"template_interval\"]}\n actual_exporter_data = {\"local interface\": actual_exporter_info[\"localIntf\"], \"template interval\": actual_exporter_info[\"templateInterval\"]}\n\n if expected_exporter_data != actual_exporter_data:\n failed_msg = get_failed_logs(expected_exporter_data, actual_exporter_data)\n failed_log += f\"\\nExporter `{exporter_name}`: {failed_msg}\"\n return failed_log\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_record_export","title":"validate_record_export","text":"validate_record_export(\n record_export: dict[str, str],\n tracker_info: dict[str, str],\n) -> str\n Validate the record export configuration against the tracker info. Parameters: Name Type Description Default record_export dict[str, str] The expected record export configuration. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str A failure message if the record export configuration does not match, otherwise blank string. Source code in anta/tests/flow_tracking.py def validate_record_export(record_export: dict[str, str], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the record export configuration against the tracker info.\n\n Parameters\n ----------\n record_export\n The expected record export configuration.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n A failure message if the record export configuration does not match, otherwise blank string.\n \"\"\"\n failed_log = \"\"\n actual_export = {\"inactive timeout\": tracker_info.get(\"inactiveTimeout\"), \"interval\": tracker_info.get(\"activeInterval\")}\n expected_export = {\"inactive timeout\": record_export.get(\"on_inactive_timeout\"), \"interval\": record_export.get(\"on_interval\")}\n if actual_export != expected_export:\n failed_log = get_failed_logs(expected_export, actual_export)\n return failed_log\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 description = \"Verifies if a GreenT policy other than the default is created.\"\n categories: ClassVar[list[str]] = [\"greent\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show monitor telemetry postcard policy profile\", revision=1)]\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 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\", revision=1)]\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 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\", revision=1)]\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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment power\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment cooling\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show inventory\", revision=2)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature transceiver\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=2)]\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 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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show lacp counters all-ports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters discards\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces status\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters errors\", revision=1)]\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 description = \"Verifies the interface IPv4 addresses.\"\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip interface {interface}\", revision=2)]\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 [template.render(interface=interface.name) for interface in self.inputs.interfaces]\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 for interface in self.inputs.interfaces:\n if interface.name == intf:\n input_interface_detail = interface\n break\n else:\n self.result.is_failure(f\"Could not find `{intf}` in the input interfaces. {GITHUB_SUGGESTION}\")\n continue\n\n input_primary_ip = str(input_interface_detail.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 (param_secondary_ips := input_interface_detail.secondary_ips) is not None:\n input_secondary_ips = sorted([str(network) for network in param_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. This test has been implemented for full-duplex interfaces only. 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. Error: The test will error out if the device has at least one non full-duplex interface. 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 This test has been implemented for full-duplex interfaces only.\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 * Error: The test will error out if the device has at least one non full-duplex interface.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfaceUtilization:\n threshold: 70.0\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show interfaces counters rates\", revision=1),\n AntaCommand(command=\"show interfaces\", revision=1),\n ]\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 # The utilization logic has been implemented for full-duplex interfaces only\n if ((duplex := (interface := interfaces[\"interfaces\"][intf]).get(\"duplex\", None)) is not None and duplex != duplex_full) or (\n (members := interface.get(\"memberInterfaces\", None)) is not None and any(stats[\"duplex\"] != duplex_full for stats in members.values())\n ):\n self.result.is_failure(f\"Interface {intf} or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented.\")\n return\n\n if (bandwidth := interfaces[\"interfaces\"][intf][\"bandwidth\"]) == 0:\n self.logger.debug(\"Interface %s has been ignored due to null bandwidth value\", intf)\n continue\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.VerifyInterfacesSpeed","title":"VerifyInterfacesSpeed","text":"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces. If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input. If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input. Expected Results Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex. Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex. Examples anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n Source code in anta/tests/interfaces.py class VerifyInterfacesSpeed(AntaTest):\n \"\"\"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces.\n\n - If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input.\n - If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input.\n\n Expected Results\n ----------------\n * Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex.\n * Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n class Input(AntaTest.Input):\n \"\"\"Inputs for the VerifyInterfacesSpeed test.\"\"\"\n\n interfaces: list[InterfaceDetail]\n \"\"\"List of interfaces to be tested\"\"\"\n\n class InterfaceDetail(BaseModel):\n \"\"\"Detail of an interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"The name of the interface.\"\"\"\n auto: bool\n \"\"\"The auto-negotiation status of the interface.\"\"\"\n speed: float = Field(ge=1, le=1000)\n \"\"\"The speed of the interface in Gigabits per second. Valid range is 1 to 1000.\"\"\"\n lanes: None | int = Field(None, ge=1, le=8)\n \"\"\"The number of lanes in the interface. Valid range is 1 to 8. This field is optional.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesSpeed.\"\"\"\n self.result.is_success()\n command_output = self.instance_commands[0].json_output\n\n # Iterate over all the interfaces\n for interface in self.inputs.interfaces:\n intf = interface.name\n\n # Check if interface exists\n if not (interface_output := get_value(command_output, f\"interfaces.{intf}\")):\n self.result.is_failure(f\"Interface `{intf}` is not found.\")\n continue\n\n auto_negotiation = interface_output.get(\"autoNegotiate\")\n actual_lanes = interface_output.get(\"lanes\")\n\n # Collecting actual interface details\n actual_interface_output = {\n \"auto negotiation\": auto_negotiation if interface.auto is True else None,\n \"duplex mode\": interface_output.get(\"duplex\"),\n \"speed\": interface_output.get(\"bandwidth\"),\n \"lanes\": actual_lanes if interface.lanes is not None else None,\n }\n\n # Forming expected interface details\n expected_interface_output = {\n \"auto negotiation\": \"success\" if interface.auto is True else None,\n \"duplex mode\": \"duplexFull\",\n \"speed\": interface.speed * BPS_GBPS_CONVERSIONS,\n \"lanes\": interface.lanes,\n }\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n for output in [actual_interface_output, expected_interface_output]:\n # Convert speed to Gbps for readability\n if output[\"speed\"] is not None:\n output[\"speed\"] = f\"{custom_division(output['speed'], BPS_GBPS_CONVERSIONS)}Gbps\"\n failed_log = get_failed_logs(expected_interface_output, actual_interface_output)\n self.result.is_failure(f\"For interface {intf}:{failed_log}\\n\")\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceDetail] List of interfaces to be tested -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"InterfaceDetail","text":"Name Type Description Default name EthernetInterface The name of the interface. - auto bool The auto-negotiation status of the interface. - speed float The speed of the interface in Gigabits per second. Valid range is 1 to 1000. Field(ge=1, le=1000) lanes None | int The number of lanes in the interface. Valid range is 1 to 8. This field is optional. Field(None, ge=1, le=8)"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus","title":"VerifyInterfacesStatus","text":"Verifies the operational states of specified interfaces to ensure they match expected configurations. This test performs the following checks for each specified interface: If line_protocol_status is defined, both status and line_protocol_status are verified for the specified interface. If line_protocol_status is not provided but the status is \u201cup\u201d, it is assumed that both the status and line protocol should be \u201cup\u201d. If the interface status is not \u201cup\u201d, only the interface\u2019s status is validated, with no line protocol check performed. Expected Results Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces. Failure: If any of the following occur: The specified interface is not configured. The specified interface status and line protocol status does not match the expected operational state for any interface. 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 the operational states of specified interfaces to ensure they match expected configurations.\n\n This test performs the following checks for each specified interface:\n\n 1. If `line_protocol_status` is defined, both `status` and `line_protocol_status` are verified for the specified interface.\n 2. If `line_protocol_status` is not provided but the `status` is \"up\", it is assumed that both the status and line protocol should be \"up\".\n 3. If the interface `status` is not \"up\", only the interface's status is validated, with no line protocol check performed.\n\n Expected Results\n ----------------\n * Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces.\n * Failure: If any of the following occur:\n - The specified interface is not configured.\n - The specified interface status and line protocol status does not match the expected operational state for any interface.\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesStatus.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output\n for interface in self.inputs.interfaces:\n if (intf_status := get_value(command_output[\"interfaceDescriptions\"], interface.name, separator=\"..\")) is None:\n self.result.is_failure(f\"{interface.name} - Not configured\")\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 actual_state = f\"Expected: {interface.status}/{interface.line_protocol_status}, Actual: {status}/{proto}\"\n self.result.is_failure(f\"{interface.name} - {actual_state}\")\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\"):\n self.result.is_failure(f\"{interface.name} - Expected: up/up, Actual: {status}/{proto}\")\n elif interface.status != status:\n self.result.is_failure(f\"{interface.name} - Expected: {interface.status}, Actual: {status}\")\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.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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip virtual-router\", revision=2)]\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 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\", revision=1)]\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 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\", revision=1)]\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.VerifyLACPInterfacesStatus","title":"VerifyLACPInterfacesStatus","text":"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces. Verifies that the interface is a member of the LACP port channel. Ensures that the synchronization is established. Ensures the interfaces are in the correct state for collecting and distributing traffic. Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \u201cslow\u201d mode, is the default setting.) Expected Results Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct. Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct. Examples anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n Source code in anta/tests/interfaces.py class VerifyLACPInterfacesStatus(AntaTest):\n \"\"\"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces.\n\n - Verifies that the interface is a member of the LACP port channel.\n - Ensures that the synchronization is established.\n - Ensures the interfaces are in the correct state for collecting and distributing traffic.\n - Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \"slow\" mode, is the default setting.)\n\n Expected Results\n ----------------\n * Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct.\n * Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show lacp interface {interface}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLACPInterfacesStatus test.\"\"\"\n\n interfaces: list[LACPInterface]\n \"\"\"List of LACP member interface.\"\"\"\n\n class LACPInterface(BaseModel):\n \"\"\"Model for an LACP member interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"Ethernet interface to validate.\"\"\"\n portchannel: PortChannelInterface\n \"\"\"Port Channel in which the interface is bundled.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each interface in the input list.\"\"\"\n return [template.render(interface=interface.name) for interface in self.inputs.interfaces]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLACPInterfacesStatus.\"\"\"\n self.result.is_success()\n\n # Member port verification parameters.\n member_port_details = [\"activity\", \"aggregation\", \"synchronization\", \"collecting\", \"distributing\", \"timeout\"]\n\n # Iterating over command output for different interfaces\n for command, input_entry in zip(self.instance_commands, self.inputs.interfaces):\n interface = input_entry.name\n portchannel = input_entry.portchannel\n\n # Verify if a PortChannel is configured with the provided interface\n if not (interface_details := get_value(command.json_output, f\"portChannels.{portchannel}.interfaces.{interface}\")):\n self.result.is_failure(f\"Interface '{interface}' is not configured to be a member of LACP '{portchannel}'.\")\n continue\n\n # Verify the interface is bundled in port channel.\n actor_port_status = interface_details.get(\"actorPortStatus\")\n if actor_port_status != \"bundled\":\n message = f\"For Interface {interface}:\\nExpected `bundled` as the local port status, but found `{actor_port_status}` instead.\\n\"\n self.result.is_failure(message)\n continue\n\n # Collecting actor and partner port details\n actor_port_details = interface_details.get(\"actorPortState\", {})\n partner_port_details = interface_details.get(\"partnerPortState\", {})\n\n # Collecting actual interface details\n actual_interface_output = {\n \"actor_port_details\": {param: actor_port_details.get(param, \"NotFound\") for param in member_port_details},\n \"partner_port_details\": {param: partner_port_details.get(param, \"NotFound\") for param in member_port_details},\n }\n\n # Forming expected interface details\n expected_details = {param: param != \"timeout\" for param in member_port_details}\n expected_interface_output = {\"actor_port_details\": expected_details, \"partner_port_details\": expected_details}\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n message = f\"For Interface {interface}:\\n\"\n actor_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"actor_port_details\", {}), actual_interface_output.get(\"actor_port_details\", {})\n )\n partner_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"partner_port_details\", {}), actual_interface_output.get(\"partner_port_details\", {})\n )\n\n if actor_port_failed_log:\n message += f\"Actor port details:{actor_port_failed_log}\\n\"\n if partner_port_failed_log:\n message += f\"Partner port details:{partner_port_failed_log}\\n\"\n\n self.result.is_failure(message)\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[LACPInterface] List of LACP member interface. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"LACPInterface","text":"Name Type Description Default name EthernetInterface Ethernet interface to validate. - portchannel PortChannelInterface Port Channel in which the interface is bundled. -"},{"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 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\", revision=1)]\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: # pragma: no branch\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show port-channel\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip interface brief\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show storm-control\", revision=1)]\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.interfaces/#input-models","title":"Input models","text":""},{"location":"api/tests.interfaces/#anta.input_models.interfaces.InterfaceState","title":"InterfaceState","text":"Model for an interface state. 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 Source code in anta/input_models/interfaces.py 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"},{"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 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\", revision=1)]\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 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 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. This test performs the following checks: Retrieves the device\u2019s configured FQDN Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message includes the complete FQDN of the device Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the device\u2019s complete FQDN. Failure: If any of the following occur: The test message is not found in recent logs The log message does not include the device\u2019s FQDN The FQDN in the log message doesn\u2019t match the configured 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 This test performs the following checks:\n\n 1. Retrieves the device's configured FQDN\n 2. Sends a test log message at the **informational** level\n 3. Retrieves the most recent logs (last 30 seconds)\n 4. Verifies that the test message includes the complete FQDN of the device\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the device's complete FQDN.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The log message does not include the device's FQDN\n - The FQDN in the log message doesn't match the configured FQDN\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingHostname:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show hostname\", revision=1),\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingHostname validation\", ofmt=\"text\"),\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 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. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message was successfully logged Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are being generated and the test message is found in recent logs. Failure: If any of the following occur: The test message is not found in recent logs The logging system is not capturing new messages No logs are being 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 This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message was successfully logged\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are being generated and the test message is found in recent logs.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The logging system is not capturing new messages\n - No logs are being generated\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingLogsGeneration:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingLogsGeneration validation\", ofmt=\"text\"),\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 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 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 VerifyLoggingSourceIntf 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 VerifyLoggingSourceIntf.\"\"\"\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 appropriate timestamp. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message is present with a high-resolution RFC3339 timestamp format Example format: 2024-01-25T15:30:45.123456+00:00 Includes microsecond precision Contains timezone offset Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the correct high-resolution RFC3339 timestamp format. Failure: If any of the following occur: The test message is not found in recent logs The timestamp format does not match the expected RFC3339 format 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 appropriate timestamp.\n\n This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message is present with a high-resolution RFC3339 timestamp format\n - Example format: `2024-01-25T15:30:45.123456+00:00`\n - Includes microsecond precision\n - Contains timezone offset\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the correct high-resolution RFC3339 timestamp format.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The timestamp format does not match the expected RFC3339 format\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingTimestamp:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingTimestamp validation\", ofmt=\"text\"),\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(\n logger: logging.Logger, command_output: str\n) -> str\n Parse show logging output and gets operational logging states used in the tests in this module. Parameters: Name Type Description Default logger Logger The logger object. required command_output str The show logging output. required 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 Parameters\n ----------\n logger\n The logger object.\n command_output\n The `show logging` output.\n\n Returns\n -------\n str\n 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":"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 Adaptive Virtual Topology BFD Configuration Connectivity Field Notices Flow Tracking GreenT Hardware Interfaces LANZ Logging MLAG Multicast Profiles PTP Router Path Selection Routing Generic Routing BGP Routing ISIS Routing OSPF Security Services SNMP Software STP STUN 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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag config-sanity\", revision=1)]\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 command_output[\"mlagActive\"] 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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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.path_selection/","title":"Router Path Selection","text":""},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifyPathsHealth","title":"VerifyPathsHealth","text":"Verifies the path and telemetry state of all paths under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if all path states under router path-selection are either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and their telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if any path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifyPathsHealth:\n Source code in anta/tests/path_selection.py class VerifyPathsHealth(AntaTest):\n \"\"\"Verifies the path and telemetry state of all paths under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if all path states under router path-selection are either 'IPsec established' or 'Resolved'\n and their telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if any path state is not 'IPsec established' or 'Resolved',\n or the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifyPathsHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show path-selection paths\", revision=1)]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyPathsHealth.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"dpsPeers\"]\n\n # If no paths are configured for router path-selection, the test fails\n if not command_output:\n self.result.is_failure(\"No path configured for router path-selection.\")\n return\n\n # Check the state of each path\n for peer, peer_data in command_output.items():\n for group, group_data in peer_data[\"dpsGroups\"].items():\n for path_data in group_data[\"dpsPaths\"].values():\n path_state = path_data[\"state\"]\n session = path_data[\"dpsSessions\"][\"0\"][\"active\"]\n\n # If the path state of any path is not 'ipsecEstablished' or 'routeResolved', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for peer {peer} in path-group {group} is `{path_state}`.\")\n\n # If the telemetry state of any path is inactive, the test fails\n elif not session:\n self.result.is_failure(f\"Telemetry state for peer {peer} in path-group {group} is `inactive`.\")\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath","title":"VerifySpecificPath","text":"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if the path state under router path-selection is either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if the path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or if the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n Source code in anta/tests/path_selection.py class VerifySpecificPath(AntaTest):\n \"\"\"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if the path state under router path-selection is either 'IPsec established' or 'Resolved'\n and telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if the path state is not 'IPsec established' or 'Resolved',\n or if the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show path-selection paths peer {peer} path-group {group} source {source} destination {destination}\", revision=1)\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificPath test.\"\"\"\n\n paths: list[RouterPath]\n \"\"\"List of router paths to verify.\"\"\"\n\n class RouterPath(BaseModel):\n \"\"\"Detail of a router path.\"\"\"\n\n peer: IPv4Address\n \"\"\"Static peer IPv4 address.\"\"\"\n\n path_group: str\n \"\"\"Router path group name.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of path.\"\"\"\n\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of path.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each router path.\"\"\"\n return [\n template.render(peer=path.peer, group=path.path_group, source=path.source_address, destination=path.destination_address) for path in self.inputs.paths\n ]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificPath.\"\"\"\n self.result.is_success()\n\n # Check the state of each path\n for command in self.instance_commands:\n peer = command.params.peer\n path_group = command.params.group\n source = command.params.source\n destination = command.params.destination\n command_output = command.json_output.get(\"dpsPeers\", [])\n\n # If the peer is not configured for the path group, the test fails\n if not command_output:\n self.result.is_failure(f\"Path `peer: {peer} source: {source} destination: {destination}` is not configured for path-group `{path_group}`.\")\n continue\n\n # Extract the state of the path\n path_output = get_value(command_output, f\"{peer}..dpsGroups..{path_group}..dpsPaths\", separator=\"..\")\n path_state = next(iter(path_output.values())).get(\"state\")\n session = get_value(next(iter(path_output.values())), \"dpsSessions.0.active\")\n\n # If the state of the path is not 'ipsecEstablished' or 'routeResolved', or the telemetry state is 'inactive', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `{path_state}`.\")\n elif not session:\n self.result.is_failure(\n f\"Telemetry state for path `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `inactive`.\"\n )\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"Inputs","text":"Name Type Description Default paths list[RouterPath] List of router paths to verify. -"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"RouterPath","text":"Name Type Description Default peer IPv4Address Static peer IPv4 address. - path_group str Router path group name. - source_address IPv4Address Source IPv4 address of path. - destination_address IPv4Address Destination IPv4 address of path. -"},{"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 description = \"Verifies the device TCAM profile.\"\n categories: ClassVar[list[str]] = [\"profiles\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hardware tcam profile\", revision=1)]\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 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\", revision=1)]\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\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 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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpLockStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpModeStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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 on the device. 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 on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpOffset:\n ```\n \"\"\"\n\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\", revision=1)]\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 description = \"Verifies the PTP interfaces state.\"\n categories: ClassVar[list[str]] = [\"ptp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", revision=2)]\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/#tests","title":"Tests","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 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\", revision=3)]\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 the advertised and received routes of BGP peers. 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 the advertised and received routes of BGP peers.\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show bgp neighbors {peer} advertised-routes vrf {vrf}\", revision=3),\n AntaTemplate(template=\"show bgp neighbors {peer} routes vrf {vrf}\", revision=3),\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 [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\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 = command.params.peer\n vrf = command.params.vrf\n for input_entry in self.inputs.bgp_peers:\n if str(input_entry.peer_address) == peer and input_entry.vrf == vrf:\n advertised_routes = input_entry.advertised_routes\n received_routes = input_entry.received_routes\n break\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 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\", revision=3)]\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 given address families. This test performs the following checks for each specified address family: Confirms that the specified VRF is configured. Counts the number of peers that are: If check_peer_state is set to True, Counts the number of BGP peers that are in the Established state and have successfully negotiated the specified AFI/SAFI If check_peer_state is set to False, skips validation of the Established state and AFI/SAFI negotiation. Expected Results Success: If the count of BGP peers matches the expected count with check_peer_state enabled/disabled. Failure: If any of the following occur: The specified VRF is not configured. The BGP peer count does not match expected value with check_peer_state enabled/disabled.\u201d 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 given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Confirms that the specified VRF is configured.\n 2. Counts the number of peers that are:\n - If `check_peer_state` is set to True, Counts the number of BGP peers that are in the `Established` state and\n have successfully negotiated the specified AFI/SAFI\n - If `check_peer_state` is set to False, skips validation of the `Established` state and AFI/SAFI negotiation.\n\n Expected Results\n ----------------\n * Success: If the count of BGP peers matches the expected count with `check_peer_state` enabled/disabled.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - The BGP peer count does not match expected value with `check_peer_state` enabled/disabled.\"\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp summary vrf all\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerCount test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'num_peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.num_peers is None:\n msg = f\"{af} 'num_peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerCount.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n peers_data = vrf_output.get(\"peers\", {}).values()\n if not address_family.check_peer_state:\n # Count the number of peers without considering the state and negotiated AFI/SAFI check if the count matches the expected count\n peer_count = sum(1 for peer_data in peers_data if address_family.eos_key in peer_data)\n else:\n # Count the number of established peers with negotiated AFI/SAFI\n peer_count = sum(\n 1\n for peer_data in peers_data\n if peer_data.get(\"peerState\") == \"Established\" and get_value(peer_data, f\"{address_family.eos_key}.afiSafiState\") == \"negotiated\"\n )\n\n # Check if the count matches the expected count\n if address_family.num_peers != peer_count:\n self.result.is_failure(f\"{address_family} - Expected: {address_family.num_peers}, Actual: {peer_count}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats","title":"VerifyBGPPeerDropStats","text":"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s). By default, all drop statistics counters will be checked for any non-zero values. An optional list of specific drop statistics can be provided for granular testing. Expected Results Success: The test will pass if the BGP peer\u2019s drop statistic(s) are zero. Failure: The test will fail if the BGP peer\u2019s drop statistic(s) are non-zero/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerDropStats(AntaTest):\n \"\"\"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s).\n\n By default, all drop statistics counters will be checked for any non-zero values.\n An optional list of specific drop statistics can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's drop statistic(s) are zero.\n * Failure: The test will fail if the BGP peer's drop statistic(s) are non-zero/Not Found or peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerDropStats 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 drop_stats: list[BgpDropStats] | None = None\n \"\"\"Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerDropStats.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n drop_statistics = input_entry.drop_stats\n\n # Verify BGP peer\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify BGP peer's drop stats\n drop_stats_output = peer_detail.get(\"dropStats\", {})\n\n # In case drop stats not provided, It will check all drop statistics\n if not drop_statistics:\n drop_statistics = drop_stats_output\n\n # Verify BGP peer's drop stats\n drop_stats_not_ok = {\n drop_stat: drop_stats_output.get(drop_stat, \"Not Found\") for drop_stat in drop_statistics if drop_stats_output.get(drop_stat, \"Not Found\")\n }\n if any(drop_stats_not_ok):\n failures[peer] = {vrf: drop_stats_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero NLRI drop statistics counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats-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.VerifyBGPPeerDropStats-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' drop_stats list[BgpDropStats] | None Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics. None"},{"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 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\", revision=3)]\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. Supports strict: True to verify that only the specified capabilities are configured, requiring an exact match. 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 strict: False\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 Supports `strict: True` to verify that only the specified capabilities are configured, requiring an exact match.\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 strict: False\n capabilities:\n - ipv4Unicast\n ```\n \"\"\"\n\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\", revision=3)]\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 strict: bool = False\n \"\"\"If True, requires exact matching of provided capabilities. Defaults to False.\"\"\"\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 # Fetching the capabilities output.\n bgp_output = get_value(bgp_output, \"neighborCapabilities.multiprotocolCaps\")\n\n if bgp_peer.strict and sorted(capabilities) != sorted(bgp_output):\n failure[\"bgp_peers\"][peer][vrf] = {\n \"status\": f\"Expected only `{', '.join(capabilities)}` capabilities should be listed but found `{', '.join(bgp_output)}` instead.\"\n }\n failures = deep_update(failures, failure)\n continue\n\n # Check each capability\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' strict bool If True, requires exact matching of provided capabilities. Defaults to False. False capabilities list[MultiProtocolCaps] List of multiprotocol capabilities to be verified. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit","title":"VerifyBGPPeerRouteLimit","text":"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s). Expected Results Success: The test will pass if the BGP peer\u2019s maximum routes and, if provided, the maximum routes warning limit are equal to the given limits. Failure: The test will fail if the BGP peer\u2019s maximum routes do not match the given limit, or if the maximum routes warning limit is provided and does not match the given limit, or if the peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerRouteLimit(AntaTest):\n \"\"\"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's maximum routes and, if provided, the maximum routes warning limit are equal to the given limits.\n * Failure: The test will fail if the BGP peer's maximum routes do not match the given limit, or if the maximum routes warning limit is provided\n and does not match the given limit, or if the peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerRouteLimit 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 maximum_routes: int = Field(ge=0, le=4294967294)\n \"\"\"The maximum allowable number of BGP routes, `0` means unlimited.\"\"\"\n warning_limit: int = Field(default=0, ge=0, le=4294967294)\n \"\"\"Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerRouteLimit.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n maximum_routes = input_entry.maximum_routes\n warning_limit = input_entry.warning_limit\n failure: dict[Any, Any] = {}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify maximum routes configured.\n if (actual_routes := peer_detail.get(\"maxTotalRoutes\", \"Not Found\")) != maximum_routes:\n failure[\"Maximum total routes\"] = actual_routes\n\n # Verify warning limit if given.\n if warning_limit and (actual_warning_limit := peer_detail.get(\"totalRoutesWarnLimit\", \"Not Found\")) != warning_limit:\n failure[\"Warning limit\"] = actual_warning_limit\n\n # Updated failures if any.\n if failure:\n failures[peer] = {vrf: failure}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peer(s) are not configured or maximum routes and maximum routes warning limit is not correct:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit-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.VerifyBGPPeerRouteLimit-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' maximum_routes int The maximum allowable number of BGP routes, `0` means unlimited. Field(ge=0, le=4294967294) warning_limit int Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit. Field(default=0, ge=0, le=4294967294)"},{"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 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\", revision=3)]\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.VerifyBGPPeerUpdateErrors","title":"VerifyBGPPeerUpdateErrors","text":"Verifies BGP update error counters for the provided BGP IPv4 peer(s). By default, all update error counters will be checked for any non-zero values. An optional list of specific update error counters can be provided for granular testing. Note: For \u201cdisabledAfiSafi\u201d error counter field, checking that it\u2019s not \u201cNone\u201d versus 0. Expected Results Success: The test will pass if the BGP peer\u2019s update error counter(s) are zero/None. Failure: The test will fail if the BGP peer\u2019s update error counter(s) are non-zero/not None/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerUpdateErrors(AntaTest):\n \"\"\"Verifies BGP update error counters for the provided BGP IPv4 peer(s).\n\n By default, all update error counters will be checked for any non-zero values.\n An optional list of specific update error counters can be provided for granular testing.\n\n Note: For \"disabledAfiSafi\" error counter field, checking that it's not \"None\" versus 0.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's update error counter(s) are zero/None.\n * Failure: The test will fail if the BGP peer's update error counter(s) are non-zero/not None/Not Found or\n peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerUpdateErrors 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 update_errors: list[BgpUpdateError] | None = None\n \"\"\"Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerUpdateErrors.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n update_error_counters = input_entry.update_errors\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Getting the BGP peer's error counters output.\n error_counters_output = peer_detail.get(\"peerInUpdateErrors\", {})\n\n # In case update error counters not provided, It will check all the update error counters.\n if not update_error_counters:\n update_error_counters = error_counters_output\n\n # verifying the error counters.\n error_counters_not_ok = {\n (\"disabledAfiSafi\" if error_counter == \"disabledAfiSafi\" else error_counter): value\n for error_counter in update_error_counters\n if (value := error_counters_output.get(error_counter, \"Not Found\")) != \"None\" and value != 0\n }\n if error_counters_not_ok:\n failures[peer] = {vrf: error_counters_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero update error counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerUpdateErrors-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.VerifyBGPPeerUpdateErrors-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' update_errors list[BgpUpdateError] | None Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters. None"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth","title":"VerifyBGPPeersHealth","text":"Verifies the health of BGP peers for given address families. This test performs the following checks for each specified address family: Validates that the VRF is configured. Checks if there are any peers for the given AFI/SAFI. For each relevant peer: Verifies that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Checks that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified address families and their peers. Failure: If any of the following occur: The specified VRF is not configured. No peers are found for a given AFI/SAFI. Any BGP session is not in the Established state. The AFI/SAFI state is not \u2018negotiated\u2019 for any peer. Any TCP message queue (input or output) is not empty when check_tcp_queues is True (default). 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 check_tcp_queues: false\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeersHealth(AntaTest):\n \"\"\"Verifies the health of BGP peers for given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Validates that the VRF is configured.\n 2. Checks if there are any peers for the given AFI/SAFI.\n 3. For each relevant peer:\n - Verifies that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Checks that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified address families and their peers.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - No peers are found for a given AFI/SAFI.\n - Any BGP session is not in the `Established` state.\n - The AFI/SAFI state is not 'negotiated' for any peer.\n - Any TCP message queue (input or output) is not empty when `check_tcp_queues` is `True` (default).\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 check_tcp_queues: false\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeersHealth test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeersHealth.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n # Check if any peers are found for this AFI/SAFI\n relevant_peers = [\n peer for peer in vrf_output.get(\"peerList\", []) if get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\") is not None\n ]\n\n if not relevant_peers:\n self.result.is_failure(f\"{address_family} - No peers found\")\n continue\n\n for peer in relevant_peers:\n # Check if the BGP session is established\n if peer[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session state is not established; State: {peer['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers","title":"VerifyBGPSpecificPeers","text":"Verifies the health of specific BGP peer(s) for given address families. This test performs the following checks for each specified address family and peer: Confirms that the specified VRF is configured. For each specified peer: Verifies that the peer is found in the BGP configuration. Checks that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Ensures that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified peers in all address families. Failure: If any of the following occur: The specified VRF is not configured. A specified peer is not found in the BGP configuration. The BGP session for a peer is not in the Established state. The AFI/SAFI state is not negotiated for a peer. Any TCP message queue (input or output) is not empty for a peer when check_tcp_queues is True (default). 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) for given address families.\n\n This test performs the following checks for each specified address family and peer:\n\n 1. Confirms that the specified VRF is configured.\n 2. For each specified peer:\n - Verifies that the peer is found in the BGP configuration.\n - Checks that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Ensures that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified peers in all address families.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - A specified peer is not found in the BGP configuration.\n - The BGP session for a peer is not in the `Established` state.\n - The AFI/SAFI state is not `negotiated` for a peer.\n - Any TCP message queue (input or output) is not empty for a peer when `check_tcp_queues` is `True` (default).\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPSpecificPeers test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.peers is None:\n msg = f\"{af} 'peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPSpecificPeers.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n for peer in address_family.peers:\n peer_ip = str(peer)\n\n # Check if the peer is found\n if (peer_data := get_item(vrf_output[\"peerList\"], \"peerAddress\", peer_ip)) is None:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Not configured\")\n continue\n\n # Check if the BGP session is established\n if peer_data[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session state is not established; State: {peer_data['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer_data, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not capability_status:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated\")\n\n if capability_status and not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer_data[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer_data[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"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 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\", revision=3)]\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.VerifyBgpRouteMaps","title":"VerifyBgpRouteMaps","text":"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s). Expected Results Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF. Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction. Examples anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n Source code in anta/tests/routing/bgp.py class VerifyBgpRouteMaps(AntaTest):\n \"\"\"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF.\n * Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBgpRouteMaps 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 inbound_route_map: str | None = None\n \"\"\"Inbound route map applied, defaults to None.\"\"\"\n outbound_route_map: str | None = None\n \"\"\"Outbound route map applied, defaults to None.\"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpPeer class.\n\n At least one of 'inbound' or 'outbound' route-map must be provided.\n \"\"\"\n if not (self.inbound_route_map or self.outbound_route_map):\n msg = \"At least one of 'inbound_route_map' or 'outbound_route_map' must be provided.\"\n raise ValueError(msg)\n return self\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBgpRouteMaps.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n inbound_route_map = input_entry.inbound_route_map\n outbound_route_map = input_entry.outbound_route_map\n failure: dict[Any, Any] = {vrf: {}}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify Inbound route-map\n if inbound_route_map and (inbound_map := peer_detail.get(\"routeMapInbound\", \"Not Configured\")) != inbound_route_map:\n failure[vrf].update({\"Inbound route-map\": inbound_map})\n\n # Verify Outbound route-map\n if outbound_route_map and (outbound_map := peer_detail.get(\"routeMapOutbound\", \"Not Configured\")) != outbound_route_map:\n failure[vrf].update({\"Outbound route-map\": outbound_map})\n\n if failure[vrf]:\n failures[peer] = failure\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(\n f\"The following BGP peers are not configured or has an incorrect or missing route map in either the inbound or outbound direction:\\n{failures}\"\n )\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBgpRouteMaps-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.VerifyBgpRouteMaps-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' inbound_route_map str | None Inbound route map applied, defaults to None. None outbound_route_map str | None Outbound route map applied, defaults to None. None"},{"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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp evpn route-type mac-ip {address} vni {vni}\", revision=2)]\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=str(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 = 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/#input-models","title":"Input models","text":""},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily","title":"BgpAddressFamily","text":"Model for a BGP address family. Name Type Description Default afi Afi BGP Address Family Identifier (AFI). - safi Safi | None BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`. None vrf str Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`. If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`. These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6. 'default' num_peers PositiveInt | None Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test. None peers list[IPv4Address | IPv6Address] | None List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test. None check_tcp_queues bool Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`. Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests. True check_peer_state bool Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`. Can be enabled in the `VerifyBGPPeerCount` tests. False Source code in anta/input_models/routing/bgp.py class BgpAddressFamily(BaseModel):\n \"\"\"Model for a BGP address family.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n afi: Afi\n \"\"\"BGP Address Family Identifier (AFI).\"\"\"\n safi: Safi | None = None\n \"\"\"BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`.\"\"\"\n vrf: str = \"default\"\n \"\"\"Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`.\n\n If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`.\n\n These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6.\n \"\"\"\n num_peers: PositiveInt | None = None\n \"\"\"Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test.\"\"\"\n peers: list[IPv4Address | IPv6Address] | None = None\n \"\"\"List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test.\"\"\"\n check_tcp_queues: bool = True\n \"\"\"Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`.\n\n Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests.\n \"\"\"\n check_peer_state: bool = False\n \"\"\"Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`.\n\n Can be enabled in the `VerifyBGPPeerCount` tests.\n \"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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 @property\n def eos_key(self) -> str:\n \"\"\"AFI/SAFI EOS key representation.\"\"\"\n # Pydantic handles the validation of the AFI/SAFI combination, so we can ignore error handling here.\n return AFI_SAFI_EOS_KEY[(self.afi, self.safi)]\n\n def __str__(self) -> str:\n \"\"\"Return a string representation of the BgpAddressFamily model. Used in failure messages.\n\n Examples\n --------\n - AFI:ipv4 SAFI:unicast VRF:default\n - AFI:evpn\n \"\"\"\n base_string = f\"AFI: {self.afi}\"\n if self.safi is not None:\n base_string += f\" SAFI: {self.safi}\"\n if self.afi in [\"ipv4\", \"ipv6\"]:\n base_string += f\" VRF: {self.vrf}\"\n return base_string\n"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily.validate_inputs","title":"validate_inputs","text":"validate_inputs() -> Self\n Validate the inputs provided to the BgpAddressFamily class. If afi is either ipv4 or ipv6, safi must be provided. If afi is not ipv4 or ipv6, safi must NOT be provided and vrf must be default. Source code in anta/input_models/routing/bgp.py @model_validator(mode=\"after\")\ndef validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAfi","title":"BgpAfi","text":"Alias for the BgpAddressFamily model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the BgpAddressFamily model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/routing/bgp.py class BgpAfi(BgpAddressFamily): # pragma: no cover\n \"\"\"Alias for the BgpAddressFamily model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the BgpAddressFamily model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the BgpAfi class, emitting a deprecation warning.\"\"\"\n warn(\n message=\"BgpAfi model is deprecated and will be removed in ANTA v2.0.0. Use the BgpAddressFamily model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\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. 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.\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 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 categories: ClassVar[list[str]] = [\"routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show ip route vrf {vrf} {route}\", revision=4),\n AntaTemplate(template=\"show ip route vrf {vrf}\", revision=4),\n ]\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 collect: Literal[\"one\", \"all\"] = \"one\"\n \"\"\"Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one`\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for the input vrf.\"\"\"\n if template == VerifyRoutingTableEntry.commands[0] and self.inputs.collect == \"one\":\n return [template.render(vrf=self.inputs.vrf, route=route) for route in self.inputs.routes]\n\n if template == VerifyRoutingTableEntry.commands[1] and self.inputs.collect == \"all\":\n return [template.render(vrf=self.inputs.vrf)]\n\n return []\n\n @staticmethod\n @cache\n def ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRoutingTableEntry.\"\"\"\n commands_output_route_ips = set()\n\n for command in self.instance_commands:\n command_output_vrf = command.json_output[\"vrfs\"][self.inputs.vrf]\n commands_output_route_ips |= {self.ip_interface_ip(route) for route in command_output_vrf[\"routes\"]}\n\n missing_routes = [str(route) for route in self.inputs.routes if route not in commands_output_route_ips]\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. - collect Literal['one', 'all'] Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one` 'one'"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableEntry.ip_interface_ip","title":"ip_interface_ip cached staticmethod","text":"ip_interface_ip(route: str) -> IPv4Address\n Return the IP address of the provided ip route with mask. Source code in anta/tests/routing/generic.py @staticmethod\n@cache\ndef ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n"},{"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 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: PositiveInteger\n \"\"\"Expected minimum routing table size.\"\"\"\n maximum: PositiveInteger\n \"\"\"Expected maximum routing table size.\"\"\"\n\n @model_validator(mode=\"after\")\n def check_min_max(self) -> Self:\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 PositiveInteger Expected minimum routing table size. - maximum PositiveInteger Expected maximum routing table size. -"},{"location":"api/tests.routing.isis/","title":"ISIS","text":""},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode","title":"VerifyISISInterfaceMode","text":"Verifies ISIS Interfaces are running in correct mode. Expected Results Success: The test will pass if all listed interfaces are running in correct mode. Failure: The test will fail if any of the listed interfaces is not running in correct mode. Skipped: The test will be skipped if no ISIS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISInterfaceMode(AntaTest):\n \"\"\"Verifies ISIS Interfaces are running in correct mode.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces are running in correct mode.\n * Failure: The test will fail if any of the listed interfaces is not running in correct mode.\n * Skipped: The test will be skipped if no ISIS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n ```\n \"\"\"\n\n description = \"Verifies interface mode for IS-IS\"\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceState]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceState(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n mode: Literal[\"point-to-point\", \"broadcast\", \"passive\"]\n \"\"\"Number of IS-IS neighbors.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF where the interface should be configured\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISInterfaceMode.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # Check for p2p interfaces\n for interface in self.inputs.interfaces:\n interface_data = _get_interface_data(\n interface=interface.name,\n vrf=interface.vrf,\n command_output=command_output,\n )\n # Check for correct VRF\n if interface_data is not None:\n interface_type = get_value(dictionary=interface_data, key=\"interfaceType\", default=\"unset\")\n # Check for interfaceType\n if interface.mode == \"point-to-point\" and interface.mode != interface_type:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in {interface.mode} reporting {interface_type}\")\n # Check for passive\n elif interface.mode == \"passive\":\n json_path = f\"intfLevels.{interface.level}.passive\"\n if interface_data is None or get_value(dictionary=interface_data, key=json_path, default=False) is False:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in passive mode\")\n else:\n self.result.is_failure(f\"Interface {interface.name} not found in VRF {interface.vrf}\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceState] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"InterfaceState","text":"Name Type Description Default name Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 mode Literal['point-to-point', 'broadcast', 'passive'] Number of IS-IS neighbors. - vrf str VRF where the interface should be configured 'default'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount","title":"VerifyISISNeighborCount","text":"Verifies number of IS-IS neighbors per level and per interface. Expected Results Success: The test will pass if the number of neighbors is correct. Failure: The test will fail if the number of neighbors is incorrect. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborCount(AntaTest):\n \"\"\"Verifies number of IS-IS neighbors per level and per interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if the number of neighbors is correct.\n * Failure: The test will fail if the number of neighbors is incorrect.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceCount]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceCount(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: int = 2\n \"\"\"IS-IS level to check.\"\"\"\n count: int\n \"\"\"Number of IS-IS neighbors.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborCount.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n isis_neighbor_count = _get_isis_neighbors_count(command_output)\n if len(isis_neighbor_count) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n for interface in self.inputs.interfaces:\n eos_data = [ifl_data for ifl_data in isis_neighbor_count if ifl_data[\"interface\"] == interface.name and ifl_data[\"level\"] == interface.level]\n if not eos_data:\n self.result.is_failure(f\"No neighbor detected for interface {interface.name}\")\n continue\n if eos_data[0][\"count\"] != interface.count:\n self.result.is_failure(\n f\"Interface {interface.name}: \"\n f\"expected Level {interface.level}: count {interface.count}, \"\n f\"got Level {eos_data[0]['level']}: count {eos_data[0]['count']}\"\n )\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceCount] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"InterfaceCount","text":"Name Type Description Default name Interface Interface name to check. - level int IS-IS level to check. 2 count int Number of IS-IS neighbors. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborState","title":"VerifyISISNeighborState","text":"Verifies all IS-IS neighbors are in UP state. Expected Results Success: The test will pass if all IS-IS neighbors are in UP state. Failure: The test will fail if some IS-IS neighbors are not in UP state. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborState(AntaTest):\n \"\"\"Verifies all IS-IS neighbors are in UP state.\n\n Expected Results\n ----------------\n * Success: The test will pass if all IS-IS neighbors are in UP state.\n * Failure: The test will fail if some IS-IS neighbors are not in UP state.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis neighbors\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborState.\"\"\"\n command_output = self.instance_commands[0].json_output\n if _count_isis_neighbor(command_output) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n self.result.is_success()\n not_full_neighbors = _get_not_full_isis_neighbors(command_output)\n if not_full_neighbors:\n self.result.is_failure(f\"Some neighbors are not in the correct state (UP): {not_full_neighbors}.\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments","title":"VerifyISISSegmentRoutingAdjacencySegments","text":"Verify that all expected Adjacency segments are correctly visible for each interface. Expected Results Success: The test will pass if all listed interfaces have correct adjacencies. Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies. Skipped: The test will be skipped if no ISIS SR Adjacency is found. Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):\n \"\"\"Verify that all expected Adjacency segments are correctly visible for each interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces have correct adjacencies.\n * Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies.\n * Skipped: The test will be skipped if no ISIS SR Adjacency is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing adjacency-segments\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingAdjacencySegments test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n segments: list[Segment]\n \"\"\"List of Adjacency segments configured in this instance.\"\"\"\n\n class Segment(BaseModel):\n \"\"\"Segment model definition.\"\"\"\n\n interface: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n sid_origin: Literal[\"dynamic\"] = \"dynamic\"\n \"\"\"Adjacency type\"\"\"\n address: IPv4Address\n \"\"\"IP address of remote end of segment.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingAdjacencySegments.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routging.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n for input_segment in instance.segments:\n eos_segment = _get_adjacency_segment_data_by_neighbor(\n neighbor=str(input_segment.address),\n instance=instance.name,\n vrf=instance.vrf,\n command_output=command_output,\n )\n if eos_segment is None:\n failure_message.append(f\"Your segment has not been found: {input_segment}.\")\n\n elif (\n eos_segment[\"localIntf\"] != input_segment.interface\n or eos_segment[\"level\"] != input_segment.level\n or eos_segment[\"sidOrigin\"] != input_segment.sid_origin\n ):\n failure_message.append(f\"Your segment is not correct: Expected: {input_segment} - Found: {eos_segment}.\")\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' segments list[Segment] List of Adjacency segments configured in this instance. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Segment","text":"Name Type Description Default interface Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 sid_origin Literal['dynamic'] Adjacency type 'dynamic' address IPv4Address IP address of remote end of segment. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane","title":"VerifyISISSegmentRoutingDataplane","text":"Verify dataplane of a list of ISIS-SR instances. Expected Results Success: The test will pass if all instances have correct dataplane configured Failure: The test will fail if one of the instances has incorrect dataplane configured Skipped: The test will be skipped if ISIS is not running Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingDataplane(AntaTest):\n \"\"\"Verify dataplane of a list of ISIS-SR instances.\n\n Expected Results\n ----------------\n * Success: The test will pass if all instances have correct dataplane configured\n * Failure: The test will fail if one of the instances has incorrect dataplane configured\n * Skipped: The test will be skipped if ISIS is not running\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingDataplane test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n dataplane: Literal[\"MPLS\", \"mpls\", \"unset\"] = \"MPLS\"\n \"\"\"Configured dataplane for the instance.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingDataplane.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routing.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n eos_dataplane = get_value(dictionary=command_output, key=f\"vrfs.{instance.vrf}.isisInstances.{instance.name}.dataPlane\", default=None)\n if instance.dataplane.upper() != eos_dataplane:\n failure_message.append(f\"ISIS instance {instance.name} is not running dataplane {instance.dataplane} ({eos_dataplane})\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' dataplane Literal['MPLS', 'mpls', 'unset'] Configured dataplane for the instance. 'MPLS'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels","title":"VerifyISISSegmentRoutingTunnels","text":"Verify ISIS-SR tunnels computed by device. Expected Results Success: The test will pass if all listed tunnels are computed on device. Failure: The test will fail if one of the listed tunnels is missing. Skipped: The test will be skipped if ISIS-SR is not configured. Examples anta.tests.routing:\nisis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingTunnels(AntaTest):\n \"\"\"Verify ISIS-SR tunnels computed by device.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed tunnels are computed on device.\n * Failure: The test will fail if one of the listed tunnels is missing.\n * Skipped: The test will be skipped if ISIS-SR is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing tunnel\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingTunnels test.\"\"\"\n\n entries: list[Entry]\n \"\"\"List of tunnels to check on device.\"\"\"\n\n class Entry(BaseModel):\n \"\"\"Definition of a tunnel entry.\"\"\"\n\n endpoint: IPv4Network\n \"\"\"Endpoint IP of the tunnel.\"\"\"\n vias: list[Vias] | None = None\n \"\"\"Optional list of path to reach endpoint.\"\"\"\n\n class Vias(BaseModel):\n \"\"\"Definition of a tunnel path.\"\"\"\n\n nexthop: IPv4Address | None = None\n \"\"\"Nexthop of the tunnel. If None, then it is not tested. Default: None\"\"\"\n type: Literal[\"ip\", \"tunnel\"] | None = None\n \"\"\"Type of the tunnel. If None, then it is not tested. Default: None\"\"\"\n interface: Interface | None = None\n \"\"\"Interface of the tunnel. If None, then it is not tested. Default: None\"\"\"\n tunnel_id: Literal[\"TI-LFA\", \"ti-lfa\", \"unset\"] | None = None\n \"\"\"Computation method of the tunnel. If None, then it is not tested. Default: None\"\"\"\n\n def _eos_entry_lookup(self, search_value: IPv4Network, entries: dict[str, Any], search_key: str = \"endpoint\") -> dict[str, Any] | None:\n return next(\n (entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == str(search_value)),\n None,\n )\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingTunnels.\n\n This method performs the main test logic for verifying ISIS Segment Routing tunnels.\n It checks the command output, initiates defaults, and performs various checks on the tunnels.\n \"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n # initiate defaults\n failure_message = []\n\n if len(command_output[\"entries\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n for input_entry in self.inputs.entries:\n eos_entry = self._eos_entry_lookup(search_value=input_entry.endpoint, entries=command_output[\"entries\"])\n if eos_entry is None:\n failure_message.append(f\"Tunnel to {input_entry} is not found.\")\n elif input_entry.vias is not None:\n failure_src = []\n for via_input in input_entry.vias:\n if not self._check_tunnel_type(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel type\")\n if not self._check_tunnel_nexthop(via_input, eos_entry):\n failure_src.append(\"incorrect nexthop\")\n if not self._check_tunnel_interface(via_input, eos_entry):\n failure_src.append(\"incorrect interface\")\n if not self._check_tunnel_id(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel ID\")\n\n if failure_src:\n failure_message.append(f\"Tunnel to {input_entry.endpoint!s} is incorrect: {', '.join(failure_src)}\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n\n def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input tunnel type to check.\n eos_entry : dict[str, Any]\n The EOS entry containing the tunnel types.\n\n Returns\n -------\n bool\n True if the tunnel type matches any of the tunnel types in `eos_entry`, False otherwise.\n \"\"\"\n if via_input.type is not None:\n return any(\n via_input.type\n == get_value(\n dictionary=eos_via,\n key=\"type\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel nexthop matches the given input.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel nexthop matches, False otherwise.\n \"\"\"\n if via_input.nexthop is not None:\n return any(\n str(via_input.nexthop)\n == get_value(\n dictionary=eos_via,\n key=\"nexthop\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel interface exists in the given EOS entry.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel interface exists, False otherwise.\n \"\"\"\n if via_input.interface is not None:\n return any(\n via_input.interface\n == get_value(\n dictionary=eos_via,\n key=\"interface\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input vias to check.\n eos_entry : dict[str, Any])\n The EOS entry to compare against.\n\n Returns\n -------\n bool\n True if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias, False otherwise.\n \"\"\"\n if via_input.tunnel_id is not None:\n return any(\n via_input.tunnel_id.upper()\n == get_value(\n dictionary=eos_via,\n key=\"tunnelId.type\",\n default=\"undefined\",\n ).upper()\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Inputs","text":"Name Type Description Default entries list[Entry] List of tunnels to check on device. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Entry","text":"Name Type Description Default endpoint IPv4Network Endpoint IP of the tunnel. - vias list[Vias] | None Optional list of path to reach endpoint. None"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Vias","text":"Name Type Description Default nexthop IPv4Address | None Nexthop of the tunnel. If None, then it is not tested. Default: None None type Literal['ip', 'tunnel'] | None Type of the tunnel. If None, then it is not tested. Default: None None interface Interface | None Interface of the tunnel. If None, then it is not tested. Default: None None tunnel_id Literal['TI-LFA', 'ti-lfa', 'unset'] | None Computation method of the tunnel. If None, then it is not tested. Default: None None"},{"location":"api/tests.routing.ospf/","title":"OSPF","text":""},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFMaxLSA","title":"VerifyOSPFMaxLSA","text":"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold. Expected Results Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold. Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold. Skipped: The test will be skipped if no OSPF instance is found. Examples anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n Source code in anta/tests/routing/ospf.py class VerifyOSPFMaxLSA(AntaTest):\n \"\"\"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold.\n * Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold.\n * Skipped: The test will be skipped if no OSPF instance is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n ```\n \"\"\"\n\n description = \"Verifies all OSPF instances did not cross the maximum LSA threshold.\"\n categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyOSPFMaxLSA.\"\"\"\n command_output = self.instance_commands[0].json_output\n ospf_instance_info = _get_ospf_max_lsa_info(command_output)\n if not ospf_instance_info:\n self.result.is_skipped(\"No OSPF instance found.\")\n return\n all_instances_within_threshold = all(instance[\"numLsa\"] <= instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100) for instance in ospf_instance_info)\n if all_instances_within_threshold:\n self.result.is_success()\n else:\n exceeded_instances = [\n instance[\"instance\"] for instance in ospf_instance_info if instance[\"numLsa\"] > instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100)\n ]\n self.result.is_failure(f\"OSPF Instances {exceeded_instances} crossed the maximum LSA threshold.\")\n"},{"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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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.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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ip access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ipv6 access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show management security ssl certificate\", revision=1),\n AntaCommand(command=\"show clock\", revision=1),\n ]\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) -> Self:\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 get_args(RsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for RSA encryption. Allowed sizes are {get_args(RsaKeySize)}.\"\n raise ValueError(msg)\n\n if self.encryption_algorithm == \"ECDSA\" and self.key_size not in get_args(EcdsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for ECDSA encryption. Allowed sizes are {get_args(EcdsaKeySize)}.\"\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner login\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner motd\", revision=1)]\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.VerifyHardwareEntropy","title":"VerifyHardwareEntropy","text":"Verifies hardware entropy generation is enabled on device. Expected Results Success: The test will pass if hardware entropy generation is enabled. Failure: The test will fail if hardware entropy generation is not enabled. Examples anta.tests.security:\n - VerifyHardwareEntropy:\n Source code in anta/tests/security.py class VerifyHardwareEntropy(AntaTest):\n \"\"\"Verifies hardware entropy generation is enabled on device.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware entropy generation is enabled.\n * Failure: The test will fail if hardware entropy generation is not enabled.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyHardwareEntropy:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management security\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareEntropy.\"\"\"\n command_output = self.instance_commands[0].json_output\n\n # Check if hardware entropy generation is enabled.\n if not command_output.get(\"hardwareEntropyEnabled\"):\n self.result.is_failure(\"Hardware entropy generation is disabled.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.security/#anta.tests.security.VerifyIPSecConnHealth","title":"VerifyIPSecConnHealth","text":"Verifies all IPv4 security connections. Expected Results Success: The test will pass if all the IPv4 security connections are established in all vrf. Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf. Examples anta.tests.security:\n - VerifyIPSecConnHealth:\n Source code in anta/tests/security.py class VerifyIPSecConnHealth(AntaTest):\n \"\"\"Verifies all IPv4 security connections.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the IPv4 security connections are established in all vrf.\n * Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyIPSecConnHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip security connection vrf all\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyIPSecConnHealth.\"\"\"\n self.result.is_success()\n failure_conn = []\n command_output = self.instance_commands[0].json_output[\"connections\"]\n\n # Check if IP security connection is configured\n if not command_output:\n self.result.is_failure(\"No IPv4 security connection configured.\")\n return\n\n # Iterate over all ipsec connections\n for conn_data in command_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n failure_conn.append(f\"source:{source} destination:{destination} vrf:{vrf}\")\n if failure_conn:\n failure_msg = \"\\n\".join(failure_conn)\n self.result.is_failure(f\"The following IPv4 security connections are not established:\\n{failure_msg}.\")\n"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip access-lists {acl}\", revision=1)]\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) 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, acl in zip(self.instance_commands, self.inputs.ipv4_access_lists):\n # Collecting input ACL details\n acl_name = command_output.params.acl\n # Retrieve the expected entries from the inputs\n acl_entries = acl.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 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\", revision=1)]\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 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\", revision=1)]\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 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 try:\n line = next(line for line in command_output.split(\"\\n\") if line.startswith(\"SSHD status\"))\n except StopIteration:\n self.result.is_failure(\"Could not find SSH status in returned output.\")\n return\n status = line.split()[-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.VerifySpecificIPSecConn","title":"VerifySpecificIPSecConn","text":"Verifies the state of IPv4 security connections for a specified peer. It optionally allows for the verification of a specific path for a peer by providing source and destination addresses. If these addresses are not provided, it will verify all paths for the specified peer. Expected Results Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF. Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF. Examples anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n Source code in anta/tests/security.py class VerifySpecificIPSecConn(AntaTest):\n \"\"\"Verifies the state of IPv4 security connections for a specified peer.\n\n It optionally allows for the verification of a specific path for a peer by providing source and destination addresses.\n If these addresses are not provided, it will verify all paths for the specified peer.\n\n Expected Results\n ----------------\n * Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF.\n * Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n ```\n \"\"\"\n\n description = \"Verifies IPv4 security connections for a peer.\"\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip security connection vrf {vrf} path peer {peer}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificIPSecConn test.\"\"\"\n\n ip_security_connections: list[IPSecPeers]\n \"\"\"List of IP4v security peers.\"\"\"\n\n class IPSecPeers(BaseModel):\n \"\"\"Details of IPv4 security peers.\"\"\"\n\n peer: IPv4Address\n \"\"\"IPv4 address of the peer.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"Optional VRF for the IP security peer.\"\"\"\n\n connections: list[IPSecConn] | None = None\n \"\"\"Optional list of IPv4 security connections of a peer.\"\"\"\n\n class IPSecConn(BaseModel):\n \"\"\"Details of IPv4 security connections for a peer.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of the connection.\"\"\"\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of the connection.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input IP Sec connection.\"\"\"\n return [template.render(peer=conn.peer, vrf=conn.vrf) for conn in self.inputs.ip_security_connections]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificIPSecConn.\"\"\"\n self.result.is_success()\n for command_output, input_peer in zip(self.instance_commands, self.inputs.ip_security_connections):\n conn_output = command_output.json_output[\"connections\"]\n peer = command_output.params.peer\n vrf = command_output.params.vrf\n conn_input = input_peer.connections\n\n # Check if IPv4 security connection is configured\n if not conn_output:\n self.result.is_failure(f\"No IPv4 security connection configured for peer `{peer}`.\")\n continue\n\n # If connection details are not provided then check all connections of a peer\n if conn_input is None:\n for conn_data in conn_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source} destination:{destination} vrf:{vrf}` for peer `{peer}` is `Established` \"\n f\"but found `{state}` instead.\"\n )\n continue\n\n # Create a dictionary of existing connections for faster lookup\n existing_connections = {\n (conn_data.get(\"saddr\"), conn_data.get(\"daddr\"), conn_data.get(\"tunnelNs\")): next(iter(conn_data[\"pathDict\"].values()))\n for conn_data in conn_output.values()\n }\n for connection in conn_input:\n source_input = str(connection.source_address)\n destination_input = str(connection.destination_address)\n\n if (source_input, destination_input, vrf) in existing_connections:\n existing_state = existing_connections[(source_input, destination_input, vrf)]\n if existing_state != \"Established\":\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` \"\n f\"for peer `{peer}` is `Established` but found `{existing_state}` instead.\"\n )\n else:\n self.result.is_failure(\n f\"IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` for peer `{peer}` is not found.\"\n )\n"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"Inputs","text":"Name Type Description Default ip_security_connections list[IPSecPeers] List of IP4v security peers. -"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecPeers","text":"Name Type Description Default peer IPv4Address IPv4 address of the peer. - vrf str Optional VRF for the IP security peer. 'default' connections list[IPSecConn] | None Optional list of IPv4 security connections of a peer. None"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecConn","text":"Name Type Description Default source_address IPv4Address Source IPv4 address of the connection. - destination_address IPv4Address Destination IPv4 address of the connection. -"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management telnet\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=1)]\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. This test performs the following checks for each specified DNS Server: Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF. Ensuring an appropriate priority level. 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 any of the following conditions are met: The provided DNS server is not configured. The provided DNS server with designated VRF and priority does not match the expected information. 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 This test performs the following checks for each specified DNS Server:\n\n 1. Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF.\n 2. Ensuring an appropriate priority level.\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 any of the following conditions are met:\n - The provided DNS server is not configured.\n - The provided DNS server with designated VRF and priority does not match the expected information.\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 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\", revision=1)]\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 DnsServer: ClassVar[type[DnsServer]] = DnsServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyDNSServers.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"nameServerConfigs\"]\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 # Check if the DNS server is configured with specified VRF.\n if (output := get_dict_superset(command_output, input_dict)) is None:\n self.result.is_failure(f\"{server} - Not configured\")\n continue\n\n # Check if the DNS server priority matches with expected.\n if output[\"priority\"] != priority:\n self.result.is_failure(f\"{server} - Incorrect priority; Priority: {output['priority']}\")\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.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 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 categories: ClassVar[list[str]] = [\"services\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hostname\", revision=1)]\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.services/#input-models","title":"Input models","text":""},{"location":"api/tests.services/#anta.input_models.services.DnsServer","title":"DnsServer","text":"Model for a DNS server configuration. Name Type Description Default server_address IPv4Address | IPv6Address The IPv4 or IPv6 address of the DNS server. - vrf str The VRF instance in which the DNS server resides. Defaults to 'default'. 'default' priority int The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest. Field(ge=0, le=4) Source code in anta/input_models/services.py class DnsServer(BaseModel):\n \"\"\"Model for a DNS server configuration.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: IPv4Address | IPv6Address\n \"\"\"The IPv4 or IPv6 address of the DNS server.\"\"\"\n vrf: str = \"default\"\n \"\"\"The VRF instance in which the DNS server resides. Defaults to 'default'.\"\"\"\n priority: int = Field(ge=0, le=4)\n \"\"\"The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the DnsServer for reporting.\n\n Examples\n --------\n Server 10.0.0.1 (VRF: default, Priority: 1)\n \"\"\"\n return f\"Server {self.server_address} (VRF: {self.vrf}, Priority: {self.priority})\"\n"},{"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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP contact is configured.\n if not (contact := get_value(self.instance_commands[0].json_output, \"contact.contact\")):\n self.result.is_failure(\"SNMP contact is not configured.\")\n return\n\n # Verifies the expected SNMP contact.\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.VerifySnmpErrorCounters","title":"VerifySnmpErrorCounters","text":"Verifies the SNMP error counters. By default, all error counters will be checked for any non-zero values. An optional list of specific error counters can be provided for granular testing. Expected Results Success: The test will pass if the SNMP error counter(s) are zero/None. Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured. Examples ```yaml anta.tests.snmp: - VerifySnmpErrorCounters: error_counters: - inVersionErrs - inBadCommunityNames Source code in anta/tests/snmp.py class VerifySnmpErrorCounters(AntaTest):\n \"\"\"Verifies the SNMP error counters.\n\n By default, all error counters will be checked for any non-zero values.\n An optional list of specific error counters can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP error counter(s) are zero/None.\n * Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpErrorCounters:\n error_counters:\n - inVersionErrs\n - inBadCommunityNames\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpErrorCounters test.\"\"\"\n\n error_counters: list[SnmpErrorCounter] | None = None\n \"\"\"Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpErrorCounters.\"\"\"\n error_counters = self.inputs.error_counters\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (snmp_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP error counters not provided, It will check all the error counters.\n if not error_counters:\n error_counters = list(get_args(SnmpErrorCounter))\n\n error_counters_not_ok = {counter: value for counter in error_counters if (value := snmp_counters.get(counter))}\n\n # Check if any failures\n if not error_counters_not_ok:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP error counters are not found or have non-zero error counters:\\n{error_counters_not_ok}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpErrorCounters-attributes","title":"Inputs","text":"Name Type Description Default error_counters list[SnmpErrorCounter] | None Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters. None"},{"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 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\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP location is configured.\n if not (location := get_value(self.instance_commands[0].json_output, \"location.location\")):\n self.result.is_failure(\"SNMP location is not configured.\")\n return\n\n # Verifies the expected SNMP location.\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.VerifySnmpPDUCounters","title":"VerifySnmpPDUCounters","text":"Verifies the SNMP PDU counters. By default, all SNMP PDU counters will be checked for any non-zero values. An optional list of specific SNMP PDU(s) can be provided for granular testing. Expected Results Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero. Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found. Examples anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n Source code in anta/tests/snmp.py class VerifySnmpPDUCounters(AntaTest):\n \"\"\"Verifies the SNMP PDU counters.\n\n By default, all SNMP PDU counters will be checked for any non-zero values.\n An optional list of specific SNMP PDU(s) can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero.\n * Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpPDUCounters test.\"\"\"\n\n pdus: list[SnmpPdu] | None = None\n \"\"\"Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpPDUCounters.\"\"\"\n snmp_pdus = self.inputs.pdus\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (pdu_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP PDUs not provided, It will check all the update error counters.\n if not snmp_pdus:\n snmp_pdus = list(get_args(SnmpPdu))\n\n failures = {pdu: value for pdu in snmp_pdus if (value := pdu_counters.get(pdu, \"Not Found\")) == \"Not Found\" or value == 0}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP PDU counters are not found or have zero PDU counters:\\n{failures}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpPDUCounters-attributes","title":"Inputs","text":"Name Type Description Default pdus list[SnmpPdu] | None Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters. None"},{"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 description = \"Verifies if the SNMP agent is enabled.\"\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show extensions\", revision=2),\n AntaCommand(command=\"show boot-extensions\", revision=1),\n ]\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 description = \"Verifies the EOS version of the device.\"\n categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree blockedports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree counters\", revision=1)]\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 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\", revision=1)]\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 vlan_id = command.params.vlan\n if not (topologies := get_value(command.json_output, \"topologies\")):\n not_configured.append(vlan_id)\n else:\n interfaces_not_forwarding = []\n for value in topologies.values():\n if vlan_id and 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 forwarding 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show spanning-tree vlan {vlan}\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree root detail\", revision=1)]\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.stp/#anta.tests.stp.VerifyStpTopologyChanges","title":"VerifyStpTopologyChanges","text":"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold. Expected Results Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold. Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold, indicating potential instability in the topology. Examples anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n Source code in anta/tests/stp.py class VerifyStpTopologyChanges(AntaTest):\n \"\"\"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold.\n * Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold,\n indicating potential instability in the topology.\n\n Examples\n --------\n ```yaml\n anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree topology status detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStpTopologyChanges test.\"\"\"\n\n threshold: int\n \"\"\"The threshold number of changes in the STP topology.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStpTopologyChanges.\"\"\"\n failures: dict[str, Any] = {\"topologies\": {}}\n\n command_output = self.instance_commands[0].json_output\n stp_topologies = command_output.get(\"topologies\", {})\n\n # verifies all available topologies except the \"NoStp\" topology.\n stp_topologies.pop(\"NoStp\", None)\n\n # Verify the STP topology(s).\n if not stp_topologies:\n self.result.is_failure(\"STP is not configured.\")\n return\n\n # Verifies the number of changes across all interfaces\n for topology, topology_details in stp_topologies.items():\n interfaces = {\n interface: {\"Number of changes\": num_of_changes}\n for interface, details in topology_details.get(\"interfaces\", {}).items()\n if (num_of_changes := details.get(\"numChanges\")) > self.inputs.threshold\n }\n if interfaces:\n failures[\"topologies\"][topology] = interfaces\n\n if failures[\"topologies\"]:\n self.result.is_failure(f\"The following STP topologies are not configured or number of changes not within the threshold:\\n{failures}\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.stp/#anta.tests.stp.VerifyStpTopologyChanges-attributes","title":"Inputs","text":"Name Type Description Default threshold int The threshold number of changes in the STP topology. -"},{"location":"api/tests.stun/","title":"STUN","text":""},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient","title":"VerifyStunClient","text":"Verifies STUN client settings, including local IP/port and optionally public IP/port. Expected Results Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port. Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect. Examples anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n Source code in anta/tests/stun.py class VerifyStunClient(AntaTest):\n \"\"\"Verifies STUN client settings, including local IP/port and optionally public IP/port.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port.\n * Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show stun client translations {source_address} {source_port}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStunClient test.\"\"\"\n\n stun_clients: list[ClientAddress]\n\n class ClientAddress(BaseModel):\n \"\"\"Source and public address/port details of STUN client.\"\"\"\n\n source_address: IPv4Address\n \"\"\"IPv4 source address of STUN client.\"\"\"\n source_port: Port = 4500\n \"\"\"Source port number for STUN client.\"\"\"\n public_address: IPv4Address | None = None\n \"\"\"Optional IPv4 public address of STUN client.\"\"\"\n public_port: Port | None = None\n \"\"\"Optional public port number for STUN client.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each STUN translation.\"\"\"\n return [template.render(source_address=client.source_address, source_port=client.source_port) for client in self.inputs.stun_clients]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunClient.\"\"\"\n self.result.is_success()\n\n # Iterate over each command output and corresponding client input\n for command, client_input in zip(self.instance_commands, self.inputs.stun_clients):\n bindings = command.json_output[\"bindings\"]\n source_address = str(command.params.source_address)\n source_port = command.params.source_port\n\n # If no bindings are found for the STUN client, mark the test as a failure and continue with the next client\n if not bindings:\n self.result.is_failure(f\"STUN client transaction for source `{source_address}:{source_port}` is not found.\")\n continue\n\n # Extract the public address and port from the client input\n public_address = client_input.public_address\n public_port = client_input.public_port\n\n # Extract the transaction ID from the bindings\n transaction_id = next(iter(bindings.keys()))\n\n # Prepare the actual and expected STUN data for comparison\n actual_stun_data = {\n \"source ip\": get_value(bindings, f\"{transaction_id}.sourceAddress.ip\"),\n \"source port\": get_value(bindings, f\"{transaction_id}.sourceAddress.port\"),\n }\n expected_stun_data = {\"source ip\": source_address, \"source port\": source_port}\n\n # If public address is provided, add it to the actual and expected STUN data\n if public_address is not None:\n actual_stun_data[\"public ip\"] = get_value(bindings, f\"{transaction_id}.publicAddress.ip\")\n expected_stun_data[\"public ip\"] = str(public_address)\n\n # If public port is provided, add it to the actual and expected STUN data\n if public_port is not None:\n actual_stun_data[\"public port\"] = get_value(bindings, f\"{transaction_id}.publicAddress.port\")\n expected_stun_data[\"public port\"] = public_port\n\n # If the actual STUN data does not match the expected STUN data, mark the test as failure\n if actual_stun_data != expected_stun_data:\n failed_log = get_failed_logs(expected_stun_data, actual_stun_data)\n self.result.is_failure(f\"For STUN source `{source_address}:{source_port}`:{failed_log}\")\n"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"ClientAddress","text":"Name Type Description Default source_address IPv4Address IPv4 source address of STUN client. - source_port Port Source port number for STUN client. 4500 public_address IPv4Address | None Optional IPv4 public address of STUN client. None public_port Port | None Optional public port number for STUN client. None"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunServer","title":"VerifyStunServer","text":"Verifies the STUN server status is enabled and running. Expected Results Success: The test will pass if the STUN server status is enabled and running. Failure: The test will fail if the STUN server is disabled or not running. Examples anta.tests.stun:\n - VerifyStunServer:\n Source code in anta/tests/stun.py class VerifyStunServer(AntaTest):\n \"\"\"Verifies the STUN server status is enabled and running.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN server status is enabled and running.\n * Failure: The test will fail if the STUN server is disabled or not running.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunServer:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show stun server status\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunServer.\"\"\"\n command_output = self.instance_commands[0].json_output\n status_disabled = not command_output.get(\"enabled\")\n not_running = command_output.get(\"pid\") == 0\n\n if status_disabled and not_running:\n self.result.is_failure(\"STUN server status is disabled and not running.\")\n elif status_disabled:\n self.result.is_failure(\"STUN server status is disabled.\")\n elif not_running:\n self.result.is_failure(\"STUN server is not running.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.system/","title":"System","text":""},{"location":"api/tests.system/#tests","title":"Tests","text":""},{"location":"api/tests.system/#anta.tests.system.VerifyAgentLogs","title":"VerifyAgentLogs","text":"Verifies there are no agent crash reports. 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 there are no agent crash reports.\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show processes top once\", revision=1)]\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 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\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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.VerifyNTPAssociations","title":"VerifyNTPAssociations","text":"Verifies the Network Time Protocol (NTP) associations. Expected Results Success: The test will pass if the Primary NTP server (marked as preferred) has the condition \u2018sys.peer\u2019 and all other NTP servers have the condition \u2018candidate\u2019. Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition \u2018sys.peer\u2019 or if any other NTP server does not have the condition \u2018candidate\u2019. Examples anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n Source code in anta/tests/system.py class VerifyNTPAssociations(AntaTest):\n \"\"\"Verifies the Network Time Protocol (NTP) associations.\n\n Expected Results\n ----------------\n * Success: The test will pass if the Primary NTP server (marked as preferred) has the condition 'sys.peer' and\n all other NTP servers have the condition 'candidate'.\n * Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition 'sys.peer' or\n if any other NTP server does not have the condition 'candidate'.\n\n Examples\n --------\n ```yaml\n anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ntp associations\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyNTPAssociations test.\"\"\"\n\n ntp_servers: list[NTPServer]\n \"\"\"List of NTP servers.\"\"\"\n NTPServer: ClassVar[type[NTPServer]] = NTPServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyNTPAssociations.\"\"\"\n self.result.is_success()\n\n if not (peers := get_value(self.instance_commands[0].json_output, \"peers\")):\n self.result.is_failure(\"No NTP peers configured\")\n return\n\n # Iterate over each NTP server.\n for ntp_server in self.inputs.ntp_servers:\n server_address = str(ntp_server.server_address)\n\n # We check `peerIpAddr` in the peer details - covering IPv4Address input, or the peer key - covering Hostname input.\n matching_peer = next((peer for peer, peer_details in peers.items() if (server_address in {peer_details[\"peerIpAddr\"], peer})), None)\n\n if not matching_peer:\n self.result.is_failure(f\"{ntp_server} - Not configured\")\n continue\n\n # Collecting the expected/actual NTP peer details.\n exp_condition = \"sys.peer\" if ntp_server.preferred else \"candidate\"\n exp_stratum = ntp_server.stratum\n act_condition = get_value(peers[matching_peer], \"condition\")\n act_stratum = get_value(peers[matching_peer], \"stratumLevel\")\n\n if act_condition != exp_condition or act_stratum != exp_stratum:\n self.result.is_failure(f\"{ntp_server} - Bad association; Condition: {act_condition}, Stratum: {act_stratum}\")\n"},{"location":"api/tests.system/#anta.tests.system.VerifyNTPAssociations-attributes","title":"Inputs","text":"Name Type Description Default ntp_servers list[NTPServer] List of NTP servers. -"},{"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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show reload cause\", revision=1)]\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 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 description = \"Verifies the device uptime.\"\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show uptime\", revision=1)]\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.system/#input-models","title":"Input models","text":""},{"location":"api/tests.system/#anta.input_models.system.NTPServer","title":"NTPServer","text":"Model for a NTP server. Name Type Description Default server_address Hostname | IPv4Address The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name. For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output. - preferred bool Optional preferred for NTP server. If not provided, it defaults to `False`. False stratum int NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized. Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state. Field(ge=0, le=16) Source code in anta/input_models/system.py class NTPServer(BaseModel):\n \"\"\"Model for a NTP server.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: Hostname | IPv4Address\n \"\"\"The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration\n of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name.\n For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output.\"\"\"\n preferred: bool = False\n \"\"\"Optional preferred for NTP server. If not provided, it defaults to `False`.\"\"\"\n stratum: int = Field(ge=0, le=16)\n \"\"\"NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized.\n Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Representation of the NTPServer model.\"\"\"\n return f\"{self.server_address} (Preferred: {self.preferred}, Stratum: {self.stratum})\"\n"},{"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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\", revision=1)]\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 description = \"Verifies the Vxlan1 interface status.\"\n categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 there are no VXLAN config-sanity inconsistencies. 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 there are no VXLAN config-sanity inconsistencies.\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan config-sanity\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vni\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vtep\", revision=1)]\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[\n str, AfterValidator(aaa_group_prefix)\n]\n"},{"location":"api/types/#anta.custom_types.Afi","title":"Afi module-attribute","text":"Afi = Literal[\n \"ipv4\",\n \"ipv6\",\n \"vpn-ipv4\",\n \"vpn-ipv6\",\n \"evpn\",\n \"rt-membership\",\n \"path-selection\",\n \"link-state\",\n]\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.BfdProtocol","title":"BfdProtocol module-attribute","text":"BfdProtocol = Literal[\n \"bgp\",\n \"isis\",\n \"lag\",\n \"ospf\",\n \"ospfv3\",\n \"pim\",\n \"route-input\",\n \"static-bfd\",\n \"static-route\",\n \"vrrp\",\n \"vxlan\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpDropStats","title":"BgpDropStats module-attribute","text":"BgpDropStats = Literal[\n \"inDropAsloop\",\n \"inDropClusterIdLoop\",\n \"inDropMalformedMpbgp\",\n \"inDropOrigId\",\n \"inDropNhLocal\",\n \"inDropNhAfV6\",\n \"prefixDroppedMartianV4\",\n \"prefixDroppedMaxRouteLimitViolatedV4\",\n \"prefixDroppedMartianV6\",\n \"prefixDroppedMaxRouteLimitViolatedV6\",\n \"prefixLuDroppedV4\",\n \"prefixLuDroppedMartianV4\",\n \"prefixLuDroppedMaxRouteLimitViolatedV4\",\n \"prefixLuDroppedV6\",\n \"prefixLuDroppedMartianV6\",\n \"prefixLuDroppedMaxRouteLimitViolatedV6\",\n \"prefixEvpnDroppedUnsupportedRouteType\",\n \"prefixBgpLsDroppedReceptionUnsupported\",\n \"outDropV4LocalAddr\",\n \"outDropV6LocalAddr\",\n \"prefixVpnIpv4DroppedImportMatchFailure\",\n \"prefixVpnIpv4DroppedMaxRouteLimitViolated\",\n \"prefixVpnIpv6DroppedImportMatchFailure\",\n \"prefixVpnIpv6DroppedMaxRouteLimitViolated\",\n \"prefixEvpnDroppedImportMatchFailure\",\n \"prefixEvpnDroppedMaxRouteLimitViolated\",\n \"prefixRtMembershipDroppedLocalAsReject\",\n \"prefixRtMembershipDroppedMaxRouteLimitViolated\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpUpdateError","title":"BgpUpdateError module-attribute","text":"BgpUpdateError = Literal[\n \"inUpdErrWithdraw\",\n \"inUpdErrIgnore\",\n \"inUpdErrDisableAfiSafi\",\n \"disabledAfiSafi\",\n \"lastUpdErrTime\",\n]\n"},{"location":"api/types/#anta.custom_types.EcdsaKeySize","title":"EcdsaKeySize module-attribute","text":"EcdsaKeySize = Literal[256, 384, 512]\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[\n \"acl\",\n \"arp-inspection\",\n \"bpduguard\",\n \"dot1x-session-replace\",\n \"hitless-reload-down\",\n \"lacp-rate-limit\",\n \"link-flap\",\n \"no-internal-vlan\",\n \"portchannelguard\",\n \"portsec\",\n \"tapagg\",\n \"uplink-failure-detection\",\n]\n"},{"location":"api/types/#anta.custom_types.EthernetInterface","title":"EthernetInterface module-attribute","text":"EthernetInterface = Annotated[\n str,\n Field(pattern=\"^Ethernet[0-9]+(\\\\/[0-9]+)*$\"),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\n"},{"location":"api/types/#anta.custom_types.Hostname","title":"Hostname module-attribute","text":"Hostname = Annotated[\n str, Field(pattern=REGEXP_TYPE_HOSTNAME)\n]\n"},{"location":"api/types/#anta.custom_types.Interface","title":"Interface module-attribute","text":"Interface = Annotated[\n str,\n Field(pattern=REGEXP_TYPE_EOS_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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[\n str,\n BeforeValidator(\n bgp_multiprotocol_capabilities_abbreviations\n ),\n]\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.PortChannelInterface","title":"PortChannelInterface module-attribute","text":"PortChannelInterface = Annotated[\n str,\n Field(pattern=REGEX_TYPE_PORTCHANNEL),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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.REGEXP_BGP_IPV4_MPLS_LABELS","title":"REGEXP_BGP_IPV4_MPLS_LABELS module-attribute","text":"REGEXP_BGP_IPV4_MPLS_LABELS = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?label(s)?)\\\\b\"\n)\n Match IPv4 MPLS Labels."},{"location":"api/types/#anta.custom_types.REGEXP_BGP_L2VPN_AFI","title":"REGEXP_BGP_L2VPN_AFI module-attribute","text":"REGEXP_BGP_L2VPN_AFI = \"\\\\b(l2[\\\\s\\\\-]?vpn[\\\\s\\\\-]?evpn)\\\\b\"\n Match L2VPN EVPN AFI."},{"location":"api/types/#anta.custom_types.REGEXP_EOS_BLACKLIST_CMDS","title":"REGEXP_EOS_BLACKLIST_CMDS module-attribute","text":"REGEXP_EOS_BLACKLIST_CMDS = [\n \"^reload.*\",\n \"^conf\\\\w*\\\\s*(terminal|session)*\",\n \"^wr\\\\w*\\\\s*\\\\w+\",\n]\n List of regular expressions to blacklist from eos commands."},{"location":"api/types/#anta.custom_types.REGEXP_INTERFACE_ID","title":"REGEXP_INTERFACE_ID module-attribute","text":"REGEXP_INTERFACE_ID = '\\\\d+(\\\\/\\\\d+)*(\\\\.\\\\d+)?'\n Match Interface ID lilke 1/1.1."},{"location":"api/types/#anta.custom_types.REGEXP_PATH_MARKERS","title":"REGEXP_PATH_MARKERS module-attribute","text":"REGEXP_PATH_MARKERS = '[\\\\\\\\\\\\/\\\\s]'\n Match directory path from string."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_EOS_INTERFACE","title":"REGEXP_TYPE_EOS_INTERFACE module-attribute","text":"REGEXP_TYPE_EOS_INTERFACE = \"^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\\\\/[0-9]+)*(\\\\.[0-9]+)?$\"\n Match EOS interface types like Ethernet1/1, Vlan1, Loopback1, etc."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_HOSTNAME","title":"REGEXP_TYPE_HOSTNAME module-attribute","text":"REGEXP_TYPE_HOSTNAME = \"^(([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 Match hostname like my-hostname, my-hostname-1, my-hostname-1-2."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_VXLAN_SRC_INTERFACE","title":"REGEXP_TYPE_VXLAN_SRC_INTERFACE module-attribute","text":"REGEXP_TYPE_VXLAN_SRC_INTERFACE = \"^(Loopback)([0-9]|[1-9][0-9]{1,2}|[1-7][0-9]{3}|8[01][0-9]{2}|819[01])$\"\n Match Vxlan source interface like Loopback10."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_MPLS_VPN","title":"REGEX_BGP_IPV4_MPLS_VPN module-attribute","text":"REGEX_BGP_IPV4_MPLS_VPN = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?vpn)\\\\b\"\n)\n Match IPv4 MPLS VPN."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_UNICAST","title":"REGEX_BGP_IPV4_UNICAST module-attribute","text":"REGEX_BGP_IPV4_UNICAST = (\n \"\\\\b(ipv4[\\\\s\\\\-]?uni[\\\\s\\\\-]?cast)\\\\b\"\n)\n Match IPv4 Unicast."},{"location":"api/types/#anta.custom_types.REGEX_TYPE_PORTCHANNEL","title":"REGEX_TYPE_PORTCHANNEL module-attribute","text":"REGEX_TYPE_PORTCHANNEL = '^Port-Channel[0-9]{1,6}$'\n Match Port Channel interface like Port-Channel5."},{"location":"api/types/#anta.custom_types.RegexString","title":"RegexString module-attribute","text":"RegexString = Annotated[str, AfterValidator(validate_regex)]\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[\n \"unicast\", \"multicast\", \"labeled-unicast\", \"sr-te\"\n]\n"},{"location":"api/types/#anta.custom_types.SnmpErrorCounter","title":"SnmpErrorCounter module-attribute","text":"SnmpErrorCounter = Literal[\n \"inVersionErrs\",\n \"inBadCommunityNames\",\n \"inBadCommunityUses\",\n \"inParseErrs\",\n \"outTooBigErrs\",\n \"outNoSuchNameErrs\",\n \"outBadValueErrs\",\n \"outGeneralErrs\",\n]\n"},{"location":"api/types/#anta.custom_types.SnmpPdu","title":"SnmpPdu module-attribute","text":"SnmpPdu = Literal[\n \"inGetPdus\",\n \"inGetNextPdus\",\n \"inSetPdus\",\n \"outGetResponsePdus\",\n \"outTrapPdus\",\n]\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[\n str,\n Field(pattern=REGEXP_TYPE_VXLAN_SRC_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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(\n value: str,\n) -> str\n Abbreviations for different BGP multiprotocol capabilities. Examples IPv4 Unicast L2vpnEVPN ipv4 MPLS Labels ipv4Mplsvpn 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 REGEXP_BGP_L2VPN_AFI: \"l2VpnEvpn\",\n REGEXP_BGP_IPV4_MPLS_LABELS: \"ipv4MplsLabels\",\n REGEX_BGP_IPV4_MPLS_VPN: \"ipv4MplsVpn\",\n REGEX_BGP_IPV4_UNICAST: \"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(REGEXP_INTERFACE_ID)\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 return next((f\"{full_name}{intf_id}\" for alias, full_name in alias_map.items() if v.lower().startswith(alias)), 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 vlan -> Vlan loopback -> Loopback 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 v != \"\" and not v[0].isupper():\n return f\"{v[0].upper()}{v[1:]}\"\n return v\n"},{"location":"api/types/#anta.custom_types.validate_regex","title":"validate_regex","text":"validate_regex(value: str) -> str\n Validate that the input value is a valid regex format. Source code in anta/custom_types.py def validate_regex(value: str) -> str:\n \"\"\"Validate that the input value is a valid regex format.\"\"\"\n try:\n re.compile(value)\n except re.error as e:\n msg = f\"Invalid regex: {e}\"\n raise ValueError(msg) from e\n return value\n"},{"location":"cli/check/","title":"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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n --help Show this message and exit.\n"},{"location":"cli/debug/","title":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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":"Usage: 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.\n\n 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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"},{"location":"cli/debug/#example-of-multiple-arguments","title":"Example of multiple arguments","text":"Warning If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters. 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":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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_2024-04-09_15_56_19]\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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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 [DEPRECATED] Ensure devices have 'aaa authorization\n exec default local' configured (required for SCP on\n EOS). THIS WILL CHANGE THE CONFIGURATION OF YOUR\n 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. Warning ANTA can automatically configure aaa authorization exec default local using the anta exec collect-tech-support --configure option but this option is deprecated and will be removed in ANTA 2.0.0. 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":"The ANTA CLI offers multiple commands to access data from your local inventory."},{"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 (visit this page to learn more about tags). The --connected option allows to display only the devices where a connection has been established."},{"location":"cli/get-inventory-information/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 By default, anta get inventory only provides information that doesn\u2019t rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, use the --connected option."},{"location":"cli/get-inventory-information/#example","title":"Example","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 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={'DC1-SPINE1', 'DC1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC1-SPINE2': AsyncEOSDevice(\n name='DC1-SPINE2',\n tags={'DC1', 'SPINE', 'DC1-SPINE2'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE1': AsyncEOSDevice(\n name='DC2-SPINE1',\n tags={'DC2', 'DC2-SPINE1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE2': AsyncEOSDevice(\n name='DC2-SPINE2',\n tags={'DC2', 'DC2-SPINE2', 'SPINE'},\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=False,\n insecure=False\n )\n}\n"},{"location":"cli/inv-from-ansible/","title":"Inventory from Ansible","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\n NOTE: This command does not support inline vaulted variables. Make sure to\n comment them out.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var:\n ANTA_INVENTORY; required]\n --overwrite Do not prompt when overriding current inventory\n [env var: ANTA_GET_FROM_ANSIBLE_OVERWRITE]\n -g, --ansible-group TEXT Ansible group to filter\n --ansible-inventory FILE Path to your ansible inventory file to read\n [required]\n --help Show this message and exit.\n Warnings anta get from-ansible does not support inline vaulted variables, comment them out to generate your inventory. If the vaulted variable is necessary to build the inventory (e.g. ansible_host), it needs to be unvaulted for from-ansible command to work.\u201d The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritance 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: ---\nall:\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 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"},{"location":"cli/inv-from-cvp/","title":"Inventory from CVP","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. Info The current implementation only works with on-premises CloudVision instances, not with CloudVision as a Service (CVaaS)."},{"location":"cli/inv-from-cvp/#command-overview","title":"Command overview","text":"Usage: anta get from-cvp [OPTIONS]\n\n Build ANTA inventory from CloudVision.\n\n NOTE: Only username/password authentication is supported for on-premises CloudVision instances.\n Token authentication for both on-premises and CloudVision as a Service (CVaaS) is not supported.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var: ANTA_INVENTORY;\n required]\n --overwrite Do not prompt when overriding current inventory [env\n var: ANTA_GET_FROM_CVP_OVERWRITE]\n -host, --host TEXT CloudVision instance FQDN or IP [required]\n -u, --username TEXT CloudVision username [required]\n -p, --password TEXT CloudVision password [required]\n -c, --container TEXT CloudVision container where devices are configured\n --ignore-cert By default connection to CV will use HTTPS\n certificate, set this flag to disable it [env var:\n ANTA_GET_FROM_CVP_IGNORE_CERT]\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":"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 report Table report JSON report Custom template report CSV report Markdown report "},{"location":"cli/nrfu/#nrfu-command-overview","title":"NRFU Command overview","text":"Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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. Refer to the dedicated page for more information."},{"location":"cli/nrfu/#device-and-test-filtering","title":"Device and test filtering","text":"Options --device and --test can be used to target one or multiple devices and/or tests to run in your environment. The options can be repeated. Example: anta nrfu --device leaf1a --device leaf1b --test VerifyUptime --test VerifyReloadCause."},{"location":"cli/nrfu/#hide-results","title":"Hide results","text":"Option --hide can be used to hide test results in the output or report file based on their status. The option can be repeated. Example: anta nrfu --hide error --hide skipped."},{"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":"Usage: anta nrfu text [OPTIONS]\n\n ANTA command to check network states with text result.\n\nOptions:\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example","title":"Example","text":"anta nrfu --device DC1-LEAF1A text\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":"Usage: anta nrfu table [OPTIONS]\n\n ANTA command to check network states with table result.\n\nOptions:\n --group-by [device|test] Group result by test or device.\n --help Show this message and exit.\n 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 --device spine1 table\n anta nrfu --test VerifyZeroTouch table\n "},{"location":"cli/nrfu/#performing-nrfu-with-json-rendering","title":"Performing NRFU with JSON rendering","text":"The JSON rendering command in NRFU testing will generate an output of all test results in JSON format."},{"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 JSON 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. If specified, no output will be displayed in the terminal. This is useful for further processing or integration with other tools."},{"location":"cli/nrfu/#example_1","title":"Example","text":"anta nrfu --tags LEAF json\n"},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-csv-file","title":"Performing NRFU and saving results in a CSV file","text":"The csv command in NRFU testing is useful for generating a CSV file with all tests result. This file can be easily analyzed and filtered by operator for reporting purposes."},{"location":"cli/nrfu/#command-overview_3","title":"Command overview","text":"anta nrfu csv --help\nUsage: anta nrfu csv [OPTIONS]\n\n ANTA command to check network states with CSV result.\n\nOptions:\n --csv-output FILE Path to save report as a CSV file [env var:\n ANTA_NRFU_CSV_CSV_OUTPUT]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_2","title":"Example","text":""},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-markdown-file","title":"Performing NRFU and saving results in a Markdown file","text":"The md-report command in NRFU testing generates a comprehensive Markdown report containing various sections, including detailed statistics for devices and test categories."},{"location":"cli/nrfu/#command-overview_4","title":"Command overview","text":"anta nrfu md-report --help\n\nUsage: anta nrfu md-report [OPTIONS]\n\n ANTA command to check network state with Markdown report.\n\nOptions:\n --md-output FILE Path to save the report as a Markdown file [env var:\n ANTA_NRFU_MD_REPORT_MD_OUTPUT; required]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_3","title":"Example","text":""},{"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_5","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_4","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/nrfu/#dry-run-mode","title":"Dry-run mode","text":"It is possible to run anta nrfu --dry-run to execute ANTA up to the point where it should communicate with the network to execute the tests. When using --dry-run, all inventory devices are assumed to be online. This can be useful to check how many tests would be run using the catalog and inventory. "},{"location":"cli/overview/","title":"Overview","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."},{"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 selected inventory devices.\n"},{"location":"cli/overview/#anta-environment-variables","title":"ANTA environment 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_CATALOG=tests.yml\n Then, run the CLI without options: anta nrfu\n Note All environment variables may not be needed for every commands. Refer to <command> --help for the comprehensive environment variables names. Below are the environment 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":"ANTA uses tags to define test-to-device mappings (tests run on devices with matching tags) and the --tags CLI option acts as a filter to execute specific test/device combinations."},{"location":"cli/tag-management/#defining-tags","title":"Defining tags","text":""},{"location":"cli/tag-management/#device-tags","title":"Device tags","text":"Device tags can be defined in the inventory: anta_inventory:\n hosts:\n - name: leaf1\n host: leaf1.anta.arista.com\n tags: [\"leaf\"]\n - name: leaf2\n host: leaf2.anta.arista.com\n tags: [\"leaf\"]\n - name: spine1\n host: spine1.anta.arista.com\n tags: [\"spine\"]\n Each device also has its own name automatically added as a tag: $ anta get inventory\nCurrent inventory content is:\n{\n 'leaf1': AsyncEOSDevice(\n name='leaf1',\n tags={'leaf', 'leaf1'}, <--\n [...]\n host='leaf1.anta.arista.com',\n [...]\n ),\n 'leaf2': AsyncEOSDevice(\n name='leaf2',\n tags={'leaf', 'leaf2'}, <--\n [...]\n host='leaf2.anta.arista.com',\n [...]\n ),\n 'spine1': AsyncEOSDevice(\n name='spine1',\n tags={'spine1', 'spine'}, <--\n [...]\n host='spine1.anta.arista.com',\n [...]\n )\n}\n"},{"location":"cli/tag-management/#test-tags","title":"Test tags","text":"Tags can be defined in the test catalog to restrict tests to tagged devices: anta.tests.system:\n - VerifyUptime:\n minimum: 10\n filters:\n tags: ['spine']\n - VerifyUptime:\n minimum: 9\n filters:\n tags: ['leaf']\n - VerifyReloadCause:\n filters:\n tags: ['spine', 'leaf']\n - VerifyCoredump:\n - VerifyAgentLogs:\n - VerifyCPUUtilization:\n - VerifyMemoryUtilization:\n - VerifyFileSystemUtilization:\n - VerifyNTP:\n\nanta.tests.mlag:\n - VerifyMlagStatus:\n filters:\n tags: ['leaf']\n\nanta.tests.interfaces:\n - VerifyL3MTU:\n mtu: 1500\n filters:\n tags: ['spine']\n A tag used to filter a test can also be a device name Use different input values for a specific test Leverage tags to define different input values for a specific test. See the VerifyUptime example above."},{"location":"cli/tag-management/#using-tags","title":"Using tags","text":"Command Description No --tags option Run all tests on all devices according to the tag definitions in your inventory and test catalog. Tests without tags are executed on all devices. --tags leaf Run all tests marked with the leaf tag on all devices configured with the leaf tag. All other tests are ignored. --tags leaf,spine Run all tests marked with the leaf tag on all devices configured with the leaf tag.Run all tests marked with the spine tag on all devices configured with the spine tag. All other tests are ignored."},{"location":"cli/tag-management/#examples","title":"Examples","text":"The following examples use the inventory and test catalog defined above."},{"location":"cli/tag-management/#no-tags-option","title":"No --tags option","text":"Tests without tags are run on all devices. Tests with tags will only run on devices with matching tags. $ anta nrfu table --group-by device\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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 27\n---------------------------------\n Summary per device\n\u250f\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\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 leaf1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 leaf2 \u2502 7 \u2502 1 \u2502 1 \u2502 0 \u2502 VerifyAgentLogs \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 spine1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u2514\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/#single-tag","title":"Single tag","text":"With a tag specified, only tests matching this tag will be run on matching devices. $ anta nrfu --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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (2 established)\nTotal number of selected tests: 6\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\n In this case, only leaf devices defined in the inventory are used to run tests marked with the leaf in the test catalog."},{"location":"cli/tag-management/#multiple-tags","title":"Multiple tags","text":"It is possible to use multiple tags using the --tags tag1,tag2 syntax. $ anta nrfu --tags leaf,spine 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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 15\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyL3MTU :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyL3MTU :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyReloadCause :: SUCCESS\nspine1 :: VerifyMlagStatus :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyL3MTU :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\n"},{"location":"cli/tag-management/#obtaining-all-configured-tags","title":"Obtaining all configured tags","text":"As most ANTA commands accommodate tag filtering, this command is useful for enumerating all tags configured in the inventory. Running the anta get tags command will return a list of all tags configured in the inventory."},{"location":"cli/tag-management/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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/tag-management/#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 \"leaf\",\n \"leaf1\",\n \"leaf2\",\n \"spine\",\n \"spine1\"\n]\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: As a Python library in your own application The ANTA CLI "},{"location":"#install-anta-library","title":"Install ANTA library","text":"The library will NOT install the necessary dependencies for the CLI. # Install ANTA as a library\npip install anta\n"},{"location":"#install-anta-cli","title":"Install ANTA CLI","text":"If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx is a tool to install and run python applications in isolated environments. Refer to pipx instructions to install on your system. pipx installs ANTA in an isolated python environment and makes it available globally. This is not recommended if you plan to contribute to ANTA # Install ANTA CLI with pipx\n$ pipx install anta[cli]\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 You can also still choose to install it with directly with pip: pip install anta[cli]\n"},{"location":"#documentation","title":"Documentation","text":"The documentation is published on ANTA package website."},{"location":"#contribution-guide","title":"Contribution guide","text":"Contributions are welcome. Please refer to the contribution guide"},{"location":"#credits","title":"Credits","text":"Thank you to Jeremy Schulman for aio-eapi. 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":"Contribution model is based on a fork-model. Don\u2019t push to aristanetworks/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-environment","title":"Create a development environment","text":"Run the following commands to create an ANTA development environment: # Clone repository\n$ git clone https://github.com/aristanetworks/anta.git\n$ cd anta\n\n# Install ANTA in editable mode and its development tools\n$ pip install -e .[dev]\n# To also install the CLI\n$ pip install -e .[dev,cli]\n\n# Verify installation\n$ pip list -e\nPackage Version Editable project location\n------- ------- -------------------------\nanta 1.1.0 /mnt/lab/projects/anta\n Then, tox is configured 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\npy39 -> Run pytest with py39\npy310 -> Run pytest with py310\npy311 -> Run pytest with py311\npy312 -> Run pytest with py312\nreport -> Generate coverage report\n"},{"location":"contribution/#code-linting","title":"Code linting","text":"tox -e lint\n[...]\nlint: commands[0]> ruff check .\nAll checks passed!\nlint: commands[1]> ruff format . --check\n158 files already formatted\nlint: commands[2]> pylint anta\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\nlint: commands[3]> pylint tests\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\n lint: OK (22.69=setup[2.19]+cmd[0.02,0.02,9.71,10.75] seconds)\n congratulations :) (22.72 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 68 source files\ntype: commands[1]> mypy --config-file=pyproject.toml tests\nSuccess: no issues found in 82 source files\n type: OK (31.15=setup[14.62]+cmd[6.05,10.48] seconds)\n congratulations :) (31.18 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.units.anta_tests 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.units.anta_tests import test\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 pre-commit\ntrim trailing whitespace.................................................Passed\nfix end of files.........................................................Passed\ncheck for added large files..............................................Passed\ncheck for merge conflicts................................................Passed\nCheck and insert license on Python files.................................Passed\nCheck and insert license on Markdown files...............................Passed\nRun Ruff linter..........................................................Passed\nRun Ruff formatter.......................................................Passed\nCheck code style with pylint.............................................Passed\nChecks for common misspellings in text files.............................Passed\nCheck typing with mypy...................................................Passed\nCheck Markdown files style...............................................Passed\n"},{"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. The results can be viewed here."},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#a-local-os-error-occurred-while-connecting-to-a-device","title":"A local OS error occurred while connecting to a device","text":"A local OS error occurred while connecting to a device When running ANTA, you can receive A local OS error occurred while connecting to <device> errors. The underlying OSError exception can have various reasons: [Errno 24] Too many open files or [Errno 16] Device or resource busy. This usually means that the operating system refused to open a new file descriptor (or socket) for the ANTA process. This might be due to the hard limit for open file descriptors currently set for the ANTA process. At startup, ANTA sets the soft limit of its process to the hard limit up to 16384. This is because the soft limit is usually 1024 and the hard limit is usually higher (depends on the system). If the hard limit of the ANTA process is still lower than the number of selected tests in ANTA, the ANTA process may request to the operating system too many file descriptors and get an error, a WARNING is displayed at startup if this is the case."},{"location":"faq/#solution","title":"Solution","text":"One solution could be to raise the hard limit for the user starting the ANTA process. You can get the current hard limit for a user using the command ulimit -n -H while logged in. Create the file /etc/security/limits.d/10-anta.conf with the following content: <user> hard nofile <value>\n The user is the one with which the ANTA process is started. The value is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again."},{"location":"faq/#timeout-error-in-the-logs","title":"Timeout error in the logs","text":"Timeout error in the logs When running ANTA, you can receive <Foo>Timeout errors in the logs (could be ReadTimeout, WriteTimeout, ConnectTimeout or PoolTimeout). More details on the timeouts of the underlying library are available here: https://www.python-httpx.org/advanced/timeouts. This might be due to the time the host on which ANTA is run takes to reach the target devices (for instance if going through firewalls, NATs, \u2026) or when a lot of tests are being run at the same time on a device (eAPI has a queue mechanism to avoid exhausting EOS resources because of a high number of simultaneous eAPI requests)."},{"location":"faq/#solution_1","title":"Solution","text":"Use the timeout option. As an example for the nrfu command: anta nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml --timeout 50 text\n The previous command set a couple of options for ANTA NRFU, one them being the timeout command, by default, when running ANTA from CLI, it is set to 30s. The timeout is increased to 50s to allow ANTA to wait for API calls a little longer."},{"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_2","title":"Solution","text":" 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 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_3","title":"Solution","text":" 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_4","title":"Solution","text":" 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 follow the troubleshooting instructions and report the issue in our GitHub repository."},{"location":"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 install ANTA package is to run Python (>=3.9) and its pip package to install: pip install anta[cli]\n For more details about how to install package, please see the requirements and installation 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: s1-spine1\n tags: ['fabric', 'spine']\n - host: 192.168.0.11\n name: s1-spine2\n tags: ['fabric', 'spine']\n - host: 192.168.0.12\n name: s1-leaf1\n tags: ['fabric', 'leaf']\n - host: 192.168.0.13\n name: s1-leaf2\n tags: ['fabric', 'leaf']\n - host: 192.168.0.14\n name: s1-leaf3\n tags: ['fabric', 'leaf']\n - host: 192.168.0.15\n name: s1-leaf3\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: ---\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\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":""},{"location":"getting-started/#cli","title":"CLI","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. Usage: 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 selected inventory devices.\n Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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 Note The following examples shows how to pass all the CLI options. See how to use environment variables instead in the CLI overview"},{"location":"getting-started/#default-report-using-table","title":"Default report using table","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n `# table is default if not provided` \\\n table\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:01] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.058. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.069. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:02] INFO Running ANTA tests completed in: 0:00:00.969. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 s1-spine1 \u2502 VerifyMlagConfigSanity \u2502 skipped \u2502 MLAG is disabled \u2502 Verifies there are no MLAG config-sanity \u2502 MLAG \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502 inconsistencies. \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-spine1 \u2502 VerifyEOSVersion \u2502 failure \u2502 device is running version \u2502 Verifies the EOS version of the device. \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 \"4.32.2F-38195967.4322F (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)\" not in expected versions: \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['4.25.4M', '4.26.1F', \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 '4.28.3M-28837868.4283M (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n[...]\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyTerminAttrVersion \u2502 failure \u2502 device is running TerminAttr version \u2502 Verifies the TerminAttr version of the \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 v1.34.0 and is not in the allowed list: \u2502 device. \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['v1.22.1'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyZeroTouch \u2502 success \u2502 \u2502 Verifies ZeroTouch is disabled \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\u2500\u2500\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\u2500\u2500\u2500\u2500\u2500\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 arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n 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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:52:39] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.057. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.068. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:52:40] INFO Running ANTA tests completed in: 0:00:00.863. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\n\ns1-spine1 :: VerifyEOSVersion :: FAILURE(device is running version \"4.32.2F-38195967.4322F (engineering build)\" not in expected versions: ['4.25.4M', '4.26.1F',\n'4.28.3M-28837868.4283M (engineering build)'])\ns1-spine1 :: VerifyTerminAttrVersion :: FAILURE(device is running TerminAttr version v1.34.0 and is not in the allowed list: ['v1.22.1'])\ns1-spine1 :: VerifyZeroTouch :: SUCCESS()\ns1-spine1 :: VerifyMlagConfigSanity :: SKIPPED(MLAG is disabled)\n"},{"location":"getting-started/#report-in-json-format","title":"Report in JSON format","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable `\\\n `# --enable-password <password> `\\\n --catalog ./catalog.yml \\\n json\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:11] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.053. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.065. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:12] INFO Running ANTA tests completed in: 0:00:00.857. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \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\": \"s1-spine1\",\n \"test\": \"VerifyNTP\",\n \"categories\": [\n \"system\"\n ],\n \"description\": \"Verifies if NTP is synchronised.\",\n \"result\": \"success\",\n \"messages\": [],\n \"custom_field\": null\n },\n {\n \"name\": \"s1-spine1\",\n \"test\": \"VerifyMlagConfigSanity\",\n \"categories\": [\n \"mlag\"\n ],\n \"description\": \"Verifies there are no MLAG config-sanity inconsistencies.\",\n \"result\": \"skipped\",\n \"messages\": [\n \"MLAG is disabled\"\n ],\n \"custom_field\": null\n },\n [...]\n"},{"location":"getting-started/#basic-usage-in-a-python-script","title":"Basic usage in a Python script","text":"# 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\"\"\"Example script for ANTA.\n\nusage:\n\npython anta_runner.py\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport sys\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.cli.nrfu.utils import anta_progress_bar\nfrom anta.inventory import AntaInventory\nfrom anta.logger import Log, setup_logging\nfrom anta.models import AntaTest\nfrom anta.result_manager import ResultManager\nfrom anta.runner import main as anta_runner\n\n# setup logging\nsetup_logging(Log.INFO, Path(\"/tmp/anta.log\"))\nLOGGER = logging.getLogger()\nSCRIPT_LOG_PREFIX = \"[bold magenta][ANTA RUNNER SCRIPT][/] \" # For convenience purpose - there are nicer way to do this.\n\n\n# NOTE: The inventory and catalog files are not delivered with this script\nUSERNAME = \"admin\"\nPASSWORD = \"admin\"\nCATALOG_PATH = Path(\"/tmp/anta_catalog.yml\")\nINVENTORY_PATH = Path(\"/tmp/anta_inventory.yml\")\n\n# Load catalog file\ntry:\n catalog = AntaCatalog.parse(CATALOG_PATH)\nexcept Exception:\n LOGGER.exception(\"%s Catalog failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Catalog loaded!\", SCRIPT_LOG_PREFIX)\n\n# Load inventory\ntry:\n inventory = AntaInventory.parse(INVENTORY_PATH, username=USERNAME, password=PASSWORD)\nexcept Exception:\n LOGGER.exception(\"%s Inventory failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Inventory loaded!\", SCRIPT_LOG_PREFIX)\n\n# Create result manager object\nmanager = ResultManager()\n\n# Launch ANTA\nLOGGER.info(\"%s Starting ANTA runner...\", SCRIPT_LOG_PREFIX)\nwith anta_progress_bar() as AntaTest.progress:\n # Set dry_run to True to avoid connecting to the devices\n asyncio.run(anta_runner(manager, inventory, catalog, dry_run=False))\n\nLOGGER.info(\"%s ANTA run completed!\", SCRIPT_LOG_PREFIX)\n\n# Manipulate the test result object\nfor test_result in manager.results:\n LOGGER.info(\"%s %s:%s:%s\", SCRIPT_LOG_PREFIX, test_result.name, test_result.test, test_result.result)\n"},{"location":"requirements-and-installation/","title":"Installation","text":""},{"location":"requirements-and-installation/#python-version","title":"Python version","text":"Python 3 (>=3.9) is required: python --version\nPython 3.11.8\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-library-from-pypi-server","title":"Install library from Pypi server","text":"pip install anta\n Warning This command alone will not install the ANTA CLI requirements. "},{"location":"requirements-and-installation/#install-anta-cli-as-an-application-with-pipx","title":"Install ANTA CLI as an application with pipx","text":"pipx is a tool to install and run python applications in isolated environments. If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx installs ANTA in an isolated python environment and makes it available globally. pipx install anta[cli]\n Info Please take the time to read through the installation instructions of pipx before getting started."},{"location":"requirements-and-installation/#install-cli-from-pypi-server","title":"Install CLI from Pypi server","text":"Alternatively, pip install with cli extra is enough to install the ANTA CLI. pip install anta[cli]\n"},{"location":"requirements-and-installation/#install-anta-from-github","title":"Install ANTA from github","text":"pip install git+https://github.com/aristanetworks/anta.git\npip install git+https://github.com/aristanetworks/anta.git#egg=anta[cli]\n\n# You can even specify the branch, tag or commit:\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>#egg=anta[cli]\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 v1.1.0\n"},{"location":"requirements-and-installation/#eos-requirements","title":"EOS Requirements","text":"To get ANTA working, the targeted Arista EOS devices must have eAPI enabled. They need to use 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":"troubleshooting/","title":"Troubleshooting ANTA","text":"A couple of things to check when hitting an issue with ANTA: flowchart LR\n A>Hitting an issue with ANTA] --> B{Is my issue <br >listed in the FAQ?}\n B -- Yes --> C{Does the FAQ solution<br />works for me?}\n C -- Yes --> V(((Victory)))\n B -->|No| E{Is my problem<br />mentioned in one<br />of the open issues?}\n C -->|No| E\n E -- Yes --> F{Has the issue been<br />fixed in a newer<br />release or in main?}\n F -- Yes --> U[Upgrade]\n E -- No ---> H((Follow the steps below<br />and open a Github issue))\n U --> I{Did it fix<br /> your problem}\n I -- Yes --> V\n I -- No --> H\n F -- No ----> G((Add a comment on the <br />issue indicating you<br >are hitting this and<br />describing your setup<br /> and adding your logs.))\n\n click B \"../faq\" \"FAQ\"\n click E \"https://github.com/aristanetworks/anta/issues\"\n click H \"https://github.com/aristanetworks/anta/issues\"\n style A stroke:#f00,stroke-width:2px"},{"location":"troubleshooting/#capturing-logs","title":"Capturing logs","text":"To help document the issue in Github, it is important to capture some logs so the developers can understand what is affecting your system. No logs mean that the first question asked on the issue will probably be \u201cCan you share some logs please?\u201d. ANTA provides very verbose logs when using the DEBUG level. When using DEBUG log level with a log file, the DEBUG logging level is not sent to stdout, but only to the file. Danger On real deployments, do not use DEBUG logging level without setting a log file at the same time. To save the logs to a file called anta.log, use the following flags: # Where ANTA_COMMAND is one of nrfu, debug, get, exec, check\nanta -l DEBUG \u2013log-file anta.log <ANTA_COMMAND>\n See anta --help for more information. These have to precede the nrfu cmd. Tip Remember that in ANTA, each level of command has its own options and they can only be set at this level. so the -l and --log-file MUST be between anta and the ANTA_COMMAND. similarly, all the nrfu options MUST be set between the nrfu and the ANTA_NRFU_SUBCOMMAND (json, text, table or tpl-report). As an example, for the nrfu command, it would look like: anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#anta_debug-environment-variable","title":"ANTA_DEBUG environment variable","text":"Warning Do not use this if you do not know why. This produces a lot of logs and can create confusion if you do not know what to look for. The environment variable ANTA_DEBUG=true enable ANTA Debug Mode. This flag is used by various functions in ANTA: when set to true, the function will display or log more information. In particular, when an Exception occurs in the code and this variable is set, the logging function used by ANTA is different to also produce the Python traceback for debugging. This typically needs to be done when opening a GitHub issue and an Exception is seen at runtime. Example: ANTA_DEBUG=true anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#troubleshooting-on-eos","title":"Troubleshooting on EOS","text":"ANTA is using a specific ID in eAPI requests towards EOS. This allows for easier eAPI requests debugging on the device using EOS configuration trace CapiApp setting UwsgiRequestContext/4,CapiUwsgiServer/4 to set up CapiApp agent logs. Then, you can view agent logs using: bash tail -f /var/log/agents/CapiApp-*\n\n2024-05-15 15:32:54.056166 1429 UwsgiRequestContext 4 request content b'{\"jsonrpc\": \"2.0\", \"method\": \"runCmds\", \"params\": {\"version\": \"latest\", \"cmds\": [{\"cmd\": \"show ip route vrf default 10.255.0.3\", \"revision\": 4}], \"format\": \"json\", \"autoComplete\": false, \"expandAliases\": false}, \"id\": \"ANTA-VerifyRoutingTableEntry-132366530677328\"}'\n"},{"location":"usage-inventory-catalog/","title":"Inventory and Test catalog","text":"The ANTA framework needs 2 important inputs from the user to run: A device inventory 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 accessible via eAPI ranges: scan a range for devices accessible 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 in either YAML or JSON: ---\n<Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\n {\n \"<Python module>\": [\n {\n \"<AntaTest subclass>\": <AntaTest.Input compliant dictionary>\n }\n ]\n}\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 or equivalent in JSON: {\n \"anta.tests.connectivity\": [\n {\n \"VerifyReachability\": {\n \"result_overwrite\": {\n \"description\": \"Test with overwritten description\",\n \"categories\": [\n \"Overwritten category 1\"\n ],\n \"custom_field\": \"Test run by John Doe\"\n },\n \"filters\": {\n \"tags\": [\n \"leaf\"\n ]\n },\n \"hosts\": [\n {\n \"destination\": \"1.1.1.1\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n },\n {\n \"destination\": \"8.8.8.8\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n }\n ]\n }\n }\n ]\n}\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 or JSON file: anta.tests.software:\n - VerifyEOSVersion:\n # List of allowed EOS versions.\n versions:\n - 4.25.4M\n - 4.26.1F\n {\n \"anta.tests.software\": [\n {\n \"VerifyEOSVersion\": {\n \"versions\": [\n \"4.25.4M\",\n \"4.31.1F\"\n ]\n }\n }\n ]\n}\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 custom.tests.system Python module, the test catalog will be: custom.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":"usage-inventory-catalog/#example-script-to-merge-catalogs","title":"Example script to merge catalogs","text":"The following script reads all the files in intended/test_catalogs/ with names <device_name>-catalog.yml and merge them together inside one big catalog anta-catalog.yml using the new AntaCatalog.merge_catalogs() class method. # Copyright (c) 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\"\"\"Script that merge a collection of catalogs into one AntaCatalog.\"\"\"\n\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.models import AntaTest\n\nCATALOG_SUFFIX = \"-catalog.yml\"\nCATALOG_DIR = \"intended/test_catalogs/\"\n\nif __name__ == \"__main__\":\n catalogs = []\n for file in Path(CATALOG_DIR).glob(\"*\" + CATALOG_SUFFIX):\n device = str(file).removesuffix(CATALOG_SUFFIX).removeprefix(CATALOG_DIR)\n print(f\"Loading test catalog for device {device}\")\n catalog = AntaCatalog.parse(file)\n # Add the device name as a tag to all tests in the catalog\n for test in catalog.tests:\n test.inputs.filters = AntaTest.Input.Filters(tags={device})\n catalogs.append(catalog)\n\n # Merge all catalogs\n merged_catalog = AntaCatalog.merge_catalogs(catalogs)\n\n # Save the merged catalog to a file\n with Path(\"anta-catalog.yml\").open(\"w\") as f:\n f.write(merged_catalog.dump().yaml())\n Warning The AntaCatalog.merge() method is deprecated and will be removed in ANTA v2.0. Please use the AntaCatalog.merge_catalogs() class method instead."},{"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. "},{"location":"advanced_usages/as-python-lib/#examples","title":"Examples","text":""},{"location":"advanced_usages/as-python-lib/#parse-an-anta-inventory-file","title":"Parse an ANTA inventory file","text":"# Copyright (c) 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\"\"\"Script that parses an ANTA inventory file, connects to devices and print their status.\"\"\"\n\nimport asyncio\n\nfrom anta.inventory import AntaInventory\n\n\nasync def main(inv: AntaInventory) -> None:\n \"\"\"Read an AntaInventory and try to connect to every device in the inventory.\n\n 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\n\nif __name__ == \"__main__\":\n # Create the AntaInventory instance\n inventory = AntaInventory.parse(\n filename=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\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."},{"location":"advanced_usages/as-python-lib/#run-eos-commands","title":"Run EOS commands","text":"# Copyright (c) 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\"\"\"Script that runs a list of EOS commands on reachable devices.\"\"\"\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 \"\"\"Run a list of commands against each valid device in the inventory.\n\n Take an AntaInventory and a list of commands as string\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 -------\n dict[str, list[AntaCommand]]\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=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\n )\n\n # Create a list of commands with json output\n command_list = [\"show version\", \"show ip bgp summary\"]\n\n # Run the main asyncio entry point\n res = asyncio.run(main(inventory, command_list))\n\n pprint(res)\n"},{"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: 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 Caching can be disabled per device, network or range by setting the disable_cache key to True when defining 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. For tests developers, 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: set = None) -> None:\n super().__init__(name, tags, disable_cache=True)\n"},{"location":"advanced_usages/custom-tests/","title":"Developing ANTA tests","text":"Info 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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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":"Full AntaTest API documentation is available in the API documentation section"},{"location":"advanced_usages/custom-tests/#class-attributes","title":"Class Attributes","text":" name (str, optional): Name of the test. Used during reporting. By default set to the Class name. description (str, optional): A human readable description of your test. By default set to the first line of the docstring. 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":"Attributes: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. 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":"Full Input model documentation is available in API documentation section Attributes: Name Type Description result_overwrite ResultOverwrite | None Define fields to overwrite in the TestResult object."},{"location":"advanced_usages/custom-tests/#resultoverwrite-model","title":"ResultOverwrite model","text":"Full ResultOverwrite model documentation is available in API documentation section Attributes: Name Type Description description str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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 occurrence 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: 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. 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. 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. 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. 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, the first line is used as description of the test by default>\n \"\"\"\n\n # name = <override> # uncomment to override default behavior of name=Class Name\n # description = <override> # uncomment to override default behavior of description=first line of docstring\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 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 values 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, integrations with existing tools are not broken. This is done by using by default version=1 in eAPI calls. By default, ANTA uses version=\"latest\" in AntaCommand, but when developing tests, the revision MUST be provided when the outformat of the command is json. As explained earlier, this is to ensure that the eAPI always returns the same output model and that the test remains always valid from the day it was created. For some commands, you may also want to run them with a different revision or version. For instance, the VerifyBFDPeersHealth 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/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: Parse the command outputs using the self.instance_commands instance attribute. If needed, access the test inputs using the self.inputs instance attribute and write your conditional logic. 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_custom and the test is defined in anta_custom.dc_project Python module, the test catalog would look like: anta_custom.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(\n tests: list[AntaTestDefinition] | None = None,\n filename: str | Path | None = None,\n)\n Class representing an ANTA Catalog. It can be instantiated using its constructor or one of the static methods: parse(), from_list() or from_dict() Parameters: Name Type Description Default tests list[AntaTestDefinition] | None A list of AntaTestDefinition instances. None filename str | Path | None The path from which the catalog is loaded. None"},{"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.build_indexes","title":"build_indexes","text":"build_indexes(\n filtered_tests: set[str] | None = None,\n) -> None\n Indexes tests by their tags for quick access during filtering operations. If a filtered_tests set is provided, only the tests in this set will be indexed. This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests. Once the indexes are built, the indexes_built attribute is set to True. Source code in anta/catalog.py def build_indexes(self, filtered_tests: set[str] | None = None) -> None:\n \"\"\"Indexes tests by their tags for quick access during filtering operations.\n\n If a `filtered_tests` set is provided, only the tests in this set will be indexed.\n\n This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests.\n\n Once the indexes are built, the `indexes_built` attribute is set to True.\n \"\"\"\n for test in self.tests:\n # Skip tests that are not in the specified filtered_tests set\n if filtered_tests and test.test.name not in filtered_tests:\n continue\n\n # Indexing by tag\n if test.inputs.filters and (test_tags := test.inputs.filters.tags):\n for tag in test_tags:\n self.tag_to_tests[tag].add(test)\n else:\n self.tag_to_tests[None].add(test)\n\n self.indexes_built = True\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.clear_indexes","title":"clear_indexes","text":"clear_indexes() -> None\n Clear this AntaCatalog instance indexes. Source code in anta/catalog.py def clear_indexes(self) -> None:\n \"\"\"Clear this AntaCatalog instance indexes.\"\"\"\n self._init_indexes()\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.dump","title":"dump","text":"dump() -> AntaCatalogFile\n Return an AntaCatalogFile instance from this AntaCatalog instance. Returns: Type Description AntaCatalogFile An AntaCatalogFile instance containing tests of this AntaCatalog instance. Source code in anta/catalog.py def dump(self) -> AntaCatalogFile:\n \"\"\"Return an AntaCatalogFile instance from this AntaCatalog instance.\n\n Returns\n -------\n AntaCatalogFile\n An AntaCatalogFile instance containing tests of this AntaCatalog instance.\n \"\"\"\n root: dict[ImportString[Any], list[AntaTestDefinition]] = {}\n for test in self.tests:\n # Cannot use AntaTest.module property as the class is not instantiated\n root.setdefault(test.test.__module__, []).append(test)\n return AntaCatalogFile(root=root)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.from_dict","title":"from_dict staticmethod","text":"from_dict(\n data: RawCatalogInput,\n filename: str | Path | None = None,\n) -> 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. Parameters: Name Type Description Default data RawCatalogInput Python dictionary used to instantiate the AntaCatalog instance. required filename str | Path | None value to be set as AntaCatalog instance attribute None Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 dictionary content. 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 Parameters\n ----------\n data\n Python dictionary used to instantiate the AntaCatalog instance.\n filename\n value to be set as AntaCatalog instance attribute\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' dictionary content.\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. Parameters: Name Type Description Default data ListAntaTestTuples Python list used to instantiate the AntaCatalog instance. required Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 list content. 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 Parameters\n ----------\n data\n Python list used to instantiate the AntaCatalog instance.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' list content.\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(\n tags: set[str], *, strict: bool = False\n) -> set[AntaTestDefinition]\n Return all tests that match a given set of tags, according to the specified strictness. Parameters: Name Type Description Default tags set[str] The tags to filter tests by. If empty, return all tests without tags. required strict bool If True, returns only tests that contain all specified tags (intersection). If False, returns tests that contain any of the specified tags (union). False Returns: Type Description set[AntaTestDefinition] A set of tests that match the given tags. Raises: Type Description ValueError If the indexes have not been built prior to method call. Source code in anta/catalog.py def get_tests_by_tags(self, tags: set[str], *, strict: bool = False) -> set[AntaTestDefinition]:\n \"\"\"Return all tests that match a given set of tags, according to the specified strictness.\n\n Parameters\n ----------\n tags\n The tags to filter tests by. If empty, return all tests without tags.\n strict\n If True, returns only tests that contain all specified tags (intersection).\n If False, returns tests that contain any of the specified tags (union).\n\n Returns\n -------\n set[AntaTestDefinition]\n A set of tests that match the given tags.\n\n Raises\n ------\n ValueError\n If the indexes have not been built prior to method call.\n \"\"\"\n if not self.indexes_built:\n msg = \"Indexes have not been built yet. Call build_indexes() first.\"\n raise ValueError(msg)\n if not tags:\n return self.tag_to_tests[None]\n\n filtered_sets = [self.tag_to_tests[tag] for tag in tags if tag in self.tag_to_tests]\n if not filtered_sets:\n return set()\n\n if strict:\n return set.intersection(*filtered_sets)\n return set.union(*filtered_sets)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge","title":"merge","text":"merge(catalog: AntaCatalog) -> AntaCatalog\n Merge two AntaCatalog instances. Warning This method is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead. Parameters: Name Type Description Default catalog AntaCatalog AntaCatalog instance to merge to this instance. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of the two instances. Source code in anta/catalog.py def merge(self, catalog: AntaCatalog) -> AntaCatalog:\n \"\"\"Merge two AntaCatalog instances.\n\n Warning\n -------\n This method is deprecated and will be removed in ANTA v2.0. Use `AntaCatalog.merge_catalogs()` instead.\n\n Parameters\n ----------\n catalog\n AntaCatalog instance to merge to this instance.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of the two instances.\n \"\"\"\n # TODO: Use a decorator to deprecate this method instead. See https://github.com/aristanetworks/anta/issues/754\n warn(\n message=\"AntaCatalog.merge() is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n return self.merge_catalogs([self, catalog])\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge_catalogs","title":"merge_catalogs classmethod","text":"merge_catalogs(catalogs: list[AntaCatalog]) -> AntaCatalog\n Merge multiple AntaCatalog instances. Parameters: Name Type Description Default catalogs list[AntaCatalog] A list of AntaCatalog instances to merge. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of all the input catalogs. Source code in anta/catalog.py @classmethod\ndef merge_catalogs(cls, catalogs: list[AntaCatalog]) -> AntaCatalog:\n \"\"\"Merge multiple AntaCatalog instances.\n\n Parameters\n ----------\n catalogs\n A list of AntaCatalog instances to merge.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of all the input catalogs.\n \"\"\"\n combined_tests = list(chain(*(catalog.tests for catalog in catalogs)))\n return cls(tests=combined_tests)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.parse","title":"parse staticmethod","text":"parse(\n filename: str | Path,\n file_format: Literal[\"yaml\", \"json\"] = \"yaml\",\n) -> AntaCatalog\n Create an AntaCatalog instance from a test catalog file. Parameters: Name Type Description Default filename str | Path Path to test catalog YAML or JSON file. required file_format Literal['yaml', 'json'] Format of the file, either \u2018yaml\u2019 or \u2018json\u2019. 'yaml' Returns: Type Description AntaCatalog An AntaCatalog populated with the file content. Source code in anta/catalog.py @staticmethod\ndef parse(filename: str | Path, file_format: Literal[\"yaml\", \"json\"] = \"yaml\") -> AntaCatalog:\n \"\"\"Create an AntaCatalog instance from a test catalog file.\n\n Parameters\n ----------\n filename\n Path to test catalog YAML or JSON file.\n file_format\n Format of the file, either 'yaml' or 'json'.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the file content.\n \"\"\"\n if file_format not in [\"yaml\", \"json\"]:\n message = f\"'{file_format}' is not a valid format for an AntaCatalog file. Only 'yaml' and 'json' are supported.\"\n raise ValueError(message)\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) if file_format == \"yaml\" else json_load(f)\n except (TypeError, YAMLError, OSError, ValueError) 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(\n **data: (\n type[AntaTest]\n | AntaTest.Input\n | dict[str, Any]\n | None\n )\n)\n Bases: BaseModel Define a test with its associated inputs. Attributes: Name Type Description test type[AntaTest] An AntaTest concrete subclass. inputs Input The associated AntaTest.Input subclass instance. https://docs.pydantic.dev/2.0/usage/validators/#using-validation-context-with-basemodel-initialization."},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.check_inputs","title":"check_inputs","text":"check_inputs() -> Self\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) -> Self:\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(\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. 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\"Could not instantiate inputs as type {type(data).__name__} is not valid\"\n raise ValueError(msg)\n"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.serialize_model","title":"serialize_model","text":"serialize_model() -> dict[str, AntaTest.Input]\n Serialize the AntaTestDefinition model. The dictionary representing the model will be look like: <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n Returns: Type Description dict A dictionary representing the model. Source code in anta/catalog.py @model_serializer()\ndef serialize_model(self) -> dict[str, AntaTest.Input]:\n \"\"\"Serialize the AntaTestDefinition model.\n\n The dictionary representing the model will be look like:\n ```\n <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n ```\n\n Returns\n -------\n dict\n A dictionary representing the model.\n \"\"\"\n return {self.test.__name__: self.inputs}\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: <Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\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 if not data:\n return data\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 isinstance(test_definition, AntaTestDefinition):\n test_definitions.append(test_definition)\n continue\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 return data\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.flatten_modules","title":"flatten_modules staticmethod","text":"flatten_modules(\n data: dict[str, Any], package: str | None = None\n) -> dict[ModuleType, list[Any]]\n Allow the user to provide a data structure with nested Python modules. Example anta.tests.routing:\n generic:\n - <AntaTestDefinition>\n bgp:\n - <AntaTestDefinition>\n anta.tests.routing.generic and anta.tests.routing.bgp are importable Python modules. 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:\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 elif isinstance(tests, list):\n # This is a list of AntaTestDefinition\n modules[module] = tests\n else:\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 return modules\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.to_json","title":"to_json","text":"to_json() -> str\n Return a JSON representation string of this model. Returns: Type Description str The JSON representation string of this model. Source code in anta/catalog.py def to_json(self) -> str:\n \"\"\"Return a JSON representation string of this model.\n\n Returns\n -------\n str\n The JSON representation string of this model.\n \"\"\"\n return self.model_dump_json(serialize_as_any=True, exclude_unset=True, indent=2)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/catalog.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return safe_dump(safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/csv_reporter/","title":"CSV reporter","text":"CSV Report management for ANTA."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv","title":"ReportCsv","text":"Build a CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_name: str = \"Test Name\",\n test_status: str = \"Test Status\",\n messages: str = \"Message(s)\",\n description: str = \"Test description\",\n categories: str = \"Test category\",\n)\n Headers for the CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.convert_to_list","title":"convert_to_list classmethod","text":"convert_to_list(result: TestResult) -> list[str]\n Convert a TestResult into a list of string for creating file content. Parameters: Name Type Description Default result TestResult A TestResult to convert into list. required Returns: Type Description list[str] TestResult converted into a list. Source code in anta/reporter/csv_reporter.py @classmethod\ndef convert_to_list(cls, result: TestResult) -> list[str]:\n \"\"\"Convert a TestResult into a list of string for creating file content.\n\n Parameters\n ----------\n result\n A TestResult to convert into list.\n\n Returns\n -------\n list[str]\n TestResult converted into a list.\n \"\"\"\n message = cls.split_list_to_txt_list(result.messages) if len(result.messages) > 0 else \"\"\n categories = cls.split_list_to_txt_list(convert_categories(result.categories)) if len(result.categories) > 0 else \"None\"\n return [\n str(result.name),\n result.test,\n result.result,\n message,\n result.description,\n categories,\n ]\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.generate","title":"generate classmethod","text":"generate(\n results: ResultManager, csv_filename: pathlib.Path\n) -> None\n Build CSV flle with tests results. Parameters: Name Type Description Default results ResultManager A ResultManager instance. required csv_filename Path File path where to save CSV data. required Raises: Type Description OSError if any is raised while writing the CSV file. Source code in anta/reporter/csv_reporter.py @classmethod\ndef generate(cls, results: ResultManager, csv_filename: pathlib.Path) -> None:\n \"\"\"Build CSV flle with tests results.\n\n Parameters\n ----------\n results\n A ResultManager instance.\n csv_filename\n File path where to save CSV data.\n\n Raises\n ------\n OSError\n if any is raised while writing the CSV file.\n \"\"\"\n headers = [\n cls.Headers.device,\n cls.Headers.test_name,\n cls.Headers.test_status,\n cls.Headers.messages,\n cls.Headers.description,\n cls.Headers.categories,\n ]\n\n try:\n with csv_filename.open(mode=\"w\", encoding=\"utf-8\") as csvfile:\n csvwriter = csv.writer(\n csvfile,\n delimiter=\",\",\n )\n csvwriter.writerow(headers)\n for entry in results.results:\n csvwriter.writerow(cls.convert_to_list(entry))\n except OSError as exc:\n message = f\"OSError caught while writing the CSV file '{csv_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.split_list_to_txt_list","title":"split_list_to_txt_list classmethod","text":"split_list_to_txt_list(\n usr_list: list[str], delimiter: str = \" - \"\n) -> str\n Split list to multi-lines string. Parameters: Name Type Description Default usr_list list[str] List of string to concatenate. required delimiter str A delimiter to use to start string. Defaults to None. ' - ' Returns: Type Description str Multi-lines string. Source code in anta/reporter/csv_reporter.py @classmethod\ndef split_list_to_txt_list(cls, usr_list: list[str], delimiter: str = \" - \") -> str:\n \"\"\"Split list to multi-lines string.\n\n Parameters\n ----------\n usr_list\n List of string to concatenate.\n delimiter\n A delimiter to use to start string. Defaults to None.\n\n Returns\n -------\n str\n Multi-lines string.\n\n \"\"\"\n return f\"{delimiter}\".join(f\"{line}\" for line in usr_list)\n"},{"location":"api/device/","title":"Device","text":""},{"location":"api/device/#antadevice-base-class","title":"AntaDevice base class","text":""},{"location":"api/device/#anta.device.AntaDevice","title":"AntaDevice","text":"AntaDevice(\n name: str,\n tags: set[str] | None = None,\n *,\n disable_cache: bool = False\n)\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 str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. cache Cache | None In-memory cache from aiocache library for this device (None if cache is disabled). cache_locks dict Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled. Parameters: Name Type Description Default name str Device name. required tags set[str] | None Tags for this device. None disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AntaDevice.cache_statistics","title":"cache_statistics property","text":"cache_statistics: dict[str, Any] | None\n Return 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.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AntaDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AntaDevice.\"\"\"\n return (\n f\"AntaDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AntaDevice._collect","title":"_collect abstractmethod async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output. This abstract coroutine can be used to implement any command collection method for a device in ANTA. The _collect() implementation needs to populate the output attribute of the AntaCommand object passed as argument. If a failure occurs, the _collect() implementation is expected to catch the exception and implement proper logging, the output attribute of the AntaCommand object passed as argument would be None in this case. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py @abstractmethod\nasync def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None:\n \"\"\"Collect device command output.\n\n This abstract coroutine can be used to implement any command collection method\n for a device in ANTA.\n\n The `_collect()` implementation needs to populate the `output` attribute\n of the `AntaCommand` object passed as argument.\n\n If a failure occurs, the `_collect()` implementation is expected to catch the\n exception and implement proper logging, the `output` attribute of the\n `AntaCommand` object passed as argument would be `None` in this case.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n"},{"location":"api/device/#anta.device.AntaDevice.collect","title":"collect async","text":"collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> 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. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect(self, command: AntaCommand, *, collection_id: str | None = None) -> 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 Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\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, collection_id=collection_id)\n await self.cache.set(command.uid, command.output) # pylint: disable=no-member\n else:\n await self._collect(command=command, collection_id=collection_id)\n"},{"location":"api/device/#anta.device.AntaDevice.collect_commands","title":"collect_commands async","text":"collect_commands(\n commands: list[AntaCommand],\n *,\n collection_id: str | None = None\n) -> None\n Collect multiple commands. Parameters: Name Type Description Default commands list[AntaCommand] The commands to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect_commands(self, commands: list[AntaCommand], *, collection_id: str | None = None) -> None:\n \"\"\"Collect multiple commands.\n\n Parameters\n ----------\n commands\n The commands to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n await asyncio.gather(*(self.collect(command=command, collection_id=collection_id) for command in commands))\n"},{"location":"api/device/#anta.device.AntaDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device, usually through SCP. It is not mandatory to implement this for a valid AntaDevice subclass. Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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\n - `is_online`: When the device IP is reachable and a port can be open.\n\n - `established`: When a command execution succeeds.\n\n - `hw_model`: The hardware model of the device.\n \"\"\"\n"},{"location":"api/device/#async-eos-device-class","title":"Async EOS device class","text":""},{"location":"api/device/#anta.device.AsyncEOSDevice","title":"AsyncEOSDevice","text":"AsyncEOSDevice(\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: set[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)\n Bases: AntaDevice Implementation of AntaDevice for EOS using aio-eapi. Attributes: Name Type Description name str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. Parameters: Name Type Description Default host str Device FQDN or IP. required username str Username to connect to eAPI and SSH. required password str Password to connect to eAPI and SSH. required name str | None Device name. None enable bool Collect commands using privileged mode. False enable_password str | None Password used to gain privileged access on EOS. None port int | None eAPI port. Defaults to 80 is proto is \u2018http\u2019 or 443 if proto is \u2018https\u2019. None ssh_port int | None SSH port. 22 tags set[str] | None Tags for this device. None timeout float | None Timeout value in seconds for outgoing API calls. None insecure bool Disable SSH Host Key validation. False proto Literal['http', 'https'] eAPI protocol. Value can be \u2018http\u2019 or \u2018https\u2019. 'https' disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AsyncEOSDevice.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AsyncEOSDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AsyncEOSDevice.\"\"\"\n return (\n f\"AsyncEOSDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r}, \"\n f\"host={self._session.host!r}, \"\n f\"eapi_port={self._session.port!r}, \"\n f\"username={self._ssh_opts.username!r}, \"\n f\"enable={self.enable!r}, \"\n f\"insecure={self._ssh_opts.known_hosts is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AsyncEOSDevice._collect","title":"_collect async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output from EOS using aio-eapi. Supports outformat json and text as output structure. Gain privileged access using the enable_password attribute of the AntaDevice instance if populated. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None: # noqa: C901 function is too complex - because of many required except blocks\n \"\"\"Collect device command output from EOS using aio-eapi.\n\n Supports outformat `json` and `text` as output structure.\n Gain privileged access using the `enable_password` attribute\n of the `AntaDevice` instance if populated.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n commands: list[dict[str, str | int]] = []\n if self.enable and self._enable_password is not None:\n commands.append(\n {\n \"cmd\": \"enable\",\n \"input\": str(self._enable_password),\n },\n )\n elif self.enable:\n # No password\n commands.append({\"cmd\": \"enable\"})\n commands += [{\"cmd\": command.command, \"revision\": command.revision}] if command.revision else [{\"cmd\": command.command}]\n try:\n response: list[dict[str, Any] | str] = await self._session.cli(\n commands=commands,\n ofmt=command.ofmt,\n version=command.version,\n req_id=f\"ANTA-{collection_id}-{id(command)}\" if collection_id else f\"ANTA-{id(command)}\",\n ) # type: ignore[assignment] # multiple commands returns a list\n # Do not keep response of 'enable' command\n command.output = response[-1]\n except asynceapi.EapiCommandError as e:\n # This block catches exceptions related to EOS issuing an error.\n command.errors = e.errors\n if command.requires_privileges:\n logger.error(\n \"Command '%s' requires privileged mode on %s. Verify user permissions and if the `enable` option is required.\", command.command, self.name\n )\n if command.supported:\n logger.error(\"Command '%s' failed on %s: %s\", command.command, self.name, e.errors[0] if len(e.errors) == 1 else e.errors)\n else:\n logger.debug(\"Command '%s' is not supported on '%s' (%s)\", command.command, self.name, self.hw_model)\n except TimeoutException as e:\n # This block catches Timeout exceptions.\n command.errors = [exc_to_str(e)]\n timeouts = self._session.timeout.as_dict()\n logger.error(\n \"%s occurred while sending a command to %s. Consider increasing the timeout.\\nCurrent timeouts: Connect: %s | Read: %s | Write: %s | Pool: %s\",\n exc_to_str(e),\n self.name,\n timeouts[\"connect\"],\n timeouts[\"read\"],\n timeouts[\"write\"],\n timeouts[\"pool\"],\n )\n except (ConnectError, OSError) as e:\n # This block catches OSError and socket issues related exceptions.\n command.errors = [exc_to_str(e)]\n if (isinstance(exc := e.__cause__, httpcore.ConnectError) and isinstance(os_error := exc.__context__, OSError)) or isinstance(os_error := e, OSError): # pylint: disable=no-member\n if isinstance(os_error.__cause__, OSError):\n os_error = os_error.__cause__\n logger.error(\"A local OS error occurred while connecting to %s: %s.\", self.name, os_error)\n else:\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n except HTTPError as e:\n # This block catches most of the httpx Exceptions and logs a general message.\n command.errors = [exc_to_str(e)]\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n logger.debug(\"%s: %s\", self.name, command)\n"},{"location":"api/device/#anta.device.AsyncEOSDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device using asyncssh.scp(). Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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() function 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 = AntaCommand(command=\"show version\")\n await self._collect(show_version)\n if not show_version.collected:\n logger.warning(\"Cannot get hardware information from device %s\", self.name)\n else:\n self.hw_model = show_version.json_output.get(\"modelName\", None)\n if self.hw_model is None:\n logger.critical(\"Cannot parse 'show version' returned by device %s\", self.name)\n # in some cases it is possible that 'modelName' comes back empty\n # and it is nice to get a meaninfule error message\n elif self.hw_model == \"\":\n logger.critical(\"Got an empty 'modelName' in the 'show version' returned by device %s\", self.name)\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.devices","title":"devices property","text":"devices: list[AntaDevice]\n List of AntaDevice in this inventory."},{"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. Parameters: Name Type Description Default device AntaDevice Device object to be added. required Source code in anta/inventory/__init__.py def add_device(self, device: AntaDevice) -> None:\n \"\"\"Add a device to final inventory.\n\n Parameters\n ----------\n device\n 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(\n *,\n established_only: bool = False,\n tags: set[str] | None = None,\n devices: set[str] | None = None\n) -> AntaInventory\n Return a filtered inventory. Parameters: Name Type Description Default established_only bool Whether or not to include only established devices. False tags set[str] | None Tags to filter devices. None devices set[str] | None Names to filter devices. None 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: set[str] | None = None, devices: set[str] | None = None) -> AntaInventory:\n \"\"\"Return a filtered inventory.\n\n Parameters\n ----------\n established_only\n Whether or not to include only established devices.\n tags\n Tags to filter devices.\n devices\n Names to filter devices.\n\n Returns\n -------\n AntaInventory\n An inventory with filtered AntaDevice objects.\n \"\"\"\n\n def _filter_devices(device: AntaDevice) -> bool:\n \"\"\"Select the devices based on the inputs `tags`, `devices` and `established_only`.\"\"\"\n if tags is not None and all(tag not in tags for tag in device.tags):\n return False\n if devices is None or device.name in devices:\n return bool(not established_only or device.established)\n return False\n\n filtered_devices: list[AntaDevice] = list(filter(_filter_devices, self.values()))\n result = AntaInventory()\n for device in filtered_devices:\n result.add_device(device)\n return result\n"},{"location":"api/inventory/#anta.inventory.AntaInventory.parse","title":"parse staticmethod","text":"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. The inventory devices are AsyncEOSDevice instances. Parameters: Name Type Description Default filename str | Path Path to device inventory YAML file. required username str Username to use to connect to devices. required password str Password to use to connect to devices. required enable_password str | None Enable password to use if required. None timeout float | None Timeout value in seconds for outgoing API calls. None enable bool Whether or not the commands need to be run in enable mode towards the devices. False insecure bool Disable SSH Host Key validation. False disable_cache bool Disable cache globally. False 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 Parameters\n ----------\n filename\n Path to device inventory YAML file.\n username\n Username to use to connect to devices.\n password\n Password to use to connect to devices.\n enable_password\n Enable password to use if required.\n timeout\n Timeout value in seconds for outgoing API calls.\n enable\n Whether or not the commands need to be run in enable mode towards the devices.\n insecure\n Disable SSH Host Key validation.\n disable_cache\n Disable cache globally.\n\n Raises\n ------\n InventoryRootKeyError\n Root key of inventory is missing.\n InventoryIncorrectSchemaError\n 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 Device inventory input model."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryInput.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/inventory/models.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return yaml.safe_dump(yaml.safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryHost","title":"AntaInventoryHost","text":" Bases: BaseModel Host entry of AntaInventoryInput. Attributes: Name Type Description host Hostname | IPvAnyAddress IP Address or FQDN of the device. port Port | None Custom eAPI port to use. name str | None Custom name of the device. tags set[str] Tags of the device. disable_cache bool Disable cache for this device."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryNetwork","title":"AntaInventoryNetwork","text":" Bases: BaseModel Network entry of AntaInventoryInput. Attributes: Name Type Description network IPvAnyNetwork Subnet to use for scanning. tags set[str] Tags of the devices in this network. disable_cache bool Disable cache for all devices in this network."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryRange","title":"AntaInventoryRange","text":" Bases: BaseModel IP Range entry of AntaInventoryInput. Attributes: Name Type Description start IPvAnyAddress IPv4 or IPv6 address for the beginning of the range. stop IPvAnyAddress IPv4 or IPv6 address for the end of the range. tags set[str] Tags of the devices in this IP range. disable_cache bool Disable cache for all devices in this IP range."},{"location":"api/md_reporter/","title":"Markdown reporter","text":"Markdown report generator for ANTA test results."},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport","title":"ANTAReport","text":"ANTAReport(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generate the # ANTA Report section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the # ANTA Report section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `# ANTA Report` section of the markdown report.\"\"\"\n self.write_heading(heading_level=1)\n toc = MD_REPORT_TOC\n self.mdfile.write(toc + \"\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase","title":"MDReportBase","text":"MDReportBase(mdfile: TextIOWrapper, results: ResultManager)\n Bases: ABC Base class for all sections subclasses. Every subclasses must implement the generate_section method that uses the ResultManager object to generate and write content to the provided markdown file. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_heading_name","title":"generate_heading_name","text":"generate_heading_name() -> str\n Generate a formatted heading name based on the class name. Returns: Type Description str Formatted header name. Example ANTAReport will become ANTA Report. TestResultsSummary will become Test Results Summary. Source code in anta/reporter/md_reporter.py def generate_heading_name(self) -> str:\n \"\"\"Generate a formatted heading name based on the class name.\n\n Returns\n -------\n str\n Formatted header name.\n\n Example\n -------\n - `ANTAReport` will become `ANTA Report`.\n - `TestResultsSummary` will become `Test Results Summary`.\n \"\"\"\n class_name = self.__class__.__name__\n\n # Split the class name into words, keeping acronyms together\n words = re.findall(r\"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\\d|\\W|$)|\\d+\", class_name)\n\n # Capitalize each word, but keep acronyms in all caps\n formatted_words = [word if word.isupper() else word.capitalize() for word in words]\n\n return \" \".join(formatted_words)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of a markdown table for a specific report section. Subclasses can implement this method to generate the content of the table rows. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of a markdown table for a specific report section.\n\n Subclasses can implement this method to generate the content of the table rows.\n \"\"\"\n msg = \"Subclasses should implement this method\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_section","title":"generate_section abstractmethod","text":"generate_section() -> None\n Abstract method to generate a specific section of the markdown report. Must be implemented by subclasses. Source code in anta/reporter/md_reporter.py @abstractmethod\ndef generate_section(self) -> None:\n \"\"\"Abstract method to generate a specific section of the markdown report.\n\n Must be implemented by subclasses.\n \"\"\"\n msg = \"Must be implemented by subclasses\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.safe_markdown","title":"safe_markdown","text":"safe_markdown(text: str | None) -> str\n Escape markdown characters in the text to prevent markdown rendering issues. Parameters: Name Type Description Default text str | None The text to escape markdown characters from. required Returns: Type Description str The text with escaped markdown characters. Source code in anta/reporter/md_reporter.py def safe_markdown(self, text: str | None) -> str:\n \"\"\"Escape markdown characters in the text to prevent markdown rendering issues.\n\n Parameters\n ----------\n text\n The text to escape markdown characters from.\n\n Returns\n -------\n str\n The text with escaped markdown characters.\n \"\"\"\n # Custom field from a TestResult object can be None\n if text is None:\n return \"\"\n\n # Replace newlines with spaces to keep content on one line\n text = text.replace(\"\\n\", \" \")\n\n # Replace backticks with single quotes\n return text.replace(\"`\", \"'\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_heading","title":"write_heading","text":"write_heading(heading_level: int) -> None\n Write a markdown heading to the markdown file. The heading name used is the class name. Parameters: Name Type Description Default heading_level int The level of the heading (1-6). required Example ## Test Results Summary Source code in anta/reporter/md_reporter.py def write_heading(self, heading_level: int) -> None:\n \"\"\"Write a markdown heading to the markdown file.\n\n The heading name used is the class name.\n\n Parameters\n ----------\n heading_level\n The level of the heading (1-6).\n\n Example\n -------\n `## Test Results Summary`\n \"\"\"\n # Ensure the heading level is within the valid range of 1 to 6\n heading_level = max(1, min(heading_level, 6))\n heading_name = self.generate_heading_name()\n heading = \"#\" * heading_level + \" \" + heading_name\n self.mdfile.write(f\"{heading}\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_table","title":"write_table","text":"write_table(\n table_heading: list[str], *, last_table: bool = False\n) -> None\n Write a markdown table with a table heading and multiple rows to the markdown file. Parameters: Name Type Description Default table_heading list[str] List of strings to join for the table heading. required last_table bool Flag to determine if it\u2019s the last table of the markdown file to avoid unnecessary new line. Defaults to False. False Source code in anta/reporter/md_reporter.py def write_table(self, table_heading: list[str], *, last_table: bool = False) -> None:\n \"\"\"Write a markdown table with a table heading and multiple rows to the markdown file.\n\n Parameters\n ----------\n table_heading\n List of strings to join for the table heading.\n last_table\n Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.\n \"\"\"\n self.mdfile.write(\"\\n\".join(table_heading) + \"\\n\")\n for row in self.generate_rows():\n self.mdfile.write(row)\n if not last_table:\n self.mdfile.write(\"\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator","title":"MDReportGenerator","text":"Class responsible for generating a Markdown report based on the provided ResultManager object. It aggregates different report sections, each represented by a subclass of MDReportBase, and sequentially generates their content into a markdown file. The generate class method will loop over all the section subclasses and call their generate_section method. The final report will be generated in the same order as the sections list of the method."},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator.generate","title":"generate classmethod","text":"generate(results: ResultManager, md_filename: Path) -> None\n Generate and write the various sections of the markdown report. Parameters: Name Type Description Default results ResultManager The ResultsManager instance containing all test results. required md_filename Path The path to the markdown file to write the report into. required Source code in anta/reporter/md_reporter.py @classmethod\ndef generate(cls, results: ResultManager, md_filename: Path) -> None:\n \"\"\"Generate and write the various sections of the markdown report.\n\n Parameters\n ----------\n results\n The ResultsManager instance containing all test results.\n md_filename\n The path to the markdown file to write the report into.\n \"\"\"\n try:\n with md_filename.open(\"w\", encoding=\"utf-8\") as mdfile:\n sections: list[MDReportBase] = [\n ANTAReport(mdfile, results),\n TestResultsSummary(mdfile, results),\n SummaryTotals(mdfile, results),\n SummaryTotalsDeviceUnderTest(mdfile, results),\n SummaryTotalsPerCategory(mdfile, results),\n TestResults(mdfile, results),\n ]\n for section in sections:\n section.generate_section()\n except OSError as exc:\n message = f\"OSError caught while writing the Markdown file '{md_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals","title":"SummaryTotals","text":"SummaryTotals(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals table.\"\"\"\n yield (\n f\"| {self.results.get_total_results()} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SUCCESS})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SKIPPED})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.FAILURE})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.ERROR})} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest","title":"SummaryTotalsDeviceUnderTest","text":"SummaryTotalsDeviceUnderTest(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Devices Under Tests section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals device under test table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals device under test table.\"\"\"\n for device, stat in self.results.device_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n categories_skipped = \", \".join(sorted(convert_categories(list(stat.categories_skipped))))\n categories_failed = \", \".join(sorted(convert_categories(list(stat.categories_failed))))\n yield (\n f\"| {device} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} | {stat.tests_error_count} \"\n f\"| {categories_skipped or '-'} | {categories_failed or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Devices Under Tests section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Devices Under Tests` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory","title":"SummaryTotalsPerCategory","text":"SummaryTotalsPerCategory(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Per Category section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals per category table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals per category table.\"\"\"\n for category, stat in self.results.sorted_category_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n yield (\n f\"| {category} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} \"\n f\"| {stat.tests_error_count} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Per Category section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Per Category` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults","title":"TestResults","text":"TestResults(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generates the ## Test Results section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the all test results table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the all test results table.\"\"\"\n for result in self.results.get_results(sort_by=[\"name\", \"test\"]):\n messages = self.safe_markdown(\", \".join(result.messages))\n categories = \", \".join(convert_categories(result.categories))\n yield (\n f\"| {result.name or '-'} | {categories or '-'} | {result.test or '-'} \"\n f\"| {result.description or '-'} | {self.safe_markdown(result.custom_field) or '-'} | {result.result or '-'} | {messages or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n self.write_table(table_heading=self.TABLE_HEADING, last_table=True)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary","title":"TestResultsSummary","text":"TestResultsSummary(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ## Test Results Summary section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results Summary section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results Summary` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n"},{"location":"api/models/","title":"Test models","text":""},{"location":"api/models/#test-definition","title":"Test definition","text":""},{"location":"api/models/#anta.models.AntaTest","title":"AntaTest","text":"AntaTest(\n device: AntaDevice,\n inputs: dict[str, Any] | AntaTest.Input | None = None,\n eos_data: list[dict[Any, Any] | str] | None = None,\n)\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 '''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 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: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. Parameters: Name Type Description Default device AntaDevice AntaDevice instance on which the test will be run. required inputs dict[str, Any] | Input | None Dictionary of attributes used to instantiate the AntaTest.Input instance. None eos_data list[dict[Any, Any] | str] | None Populate outputs of the test commands instead of collecting from devices. This list must have the same length and order than the instance_commands instance attribute. None"},{"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 Return 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 Return a list of all the commands that have failed."},{"location":"api/models/#anta.models.AntaTest.module","title":"module property","text":"module: str\n Return the Python module in which this AntaTest class is defined."},{"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: Name Type Description result_overwrite ResultOverwrite | None 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 set[str] | None Tag of devices on which to run 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 str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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(\n function: F,\n) -> Callable[..., Coroutine[Any, Any, TestResult]]\n Decorate the test() method in child classes. This decorator implements (in this order): Instantiate the command outputs if eos_data is provided to the test() method Collect the commands from the device Run the test() method 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 Parameters\n ----------\n self\n The test instance.\n eos_data\n 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\n Any keyword argument to pass to the test.\n\n Returns\n -------\n TestResult\n The TestResult instance attribute populated with error status if any.\n\n \"\"\"\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 AntaTest.update_progress()\n return self.result\n\n if cmds := self.failed_commands:\n unsupported_commands = [f\"'{c.command}' is not supported on {self.device.hw_model}\" for c in cmds if not c.supported]\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 else:\n self.result.is_error(message=\"\\n\".join([f\"{c.command} has failed: {', '.join(c.errors)}\" for c in cmds]))\n AntaTest.update_progress()\n return self.result\n\n try:\n function(self, **kwargs)\n except Exception as e: # noqa: BLE001\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 # TODO: find a correct way to time test execution\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, collection_id=self.name)\n except Exception as e: # noqa: BLE001\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]:\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 _ = template\n msg = f\"AntaTemplate are provided but render() method has not been implemented for {self.module}.{self.__class__.__name__}\"\n raise NotImplementedError(msg)\n"},{"location":"api/models/#anta.models.AntaTest.save_commands_data","title":"save_commands_data","text":"save_commands_data(\n eos_data: list[dict[str, Any] | str]\n) -> 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 reason\")\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 reason\")\n ```\n\n \"\"\"\n"},{"location":"api/models/#command-definition","title":"Command definition","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 str Device command. version Literal[1, 'latest'] eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision | None eAPI revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt Literal['json', 'text'] eAPI output - json or text. output dict[str, Any] | str | None Output of the command. Only defined if there was no errors. template AntaTemplate | None AntaTemplate object used to render this command. errors list[str] If the command execution fails, eAPI returns a list of strings detailing the error(s). params AntaParamsBaseModel Pydantic Model containing the variables values used to render the template. use_cache bool Enable or disable caching for this AntaCommand if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaCommand.collected","title":"collected property","text":"collected: bool\n Return True if the command has been collected, False otherwise. A command that has not been collected could have returned an error. See error property."},{"location":"api/models/#anta.models.AntaCommand.error","title":"error property","text":"error: bool\n Return True if the command returned an error, False otherwise."},{"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.requires_privileges","title":"requires_privileges property","text":"requires_privileges: bool\n Return True if the command requires privileged mode, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"location":"api/models/#anta.models.AntaCommand.supported","title":"supported property","text":"supported: bool\n Return True if the command is supported on the device hardware platform, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"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/#anta.models.AntaTemplate","title":"AntaTemplate","text":"AntaTemplate(\n template: str,\n version: Literal[1, \"latest\"] = \"latest\",\n revision: Revision | None = None,\n ofmt: Literal[\"json\", \"text\"] = \"json\",\n *,\n use_cache: bool = True\n)\n 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: \u2018show vlan {vlan_id}\u2019. version eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt eAPI output - json or text. use_cache Enable or disable caching for this AntaTemplate if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaTemplate.__repr__","title":"__repr__","text":"__repr__() -> str\n Return the representation of the class. Copying pydantic model style, excluding params_schema Source code in anta/models.py def __repr__(self) -> str:\n \"\"\"Return the representation of the class.\n\n Copying pydantic model style, excluding `params_schema`\n \"\"\"\n return \" \".join(f\"{a}={v!r}\" for a, v in vars(self).items() if a != \"params_schema\")\n"},{"location":"api/models/#anta.models.AntaTemplate.render","title":"render","text":"render(**params: str | int | bool) -> AntaCommand\n Render an AntaCommand from an AntaTemplate instance. Keep the parameters used in the AntaTemplate instance. Parameters: Name Type Description Default params str | int | bool Dictionary of variables with string values to render the Python f-string. {} Returns: Type Description AntaCommand The rendered AntaCommand. This AntaCommand instance have a template attribute that references this AntaTemplate instance. Raises: Type Description AntaTemplateRenderError If a parameter is missing to render the AntaTemplate instance. Source code in anta/models.py def render(self, **params: str | int | bool) -> AntaCommand:\n \"\"\"Render an AntaCommand from an AntaTemplate instance.\n\n Keep the parameters used in the AntaTemplate instance.\n\n Parameters\n ----------\n params\n Dictionary of variables with string values to render the Python f-string.\n\n Returns\n -------\n AntaCommand\n The rendered AntaCommand.\n This AntaCommand instance have a template attribute that references this\n AntaTemplate instance.\n\n Raises\n ------\n AntaTemplateRenderError\n If a parameter is missing to render the AntaTemplate instance.\n \"\"\"\n try:\n command = self.template.format(**params)\n except (KeyError, SyntaxError) as e:\n raise AntaTemplateRenderError(self, e.args[0]) from e\n return AntaCommand(\n command=command,\n ofmt=self.ofmt,\n version=self.version,\n revision=self.revision,\n template=self,\n params=self.params_schema(**params),\n use_cache=self.use_cache,\n )\n"},{"location":"api/reporters/","title":"Other reporters","text":"Report management for ANTA."},{"location":"api/reporters/#anta.reporter.ReportJinja","title":"ReportJinja","text":"ReportJinja(template_path: pathlib.Path)\n Report builder based on a Jinja2 template."},{"location":"api/reporters/#anta.reporter.ReportJinja.render","title":"render","text":"render(\n data: list[dict[str, Any]],\n *,\n trim_blocks: bool = True,\n lstrip_blocks: bool = True\n) -> str\n Build a report based on a Jinja2 template. Report is built based on a J2 template provided by user. Data structure sent to template is: Example >>> print(ResultManager.json)\n[\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n]\n Parameters: Name Type Description Default data list[dict[str, Any]] List of results from ResultManager.results. required trim_blocks bool enable trim_blocks for J2 rendering. True lstrip_blocks bool enable lstrip_blocks for J2 rendering. True Returns: Type Description str Rendered template Source code in anta/reporter/__init__.py def render(self, data: list[dict[str, Any]], *, trim_blocks: bool = True, lstrip_blocks: bool = True) -> str:\n \"\"\"Build a report based on a Jinja2 template.\n\n Report is built based on a J2 template provided by user.\n Data structure sent to template is:\n\n Example\n -------\n ```\n >>> print(ResultManager.json)\n [\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n ]\n ```\n\n Parameters\n ----------\n data\n List of results from `ResultManager.results`.\n trim_blocks\n enable trim_blocks for J2 rendering.\n lstrip_blocks\n enable lstrip_blocks for J2 rendering.\n\n Returns\n -------\n str\n Rendered template\n\n \"\"\"\n with self.template_path.open(encoding=\"utf-8\") as file_:\n template = Template(file_.read(), trim_blocks=trim_blocks, lstrip_blocks=lstrip_blocks)\n\n return template.render({\"data\": data})\n"},{"location":"api/reporters/#anta.reporter.ReportTable","title":"ReportTable","text":"TableReport Generate a Table based on TestResult."},{"location":"api/reporters/#anta.reporter.ReportTable.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_case: str = \"Test Name\",\n number_of_success: str = \"# of success\",\n number_of_failure: str = \"# of failure\",\n number_of_skipped: str = \"# of skipped\",\n number_of_errors: str = \"# of errors\",\n list_of_error_nodes: str = \"List of failed or error nodes\",\n list_of_error_tests: str = \"List of failed or error test cases\",\n)\n Headers for the table report."},{"location":"api/reporters/#anta.reporter.ReportTable.report_all","title":"report_all","text":"report_all(\n manager: ResultManager, title: str = \"All tests results\"\n) -> Table\n Create a table report with all tests for one or all devices. Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required title str Title for the report. Defaults to \u2018All tests results\u2019. 'All tests results' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_all(self, manager: ResultManager, title: str = \"All tests results\") -> Table:\n \"\"\"Create a table report with all tests for one or all devices.\n\n Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n title\n Title for the report. Defaults to 'All tests results'.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\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 def add_line(result: TestResult) -> None:\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(convert_categories(result.categories))\n table.add_row(str(result.name), result.test, state, message, result.description, categories)\n\n for result in manager.results:\n add_line(result)\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_devices","title":"report_summary_devices","text":"report_summary_devices(\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table\n Create a table report with result aggregated per device. Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required devices list[str] | None List of device names to include. None to select all devices. None title str Title of the report. 'Summary per device' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_devices(\n self,\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per device.\n\n Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n devices\n List of device names to include. None to select all devices.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.device,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_tests,\n ]\n table = self._build_headers(headers=headers, table=table)\n for device, stats in sorted(manager.device_stats.items()):\n if devices is None or device in devices:\n table.add_row(\n device,\n str(stats.tests_success_count),\n str(stats.tests_skipped_count),\n str(stats.tests_failure_count),\n str(stats.tests_error_count),\n \", \".join(stats.tests_failure),\n )\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_tests","title":"report_summary_tests","text":"report_summary_tests(\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table\n Create a table report with result aggregated per test. Create table with full output: Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required tests list[str] | None List of test names to include. None to select all tests. None title str Title of the report. 'Summary per test' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_tests(\n self,\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per test.\n\n Create table with full output:\n Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n tests\n List of test names to include. None to select all tests.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.test_case,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_nodes,\n ]\n table = self._build_headers(headers=headers, table=table)\n for test, stats in sorted(manager.test_stats.items()):\n if tests is None or test in tests:\n table.add_row(\n test,\n str(stats.devices_success_count),\n str(stats.devices_skipped_count),\n str(stats.devices_failure_count),\n str(stats.devices_error_count),\n \", \".join(stats.devices_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":" options:\n filters: [\"!^_[^_]\", \"!^__len__\"]\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager","title":"ResultManager","text":"ResultManager()\n Helper to manage Test Results and generate reports. Examples Create Inventory: inventory_anta = AntaInventory.parse(\n filename='examples/inventory.yml',\n username='ansible',\n password='ansible',\n)\n Create Result Manager: manager = ResultManager()\n Run tests for all connected devices: for device in inventory_anta.get_inventory().devices:\n manager.add(\n VerifyNTP(device=device).test()\n )\n manager.add(\n VerifyEOSVersion(device=device).test(version='4.28.3M')\n )\n Print result in native format: manager.results\n[\n TestResult(\n name=\"pf1\",\n test=\"VerifyZeroTouch\",\n categories=[\"configuration\"],\n description=\"Verifies ZeroTouch is disabled\",\n result=\"success\",\n messages=[],\n custom_field=None,\n ),\n TestResult(\n name=\"pf1\",\n test='VerifyNTP',\n categories=[\"software\"],\n categories=['system'],\n description='Verifies if NTP is synchronised.',\n result='failure',\n messages=[\"The device is not synchronized with the configured NTP server(s): 'NTP is disabled.'\"],\n custom_field=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."},{"location":"api/result_manager/#anta.result_manager.ResultManager.json","title":"json property","text":"json: str\n Get a JSON representation of the results."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results","title":"results property writable","text":"results: list[TestResult]\n Get the list of TestResult."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results_by_status","title":"results_by_status cached property","text":"results_by_status: dict[AntaTestStatus, list[TestResult]]\n A cached property that returns the results grouped by status."},{"location":"api/result_manager/#anta.result_manager.ResultManager.sorted_category_stats","title":"sorted_category_stats property","text":"sorted_category_stats: dict[str, CategoryStats]\n A property that returns the category_stats dictionary sorted by key name."},{"location":"api/result_manager/#anta.result_manager.ResultManager.__len__","title":"__len__","text":"__len__() -> int\n Implement len method to count number of results. Source code in anta/result_manager/__init__.py def __len__(self) -> int:\n \"\"\"Implement __len__ method to count number of results.\"\"\"\n return len(self._result_entries)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.add","title":"add","text":"add(result: TestResult) -> None\n Add a result to the ResultManager instance. The result is added to the internal list of results and the overall status of the ResultManager instance is updated based on the added test status. Parameters: Name Type Description Default result TestResult TestResult to add to the ResultManager instance. required Source code in anta/result_manager/__init__.py def add(self, result: TestResult) -> None:\n \"\"\"Add a result to the ResultManager instance.\n\n The result is added to the internal list of results and the overall status\n of the ResultManager instance is updated based on the added test status.\n\n Parameters\n ----------\n result\n TestResult to add to the ResultManager instance.\n \"\"\"\n self._result_entries.append(result)\n self._update_status(result.result)\n self._update_stats(result)\n\n # Every time a new result is added, we need to clear the cached property\n self.__dict__.pop(\"results_by_status\", None)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter","title":"filter","text":"filter(hide: set[AntaTestStatus]) -> ResultManager\n Get a filtered ResultManager based on test status. Parameters: Name Type Description Default hide set[AntaTestStatus] Set of AntaTestStatus enum members to select tests to hide based on their status. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter(self, hide: set[AntaTestStatus]) -> ResultManager:\n \"\"\"Get a filtered ResultManager based on test status.\n\n Parameters\n ----------\n hide\n Set of AntaTestStatus enum members to select tests to hide based on their status.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n possible_statuses = set(AntaTestStatus)\n manager = ResultManager()\n manager.results = self.get_results(possible_statuses - hide)\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_devices","title":"filter_by_devices","text":"filter_by_devices(devices: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific devices. Parameters: Name Type Description Default devices set[str] Set of device names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_devices(self, devices: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific devices.\n\n Parameters\n ----------\n devices\n Set of device names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.name in devices]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_tests","title":"filter_by_tests","text":"filter_by_tests(tests: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific tests. Parameters: Name Type Description Default tests set[str] Set of test names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_tests(self, tests: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific tests.\n\n Parameters\n ----------\n tests\n Set of test names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.test in tests]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_devices","title":"get_devices","text":"get_devices() -> set[str]\n Get the set of all the device names. Returns: Type Description set[str] Set of device names. Source code in anta/result_manager/__init__.py def get_devices(self) -> set[str]:\n \"\"\"Get the set of all the device names.\n\n Returns\n -------\n set[str]\n Set of device names.\n \"\"\"\n return {str(result.name) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_results","title":"get_results","text":"get_results(\n status: set[AntaTestStatus] | None = None,\n sort_by: list[str] | None = None,\n) -> list[TestResult]\n Get the results, optionally filtered by status and sorted by TestResult fields. If no status is provided, all results are returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None sort_by list[str] | None Optional list of TestResult fields to sort the results. None Returns: Type Description list[TestResult] List of results. Source code in anta/result_manager/__init__.py def get_results(self, status: set[AntaTestStatus] | None = None, sort_by: list[str] | None = None) -> list[TestResult]:\n \"\"\"Get the results, optionally filtered by status and sorted by TestResult fields.\n\n If no status is provided, all results are returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n sort_by\n Optional list of TestResult fields to sort the results.\n\n Returns\n -------\n list[TestResult]\n List of results.\n \"\"\"\n # Return all results if no status is provided, otherwise return results for multiple statuses\n results = self._result_entries if status is None else list(chain.from_iterable(self.results_by_status.get(status, []) for status in status))\n\n if sort_by:\n accepted_fields = TestResult.model_fields.keys()\n if not set(sort_by).issubset(set(accepted_fields)):\n msg = f\"Invalid sort_by fields: {sort_by}. Accepted fields are: {list(accepted_fields)}\"\n raise ValueError(msg)\n results = sorted(results, key=lambda result: [getattr(result, field) for field in sort_by])\n\n return results\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_tests","title":"get_tests","text":"get_tests() -> set[str]\n Get the set of all the test names. Returns: Type Description set[str] Set of test names. Source code in anta/result_manager/__init__.py def get_tests(self) -> set[str]:\n \"\"\"Get the set of all the test names.\n\n Returns\n -------\n set[str]\n Set of test names.\n \"\"\"\n return {str(result.test) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_total_results","title":"get_total_results","text":"get_total_results(\n status: set[AntaTestStatus] | None = None,\n) -> int\n Get the total number of results, optionally filtered by status. If no status is provided, the total number of results is returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None Returns: Type Description int Total number of results. Source code in anta/result_manager/__init__.py def get_total_results(self, status: set[AntaTestStatus] | None = None) -> int:\n \"\"\"Get the total number of results, optionally filtered by status.\n\n If no status is provided, the total number of results is returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n\n Returns\n -------\n int\n Total number of results.\n \"\"\"\n if status is None:\n # Return the total number of results\n return sum(len(results) for results in self.results_by_status.values())\n\n # Return the total number of results for multiple statuses\n return sum(len(self.results_by_status.get(status, [])) for status in status)\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/#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 str Name of the device where the test was run. test str Name of the test run on the device. categories list[str] List of categories the TestResult belongs to. Defaults to the AntaTest categories. description str Description of the TestResult. Defaults to the AntaTest description. result AntaTestStatus Result of the test. Must be one of the AntaTestStatus Enum values: unset, success, failure, error or skipped. messages list[str] Messages to report after the test, if any. custom_field str | None 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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_error(self, message: str | None = None) -> None:\n \"\"\"Set status to error.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_failure(self, message: str | None = None) -> None:\n \"\"\"Set status to failure.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_skipped(self, message: str | None = None) -> None:\n \"\"\"Set status to skipped.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_success(self, message: str | None = None) -> None:\n \"\"\"Set status to success.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.SUCCESS, message)\n"},{"location":"api/runner/","title":"Runner","text":""},{"location":"api/runner/#anta.runner","title":"runner","text":"ANTA runner function."},{"location":"api/runner/#anta.runner.adjust_rlimit_nofile","title":"adjust_rlimit_nofile","text":"adjust_rlimit_nofile() -> tuple[int, int]\n Adjust the maximum number of open file descriptors for the ANTA process. The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable. If the ANTA_NOFILE environment variable is not set or is invalid, DEFAULT_NOFILE is used. Returns: Type Description tuple[int, int] The new soft and hard limits for open file descriptors. Source code in anta/runner.py def adjust_rlimit_nofile() -> tuple[int, int]:\n \"\"\"Adjust the maximum number of open file descriptors for the ANTA process.\n\n The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable.\n\n If the `ANTA_NOFILE` environment variable is not set or is invalid, `DEFAULT_NOFILE` is used.\n\n Returns\n -------\n tuple[int, int]\n The new soft and hard limits for open file descriptors.\n \"\"\"\n try:\n nofile = int(os.environ.get(\"ANTA_NOFILE\", DEFAULT_NOFILE))\n except ValueError as exception:\n logger.warning(\"The ANTA_NOFILE environment variable value is invalid: %s\\nDefault to %s.\", exc_to_str(exception), DEFAULT_NOFILE)\n nofile = DEFAULT_NOFILE\n\n limits = resource.getrlimit(resource.RLIMIT_NOFILE)\n logger.debug(\"Initial limit numbers for open file descriptors for the current ANTA process: Soft Limit: %s | Hard Limit: %s\", limits[0], limits[1])\n nofile = min(limits[1], nofile)\n logger.debug(\"Setting soft limit for open file descriptors for the current ANTA process to %s\", nofile)\n resource.setrlimit(resource.RLIMIT_NOFILE, (nofile, limits[1]))\n return resource.getrlimit(resource.RLIMIT_NOFILE)\n"},{"location":"api/runner/#anta.runner.get_coroutines","title":"get_coroutines","text":"get_coroutines(\n selected_tests: defaultdict[\n AntaDevice, set[AntaTestDefinition]\n ],\n manager: ResultManager,\n) -> list[Coroutine[Any, Any, TestResult]]\n Get the coroutines for the ANTA run. Parameters: Name Type Description Default selected_tests defaultdict[AntaDevice, set[AntaTestDefinition]] A mapping of devices to the tests to run. The selected tests are generated by the prepare_tests function. required manager ResultManager A ResultManager required Returns: Type Description list[Coroutine[Any, Any, TestResult]] The list of coroutines to run. Source code in anta/runner.py def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]], manager: ResultManager) -> list[Coroutine[Any, Any, TestResult]]:\n \"\"\"Get the coroutines for the ANTA run.\n\n Parameters\n ----------\n selected_tests\n A mapping of devices to the tests to run. The selected tests are generated by the `prepare_tests` function.\n manager\n A ResultManager\n\n Returns\n -------\n list[Coroutine[Any, Any, TestResult]]\n The list of coroutines to run.\n \"\"\"\n coros = []\n for device, test_definitions in selected_tests.items():\n for test in test_definitions:\n try:\n test_instance = test.test(device=device, inputs=test.inputs)\n manager.add(test_instance.result)\n coros.append(test_instance.test())\n except Exception as e: # noqa: PERF203, BLE001\n # An AntaTest instance is potentially user-defined code.\n # We need to catch everything and exit gracefully with an error message.\n message = \"\\n\".join(\n [\n f\"There is an error when creating test {test.test.__module__}.{test.test.__name__}.\",\n f\"If this is not a custom test implementation: {GITHUB_SUGGESTION}\",\n ],\n )\n anta_log_exception(e, message, logger)\n return coros\n"},{"location":"api/runner/#anta.runner.log_cache_statistics","title":"log_cache_statistics","text":"log_cache_statistics(devices: list[AntaDevice]) -> None\n Log cache statistics for each device in the inventory. Parameters: Name Type Description Default devices list[AntaDevice] List of devices in the inventory. required Source code in anta/runner.py def log_cache_statistics(devices: list[AntaDevice]) -> None:\n \"\"\"Log cache statistics for each device in the inventory.\n\n Parameters\n ----------\n devices\n List of devices in the inventory.\n \"\"\"\n for device in devices:\n if device.cache_statistics is not None:\n msg = (\n f\"Cache statistics for '{device.name}': \"\n f\"{device.cache_statistics['cache_hits']} hits / {device.cache_statistics['total_commands_sent']} \"\n f\"command(s) ({device.cache_statistics['cache_hit_ratio']})\"\n )\n logger.info(msg)\n else:\n logger.info(\"Caching is not enabled on %s\", device.name)\n"},{"location":"api/runner/#anta.runner.main","title":"main async","text":"main(\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False\n) -> None\n Run ANTA. Use this as an entrypoint to the test framework in your script. ResultManager object gets updated with the test results. Parameters: Name Type Description Default manager ResultManager ResultManager object to populate with the test results. required inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required devices set[str] | None Devices on which to run tests. None means all devices. These may come from the --device / -d CLI option in NRFU. None tests set[str] | None Tests to run against devices. None means all tests. These may come from the --test / -t CLI option in NRFU. None tags set[str] | None Tags to filter devices from the inventory. These may come from the --tags CLI option in NRFU. None established_only bool Include only established device(s). True dry_run bool Build the list of coroutine to run and stop before test execution. False Source code in anta/runner.py @cprofile()\nasync def main( # noqa: PLR0913\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False,\n) -> None:\n \"\"\"Run ANTA.\n\n Use this as an entrypoint to the test framework in your script.\n ResultManager object gets updated with the test results.\n\n Parameters\n ----------\n manager\n ResultManager object to populate with the test results.\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n devices\n Devices on which to run tests. None means all devices. These may come from the `--device / -d` CLI option in NRFU.\n tests\n Tests to run against devices. None means all tests. These may come from the `--test / -t` CLI option in NRFU.\n tags\n Tags to filter devices from the inventory. These may come from the `--tags` CLI option in NRFU.\n established_only\n Include only established device(s).\n dry_run\n Build the list of coroutine to run and stop before test execution.\n \"\"\"\n # Adjust the maximum number of open file descriptors for the ANTA process\n limits = adjust_rlimit_nofile()\n\n if not catalog.tests:\n logger.info(\"The list of tests is empty, exiting\")\n return\n\n with Catchtime(logger=logger, message=\"Preparing ANTA NRFU Run\"):\n # Setup the inventory\n selected_inventory = inventory if dry_run else await setup_inventory(inventory, tags, devices, established_only=established_only)\n if selected_inventory is None:\n return\n\n with Catchtime(logger=logger, message=\"Preparing the tests\"):\n selected_tests = prepare_tests(selected_inventory, catalog, tests, tags)\n if selected_tests is None:\n return\n final_tests_count = sum(len(tests) for tests in selected_tests.values())\n\n run_info = (\n \"--- ANTA NRFU Run Information ---\\n\"\n f\"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\\n\"\n f\"Total number of selected tests: {final_tests_count}\\n\"\n f\"Maximum number of open file descriptors for the current ANTA process: {limits[0]}\\n\"\n \"---------------------------------\"\n )\n\n logger.info(run_info)\n\n if final_tests_count > limits[0]:\n logger.warning(\n \"The number of concurrent tests is higher than the open file descriptors limit for this ANTA process.\\n\"\n \"Errors may occur while running the tests.\\n\"\n \"Please consult the ANTA FAQ.\"\n )\n\n coroutines = get_coroutines(selected_tests, manager)\n\n if dry_run:\n logger.info(\"Dry-run mode, exiting before running the tests.\")\n for coro in coroutines:\n coro.close()\n return\n\n if AntaTest.progress is not None:\n AntaTest.nrfu_task = AntaTest.progress.add_task(\"Running NRFU Tests...\", total=len(coroutines))\n\n with Catchtime(logger=logger, message=\"Running ANTA tests\"):\n await asyncio.gather(*coroutines)\n\n log_cache_statistics(selected_inventory.devices)\n"},{"location":"api/runner/#anta.runner.prepare_tests","title":"prepare_tests","text":"prepare_tests(\n inventory: AntaInventory,\n catalog: AntaCatalog,\n tests: set[str] | None,\n tags: set[str] | None,\n) -> (\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n)\n Prepare the tests to run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required tests set[str] | None Tests to run against devices. None means all tests. required tags set[str] | None Tags to filter devices from the inventory. required Returns: Type Description defaultdict[AntaDevice, set[AntaTestDefinition]] | None A mapping of devices to the tests to run or None if there are no tests to run. Source code in anta/runner.py def prepare_tests(\n inventory: AntaInventory, catalog: AntaCatalog, tests: set[str] | None, tags: set[str] | None\n) -> defaultdict[AntaDevice, set[AntaTestDefinition]] | None:\n \"\"\"Prepare the tests to run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n tests\n Tests to run against devices. None means all tests.\n tags\n Tags to filter devices from the inventory.\n\n Returns\n -------\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n A mapping of devices to the tests to run or None if there are no tests to run.\n \"\"\"\n # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests\n catalog.build_indexes(filtered_tests=tests)\n\n # Using a set to avoid inserting duplicate tests\n device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)\n\n total_test_count = 0\n\n # Create the device to tests mapping from the tags\n for device in inventory.devices:\n if tags:\n # If there are CLI tags, execute tests with matching tags for this device\n if not (matching_tags := tags.intersection(device.tags)):\n # The device does not have any selected tag, skipping\n continue\n device_to_tests[device].update(catalog.get_tests_by_tags(matching_tags))\n else:\n # If there is no CLI tags, execute all tests that do not have any tags\n device_to_tests[device].update(catalog.tag_to_tests[None])\n\n # Then add the tests with matching tags from device tags\n device_to_tests[device].update(catalog.get_tests_by_tags(device.tags))\n\n total_test_count += len(device_to_tests[device])\n\n if total_test_count == 0:\n msg = (\n f\"There are no tests{f' matching the tags {tags} ' if tags else ' '}to run in the current test catalog and device inventory, please verify your inputs.\"\n )\n logger.warning(msg)\n return None\n\n return device_to_tests\n"},{"location":"api/runner/#anta.runner.setup_inventory","title":"setup_inventory async","text":"setup_inventory(\n inventory: AntaInventory,\n tags: set[str] | None,\n devices: set[str] | None,\n *,\n established_only: bool\n) -> AntaInventory | None\n Set up the inventory for the ANTA run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required tags set[str] | None Tags to filter devices from the inventory. required devices set[str] | None Devices on which to run tests. None means all devices. required established_only bool If True use return only devices where a connection is established. required Returns: Type Description AntaInventory | None The filtered inventory or None if there are no devices to run tests on. Source code in anta/runner.py async def setup_inventory(inventory: AntaInventory, tags: set[str] | None, devices: set[str] | None, *, established_only: bool) -> AntaInventory | None:\n \"\"\"Set up the inventory for the ANTA run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n tags\n Tags to filter devices from the inventory.\n devices\n Devices on which to run tests. None means all devices.\n established_only\n If True use return only devices where a connection is established.\n\n Returns\n -------\n AntaInventory | None\n The filtered inventory or None if there are no devices to run tests on.\n \"\"\"\n if len(inventory) == 0:\n logger.info(\"The inventory is empty, exiting\")\n return None\n\n # Filter the inventory based on the CLI provided tags and devices if any\n selected_inventory = inventory.get_inventory(tags=tags, devices=devices) if tags or devices else inventory\n\n with Catchtime(logger=logger, message=\"Connecting to devices\"):\n # Connect to the devices\n await selected_inventory.connect_inventory()\n\n # Remove devices that are unreachable\n selected_inventory = selected_inventory.get_inventory(established_only=established_only)\n\n # If there are no devices in the inventory after filtering, exit\n if not selected_inventory.devices:\n msg = f'No reachable device {f\"matching the tags {tags} \" if tags else \"\"}was found.{f\" Selected devices: {devices} \" if devices is not None else \"\"}'\n logger.warning(msg)\n return None\n\n return selected_inventory\n"},{"location":"api/test.cvx/","title":"Test.cvx","text":""},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX","title":"VerifyManagementCVX","text":"Verifies the management CVX global status. Expected Results Success: The test will pass if the management CVX global status matches the expected status. Failure: The test will fail if the management CVX global status does not match the expected status. Examples anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n Source code in anta/tests/cvx.py class VerifyManagementCVX(AntaTest):\n \"\"\"Verifies the management CVX global status.\n\n Expected Results\n ----------------\n * Success: The test will pass if the management CVX global status matches the expected status.\n * Failure: The test will fail if the management CVX global status does not match the expected status.\n\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyManagementCVX test.\"\"\"\n\n enabled: bool\n \"\"\"Whether management CVX must be enabled (True) or disabled (False).\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyManagementCVX.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n cluster_status = command_output[\"clusterStatus\"]\n if (cluster_state := cluster_status.get(\"enabled\")) != self.inputs.enabled:\n self.result.is_failure(f\"Management CVX status is not valid: {cluster_state}\")\n"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX-attributes","title":"Inputs","text":"Name Type Description Default enabled bool Whether management CVX must be enabled (True) or disabled (False). -"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyMcsClientMounts","title":"VerifyMcsClientMounts","text":"Verify if all MCS client mounts are in mountStateMountComplete. Expected Results Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete. Failure: The test will fail even if one switch\u2019s MCS client mount status is not mountStateMountComplete. Examples anta.tests.cvx:\n- VerifyMcsClientMounts:\n Source code in anta/tests/cvx.py class VerifyMcsClientMounts(AntaTest):\n \"\"\"Verify if all MCS client mounts are in mountStateMountComplete.\n\n Expected Results\n ----------------\n * Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete.\n * Failure: The test will fail even if one switch's MCS client mount status is not mountStateMountComplete.\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyMcsClientMounts:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx mounts\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyMcsClientMounts.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n mount_states = command_output[\"mountStates\"]\n mcs_mount_state_detected = False\n for mount_state in mount_states:\n if not mount_state[\"type\"].startswith(\"Mcs\"):\n continue\n mcs_mount_state_detected = True\n if (state := mount_state[\"state\"]) != \"mountStateMountComplete\":\n self.result.is_failure(f\"MCS Client mount states are not valid: {state}\")\n\n if not mcs_mount_state_detected:\n self.result.is_failure(\"MCS Client mount states are not present\")\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authentication\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authorization\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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.avt/","title":"Adaptive Virtual Topology","text":""},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTPathHealth","title":"VerifyAVTPathHealth","text":"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs. Expected Results Success: The test will pass if all AVT paths for all VRFs are active and valid. Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid. Examples anta.tests.avt:\n - VerifyAVTPathHealth:\n Source code in anta/tests/avt.py class VerifyAVTPathHealth(AntaTest):\n \"\"\"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for all VRFs are active and valid.\n * Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTPathHealth:\n ```\n \"\"\"\n\n description = \"Verifies the status of all AVT paths for all VRFs.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTPathHealth.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output.get(\"vrfs\", {})\n\n # Check if AVT is configured\n if not command_output:\n self.result.is_failure(\"Adaptive virtual topology paths are not configured.\")\n return\n\n # Iterate over each VRF\n for vrf, vrf_data in command_output.items():\n # Iterate over each AVT path\n for profile, avt_path in vrf_data.get(\"avts\", {}).items():\n for path, flags in avt_path.get(\"avtPaths\", {}).items():\n # Get the status of the AVT path\n valid = flags[\"flags\"][\"valid\"]\n active = flags[\"flags\"][\"active\"]\n\n # Check the status of the AVT path\n if not valid and not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid and not active.\")\n elif not valid:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid.\")\n elif not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is not active.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole","title":"VerifyAVTRole","text":"Verifies the Adaptive Virtual Topology (AVT) role of a device. Expected Results Success: The test will pass if the AVT role of the device matches the expected role. Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role. Examples anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n Source code in anta/tests/avt.py class VerifyAVTRole(AntaTest):\n \"\"\"Verifies the Adaptive Virtual Topology (AVT) role of a device.\n\n Expected Results\n ----------------\n * Success: The test will pass if the AVT role of the device matches the expected role.\n * Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n ```\n \"\"\"\n\n description = \"Verifies the AVT role of a device.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTRole test.\"\"\"\n\n role: str\n \"\"\"Expected AVT role of the device.\"\"\"\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTRole.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output\n\n # Check if the AVT role matches the expected role\n if self.inputs.role != command_output.get(\"role\"):\n self.result.is_failure(f\"Expected AVT role as `{self.inputs.role}`, but found `{command_output.get('role')}` instead.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole-attributes","title":"Inputs","text":"Name Type Description Default role str Expected AVT role of the device. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath","title":"VerifyAVTSpecificPath","text":"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF. Expected Results Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided. If multiple paths are configured, the test will pass only if all the paths are valid and active. Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid, or does not match the specified type. Examples anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n Source code in anta/tests/avt.py class VerifyAVTSpecificPath(AntaTest):\n \"\"\"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided.\n If multiple paths are configured, the test will pass only if all the paths are valid and active.\n * Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid,\n or does not match the specified type.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n ```\n \"\"\"\n\n description = \"Verifies the status and type of an AVT path for a specified VRF.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show adaptive-virtual-topology path vrf {vrf} avt {avt_name} destination {destination}\")\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTSpecificPath test.\"\"\"\n\n avt_paths: list[AVTPaths]\n \"\"\"List of AVT paths to verify.\"\"\"\n\n class AVTPaths(BaseModel):\n \"\"\"Model for the details of AVT paths.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"The VRF for the AVT path. Defaults to 'default' if not provided.\"\"\"\n avt_name: str\n \"\"\"Name of the adaptive virtual topology.\"\"\"\n destination: IPv4Address\n \"\"\"The IPv4 address of the AVT peer.\"\"\"\n next_hop: IPv4Address\n \"\"\"The IPv4 address of the next hop for the AVT peer.\"\"\"\n path_type: str | None = None\n \"\"\"The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input AVT path/peer.\"\"\"\n return [template.render(vrf=path.vrf, avt_name=path.avt_name, destination=path.destination) for path in self.inputs.avt_paths]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTSpecificPath.\"\"\"\n # Assume the test is successful until a failure is detected\n self.result.is_success()\n\n # Process each command in the instance\n for command, input_avt in zip(self.instance_commands, self.inputs.avt_paths):\n # Extract the command output and parameters\n vrf = command.params.vrf\n avt_name = command.params.avt_name\n peer = str(command.params.destination)\n\n command_output = command.json_output.get(\"vrfs\", {})\n\n # If no AVT is configured, mark the test as failed and skip to the next command\n if not command_output:\n self.result.is_failure(f\"AVT configuration for peer '{peer}' under topology '{avt_name}' in VRF '{vrf}' is not found.\")\n continue\n\n # Extract the AVT paths\n avt_paths = get_value(command_output, f\"{vrf}.avts.{avt_name}.avtPaths\")\n next_hop, input_path_type = str(input_avt.next_hop), input_avt.path_type\n\n nexthop_path_found = path_type_found = False\n\n # Check each AVT path\n for path, path_data in avt_paths.items():\n # If the path does not match the expected next hop, skip to the next path\n if path_data.get(\"nexthopAddr\") != next_hop:\n continue\n\n nexthop_path_found = True\n path_type = \"direct\" if get_value(path_data, \"flags.directPath\") else \"multihop\"\n\n # If the path type does not match the expected path type, skip to the next path\n if input_path_type and path_type != input_path_type:\n continue\n\n path_type_found = True\n valid = get_value(path_data, \"flags.valid\")\n active = get_value(path_data, \"flags.active\")\n\n # Check the path status and type against the expected values\n if not all([valid, active]):\n failure_reasons = []\n if not get_value(path_data, \"flags.active\"):\n failure_reasons.append(\"inactive\")\n if not get_value(path_data, \"flags.valid\"):\n failure_reasons.append(\"invalid\")\n # Construct the failure message prefix\n failed_log = f\"AVT path '{path}' for topology '{avt_name}' in VRF '{vrf}'\"\n self.result.is_failure(f\"{failed_log} is {', '.join(failure_reasons)}.\")\n\n # If no matching next hop or path type was found, mark the test as failed\n if not nexthop_path_found or not path_type_found:\n self.result.is_failure(\n f\"No '{input_path_type}' path found with next-hop address '{next_hop}' for AVT peer '{peer}' under topology '{avt_name}' in VRF '{vrf}'.\"\n )\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"Inputs","text":"Name Type Description Default avt_paths list[AVTPaths] List of AVT paths to verify. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"AVTPaths","text":"Name Type Description Default vrf str The VRF for the AVT path. Defaults to 'default' if not provided. 'default' avt_name str Name of the adaptive virtual topology. - destination IPv4Address The IPv4 address of the AVT peer. - next_hop IPv4Address The IPv4 address of the next hop for the AVT peer. - path_type str | None The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered. None"},{"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 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\", revision=1),\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 categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\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 tx_interval = bfd_peers.tx_interval\n rx_interval = bfd_peers.rx_interval\n multiplier = bfd_peers.multiplier\n\n # Check if BFD peer configured\n bfd_output = get_value(\n self.instance_commands[0].json_output,\n f\"vrfs..{vrf}..ipv4Neighbors..{peer}..peerStats..\",\n separator=\"..\",\n )\n if not bfd_output:\n failures[peer] = {vrf: \"Not Configured\"}\n continue\n\n # Convert interval timer(s) into milliseconds to be consistent with the inputs.\n bfd_details = bfd_output.get(\"peerStatsDetail\", {})\n op_tx_interval = bfd_details.get(\"operTxInterval\") // 1000\n op_rx_interval = bfd_details.get(\"operRxInterval\") // 1000\n detect_multiplier = bfd_details.get(\"detectMult\")\n intervals_ok = op_tx_interval == tx_interval and op_rx_interval == rx_interval and detect_multiplier == multiplier\n\n # Check timers of BFD peer\n if not intervals_ok:\n failures[peer] = {\n vrf: {\n \"tx_interval\": op_tx_interval,\n \"rx_interval\": op_rx_interval,\n \"multiplier\": detect_multiplier,\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.VerifyBFDPeersRegProtocols","title":"VerifyBFDPeersRegProtocols","text":"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered. Expected Results Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s). Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s). Examples anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n Source code in anta/tests/bfd.py class VerifyBFDPeersRegProtocols(AntaTest):\n \"\"\"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered.\n\n Expected Results\n ----------------\n * Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s).\n * Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s).\n\n Examples\n --------\n ```yaml\n anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBFDPeersRegProtocols 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 protocols: list[BfdProtocol]\n \"\"\"List of protocols to be verified.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBFDPeersRegProtocols.\"\"\"\n # Initialize failure messages\n failures: dict[Any, Any] = {}\n\n # Iterating over BFD peers, extract the parameters and command output\n for bfd_peer in self.inputs.bfd_peers:\n peer = str(bfd_peer.peer_address)\n vrf = bfd_peer.vrf\n protocols = bfd_peer.protocols\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 registered protocols\n difference = set(protocols) - set(get_value(bfd_output, \"peerStatsDetail.apps\"))\n\n if difference:\n failures[peer] = {vrf: sorted(difference)}\n\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BFD peers are not configured or have non-registered protocol(s):\\n{failures}\")\n"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-attributes","title":"Inputs","text":"Name Type Description Default bfd_peers list[BFDPeer] List of IPv4 BFD peers. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-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' protocols list[BfdProtocol] List of protocols to be verified. -"},{"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 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\", revision=1)]\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 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.VerifyRunningConfigLines","title":"VerifyRunningConfigLines","text":"Verifies the given regular expression patterns are present in the running-config. Warning Since this uses regular expression searches on the whole running-config, it can drastically impact performance and should only be used if no other test is available. If possible, try using another ANTA test that is more specific. Expected Results Success: The test will pass if all the patterns are found in the running-config. Failure: The test will fail if any of the patterns are NOT found in the running-config. Examples anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n Source code in anta/tests/configuration.py class VerifyRunningConfigLines(AntaTest):\n \"\"\"Verifies the given regular expression patterns are present in the running-config.\n\n !!! warning\n Since this uses regular expression searches on the whole running-config, it can\n drastically impact performance and should only be used if no other test is available.\n\n If possible, try using another ANTA test that is more specific.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the patterns are found in the running-config.\n * Failure: The test will fail if any of the patterns are NOT found in the running-config.\n\n Examples\n --------\n ```yaml\n anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n ```\n \"\"\"\n\n description = \"Search the Running-Config for the given RegEx patterns.\"\n categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show running-config\", ofmt=\"text\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyRunningConfigLines test.\"\"\"\n\n regex_patterns: list[RegexString]\n \"\"\"List of regular expressions.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRunningConfigLines.\"\"\"\n failure_msgs = []\n command_output = self.instance_commands[0].text_output\n\n for pattern in self.inputs.regex_patterns:\n re_search = re.compile(pattern, flags=re.MULTILINE)\n\n if not re_search.search(command_output):\n failure_msgs.append(f\"'{pattern}'\")\n\n if not failure_msgs:\n self.result.is_success()\n else:\n self.result.is_failure(\"Following patterns were not found: \" + \",\".join(failure_msgs))\n"},{"location":"api/tests.configuration/#anta.tests.configuration.VerifyRunningConfigLines-attributes","title":"Inputs","text":"Name Type Description Default regex_patterns list[RegexString] List of regular expressions. -"},{"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 categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show zerotouch\", revision=1)]\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/#tests","title":"Tests","text":""},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors","title":"VerifyLLDPNeighbors","text":"Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. This test performs the following checks for each specified LLDP neighbor: Confirming matching ports on both local and neighboring devices. Ensuring compatibility of device names and interface identifiers. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored. Expected Results Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table. The system name or port of the LLDP neighbor does not match the expected 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 the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.\n\n This test performs the following checks for each specified LLDP neighbor:\n\n 1. Confirming matching ports on both local and neighboring devices.\n 2. Ensuring compatibility of device names and interface identifiers.\n 3. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table.\n - The system name or port of the LLDP neighbor does not match the expected 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 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\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLLDPNeighbors test.\"\"\"\n\n neighbors: list[LLDPNeighbor]\n \"\"\"List of LLDP neighbors.\"\"\"\n Neighbor: ClassVar[type[Neighbor]] = Neighbor\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLLDPNeighbors.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output[\"lldpNeighbors\"]\n for neighbor in self.inputs.neighbors:\n if neighbor.port not in output:\n self.result.is_failure(f\"{neighbor} - Port not found\")\n continue\n\n if len(lldp_neighbor_info := output[neighbor.port][\"lldpNeighborInfo\"]) == 0:\n self.result.is_failure(f\"{neighbor} - No LLDP neighbors\")\n continue\n\n # Check if the system name and neighbor port matches\n match_found = any(\n info[\"systemName\"] == neighbor.neighbor_device and info[\"neighborInterfaceInfo\"][\"interfaceId_v2\"] == neighbor.neighbor_port\n for info in lldp_neighbor_info\n )\n if not match_found:\n failure_msg = [f\"{info['systemName']}/{info['neighborInterfaceInfo']['interfaceId_v2']}\" for info in lldp_neighbor_info]\n self.result.is_failure(f\"{neighbor} - Wrong LLDP neighbors: {', '.join(failure_msg)}\")\n"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors-attributes","title":"Inputs","text":"Name Type Description Default neighbors list[LLDPNeighbor] List of LLDP neighbors. -"},{"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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"connectivity\"]\n # Template uses '{size}{df_bit}' without space since df_bit includes leading space when enabled\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"ping vrf {vrf} {destination} source {source} size {size}{df_bit} repeat {repeat}\", revision=1)\n ]\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 Host: ClassVar[type[Host]] = Host\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each host in the input list.\"\"\"\n commands = []\n for host in self.inputs.hosts:\n # df_bit includes leading space when enabled, empty string when disabled\n df_bit = \" df-bit\" if host.df_bit else \"\"\n command = template.render(destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat, size=host.size, df_bit=df_bit)\n commands.append(command)\n return commands\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyReachability.\"\"\"\n self.result.is_success()\n\n for command, host in zip(self.instance_commands, self.inputs.hosts):\n if f\"{host.repeat} received\" not in command.json_output[\"messages\"][0]:\n self.result.is_failure(f\"{host} - Unreachable\")\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/#input-models","title":"Input models","text":""},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Host","title":"Host","text":"Model for a remote host to ping. 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 size int Specify datagram size. Defaults to 100. 100 df_bit bool Enable do not fragment bit in IP header. Defaults to False. False Source code in anta/input_models/connectivity.py class Host(BaseModel):\n \"\"\"Model for a remote host to ping.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\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 size: int = 100\n \"\"\"Specify datagram size. Defaults to 100.\"\"\"\n df_bit: bool = False\n \"\"\"Enable do not fragment bit in IP header. Defaults to False.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the Host for reporting.\n\n Examples\n --------\n Host 10.1.1.1 (src: 10.2.2.2, vrf: mgmt, size: 100B, repeat: 2)\n\n \"\"\"\n df_status = \", df-bit: enabled\" if self.df_bit else \"\"\n return f\"Host {self.destination} (src: {self.source}, vrf: {self.vrf}, size: {self.size}B, repeat: {self.repeat}{df_status})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.LLDPNeighbor","title":"LLDPNeighbor","text":"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information. Name Type Description Default port Interface The LLDP port for the local device. - neighbor_device str The system name of the LLDP neighbor device. - neighbor_port Interface The LLDP port on the neighboring device. - Source code in anta/input_models/connectivity.py class LLDPNeighbor(BaseModel):\n \"\"\"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n port: Interface\n \"\"\"The LLDP port for the local device.\"\"\"\n neighbor_device: str\n \"\"\"The system name of the LLDP neighbor device.\"\"\"\n neighbor_port: Interface\n \"\"\"The LLDP port on the neighboring device.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the LLDPNeighbor for reporting.\n\n Examples\n --------\n Port Ethernet1 (Neighbor: DC1-SPINE2, Neighbor Port: Ethernet2)\n\n \"\"\"\n return f\"Port {self.port} (Neighbor: {self.neighbor_device}, Neighbor Port: {self.neighbor_port})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor","title":"Neighbor","text":"Alias for the LLDPNeighbor model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the LLDPNeighbor model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/connectivity.py class Neighbor(LLDPNeighbor): # pragma: no cover\n \"\"\"Alias for the LLDPNeighbor model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the LLDPNeighbor model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor.__init__","title":"__init__","text":"__init__(**data: Any) -> None\n Source code in anta/input_models/connectivity.py def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"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 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\", revision=1)]\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 break\n else:\n self.result.is_failure(\"Aboot component not found\")\n return\n\n self.result.is_success()\n incorrect_aboot_version = (\n (aboot_version.startswith(\"4.0.\") and int(aboot_version.split(\".\")[2]) < 7)\n or (aboot_version.startswith(\"4.1.\") and int(aboot_version.split(\".\")[2]) < 1)\n or (\n (aboot_version.startswith(\"6.0.\") and int(aboot_version.split(\".\")[2]) < 9)\n or (aboot_version.startswith(\"6.1.\") 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 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\", revision=1)]\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_failure(\"Error in running test - Component FixedSystemvrm1 not found in 'show version'\")\n"},{"location":"api/tests.flow_tracking/","title":"Flow Tracking","text":""},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus","title":"VerifyHardwareFlowTrackerStatus","text":"Verifies if hardware flow tracking is running and an input tracker is active. This test optionally verifies the tracker interval/timeout and exporter configuration. Expected Results Success: The test will pass if hardware flow tracking is running and an input tracker is active. Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active, or the tracker interval/timeout and exporter configuration does not match the expected values. Examples anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n Source code in anta/tests/flow_tracking.py class VerifyHardwareFlowTrackerStatus(AntaTest):\n \"\"\"Verifies if hardware flow tracking is running and an input tracker is active.\n\n This test optionally verifies the tracker interval/timeout and exporter configuration.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware flow tracking is running and an input tracker is active.\n * Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active,\n or the tracker interval/timeout and exporter configuration does not match the expected values.\n\n Examples\n --------\n ```yaml\n anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n ```\n \"\"\"\n\n description = (\n \"Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration.\"\n )\n categories: ClassVar[list[str]] = [\"flow tracking\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show flow tracking hardware tracker {name}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyHardwareFlowTrackerStatus test.\"\"\"\n\n trackers: list[FlowTracker]\n \"\"\"List of flow trackers to verify.\"\"\"\n\n class FlowTracker(BaseModel):\n \"\"\"Detail of a flow tracker.\"\"\"\n\n name: str\n \"\"\"Name of the flow tracker.\"\"\"\n\n record_export: RecordExport | None = None\n \"\"\"Record export configuration for the flow tracker.\"\"\"\n\n exporters: list[Exporter] | None = None\n \"\"\"List of exporters for the flow tracker.\"\"\"\n\n class RecordExport(BaseModel):\n \"\"\"Record export configuration.\"\"\"\n\n on_inactive_timeout: int\n \"\"\"Timeout in milliseconds for exporting records when inactive.\"\"\"\n\n on_interval: int\n \"\"\"Interval in milliseconds for exporting records.\"\"\"\n\n class Exporter(BaseModel):\n \"\"\"Detail of an exporter.\"\"\"\n\n name: str\n \"\"\"Name of the exporter.\"\"\"\n\n local_interface: str\n \"\"\"Local interface used by the exporter.\"\"\"\n\n template_interval: int\n \"\"\"Template interval in milliseconds for the exporter.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each hardware tracker.\"\"\"\n return [template.render(name=tracker.name) for tracker in self.inputs.trackers]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareFlowTrackerStatus.\"\"\"\n self.result.is_success()\n for command, tracker_input in zip(self.instance_commands, self.inputs.trackers):\n hardware_tracker_name = command.params.name\n record_export = tracker_input.record_export.model_dump() if tracker_input.record_export else None\n exporters = [exporter.model_dump() for exporter in tracker_input.exporters] if tracker_input.exporters else None\n command_output = command.json_output\n\n # Check if hardware flow tracking is configured\n if not command_output.get(\"running\"):\n self.result.is_failure(\"Hardware flow tracking is not running.\")\n return\n\n # Check if the input hardware tracker is configured\n tracker_info = command_output[\"trackers\"].get(hardware_tracker_name)\n if not tracker_info:\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not configured.\")\n continue\n\n # Check if the input hardware tracker is active\n if not tracker_info.get(\"active\"):\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not active.\")\n continue\n\n # Check the input hardware tracker timeouts\n failure_msg = \"\"\n if record_export:\n record_export_failure = validate_record_export(record_export, tracker_info)\n if record_export_failure:\n failure_msg += record_export_failure\n\n # Check the input hardware tracker exporters' configuration\n if exporters:\n exporters_failure = validate_exporters(exporters, tracker_info)\n if exporters_failure:\n failure_msg += exporters_failure\n\n if failure_msg:\n self.result.is_failure(f\"{hardware_tracker_name}: {failure_msg}\\n\")\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Inputs","text":"Name Type Description Default trackers list[FlowTracker] List of flow trackers to verify. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"FlowTracker","text":"Name Type Description Default name str Name of the flow tracker. - record_export RecordExport | None Record export configuration for the flow tracker. None exporters list[Exporter] | None List of exporters for the flow tracker. None"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"RecordExport","text":"Name Type Description Default on_inactive_timeout int Timeout in milliseconds for exporting records when inactive. - on_interval int Interval in milliseconds for exporting records. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Exporter","text":"Name Type Description Default name str Name of the exporter. - local_interface str Local interface used by the exporter. - template_interval int Template interval in milliseconds for the exporter. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_exporters","title":"validate_exporters","text":"validate_exporters(\n exporters: list[dict[str, str]],\n tracker_info: dict[str, str],\n) -> str\n Validate the exporter configurations against the tracker info. Parameters: Name Type Description Default exporters list[dict[str, str]] The list of expected exporter configurations. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str Failure message if any exporter configuration does not match. Source code in anta/tests/flow_tracking.py def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the exporter configurations against the tracker info.\n\n Parameters\n ----------\n exporters\n The list of expected exporter configurations.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n Failure message if any exporter configuration does not match.\n \"\"\"\n failed_log = \"\"\n for exporter in exporters:\n exporter_name = exporter[\"name\"]\n actual_exporter_info = tracker_info[\"exporters\"].get(exporter_name)\n if not actual_exporter_info:\n failed_log += f\"\\nExporter `{exporter_name}` is not configured.\"\n continue\n\n expected_exporter_data = {\"local interface\": exporter[\"local_interface\"], \"template interval\": exporter[\"template_interval\"]}\n actual_exporter_data = {\"local interface\": actual_exporter_info[\"localIntf\"], \"template interval\": actual_exporter_info[\"templateInterval\"]}\n\n if expected_exporter_data != actual_exporter_data:\n failed_msg = get_failed_logs(expected_exporter_data, actual_exporter_data)\n failed_log += f\"\\nExporter `{exporter_name}`: {failed_msg}\"\n return failed_log\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_record_export","title":"validate_record_export","text":"validate_record_export(\n record_export: dict[str, str],\n tracker_info: dict[str, str],\n) -> str\n Validate the record export configuration against the tracker info. Parameters: Name Type Description Default record_export dict[str, str] The expected record export configuration. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str A failure message if the record export configuration does not match, otherwise blank string. Source code in anta/tests/flow_tracking.py def validate_record_export(record_export: dict[str, str], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the record export configuration against the tracker info.\n\n Parameters\n ----------\n record_export\n The expected record export configuration.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n A failure message if the record export configuration does not match, otherwise blank string.\n \"\"\"\n failed_log = \"\"\n actual_export = {\"inactive timeout\": tracker_info.get(\"inactiveTimeout\"), \"interval\": tracker_info.get(\"activeInterval\")}\n expected_export = {\"inactive timeout\": record_export.get(\"on_inactive_timeout\"), \"interval\": record_export.get(\"on_interval\")}\n if actual_export != expected_export:\n failed_log = get_failed_logs(expected_export, actual_export)\n return failed_log\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 description = \"Verifies if a GreenT policy other than the default is created.\"\n categories: ClassVar[list[str]] = [\"greent\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show monitor telemetry postcard policy profile\", revision=1)]\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 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\", revision=1)]\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 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\", revision=1)]\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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment power\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment cooling\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show inventory\", revision=2)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature transceiver\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=2)]\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 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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show lacp counters all-ports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters discards\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces status\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters errors\", revision=1)]\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 description = \"Verifies the interface IPv4 addresses.\"\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip interface {interface}\", revision=2)]\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 [template.render(interface=interface.name) for interface in self.inputs.interfaces]\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 for interface in self.inputs.interfaces:\n if interface.name == intf:\n input_interface_detail = interface\n break\n else:\n self.result.is_failure(f\"Could not find `{intf}` in the input interfaces. {GITHUB_SUGGESTION}\")\n continue\n\n input_primary_ip = str(input_interface_detail.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 (param_secondary_ips := input_interface_detail.secondary_ips) is not None:\n input_secondary_ips = sorted([str(network) for network in param_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. This test has been implemented for full-duplex interfaces only. 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. Error: The test will error out if the device has at least one non full-duplex interface. 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 This test has been implemented for full-duplex interfaces only.\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 * Error: The test will error out if the device has at least one non full-duplex interface.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfaceUtilization:\n threshold: 70.0\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show interfaces counters rates\", revision=1),\n AntaCommand(command=\"show interfaces\", revision=1),\n ]\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 # The utilization logic has been implemented for full-duplex interfaces only\n if ((duplex := (interface := interfaces[\"interfaces\"][intf]).get(\"duplex\", None)) is not None and duplex != duplex_full) or (\n (members := interface.get(\"memberInterfaces\", None)) is not None and any(stats[\"duplex\"] != duplex_full for stats in members.values())\n ):\n self.result.is_failure(f\"Interface {intf} or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented.\")\n return\n\n if (bandwidth := interfaces[\"interfaces\"][intf][\"bandwidth\"]) == 0:\n self.logger.debug(\"Interface %s has been ignored due to null bandwidth value\", intf)\n continue\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.VerifyInterfacesSpeed","title":"VerifyInterfacesSpeed","text":"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces. If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input. If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input. Expected Results Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex. Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex. Examples anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n Source code in anta/tests/interfaces.py class VerifyInterfacesSpeed(AntaTest):\n \"\"\"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces.\n\n - If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input.\n - If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input.\n\n Expected Results\n ----------------\n * Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex.\n * Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n class Input(AntaTest.Input):\n \"\"\"Inputs for the VerifyInterfacesSpeed test.\"\"\"\n\n interfaces: list[InterfaceDetail]\n \"\"\"List of interfaces to be tested\"\"\"\n\n class InterfaceDetail(BaseModel):\n \"\"\"Detail of an interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"The name of the interface.\"\"\"\n auto: bool\n \"\"\"The auto-negotiation status of the interface.\"\"\"\n speed: float = Field(ge=1, le=1000)\n \"\"\"The speed of the interface in Gigabits per second. Valid range is 1 to 1000.\"\"\"\n lanes: None | int = Field(None, ge=1, le=8)\n \"\"\"The number of lanes in the interface. Valid range is 1 to 8. This field is optional.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesSpeed.\"\"\"\n self.result.is_success()\n command_output = self.instance_commands[0].json_output\n\n # Iterate over all the interfaces\n for interface in self.inputs.interfaces:\n intf = interface.name\n\n # Check if interface exists\n if not (interface_output := get_value(command_output, f\"interfaces.{intf}\")):\n self.result.is_failure(f\"Interface `{intf}` is not found.\")\n continue\n\n auto_negotiation = interface_output.get(\"autoNegotiate\")\n actual_lanes = interface_output.get(\"lanes\")\n\n # Collecting actual interface details\n actual_interface_output = {\n \"auto negotiation\": auto_negotiation if interface.auto is True else None,\n \"duplex mode\": interface_output.get(\"duplex\"),\n \"speed\": interface_output.get(\"bandwidth\"),\n \"lanes\": actual_lanes if interface.lanes is not None else None,\n }\n\n # Forming expected interface details\n expected_interface_output = {\n \"auto negotiation\": \"success\" if interface.auto is True else None,\n \"duplex mode\": \"duplexFull\",\n \"speed\": interface.speed * BPS_GBPS_CONVERSIONS,\n \"lanes\": interface.lanes,\n }\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n for output in [actual_interface_output, expected_interface_output]:\n # Convert speed to Gbps for readability\n if output[\"speed\"] is not None:\n output[\"speed\"] = f\"{custom_division(output['speed'], BPS_GBPS_CONVERSIONS)}Gbps\"\n failed_log = get_failed_logs(expected_interface_output, actual_interface_output)\n self.result.is_failure(f\"For interface {intf}:{failed_log}\\n\")\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceDetail] List of interfaces to be tested -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"InterfaceDetail","text":"Name Type Description Default name EthernetInterface The name of the interface. - auto bool The auto-negotiation status of the interface. - speed float The speed of the interface in Gigabits per second. Valid range is 1 to 1000. Field(ge=1, le=1000) lanes None | int The number of lanes in the interface. Valid range is 1 to 8. This field is optional. Field(None, ge=1, le=8)"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus","title":"VerifyInterfacesStatus","text":"Verifies the operational states of specified interfaces to ensure they match expected configurations. This test performs the following checks for each specified interface: If line_protocol_status is defined, both status and line_protocol_status are verified for the specified interface. If line_protocol_status is not provided but the status is \u201cup\u201d, it is assumed that both the status and line protocol should be \u201cup\u201d. If the interface status is not \u201cup\u201d, only the interface\u2019s status is validated, with no line protocol check performed. Expected Results Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces. Failure: If any of the following occur: The specified interface is not configured. The specified interface status and line protocol status does not match the expected operational state for any interface. 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 the operational states of specified interfaces to ensure they match expected configurations.\n\n This test performs the following checks for each specified interface:\n\n 1. If `line_protocol_status` is defined, both `status` and `line_protocol_status` are verified for the specified interface.\n 2. If `line_protocol_status` is not provided but the `status` is \"up\", it is assumed that both the status and line protocol should be \"up\".\n 3. If the interface `status` is not \"up\", only the interface's status is validated, with no line protocol check performed.\n\n Expected Results\n ----------------\n * Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces.\n * Failure: If any of the following occur:\n - The specified interface is not configured.\n - The specified interface status and line protocol status does not match the expected operational state for any interface.\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesStatus.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output\n for interface in self.inputs.interfaces:\n if (intf_status := get_value(command_output[\"interfaceDescriptions\"], interface.name, separator=\"..\")) is None:\n self.result.is_failure(f\"{interface.name} - Not configured\")\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 actual_state = f\"Expected: {interface.status}/{interface.line_protocol_status}, Actual: {status}/{proto}\"\n self.result.is_failure(f\"{interface.name} - {actual_state}\")\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\"):\n self.result.is_failure(f\"{interface.name} - Expected: up/up, Actual: {status}/{proto}\")\n elif interface.status != status:\n self.result.is_failure(f\"{interface.name} - Expected: {interface.status}, Actual: {status}\")\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.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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip virtual-router\", revision=2)]\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 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\", revision=1)]\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 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\", revision=1)]\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.VerifyLACPInterfacesStatus","title":"VerifyLACPInterfacesStatus","text":"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces. Verifies that the interface is a member of the LACP port channel. Ensures that the synchronization is established. Ensures the interfaces are in the correct state for collecting and distributing traffic. Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \u201cslow\u201d mode, is the default setting.) Expected Results Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct. Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct. Examples anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n Source code in anta/tests/interfaces.py class VerifyLACPInterfacesStatus(AntaTest):\n \"\"\"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces.\n\n - Verifies that the interface is a member of the LACP port channel.\n - Ensures that the synchronization is established.\n - Ensures the interfaces are in the correct state for collecting and distributing traffic.\n - Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \"slow\" mode, is the default setting.)\n\n Expected Results\n ----------------\n * Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct.\n * Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show lacp interface {interface}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLACPInterfacesStatus test.\"\"\"\n\n interfaces: list[LACPInterface]\n \"\"\"List of LACP member interface.\"\"\"\n\n class LACPInterface(BaseModel):\n \"\"\"Model for an LACP member interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"Ethernet interface to validate.\"\"\"\n portchannel: PortChannelInterface\n \"\"\"Port Channel in which the interface is bundled.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each interface in the input list.\"\"\"\n return [template.render(interface=interface.name) for interface in self.inputs.interfaces]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLACPInterfacesStatus.\"\"\"\n self.result.is_success()\n\n # Member port verification parameters.\n member_port_details = [\"activity\", \"aggregation\", \"synchronization\", \"collecting\", \"distributing\", \"timeout\"]\n\n # Iterating over command output for different interfaces\n for command, input_entry in zip(self.instance_commands, self.inputs.interfaces):\n interface = input_entry.name\n portchannel = input_entry.portchannel\n\n # Verify if a PortChannel is configured with the provided interface\n if not (interface_details := get_value(command.json_output, f\"portChannels.{portchannel}.interfaces.{interface}\")):\n self.result.is_failure(f\"Interface '{interface}' is not configured to be a member of LACP '{portchannel}'.\")\n continue\n\n # Verify the interface is bundled in port channel.\n actor_port_status = interface_details.get(\"actorPortStatus\")\n if actor_port_status != \"bundled\":\n message = f\"For Interface {interface}:\\nExpected `bundled` as the local port status, but found `{actor_port_status}` instead.\\n\"\n self.result.is_failure(message)\n continue\n\n # Collecting actor and partner port details\n actor_port_details = interface_details.get(\"actorPortState\", {})\n partner_port_details = interface_details.get(\"partnerPortState\", {})\n\n # Collecting actual interface details\n actual_interface_output = {\n \"actor_port_details\": {param: actor_port_details.get(param, \"NotFound\") for param in member_port_details},\n \"partner_port_details\": {param: partner_port_details.get(param, \"NotFound\") for param in member_port_details},\n }\n\n # Forming expected interface details\n expected_details = {param: param != \"timeout\" for param in member_port_details}\n expected_interface_output = {\"actor_port_details\": expected_details, \"partner_port_details\": expected_details}\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n message = f\"For Interface {interface}:\\n\"\n actor_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"actor_port_details\", {}), actual_interface_output.get(\"actor_port_details\", {})\n )\n partner_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"partner_port_details\", {}), actual_interface_output.get(\"partner_port_details\", {})\n )\n\n if actor_port_failed_log:\n message += f\"Actor port details:{actor_port_failed_log}\\n\"\n if partner_port_failed_log:\n message += f\"Partner port details:{partner_port_failed_log}\\n\"\n\n self.result.is_failure(message)\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[LACPInterface] List of LACP member interface. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"LACPInterface","text":"Name Type Description Default name EthernetInterface Ethernet interface to validate. - portchannel PortChannelInterface Port Channel in which the interface is bundled. -"},{"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 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\", revision=1)]\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: # pragma: no branch\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show port-channel\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip interface brief\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show storm-control\", revision=1)]\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.interfaces/#input-models","title":"Input models","text":""},{"location":"api/tests.interfaces/#anta.input_models.interfaces.InterfaceState","title":"InterfaceState","text":"Model for an interface state. 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 Source code in anta/input_models/interfaces.py 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"},{"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 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\", revision=1)]\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 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 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. This test performs the following checks: Retrieves the device\u2019s configured FQDN Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message includes the complete FQDN of the device Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the device\u2019s complete FQDN. Failure: If any of the following occur: The test message is not found in recent logs The log message does not include the device\u2019s FQDN The FQDN in the log message doesn\u2019t match the configured 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 This test performs the following checks:\n\n 1. Retrieves the device's configured FQDN\n 2. Sends a test log message at the **informational** level\n 3. Retrieves the most recent logs (last 30 seconds)\n 4. Verifies that the test message includes the complete FQDN of the device\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the device's complete FQDN.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The log message does not include the device's FQDN\n - The FQDN in the log message doesn't match the configured FQDN\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingHostname:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show hostname\", revision=1),\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingHostname validation\", ofmt=\"text\"),\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 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. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message was successfully logged Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are being generated and the test message is found in recent logs. Failure: If any of the following occur: The test message is not found in recent logs The logging system is not capturing new messages No logs are being 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 This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message was successfully logged\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are being generated and the test message is found in recent logs.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The logging system is not capturing new messages\n - No logs are being generated\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingLogsGeneration:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingLogsGeneration validation\", ofmt=\"text\"),\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 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 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 VerifyLoggingSourceIntf 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 VerifyLoggingSourceIntf.\"\"\"\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 appropriate timestamp. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message is present with a high-resolution RFC3339 timestamp format Example format: 2024-01-25T15:30:45.123456+00:00 Includes microsecond precision Contains timezone offset Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the correct high-resolution RFC3339 timestamp format. Failure: If any of the following occur: The test message is not found in recent logs The timestamp format does not match the expected RFC3339 format 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 appropriate timestamp.\n\n This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message is present with a high-resolution RFC3339 timestamp format\n - Example format: `2024-01-25T15:30:45.123456+00:00`\n - Includes microsecond precision\n - Contains timezone offset\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the correct high-resolution RFC3339 timestamp format.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The timestamp format does not match the expected RFC3339 format\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingTimestamp:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingTimestamp validation\", ofmt=\"text\"),\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(\n logger: logging.Logger, command_output: str\n) -> str\n Parse show logging output and gets operational logging states used in the tests in this module. Parameters: Name Type Description Default logger Logger The logger object. required command_output str The show logging output. required 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 Parameters\n ----------\n logger\n The logger object.\n command_output\n The `show logging` output.\n\n Returns\n -------\n str\n 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":"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 Adaptive Virtual Topology BFD Configuration Connectivity Field Notices Flow Tracking GreenT Hardware Interfaces LANZ Logging MLAG Multicast Profiles PTP Router Path Selection Routing Generic Routing BGP Routing ISIS Routing OSPF Security Services SNMP Software STP STUN 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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag config-sanity\", revision=1)]\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 command_output[\"mlagActive\"] 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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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.path_selection/","title":"Router Path Selection","text":""},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifyPathsHealth","title":"VerifyPathsHealth","text":"Verifies the path and telemetry state of all paths under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if all path states under router path-selection are either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and their telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if any path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifyPathsHealth:\n Source code in anta/tests/path_selection.py class VerifyPathsHealth(AntaTest):\n \"\"\"Verifies the path and telemetry state of all paths under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if all path states under router path-selection are either 'IPsec established' or 'Resolved'\n and their telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if any path state is not 'IPsec established' or 'Resolved',\n or the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifyPathsHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show path-selection paths\", revision=1)]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyPathsHealth.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"dpsPeers\"]\n\n # If no paths are configured for router path-selection, the test fails\n if not command_output:\n self.result.is_failure(\"No path configured for router path-selection.\")\n return\n\n # Check the state of each path\n for peer, peer_data in command_output.items():\n for group, group_data in peer_data[\"dpsGroups\"].items():\n for path_data in group_data[\"dpsPaths\"].values():\n path_state = path_data[\"state\"]\n session = path_data[\"dpsSessions\"][\"0\"][\"active\"]\n\n # If the path state of any path is not 'ipsecEstablished' or 'routeResolved', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for peer {peer} in path-group {group} is `{path_state}`.\")\n\n # If the telemetry state of any path is inactive, the test fails\n elif not session:\n self.result.is_failure(f\"Telemetry state for peer {peer} in path-group {group} is `inactive`.\")\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath","title":"VerifySpecificPath","text":"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if the path state under router path-selection is either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if the path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or if the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n Source code in anta/tests/path_selection.py class VerifySpecificPath(AntaTest):\n \"\"\"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if the path state under router path-selection is either 'IPsec established' or 'Resolved'\n and telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if the path state is not 'IPsec established' or 'Resolved',\n or if the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show path-selection paths peer {peer} path-group {group} source {source} destination {destination}\", revision=1)\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificPath test.\"\"\"\n\n paths: list[RouterPath]\n \"\"\"List of router paths to verify.\"\"\"\n\n class RouterPath(BaseModel):\n \"\"\"Detail of a router path.\"\"\"\n\n peer: IPv4Address\n \"\"\"Static peer IPv4 address.\"\"\"\n\n path_group: str\n \"\"\"Router path group name.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of path.\"\"\"\n\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of path.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each router path.\"\"\"\n return [\n template.render(peer=path.peer, group=path.path_group, source=path.source_address, destination=path.destination_address) for path in self.inputs.paths\n ]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificPath.\"\"\"\n self.result.is_success()\n\n # Check the state of each path\n for command in self.instance_commands:\n peer = command.params.peer\n path_group = command.params.group\n source = command.params.source\n destination = command.params.destination\n command_output = command.json_output.get(\"dpsPeers\", [])\n\n # If the peer is not configured for the path group, the test fails\n if not command_output:\n self.result.is_failure(f\"Path `peer: {peer} source: {source} destination: {destination}` is not configured for path-group `{path_group}`.\")\n continue\n\n # Extract the state of the path\n path_output = get_value(command_output, f\"{peer}..dpsGroups..{path_group}..dpsPaths\", separator=\"..\")\n path_state = next(iter(path_output.values())).get(\"state\")\n session = get_value(next(iter(path_output.values())), \"dpsSessions.0.active\")\n\n # If the state of the path is not 'ipsecEstablished' or 'routeResolved', or the telemetry state is 'inactive', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `{path_state}`.\")\n elif not session:\n self.result.is_failure(\n f\"Telemetry state for path `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `inactive`.\"\n )\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"Inputs","text":"Name Type Description Default paths list[RouterPath] List of router paths to verify. -"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"RouterPath","text":"Name Type Description Default peer IPv4Address Static peer IPv4 address. - path_group str Router path group name. - source_address IPv4Address Source IPv4 address of path. - destination_address IPv4Address Destination IPv4 address of path. -"},{"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 description = \"Verifies the device TCAM profile.\"\n categories: ClassVar[list[str]] = [\"profiles\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hardware tcam profile\", revision=1)]\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 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\", revision=1)]\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\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 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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpLockStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpModeStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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 on the device. 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 on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpOffset:\n ```\n \"\"\"\n\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\", revision=1)]\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 description = \"Verifies the PTP interfaces state.\"\n categories: ClassVar[list[str]] = [\"ptp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", revision=2)]\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/#tests","title":"Tests","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 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\", revision=3)]\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 the advertised and received routes of BGP peers. 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 the advertised and received routes of BGP peers.\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show bgp neighbors {peer} advertised-routes vrf {vrf}\", revision=3),\n AntaTemplate(template=\"show bgp neighbors {peer} routes vrf {vrf}\", revision=3),\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 [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\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 = command.params.peer\n vrf = command.params.vrf\n for input_entry in self.inputs.bgp_peers:\n if str(input_entry.peer_address) == peer and input_entry.vrf == vrf:\n advertised_routes = input_entry.advertised_routes\n received_routes = input_entry.received_routes\n break\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 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\", revision=3)]\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 given address families. This test performs the following checks for each specified address family: Confirms that the specified VRF is configured. Counts the number of peers that are: If check_peer_state is set to True, Counts the number of BGP peers that are in the Established state and have successfully negotiated the specified AFI/SAFI If check_peer_state is set to False, skips validation of the Established state and AFI/SAFI negotiation. Expected Results Success: If the count of BGP peers matches the expected count with check_peer_state enabled/disabled. Failure: If any of the following occur: The specified VRF is not configured. The BGP peer count does not match expected value with check_peer_state enabled/disabled.\u201d 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 given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Confirms that the specified VRF is configured.\n 2. Counts the number of peers that are:\n - If `check_peer_state` is set to True, Counts the number of BGP peers that are in the `Established` state and\n have successfully negotiated the specified AFI/SAFI\n - If `check_peer_state` is set to False, skips validation of the `Established` state and AFI/SAFI negotiation.\n\n Expected Results\n ----------------\n * Success: If the count of BGP peers matches the expected count with `check_peer_state` enabled/disabled.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - The BGP peer count does not match expected value with `check_peer_state` enabled/disabled.\"\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp summary vrf all\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerCount test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'num_peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.num_peers is None:\n msg = f\"{af} 'num_peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerCount.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n peers_data = vrf_output.get(\"peers\", {}).values()\n if not address_family.check_peer_state:\n # Count the number of peers without considering the state and negotiated AFI/SAFI check if the count matches the expected count\n peer_count = sum(1 for peer_data in peers_data if address_family.eos_key in peer_data)\n else:\n # Count the number of established peers with negotiated AFI/SAFI\n peer_count = sum(\n 1\n for peer_data in peers_data\n if peer_data.get(\"peerState\") == \"Established\" and get_value(peer_data, f\"{address_family.eos_key}.afiSafiState\") == \"negotiated\"\n )\n\n # Check if the count matches the expected count\n if address_family.num_peers != peer_count:\n self.result.is_failure(f\"{address_family} - Expected: {address_family.num_peers}, Actual: {peer_count}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats","title":"VerifyBGPPeerDropStats","text":"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s). By default, all drop statistics counters will be checked for any non-zero values. An optional list of specific drop statistics can be provided for granular testing. Expected Results Success: The test will pass if the BGP peer\u2019s drop statistic(s) are zero. Failure: The test will fail if the BGP peer\u2019s drop statistic(s) are non-zero/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerDropStats(AntaTest):\n \"\"\"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s).\n\n By default, all drop statistics counters will be checked for any non-zero values.\n An optional list of specific drop statistics can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's drop statistic(s) are zero.\n * Failure: The test will fail if the BGP peer's drop statistic(s) are non-zero/Not Found or peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerDropStats 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 drop_stats: list[BgpDropStats] | None = None\n \"\"\"Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerDropStats.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n drop_statistics = input_entry.drop_stats\n\n # Verify BGP peer\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify BGP peer's drop stats\n drop_stats_output = peer_detail.get(\"dropStats\", {})\n\n # In case drop stats not provided, It will check all drop statistics\n if not drop_statistics:\n drop_statistics = drop_stats_output\n\n # Verify BGP peer's drop stats\n drop_stats_not_ok = {\n drop_stat: drop_stats_output.get(drop_stat, \"Not Found\") for drop_stat in drop_statistics if drop_stats_output.get(drop_stat, \"Not Found\")\n }\n if any(drop_stats_not_ok):\n failures[peer] = {vrf: drop_stats_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero NLRI drop statistics counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats-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.VerifyBGPPeerDropStats-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' drop_stats list[BgpDropStats] | None Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics. None"},{"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 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\", revision=3)]\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. Supports strict: True to verify that only the specified capabilities are configured, requiring an exact match. 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 strict: False\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 Supports `strict: True` to verify that only the specified capabilities are configured, requiring an exact match.\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 strict: False\n capabilities:\n - ipv4Unicast\n ```\n \"\"\"\n\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\", revision=3)]\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 strict: bool = False\n \"\"\"If True, requires exact matching of provided capabilities. Defaults to False.\"\"\"\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 # Fetching the capabilities output.\n bgp_output = get_value(bgp_output, \"neighborCapabilities.multiprotocolCaps\")\n\n if bgp_peer.strict and sorted(capabilities) != sorted(bgp_output):\n failure[\"bgp_peers\"][peer][vrf] = {\n \"status\": f\"Expected only `{', '.join(capabilities)}` capabilities should be listed but found `{', '.join(bgp_output)}` instead.\"\n }\n failures = deep_update(failures, failure)\n continue\n\n # Check each capability\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' strict bool If True, requires exact matching of provided capabilities. Defaults to False. False capabilities list[MultiProtocolCaps] List of multiprotocol capabilities to be verified. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit","title":"VerifyBGPPeerRouteLimit","text":"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s). Expected Results Success: The test will pass if the BGP peer\u2019s maximum routes and, if provided, the maximum routes warning limit are equal to the given limits. Failure: The test will fail if the BGP peer\u2019s maximum routes do not match the given limit, or if the maximum routes warning limit is provided and does not match the given limit, or if the peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerRouteLimit(AntaTest):\n \"\"\"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's maximum routes and, if provided, the maximum routes warning limit are equal to the given limits.\n * Failure: The test will fail if the BGP peer's maximum routes do not match the given limit, or if the maximum routes warning limit is provided\n and does not match the given limit, or if the peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerRouteLimit 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 maximum_routes: int = Field(ge=0, le=4294967294)\n \"\"\"The maximum allowable number of BGP routes, `0` means unlimited.\"\"\"\n warning_limit: int = Field(default=0, ge=0, le=4294967294)\n \"\"\"Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerRouteLimit.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n maximum_routes = input_entry.maximum_routes\n warning_limit = input_entry.warning_limit\n failure: dict[Any, Any] = {}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify maximum routes configured.\n if (actual_routes := peer_detail.get(\"maxTotalRoutes\", \"Not Found\")) != maximum_routes:\n failure[\"Maximum total routes\"] = actual_routes\n\n # Verify warning limit if given.\n if warning_limit and (actual_warning_limit := peer_detail.get(\"totalRoutesWarnLimit\", \"Not Found\")) != warning_limit:\n failure[\"Warning limit\"] = actual_warning_limit\n\n # Updated failures if any.\n if failure:\n failures[peer] = {vrf: failure}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peer(s) are not configured or maximum routes and maximum routes warning limit is not correct:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit-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.VerifyBGPPeerRouteLimit-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' maximum_routes int The maximum allowable number of BGP routes, `0` means unlimited. Field(ge=0, le=4294967294) warning_limit int Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit. Field(default=0, ge=0, le=4294967294)"},{"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 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\", revision=3)]\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.VerifyBGPPeerUpdateErrors","title":"VerifyBGPPeerUpdateErrors","text":"Verifies BGP update error counters for the provided BGP IPv4 peer(s). By default, all update error counters will be checked for any non-zero values. An optional list of specific update error counters can be provided for granular testing. Note: For \u201cdisabledAfiSafi\u201d error counter field, checking that it\u2019s not \u201cNone\u201d versus 0. Expected Results Success: The test will pass if the BGP peer\u2019s update error counter(s) are zero/None. Failure: The test will fail if the BGP peer\u2019s update error counter(s) are non-zero/not None/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerUpdateErrors(AntaTest):\n \"\"\"Verifies BGP update error counters for the provided BGP IPv4 peer(s).\n\n By default, all update error counters will be checked for any non-zero values.\n An optional list of specific update error counters can be provided for granular testing.\n\n Note: For \"disabledAfiSafi\" error counter field, checking that it's not \"None\" versus 0.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's update error counter(s) are zero/None.\n * Failure: The test will fail if the BGP peer's update error counter(s) are non-zero/not None/Not Found or\n peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerUpdateErrors 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 update_errors: list[BgpUpdateError] | None = None\n \"\"\"Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerUpdateErrors.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n update_error_counters = input_entry.update_errors\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Getting the BGP peer's error counters output.\n error_counters_output = peer_detail.get(\"peerInUpdateErrors\", {})\n\n # In case update error counters not provided, It will check all the update error counters.\n if not update_error_counters:\n update_error_counters = error_counters_output\n\n # verifying the error counters.\n error_counters_not_ok = {\n (\"disabledAfiSafi\" if error_counter == \"disabledAfiSafi\" else error_counter): value\n for error_counter in update_error_counters\n if (value := error_counters_output.get(error_counter, \"Not Found\")) != \"None\" and value != 0\n }\n if error_counters_not_ok:\n failures[peer] = {vrf: error_counters_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero update error counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerUpdateErrors-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.VerifyBGPPeerUpdateErrors-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' update_errors list[BgpUpdateError] | None Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters. None"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth","title":"VerifyBGPPeersHealth","text":"Verifies the health of BGP peers for given address families. This test performs the following checks for each specified address family: Validates that the VRF is configured. Checks if there are any peers for the given AFI/SAFI. For each relevant peer: Verifies that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Checks that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified address families and their peers. Failure: If any of the following occur: The specified VRF is not configured. No peers are found for a given AFI/SAFI. Any BGP session is not in the Established state. The AFI/SAFI state is not \u2018negotiated\u2019 for any peer. Any TCP message queue (input or output) is not empty when check_tcp_queues is True (default). 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 check_tcp_queues: false\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeersHealth(AntaTest):\n \"\"\"Verifies the health of BGP peers for given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Validates that the VRF is configured.\n 2. Checks if there are any peers for the given AFI/SAFI.\n 3. For each relevant peer:\n - Verifies that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Checks that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified address families and their peers.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - No peers are found for a given AFI/SAFI.\n - Any BGP session is not in the `Established` state.\n - The AFI/SAFI state is not 'negotiated' for any peer.\n - Any TCP message queue (input or output) is not empty when `check_tcp_queues` is `True` (default).\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 check_tcp_queues: false\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeersHealth test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeersHealth.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n # Check if any peers are found for this AFI/SAFI\n relevant_peers = [\n peer for peer in vrf_output.get(\"peerList\", []) if get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\") is not None\n ]\n\n if not relevant_peers:\n self.result.is_failure(f\"{address_family} - No peers found\")\n continue\n\n for peer in relevant_peers:\n # Check if the BGP session is established\n if peer[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session state is not established; State: {peer['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers","title":"VerifyBGPSpecificPeers","text":"Verifies the health of specific BGP peer(s) for given address families. This test performs the following checks for each specified address family and peer: Confirms that the specified VRF is configured. For each specified peer: Verifies that the peer is found in the BGP configuration. Checks that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Ensures that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified peers in all address families. Failure: If any of the following occur: The specified VRF is not configured. A specified peer is not found in the BGP configuration. The BGP session for a peer is not in the Established state. The AFI/SAFI state is not negotiated for a peer. Any TCP message queue (input or output) is not empty for a peer when check_tcp_queues is True (default). 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) for given address families.\n\n This test performs the following checks for each specified address family and peer:\n\n 1. Confirms that the specified VRF is configured.\n 2. For each specified peer:\n - Verifies that the peer is found in the BGP configuration.\n - Checks that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Ensures that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified peers in all address families.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - A specified peer is not found in the BGP configuration.\n - The BGP session for a peer is not in the `Established` state.\n - The AFI/SAFI state is not `negotiated` for a peer.\n - Any TCP message queue (input or output) is not empty for a peer when `check_tcp_queues` is `True` (default).\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPSpecificPeers test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.peers is None:\n msg = f\"{af} 'peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPSpecificPeers.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n for peer in address_family.peers:\n peer_ip = str(peer)\n\n # Check if the peer is found\n if (peer_data := get_item(vrf_output[\"peerList\"], \"peerAddress\", peer_ip)) is None:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Not configured\")\n continue\n\n # Check if the BGP session is established\n if peer_data[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session state is not established; State: {peer_data['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer_data, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not capability_status:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated\")\n\n if capability_status and not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer_data[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer_data[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"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 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\", revision=3)]\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.VerifyBgpRouteMaps","title":"VerifyBgpRouteMaps","text":"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s). Expected Results Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF. Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction. Examples anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n Source code in anta/tests/routing/bgp.py class VerifyBgpRouteMaps(AntaTest):\n \"\"\"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF.\n * Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBgpRouteMaps 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 inbound_route_map: str | None = None\n \"\"\"Inbound route map applied, defaults to None.\"\"\"\n outbound_route_map: str | None = None\n \"\"\"Outbound route map applied, defaults to None.\"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpPeer class.\n\n At least one of 'inbound' or 'outbound' route-map must be provided.\n \"\"\"\n if not (self.inbound_route_map or self.outbound_route_map):\n msg = \"At least one of 'inbound_route_map' or 'outbound_route_map' must be provided.\"\n raise ValueError(msg)\n return self\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBgpRouteMaps.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n inbound_route_map = input_entry.inbound_route_map\n outbound_route_map = input_entry.outbound_route_map\n failure: dict[Any, Any] = {vrf: {}}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify Inbound route-map\n if inbound_route_map and (inbound_map := peer_detail.get(\"routeMapInbound\", \"Not Configured\")) != inbound_route_map:\n failure[vrf].update({\"Inbound route-map\": inbound_map})\n\n # Verify Outbound route-map\n if outbound_route_map and (outbound_map := peer_detail.get(\"routeMapOutbound\", \"Not Configured\")) != outbound_route_map:\n failure[vrf].update({\"Outbound route-map\": outbound_map})\n\n if failure[vrf]:\n failures[peer] = failure\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(\n f\"The following BGP peers are not configured or has an incorrect or missing route map in either the inbound or outbound direction:\\n{failures}\"\n )\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBgpRouteMaps-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.VerifyBgpRouteMaps-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' inbound_route_map str | None Inbound route map applied, defaults to None. None outbound_route_map str | None Outbound route map applied, defaults to None. None"},{"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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp evpn route-type mac-ip {address} vni {vni}\", revision=2)]\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=str(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 = 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/#input-models","title":"Input models","text":""},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily","title":"BgpAddressFamily","text":"Model for a BGP address family. Name Type Description Default afi Afi BGP Address Family Identifier (AFI). - safi Safi | None BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`. None vrf str Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`. If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`. These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6. 'default' num_peers PositiveInt | None Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test. None peers list[IPv4Address | IPv6Address] | None List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test. None check_tcp_queues bool Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`. Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests. True check_peer_state bool Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`. Can be enabled in the `VerifyBGPPeerCount` tests. False Source code in anta/input_models/routing/bgp.py class BgpAddressFamily(BaseModel):\n \"\"\"Model for a BGP address family.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n afi: Afi\n \"\"\"BGP Address Family Identifier (AFI).\"\"\"\n safi: Safi | None = None\n \"\"\"BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`.\"\"\"\n vrf: str = \"default\"\n \"\"\"Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`.\n\n If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`.\n\n These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6.\n \"\"\"\n num_peers: PositiveInt | None = None\n \"\"\"Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test.\"\"\"\n peers: list[IPv4Address | IPv6Address] | None = None\n \"\"\"List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test.\"\"\"\n check_tcp_queues: bool = True\n \"\"\"Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`.\n\n Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests.\n \"\"\"\n check_peer_state: bool = False\n \"\"\"Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`.\n\n Can be enabled in the `VerifyBGPPeerCount` tests.\n \"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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 @property\n def eos_key(self) -> str:\n \"\"\"AFI/SAFI EOS key representation.\"\"\"\n # Pydantic handles the validation of the AFI/SAFI combination, so we can ignore error handling here.\n return AFI_SAFI_EOS_KEY[(self.afi, self.safi)]\n\n def __str__(self) -> str:\n \"\"\"Return a string representation of the BgpAddressFamily model. Used in failure messages.\n\n Examples\n --------\n - AFI:ipv4 SAFI:unicast VRF:default\n - AFI:evpn\n \"\"\"\n base_string = f\"AFI: {self.afi}\"\n if self.safi is not None:\n base_string += f\" SAFI: {self.safi}\"\n if self.afi in [\"ipv4\", \"ipv6\"]:\n base_string += f\" VRF: {self.vrf}\"\n return base_string\n"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily.validate_inputs","title":"validate_inputs","text":"validate_inputs() -> Self\n Validate the inputs provided to the BgpAddressFamily class. If afi is either ipv4 or ipv6, safi must be provided. If afi is not ipv4 or ipv6, safi must NOT be provided and vrf must be default. Source code in anta/input_models/routing/bgp.py @model_validator(mode=\"after\")\ndef validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAfi","title":"BgpAfi","text":"Alias for the BgpAddressFamily model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the BgpAddressFamily model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/routing/bgp.py class BgpAfi(BgpAddressFamily): # pragma: no cover\n \"\"\"Alias for the BgpAddressFamily model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the BgpAddressFamily model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the BgpAfi class, emitting a deprecation warning.\"\"\"\n warn(\n message=\"BgpAfi model is deprecated and will be removed in ANTA v2.0.0. Use the BgpAddressFamily model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\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. 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.\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 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 categories: ClassVar[list[str]] = [\"routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show ip route vrf {vrf} {route}\", revision=4),\n AntaTemplate(template=\"show ip route vrf {vrf}\", revision=4),\n ]\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 collect: Literal[\"one\", \"all\"] = \"one\"\n \"\"\"Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one`\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for the input vrf.\"\"\"\n if template == VerifyRoutingTableEntry.commands[0] and self.inputs.collect == \"one\":\n return [template.render(vrf=self.inputs.vrf, route=route) for route in self.inputs.routes]\n\n if template == VerifyRoutingTableEntry.commands[1] and self.inputs.collect == \"all\":\n return [template.render(vrf=self.inputs.vrf)]\n\n return []\n\n @staticmethod\n @cache\n def ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRoutingTableEntry.\"\"\"\n commands_output_route_ips = set()\n\n for command in self.instance_commands:\n command_output_vrf = command.json_output[\"vrfs\"][self.inputs.vrf]\n commands_output_route_ips |= {self.ip_interface_ip(route) for route in command_output_vrf[\"routes\"]}\n\n missing_routes = [str(route) for route in self.inputs.routes if route not in commands_output_route_ips]\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. - collect Literal['one', 'all'] Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one` 'one'"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableEntry.ip_interface_ip","title":"ip_interface_ip cached staticmethod","text":"ip_interface_ip(route: str) -> IPv4Address\n Return the IP address of the provided ip route with mask. Source code in anta/tests/routing/generic.py @staticmethod\n@cache\ndef ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n"},{"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 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: PositiveInteger\n \"\"\"Expected minimum routing table size.\"\"\"\n maximum: PositiveInteger\n \"\"\"Expected maximum routing table size.\"\"\"\n\n @model_validator(mode=\"after\")\n def check_min_max(self) -> Self:\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 PositiveInteger Expected minimum routing table size. - maximum PositiveInteger Expected maximum routing table size. -"},{"location":"api/tests.routing.isis/","title":"ISIS","text":""},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode","title":"VerifyISISInterfaceMode","text":"Verifies ISIS Interfaces are running in correct mode. Expected Results Success: The test will pass if all listed interfaces are running in correct mode. Failure: The test will fail if any of the listed interfaces is not running in correct mode. Skipped: The test will be skipped if no ISIS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISInterfaceMode(AntaTest):\n \"\"\"Verifies ISIS Interfaces are running in correct mode.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces are running in correct mode.\n * Failure: The test will fail if any of the listed interfaces is not running in correct mode.\n * Skipped: The test will be skipped if no ISIS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n ```\n \"\"\"\n\n description = \"Verifies interface mode for IS-IS\"\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceState]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceState(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n mode: Literal[\"point-to-point\", \"broadcast\", \"passive\"]\n \"\"\"Number of IS-IS neighbors.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF where the interface should be configured\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISInterfaceMode.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # Check for p2p interfaces\n for interface in self.inputs.interfaces:\n interface_data = _get_interface_data(\n interface=interface.name,\n vrf=interface.vrf,\n command_output=command_output,\n )\n # Check for correct VRF\n if interface_data is not None:\n interface_type = get_value(dictionary=interface_data, key=\"interfaceType\", default=\"unset\")\n # Check for interfaceType\n if interface.mode == \"point-to-point\" and interface.mode != interface_type:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in {interface.mode} reporting {interface_type}\")\n # Check for passive\n elif interface.mode == \"passive\":\n json_path = f\"intfLevels.{interface.level}.passive\"\n if interface_data is None or get_value(dictionary=interface_data, key=json_path, default=False) is False:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in passive mode\")\n else:\n self.result.is_failure(f\"Interface {interface.name} not found in VRF {interface.vrf}\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceState] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"InterfaceState","text":"Name Type Description Default name Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 mode Literal['point-to-point', 'broadcast', 'passive'] Number of IS-IS neighbors. - vrf str VRF where the interface should be configured 'default'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount","title":"VerifyISISNeighborCount","text":"Verifies number of IS-IS neighbors per level and per interface. Expected Results Success: The test will pass if the number of neighbors is correct. Failure: The test will fail if the number of neighbors is incorrect. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborCount(AntaTest):\n \"\"\"Verifies number of IS-IS neighbors per level and per interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if the number of neighbors is correct.\n * Failure: The test will fail if the number of neighbors is incorrect.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceCount]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceCount(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: int = 2\n \"\"\"IS-IS level to check.\"\"\"\n count: int\n \"\"\"Number of IS-IS neighbors.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborCount.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n isis_neighbor_count = _get_isis_neighbors_count(command_output)\n if len(isis_neighbor_count) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n for interface in self.inputs.interfaces:\n eos_data = [ifl_data for ifl_data in isis_neighbor_count if ifl_data[\"interface\"] == interface.name and ifl_data[\"level\"] == interface.level]\n if not eos_data:\n self.result.is_failure(f\"No neighbor detected for interface {interface.name}\")\n continue\n if eos_data[0][\"count\"] != interface.count:\n self.result.is_failure(\n f\"Interface {interface.name}: \"\n f\"expected Level {interface.level}: count {interface.count}, \"\n f\"got Level {eos_data[0]['level']}: count {eos_data[0]['count']}\"\n )\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceCount] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"InterfaceCount","text":"Name Type Description Default name Interface Interface name to check. - level int IS-IS level to check. 2 count int Number of IS-IS neighbors. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborState","title":"VerifyISISNeighborState","text":"Verifies all IS-IS neighbors are in UP state. Expected Results Success: The test will pass if all IS-IS neighbors are in UP state. Failure: The test will fail if some IS-IS neighbors are not in UP state. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborState(AntaTest):\n \"\"\"Verifies all IS-IS neighbors are in UP state.\n\n Expected Results\n ----------------\n * Success: The test will pass if all IS-IS neighbors are in UP state.\n * Failure: The test will fail if some IS-IS neighbors are not in UP state.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis neighbors\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborState.\"\"\"\n command_output = self.instance_commands[0].json_output\n if _count_isis_neighbor(command_output) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n self.result.is_success()\n not_full_neighbors = _get_not_full_isis_neighbors(command_output)\n if not_full_neighbors:\n self.result.is_failure(f\"Some neighbors are not in the correct state (UP): {not_full_neighbors}.\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments","title":"VerifyISISSegmentRoutingAdjacencySegments","text":"Verify that all expected Adjacency segments are correctly visible for each interface. Expected Results Success: The test will pass if all listed interfaces have correct adjacencies. Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies. Skipped: The test will be skipped if no ISIS SR Adjacency is found. Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):\n \"\"\"Verify that all expected Adjacency segments are correctly visible for each interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces have correct adjacencies.\n * Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies.\n * Skipped: The test will be skipped if no ISIS SR Adjacency is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing adjacency-segments\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingAdjacencySegments test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n segments: list[Segment]\n \"\"\"List of Adjacency segments configured in this instance.\"\"\"\n\n class Segment(BaseModel):\n \"\"\"Segment model definition.\"\"\"\n\n interface: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n sid_origin: Literal[\"dynamic\"] = \"dynamic\"\n \"\"\"Adjacency type\"\"\"\n address: IPv4Address\n \"\"\"IP address of remote end of segment.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingAdjacencySegments.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routging.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n for input_segment in instance.segments:\n eos_segment = _get_adjacency_segment_data_by_neighbor(\n neighbor=str(input_segment.address),\n instance=instance.name,\n vrf=instance.vrf,\n command_output=command_output,\n )\n if eos_segment is None:\n failure_message.append(f\"Your segment has not been found: {input_segment}.\")\n\n elif (\n eos_segment[\"localIntf\"] != input_segment.interface\n or eos_segment[\"level\"] != input_segment.level\n or eos_segment[\"sidOrigin\"] != input_segment.sid_origin\n ):\n failure_message.append(f\"Your segment is not correct: Expected: {input_segment} - Found: {eos_segment}.\")\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' segments list[Segment] List of Adjacency segments configured in this instance. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Segment","text":"Name Type Description Default interface Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 sid_origin Literal['dynamic'] Adjacency type 'dynamic' address IPv4Address IP address of remote end of segment. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane","title":"VerifyISISSegmentRoutingDataplane","text":"Verify dataplane of a list of ISIS-SR instances. Expected Results Success: The test will pass if all instances have correct dataplane configured Failure: The test will fail if one of the instances has incorrect dataplane configured Skipped: The test will be skipped if ISIS is not running Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingDataplane(AntaTest):\n \"\"\"Verify dataplane of a list of ISIS-SR instances.\n\n Expected Results\n ----------------\n * Success: The test will pass if all instances have correct dataplane configured\n * Failure: The test will fail if one of the instances has incorrect dataplane configured\n * Skipped: The test will be skipped if ISIS is not running\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingDataplane test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n dataplane: Literal[\"MPLS\", \"mpls\", \"unset\"] = \"MPLS\"\n \"\"\"Configured dataplane for the instance.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingDataplane.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routing.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n eos_dataplane = get_value(dictionary=command_output, key=f\"vrfs.{instance.vrf}.isisInstances.{instance.name}.dataPlane\", default=None)\n if instance.dataplane.upper() != eos_dataplane:\n failure_message.append(f\"ISIS instance {instance.name} is not running dataplane {instance.dataplane} ({eos_dataplane})\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' dataplane Literal['MPLS', 'mpls', 'unset'] Configured dataplane for the instance. 'MPLS'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels","title":"VerifyISISSegmentRoutingTunnels","text":"Verify ISIS-SR tunnels computed by device. Expected Results Success: The test will pass if all listed tunnels are computed on device. Failure: The test will fail if one of the listed tunnels is missing. Skipped: The test will be skipped if ISIS-SR is not configured. Examples anta.tests.routing:\nisis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingTunnels(AntaTest):\n \"\"\"Verify ISIS-SR tunnels computed by device.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed tunnels are computed on device.\n * Failure: The test will fail if one of the listed tunnels is missing.\n * Skipped: The test will be skipped if ISIS-SR is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing tunnel\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingTunnels test.\"\"\"\n\n entries: list[Entry]\n \"\"\"List of tunnels to check on device.\"\"\"\n\n class Entry(BaseModel):\n \"\"\"Definition of a tunnel entry.\"\"\"\n\n endpoint: IPv4Network\n \"\"\"Endpoint IP of the tunnel.\"\"\"\n vias: list[Vias] | None = None\n \"\"\"Optional list of path to reach endpoint.\"\"\"\n\n class Vias(BaseModel):\n \"\"\"Definition of a tunnel path.\"\"\"\n\n nexthop: IPv4Address | None = None\n \"\"\"Nexthop of the tunnel. If None, then it is not tested. Default: None\"\"\"\n type: Literal[\"ip\", \"tunnel\"] | None = None\n \"\"\"Type of the tunnel. If None, then it is not tested. Default: None\"\"\"\n interface: Interface | None = None\n \"\"\"Interface of the tunnel. If None, then it is not tested. Default: None\"\"\"\n tunnel_id: Literal[\"TI-LFA\", \"ti-lfa\", \"unset\"] | None = None\n \"\"\"Computation method of the tunnel. If None, then it is not tested. Default: None\"\"\"\n\n def _eos_entry_lookup(self, search_value: IPv4Network, entries: dict[str, Any], search_key: str = \"endpoint\") -> dict[str, Any] | None:\n return next(\n (entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == str(search_value)),\n None,\n )\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingTunnels.\n\n This method performs the main test logic for verifying ISIS Segment Routing tunnels.\n It checks the command output, initiates defaults, and performs various checks on the tunnels.\n \"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n # initiate defaults\n failure_message = []\n\n if len(command_output[\"entries\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n for input_entry in self.inputs.entries:\n eos_entry = self._eos_entry_lookup(search_value=input_entry.endpoint, entries=command_output[\"entries\"])\n if eos_entry is None:\n failure_message.append(f\"Tunnel to {input_entry} is not found.\")\n elif input_entry.vias is not None:\n failure_src = []\n for via_input in input_entry.vias:\n if not self._check_tunnel_type(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel type\")\n if not self._check_tunnel_nexthop(via_input, eos_entry):\n failure_src.append(\"incorrect nexthop\")\n if not self._check_tunnel_interface(via_input, eos_entry):\n failure_src.append(\"incorrect interface\")\n if not self._check_tunnel_id(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel ID\")\n\n if failure_src:\n failure_message.append(f\"Tunnel to {input_entry.endpoint!s} is incorrect: {', '.join(failure_src)}\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n\n def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input tunnel type to check.\n eos_entry : dict[str, Any]\n The EOS entry containing the tunnel types.\n\n Returns\n -------\n bool\n True if the tunnel type matches any of the tunnel types in `eos_entry`, False otherwise.\n \"\"\"\n if via_input.type is not None:\n return any(\n via_input.type\n == get_value(\n dictionary=eos_via,\n key=\"type\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel nexthop matches the given input.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel nexthop matches, False otherwise.\n \"\"\"\n if via_input.nexthop is not None:\n return any(\n str(via_input.nexthop)\n == get_value(\n dictionary=eos_via,\n key=\"nexthop\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel interface exists in the given EOS entry.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel interface exists, False otherwise.\n \"\"\"\n if via_input.interface is not None:\n return any(\n via_input.interface\n == get_value(\n dictionary=eos_via,\n key=\"interface\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input vias to check.\n eos_entry : dict[str, Any])\n The EOS entry to compare against.\n\n Returns\n -------\n bool\n True if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias, False otherwise.\n \"\"\"\n if via_input.tunnel_id is not None:\n return any(\n via_input.tunnel_id.upper()\n == get_value(\n dictionary=eos_via,\n key=\"tunnelId.type\",\n default=\"undefined\",\n ).upper()\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Inputs","text":"Name Type Description Default entries list[Entry] List of tunnels to check on device. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Entry","text":"Name Type Description Default endpoint IPv4Network Endpoint IP of the tunnel. - vias list[Vias] | None Optional list of path to reach endpoint. None"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Vias","text":"Name Type Description Default nexthop IPv4Address | None Nexthop of the tunnel. If None, then it is not tested. Default: None None type Literal['ip', 'tunnel'] | None Type of the tunnel. If None, then it is not tested. Default: None None interface Interface | None Interface of the tunnel. If None, then it is not tested. Default: None None tunnel_id Literal['TI-LFA', 'ti-lfa', 'unset'] | None Computation method of the tunnel. If None, then it is not tested. Default: None None"},{"location":"api/tests.routing.ospf/","title":"OSPF","text":""},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFMaxLSA","title":"VerifyOSPFMaxLSA","text":"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold. Expected Results Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold. Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold. Skipped: The test will be skipped if no OSPF instance is found. Examples anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n Source code in anta/tests/routing/ospf.py class VerifyOSPFMaxLSA(AntaTest):\n \"\"\"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold.\n * Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold.\n * Skipped: The test will be skipped if no OSPF instance is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n ```\n \"\"\"\n\n description = \"Verifies all OSPF instances did not cross the maximum LSA threshold.\"\n categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyOSPFMaxLSA.\"\"\"\n command_output = self.instance_commands[0].json_output\n ospf_instance_info = _get_ospf_max_lsa_info(command_output)\n if not ospf_instance_info:\n self.result.is_skipped(\"No OSPF instance found.\")\n return\n all_instances_within_threshold = all(instance[\"numLsa\"] <= instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100) for instance in ospf_instance_info)\n if all_instances_within_threshold:\n self.result.is_success()\n else:\n exceeded_instances = [\n instance[\"instance\"] for instance in ospf_instance_info if instance[\"numLsa\"] > instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100)\n ]\n self.result.is_failure(f\"OSPF Instances {exceeded_instances} crossed the maximum LSA threshold.\")\n"},{"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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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.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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ip access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ipv6 access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show management security ssl certificate\", revision=1),\n AntaCommand(command=\"show clock\", revision=1),\n ]\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) -> Self:\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 get_args(RsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for RSA encryption. Allowed sizes are {get_args(RsaKeySize)}.\"\n raise ValueError(msg)\n\n if self.encryption_algorithm == \"ECDSA\" and self.key_size not in get_args(EcdsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for ECDSA encryption. Allowed sizes are {get_args(EcdsaKeySize)}.\"\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner login\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner motd\", revision=1)]\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.VerifyHardwareEntropy","title":"VerifyHardwareEntropy","text":"Verifies hardware entropy generation is enabled on device. Expected Results Success: The test will pass if hardware entropy generation is enabled. Failure: The test will fail if hardware entropy generation is not enabled. Examples anta.tests.security:\n - VerifyHardwareEntropy:\n Source code in anta/tests/security.py class VerifyHardwareEntropy(AntaTest):\n \"\"\"Verifies hardware entropy generation is enabled on device.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware entropy generation is enabled.\n * Failure: The test will fail if hardware entropy generation is not enabled.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyHardwareEntropy:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management security\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareEntropy.\"\"\"\n command_output = self.instance_commands[0].json_output\n\n # Check if hardware entropy generation is enabled.\n if not command_output.get(\"hardwareEntropyEnabled\"):\n self.result.is_failure(\"Hardware entropy generation is disabled.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.security/#anta.tests.security.VerifyIPSecConnHealth","title":"VerifyIPSecConnHealth","text":"Verifies all IPv4 security connections. Expected Results Success: The test will pass if all the IPv4 security connections are established in all vrf. Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf. Examples anta.tests.security:\n - VerifyIPSecConnHealth:\n Source code in anta/tests/security.py class VerifyIPSecConnHealth(AntaTest):\n \"\"\"Verifies all IPv4 security connections.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the IPv4 security connections are established in all vrf.\n * Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyIPSecConnHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip security connection vrf all\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyIPSecConnHealth.\"\"\"\n self.result.is_success()\n failure_conn = []\n command_output = self.instance_commands[0].json_output[\"connections\"]\n\n # Check if IP security connection is configured\n if not command_output:\n self.result.is_failure(\"No IPv4 security connection configured.\")\n return\n\n # Iterate over all ipsec connections\n for conn_data in command_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n failure_conn.append(f\"source:{source} destination:{destination} vrf:{vrf}\")\n if failure_conn:\n failure_msg = \"\\n\".join(failure_conn)\n self.result.is_failure(f\"The following IPv4 security connections are not established:\\n{failure_msg}.\")\n"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip access-lists {acl}\", revision=1)]\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) 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, acl in zip(self.instance_commands, self.inputs.ipv4_access_lists):\n # Collecting input ACL details\n acl_name = command_output.params.acl\n # Retrieve the expected entries from the inputs\n acl_entries = acl.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 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\", revision=1)]\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 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\", revision=1)]\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 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 try:\n line = next(line for line in command_output.split(\"\\n\") if line.startswith(\"SSHD status\"))\n except StopIteration:\n self.result.is_failure(\"Could not find SSH status in returned output.\")\n return\n status = line.split()[-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.VerifySpecificIPSecConn","title":"VerifySpecificIPSecConn","text":"Verifies the state of IPv4 security connections for a specified peer. It optionally allows for the verification of a specific path for a peer by providing source and destination addresses. If these addresses are not provided, it will verify all paths for the specified peer. Expected Results Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF. Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF. Examples anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n Source code in anta/tests/security.py class VerifySpecificIPSecConn(AntaTest):\n \"\"\"Verifies the state of IPv4 security connections for a specified peer.\n\n It optionally allows for the verification of a specific path for a peer by providing source and destination addresses.\n If these addresses are not provided, it will verify all paths for the specified peer.\n\n Expected Results\n ----------------\n * Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF.\n * Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n ```\n \"\"\"\n\n description = \"Verifies IPv4 security connections for a peer.\"\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip security connection vrf {vrf} path peer {peer}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificIPSecConn test.\"\"\"\n\n ip_security_connections: list[IPSecPeers]\n \"\"\"List of IP4v security peers.\"\"\"\n\n class IPSecPeers(BaseModel):\n \"\"\"Details of IPv4 security peers.\"\"\"\n\n peer: IPv4Address\n \"\"\"IPv4 address of the peer.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"Optional VRF for the IP security peer.\"\"\"\n\n connections: list[IPSecConn] | None = None\n \"\"\"Optional list of IPv4 security connections of a peer.\"\"\"\n\n class IPSecConn(BaseModel):\n \"\"\"Details of IPv4 security connections for a peer.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of the connection.\"\"\"\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of the connection.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input IP Sec connection.\"\"\"\n return [template.render(peer=conn.peer, vrf=conn.vrf) for conn in self.inputs.ip_security_connections]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificIPSecConn.\"\"\"\n self.result.is_success()\n for command_output, input_peer in zip(self.instance_commands, self.inputs.ip_security_connections):\n conn_output = command_output.json_output[\"connections\"]\n peer = command_output.params.peer\n vrf = command_output.params.vrf\n conn_input = input_peer.connections\n\n # Check if IPv4 security connection is configured\n if not conn_output:\n self.result.is_failure(f\"No IPv4 security connection configured for peer `{peer}`.\")\n continue\n\n # If connection details are not provided then check all connections of a peer\n if conn_input is None:\n for conn_data in conn_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source} destination:{destination} vrf:{vrf}` for peer `{peer}` is `Established` \"\n f\"but found `{state}` instead.\"\n )\n continue\n\n # Create a dictionary of existing connections for faster lookup\n existing_connections = {\n (conn_data.get(\"saddr\"), conn_data.get(\"daddr\"), conn_data.get(\"tunnelNs\")): next(iter(conn_data[\"pathDict\"].values()))\n for conn_data in conn_output.values()\n }\n for connection in conn_input:\n source_input = str(connection.source_address)\n destination_input = str(connection.destination_address)\n\n if (source_input, destination_input, vrf) in existing_connections:\n existing_state = existing_connections[(source_input, destination_input, vrf)]\n if existing_state != \"Established\":\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` \"\n f\"for peer `{peer}` is `Established` but found `{existing_state}` instead.\"\n )\n else:\n self.result.is_failure(\n f\"IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` for peer `{peer}` is not found.\"\n )\n"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"Inputs","text":"Name Type Description Default ip_security_connections list[IPSecPeers] List of IP4v security peers. -"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecPeers","text":"Name Type Description Default peer IPv4Address IPv4 address of the peer. - vrf str Optional VRF for the IP security peer. 'default' connections list[IPSecConn] | None Optional list of IPv4 security connections of a peer. None"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecConn","text":"Name Type Description Default source_address IPv4Address Source IPv4 address of the connection. - destination_address IPv4Address Destination IPv4 address of the connection. -"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management telnet\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=1)]\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. This test performs the following checks for each specified DNS Server: Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF. Ensuring an appropriate priority level. 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 any of the following conditions are met: The provided DNS server is not configured. The provided DNS server with designated VRF and priority does not match the expected information. 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 This test performs the following checks for each specified DNS Server:\n\n 1. Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF.\n 2. Ensuring an appropriate priority level.\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 any of the following conditions are met:\n - The provided DNS server is not configured.\n - The provided DNS server with designated VRF and priority does not match the expected information.\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 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\", revision=1)]\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 DnsServer: ClassVar[type[DnsServer]] = DnsServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyDNSServers.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"nameServerConfigs\"]\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 # Check if the DNS server is configured with specified VRF.\n if (output := get_dict_superset(command_output, input_dict)) is None:\n self.result.is_failure(f\"{server} - Not configured\")\n continue\n\n # Check if the DNS server priority matches with expected.\n if output[\"priority\"] != priority:\n self.result.is_failure(f\"{server} - Incorrect priority; Priority: {output['priority']}\")\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.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 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 categories: ClassVar[list[str]] = [\"services\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hostname\", revision=1)]\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.services/#input-models","title":"Input models","text":""},{"location":"api/tests.services/#anta.input_models.services.DnsServer","title":"DnsServer","text":"Model for a DNS server configuration. Name Type Description Default server_address IPv4Address | IPv6Address The IPv4 or IPv6 address of the DNS server. - vrf str The VRF instance in which the DNS server resides. Defaults to 'default'. 'default' priority int The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest. Field(ge=0, le=4) Source code in anta/input_models/services.py class DnsServer(BaseModel):\n \"\"\"Model for a DNS server configuration.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: IPv4Address | IPv6Address\n \"\"\"The IPv4 or IPv6 address of the DNS server.\"\"\"\n vrf: str = \"default\"\n \"\"\"The VRF instance in which the DNS server resides. Defaults to 'default'.\"\"\"\n priority: int = Field(ge=0, le=4)\n \"\"\"The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the DnsServer for reporting.\n\n Examples\n --------\n Server 10.0.0.1 (VRF: default, Priority: 1)\n \"\"\"\n return f\"Server {self.server_address} (VRF: {self.vrf}, Priority: {self.priority})\"\n"},{"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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP contact is configured.\n if not (contact := get_value(self.instance_commands[0].json_output, \"contact.contact\")):\n self.result.is_failure(\"SNMP contact is not configured.\")\n return\n\n # Verifies the expected SNMP contact.\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.VerifySnmpErrorCounters","title":"VerifySnmpErrorCounters","text":"Verifies the SNMP error counters. By default, all error counters will be checked for any non-zero values. An optional list of specific error counters can be provided for granular testing. Expected Results Success: The test will pass if the SNMP error counter(s) are zero/None. Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured. Examples ```yaml anta.tests.snmp: - VerifySnmpErrorCounters: error_counters: - inVersionErrs - inBadCommunityNames Source code in anta/tests/snmp.py class VerifySnmpErrorCounters(AntaTest):\n \"\"\"Verifies the SNMP error counters.\n\n By default, all error counters will be checked for any non-zero values.\n An optional list of specific error counters can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP error counter(s) are zero/None.\n * Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpErrorCounters:\n error_counters:\n - inVersionErrs\n - inBadCommunityNames\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpErrorCounters test.\"\"\"\n\n error_counters: list[SnmpErrorCounter] | None = None\n \"\"\"Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpErrorCounters.\"\"\"\n error_counters = self.inputs.error_counters\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (snmp_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP error counters not provided, It will check all the error counters.\n if not error_counters:\n error_counters = list(get_args(SnmpErrorCounter))\n\n error_counters_not_ok = {counter: value for counter in error_counters if (value := snmp_counters.get(counter))}\n\n # Check if any failures\n if not error_counters_not_ok:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP error counters are not found or have non-zero error counters:\\n{error_counters_not_ok}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpErrorCounters-attributes","title":"Inputs","text":"Name Type Description Default error_counters list[SnmpErrorCounter] | None Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters. None"},{"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 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\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP location is configured.\n if not (location := get_value(self.instance_commands[0].json_output, \"location.location\")):\n self.result.is_failure(\"SNMP location is not configured.\")\n return\n\n # Verifies the expected SNMP location.\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.VerifySnmpPDUCounters","title":"VerifySnmpPDUCounters","text":"Verifies the SNMP PDU counters. By default, all SNMP PDU counters will be checked for any non-zero values. An optional list of specific SNMP PDU(s) can be provided for granular testing. Expected Results Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero. Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found. Examples anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n Source code in anta/tests/snmp.py class VerifySnmpPDUCounters(AntaTest):\n \"\"\"Verifies the SNMP PDU counters.\n\n By default, all SNMP PDU counters will be checked for any non-zero values.\n An optional list of specific SNMP PDU(s) can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero.\n * Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpPDUCounters test.\"\"\"\n\n pdus: list[SnmpPdu] | None = None\n \"\"\"Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpPDUCounters.\"\"\"\n snmp_pdus = self.inputs.pdus\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (pdu_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP PDUs not provided, It will check all the update error counters.\n if not snmp_pdus:\n snmp_pdus = list(get_args(SnmpPdu))\n\n failures = {pdu: value for pdu in snmp_pdus if (value := pdu_counters.get(pdu, \"Not Found\")) == \"Not Found\" or value == 0}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP PDU counters are not found or have zero PDU counters:\\n{failures}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpPDUCounters-attributes","title":"Inputs","text":"Name Type Description Default pdus list[SnmpPdu] | None Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters. None"},{"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 description = \"Verifies if the SNMP agent is enabled.\"\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show extensions\", revision=2),\n AntaCommand(command=\"show boot-extensions\", revision=1),\n ]\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 description = \"Verifies the EOS version of the device.\"\n categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree blockedports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree counters\", revision=1)]\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 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\", revision=1)]\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 vlan_id = command.params.vlan\n if not (topologies := get_value(command.json_output, \"topologies\")):\n not_configured.append(vlan_id)\n else:\n interfaces_not_forwarding = []\n for value in topologies.values():\n if vlan_id and 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 forwarding 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show spanning-tree vlan {vlan}\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree root detail\", revision=1)]\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.stp/#anta.tests.stp.VerifyStpTopologyChanges","title":"VerifyStpTopologyChanges","text":"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold. Expected Results Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold. Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold, indicating potential instability in the topology. Examples anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n Source code in anta/tests/stp.py class VerifyStpTopologyChanges(AntaTest):\n \"\"\"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold.\n * Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold,\n indicating potential instability in the topology.\n\n Examples\n --------\n ```yaml\n anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree topology status detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStpTopologyChanges test.\"\"\"\n\n threshold: int\n \"\"\"The threshold number of changes in the STP topology.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStpTopologyChanges.\"\"\"\n failures: dict[str, Any] = {\"topologies\": {}}\n\n command_output = self.instance_commands[0].json_output\n stp_topologies = command_output.get(\"topologies\", {})\n\n # verifies all available topologies except the \"NoStp\" topology.\n stp_topologies.pop(\"NoStp\", None)\n\n # Verify the STP topology(s).\n if not stp_topologies:\n self.result.is_failure(\"STP is not configured.\")\n return\n\n # Verifies the number of changes across all interfaces\n for topology, topology_details in stp_topologies.items():\n interfaces = {\n interface: {\"Number of changes\": num_of_changes}\n for interface, details in topology_details.get(\"interfaces\", {}).items()\n if (num_of_changes := details.get(\"numChanges\")) > self.inputs.threshold\n }\n if interfaces:\n failures[\"topologies\"][topology] = interfaces\n\n if failures[\"topologies\"]:\n self.result.is_failure(f\"The following STP topologies are not configured or number of changes not within the threshold:\\n{failures}\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.stp/#anta.tests.stp.VerifyStpTopologyChanges-attributes","title":"Inputs","text":"Name Type Description Default threshold int The threshold number of changes in the STP topology. -"},{"location":"api/tests.stun/","title":"STUN","text":""},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient","title":"VerifyStunClient","text":"Verifies STUN client settings, including local IP/port and optionally public IP/port. Expected Results Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port. Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect. Examples anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n Source code in anta/tests/stun.py class VerifyStunClient(AntaTest):\n \"\"\"Verifies STUN client settings, including local IP/port and optionally public IP/port.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port.\n * Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show stun client translations {source_address} {source_port}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStunClient test.\"\"\"\n\n stun_clients: list[ClientAddress]\n\n class ClientAddress(BaseModel):\n \"\"\"Source and public address/port details of STUN client.\"\"\"\n\n source_address: IPv4Address\n \"\"\"IPv4 source address of STUN client.\"\"\"\n source_port: Port = 4500\n \"\"\"Source port number for STUN client.\"\"\"\n public_address: IPv4Address | None = None\n \"\"\"Optional IPv4 public address of STUN client.\"\"\"\n public_port: Port | None = None\n \"\"\"Optional public port number for STUN client.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each STUN translation.\"\"\"\n return [template.render(source_address=client.source_address, source_port=client.source_port) for client in self.inputs.stun_clients]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunClient.\"\"\"\n self.result.is_success()\n\n # Iterate over each command output and corresponding client input\n for command, client_input in zip(self.instance_commands, self.inputs.stun_clients):\n bindings = command.json_output[\"bindings\"]\n source_address = str(command.params.source_address)\n source_port = command.params.source_port\n\n # If no bindings are found for the STUN client, mark the test as a failure and continue with the next client\n if not bindings:\n self.result.is_failure(f\"STUN client transaction for source `{source_address}:{source_port}` is not found.\")\n continue\n\n # Extract the public address and port from the client input\n public_address = client_input.public_address\n public_port = client_input.public_port\n\n # Extract the transaction ID from the bindings\n transaction_id = next(iter(bindings.keys()))\n\n # Prepare the actual and expected STUN data for comparison\n actual_stun_data = {\n \"source ip\": get_value(bindings, f\"{transaction_id}.sourceAddress.ip\"),\n \"source port\": get_value(bindings, f\"{transaction_id}.sourceAddress.port\"),\n }\n expected_stun_data = {\"source ip\": source_address, \"source port\": source_port}\n\n # If public address is provided, add it to the actual and expected STUN data\n if public_address is not None:\n actual_stun_data[\"public ip\"] = get_value(bindings, f\"{transaction_id}.publicAddress.ip\")\n expected_stun_data[\"public ip\"] = str(public_address)\n\n # If public port is provided, add it to the actual and expected STUN data\n if public_port is not None:\n actual_stun_data[\"public port\"] = get_value(bindings, f\"{transaction_id}.publicAddress.port\")\n expected_stun_data[\"public port\"] = public_port\n\n # If the actual STUN data does not match the expected STUN data, mark the test as failure\n if actual_stun_data != expected_stun_data:\n failed_log = get_failed_logs(expected_stun_data, actual_stun_data)\n self.result.is_failure(f\"For STUN source `{source_address}:{source_port}`:{failed_log}\")\n"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"ClientAddress","text":"Name Type Description Default source_address IPv4Address IPv4 source address of STUN client. - source_port Port Source port number for STUN client. 4500 public_address IPv4Address | None Optional IPv4 public address of STUN client. None public_port Port | None Optional public port number for STUN client. None"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunServer","title":"VerifyStunServer","text":"Verifies the STUN server status is enabled and running. Expected Results Success: The test will pass if the STUN server status is enabled and running. Failure: The test will fail if the STUN server is disabled or not running. Examples anta.tests.stun:\n - VerifyStunServer:\n Source code in anta/tests/stun.py class VerifyStunServer(AntaTest):\n \"\"\"Verifies the STUN server status is enabled and running.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN server status is enabled and running.\n * Failure: The test will fail if the STUN server is disabled or not running.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunServer:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show stun server status\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunServer.\"\"\"\n command_output = self.instance_commands[0].json_output\n status_disabled = not command_output.get(\"enabled\")\n not_running = command_output.get(\"pid\") == 0\n\n if status_disabled and not_running:\n self.result.is_failure(\"STUN server status is disabled and not running.\")\n elif status_disabled:\n self.result.is_failure(\"STUN server status is disabled.\")\n elif not_running:\n self.result.is_failure(\"STUN server is not running.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.system/","title":"System","text":""},{"location":"api/tests.system/#tests","title":"Tests","text":""},{"location":"api/tests.system/#anta.tests.system.VerifyAgentLogs","title":"VerifyAgentLogs","text":"Verifies there are no agent crash reports. 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 there are no agent crash reports.\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show processes top once\", revision=1)]\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 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\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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.VerifyNTPAssociations","title":"VerifyNTPAssociations","text":"Verifies the Network Time Protocol (NTP) associations. Expected Results Success: The test will pass if the Primary NTP server (marked as preferred) has the condition \u2018sys.peer\u2019 and all other NTP servers have the condition \u2018candidate\u2019. Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition \u2018sys.peer\u2019 or if any other NTP server does not have the condition \u2018candidate\u2019. Examples anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n Source code in anta/tests/system.py class VerifyNTPAssociations(AntaTest):\n \"\"\"Verifies the Network Time Protocol (NTP) associations.\n\n Expected Results\n ----------------\n * Success: The test will pass if the Primary NTP server (marked as preferred) has the condition 'sys.peer' and\n all other NTP servers have the condition 'candidate'.\n * Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition 'sys.peer' or\n if any other NTP server does not have the condition 'candidate'.\n\n Examples\n --------\n ```yaml\n anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ntp associations\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyNTPAssociations test.\"\"\"\n\n ntp_servers: list[NTPServer]\n \"\"\"List of NTP servers.\"\"\"\n NTPServer: ClassVar[type[NTPServer]] = NTPServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyNTPAssociations.\"\"\"\n self.result.is_success()\n\n if not (peers := get_value(self.instance_commands[0].json_output, \"peers\")):\n self.result.is_failure(\"No NTP peers configured\")\n return\n\n # Iterate over each NTP server.\n for ntp_server in self.inputs.ntp_servers:\n server_address = str(ntp_server.server_address)\n\n # We check `peerIpAddr` in the peer details - covering IPv4Address input, or the peer key - covering Hostname input.\n matching_peer = next((peer for peer, peer_details in peers.items() if (server_address in {peer_details[\"peerIpAddr\"], peer})), None)\n\n if not matching_peer:\n self.result.is_failure(f\"{ntp_server} - Not configured\")\n continue\n\n # Collecting the expected/actual NTP peer details.\n exp_condition = \"sys.peer\" if ntp_server.preferred else \"candidate\"\n exp_stratum = ntp_server.stratum\n act_condition = get_value(peers[matching_peer], \"condition\")\n act_stratum = get_value(peers[matching_peer], \"stratumLevel\")\n\n if act_condition != exp_condition or act_stratum != exp_stratum:\n self.result.is_failure(f\"{ntp_server} - Bad association; Condition: {act_condition}, Stratum: {act_stratum}\")\n"},{"location":"api/tests.system/#anta.tests.system.VerifyNTPAssociations-attributes","title":"Inputs","text":"Name Type Description Default ntp_servers list[NTPServer] List of NTP servers. -"},{"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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show reload cause\", revision=1)]\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 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 description = \"Verifies the device uptime.\"\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show uptime\", revision=1)]\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.system/#input-models","title":"Input models","text":""},{"location":"api/tests.system/#anta.input_models.system.NTPServer","title":"NTPServer","text":"Model for a NTP server. Name Type Description Default server_address Hostname | IPv4Address The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name. For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output. - preferred bool Optional preferred for NTP server. If not provided, it defaults to `False`. False stratum int NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized. Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state. Field(ge=0, le=16) Source code in anta/input_models/system.py class NTPServer(BaseModel):\n \"\"\"Model for a NTP server.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: Hostname | IPv4Address\n \"\"\"The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration\n of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name.\n For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output.\"\"\"\n preferred: bool = False\n \"\"\"Optional preferred for NTP server. If not provided, it defaults to `False`.\"\"\"\n stratum: int = Field(ge=0, le=16)\n \"\"\"NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized.\n Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Representation of the NTPServer model.\"\"\"\n return f\"{self.server_address} (Preferred: {self.preferred}, Stratum: {self.stratum})\"\n"},{"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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\", revision=1)]\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 description = \"Verifies the Vxlan1 interface status.\"\n categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 there are no VXLAN config-sanity inconsistencies. 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 there are no VXLAN config-sanity inconsistencies.\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan config-sanity\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vni\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vtep\", revision=1)]\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[\n str, AfterValidator(aaa_group_prefix)\n]\n"},{"location":"api/types/#anta.custom_types.Afi","title":"Afi module-attribute","text":"Afi = Literal[\n \"ipv4\",\n \"ipv6\",\n \"vpn-ipv4\",\n \"vpn-ipv6\",\n \"evpn\",\n \"rt-membership\",\n \"path-selection\",\n \"link-state\",\n]\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.BfdProtocol","title":"BfdProtocol module-attribute","text":"BfdProtocol = Literal[\n \"bgp\",\n \"isis\",\n \"lag\",\n \"ospf\",\n \"ospfv3\",\n \"pim\",\n \"route-input\",\n \"static-bfd\",\n \"static-route\",\n \"vrrp\",\n \"vxlan\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpDropStats","title":"BgpDropStats module-attribute","text":"BgpDropStats = Literal[\n \"inDropAsloop\",\n \"inDropClusterIdLoop\",\n \"inDropMalformedMpbgp\",\n \"inDropOrigId\",\n \"inDropNhLocal\",\n \"inDropNhAfV6\",\n \"prefixDroppedMartianV4\",\n \"prefixDroppedMaxRouteLimitViolatedV4\",\n \"prefixDroppedMartianV6\",\n \"prefixDroppedMaxRouteLimitViolatedV6\",\n \"prefixLuDroppedV4\",\n \"prefixLuDroppedMartianV4\",\n \"prefixLuDroppedMaxRouteLimitViolatedV4\",\n \"prefixLuDroppedV6\",\n \"prefixLuDroppedMartianV6\",\n \"prefixLuDroppedMaxRouteLimitViolatedV6\",\n \"prefixEvpnDroppedUnsupportedRouteType\",\n \"prefixBgpLsDroppedReceptionUnsupported\",\n \"outDropV4LocalAddr\",\n \"outDropV6LocalAddr\",\n \"prefixVpnIpv4DroppedImportMatchFailure\",\n \"prefixVpnIpv4DroppedMaxRouteLimitViolated\",\n \"prefixVpnIpv6DroppedImportMatchFailure\",\n \"prefixVpnIpv6DroppedMaxRouteLimitViolated\",\n \"prefixEvpnDroppedImportMatchFailure\",\n \"prefixEvpnDroppedMaxRouteLimitViolated\",\n \"prefixRtMembershipDroppedLocalAsReject\",\n \"prefixRtMembershipDroppedMaxRouteLimitViolated\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpUpdateError","title":"BgpUpdateError module-attribute","text":"BgpUpdateError = Literal[\n \"inUpdErrWithdraw\",\n \"inUpdErrIgnore\",\n \"inUpdErrDisableAfiSafi\",\n \"disabledAfiSafi\",\n \"lastUpdErrTime\",\n]\n"},{"location":"api/types/#anta.custom_types.EcdsaKeySize","title":"EcdsaKeySize module-attribute","text":"EcdsaKeySize = Literal[256, 384, 512]\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[\n \"acl\",\n \"arp-inspection\",\n \"bpduguard\",\n \"dot1x-session-replace\",\n \"hitless-reload-down\",\n \"lacp-rate-limit\",\n \"link-flap\",\n \"no-internal-vlan\",\n \"portchannelguard\",\n \"portsec\",\n \"tapagg\",\n \"uplink-failure-detection\",\n]\n"},{"location":"api/types/#anta.custom_types.EthernetInterface","title":"EthernetInterface module-attribute","text":"EthernetInterface = Annotated[\n str,\n Field(pattern=\"^Ethernet[0-9]+(\\\\/[0-9]+)*$\"),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\n"},{"location":"api/types/#anta.custom_types.Hostname","title":"Hostname module-attribute","text":"Hostname = Annotated[\n str, Field(pattern=REGEXP_TYPE_HOSTNAME)\n]\n"},{"location":"api/types/#anta.custom_types.Interface","title":"Interface module-attribute","text":"Interface = Annotated[\n str,\n Field(pattern=REGEXP_TYPE_EOS_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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[\n str,\n BeforeValidator(\n bgp_multiprotocol_capabilities_abbreviations\n ),\n]\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.PortChannelInterface","title":"PortChannelInterface module-attribute","text":"PortChannelInterface = Annotated[\n str,\n Field(pattern=REGEX_TYPE_PORTCHANNEL),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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.REGEXP_BGP_IPV4_MPLS_LABELS","title":"REGEXP_BGP_IPV4_MPLS_LABELS module-attribute","text":"REGEXP_BGP_IPV4_MPLS_LABELS = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?label(s)?)\\\\b\"\n)\n Match IPv4 MPLS Labels."},{"location":"api/types/#anta.custom_types.REGEXP_BGP_L2VPN_AFI","title":"REGEXP_BGP_L2VPN_AFI module-attribute","text":"REGEXP_BGP_L2VPN_AFI = \"\\\\b(l2[\\\\s\\\\-]?vpn[\\\\s\\\\-]?evpn)\\\\b\"\n Match L2VPN EVPN AFI."},{"location":"api/types/#anta.custom_types.REGEXP_EOS_BLACKLIST_CMDS","title":"REGEXP_EOS_BLACKLIST_CMDS module-attribute","text":"REGEXP_EOS_BLACKLIST_CMDS = [\n \"^reload.*\",\n \"^conf\\\\w*\\\\s*(terminal|session)*\",\n \"^wr\\\\w*\\\\s*\\\\w+\",\n]\n List of regular expressions to blacklist from eos commands."},{"location":"api/types/#anta.custom_types.REGEXP_INTERFACE_ID","title":"REGEXP_INTERFACE_ID module-attribute","text":"REGEXP_INTERFACE_ID = '\\\\d+(\\\\/\\\\d+)*(\\\\.\\\\d+)?'\n Match Interface ID lilke 1/1.1."},{"location":"api/types/#anta.custom_types.REGEXP_PATH_MARKERS","title":"REGEXP_PATH_MARKERS module-attribute","text":"REGEXP_PATH_MARKERS = '[\\\\\\\\\\\\/\\\\s]'\n Match directory path from string."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_EOS_INTERFACE","title":"REGEXP_TYPE_EOS_INTERFACE module-attribute","text":"REGEXP_TYPE_EOS_INTERFACE = \"^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\\\\/[0-9]+)*(\\\\.[0-9]+)?$\"\n Match EOS interface types like Ethernet1/1, Vlan1, Loopback1, etc."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_HOSTNAME","title":"REGEXP_TYPE_HOSTNAME module-attribute","text":"REGEXP_TYPE_HOSTNAME = \"^(([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 Match hostname like my-hostname, my-hostname-1, my-hostname-1-2."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_VXLAN_SRC_INTERFACE","title":"REGEXP_TYPE_VXLAN_SRC_INTERFACE module-attribute","text":"REGEXP_TYPE_VXLAN_SRC_INTERFACE = \"^(Loopback)([0-9]|[1-9][0-9]{1,2}|[1-7][0-9]{3}|8[01][0-9]{2}|819[01])$\"\n Match Vxlan source interface like Loopback10."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_MPLS_VPN","title":"REGEX_BGP_IPV4_MPLS_VPN module-attribute","text":"REGEX_BGP_IPV4_MPLS_VPN = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?vpn)\\\\b\"\n)\n Match IPv4 MPLS VPN."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_UNICAST","title":"REGEX_BGP_IPV4_UNICAST module-attribute","text":"REGEX_BGP_IPV4_UNICAST = (\n \"\\\\b(ipv4[\\\\s\\\\-]?uni[\\\\s\\\\-]?cast)\\\\b\"\n)\n Match IPv4 Unicast."},{"location":"api/types/#anta.custom_types.REGEX_TYPE_PORTCHANNEL","title":"REGEX_TYPE_PORTCHANNEL module-attribute","text":"REGEX_TYPE_PORTCHANNEL = '^Port-Channel[0-9]{1,6}$'\n Match Port Channel interface like Port-Channel5."},{"location":"api/types/#anta.custom_types.RegexString","title":"RegexString module-attribute","text":"RegexString = Annotated[str, AfterValidator(validate_regex)]\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[\n \"unicast\", \"multicast\", \"labeled-unicast\", \"sr-te\"\n]\n"},{"location":"api/types/#anta.custom_types.SnmpErrorCounter","title":"SnmpErrorCounter module-attribute","text":"SnmpErrorCounter = Literal[\n \"inVersionErrs\",\n \"inBadCommunityNames\",\n \"inBadCommunityUses\",\n \"inParseErrs\",\n \"outTooBigErrs\",\n \"outNoSuchNameErrs\",\n \"outBadValueErrs\",\n \"outGeneralErrs\",\n]\n"},{"location":"api/types/#anta.custom_types.SnmpPdu","title":"SnmpPdu module-attribute","text":"SnmpPdu = Literal[\n \"inGetPdus\",\n \"inGetNextPdus\",\n \"inSetPdus\",\n \"outGetResponsePdus\",\n \"outTrapPdus\",\n]\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[\n str,\n Field(pattern=REGEXP_TYPE_VXLAN_SRC_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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(\n value: str,\n) -> str\n Abbreviations for different BGP multiprotocol capabilities. Examples IPv4 Unicast L2vpnEVPN ipv4 MPLS Labels ipv4Mplsvpn 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 REGEXP_BGP_L2VPN_AFI: \"l2VpnEvpn\",\n REGEXP_BGP_IPV4_MPLS_LABELS: \"ipv4MplsLabels\",\n REGEX_BGP_IPV4_MPLS_VPN: \"ipv4MplsVpn\",\n REGEX_BGP_IPV4_UNICAST: \"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(REGEXP_INTERFACE_ID)\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 return next((f\"{full_name}{intf_id}\" for alias, full_name in alias_map.items() if v.lower().startswith(alias)), 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 vlan -> Vlan loopback -> Loopback 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 v != \"\" and not v[0].isupper():\n return f\"{v[0].upper()}{v[1:]}\"\n return v\n"},{"location":"api/types/#anta.custom_types.validate_regex","title":"validate_regex","text":"validate_regex(value: str) -> str\n Validate that the input value is a valid regex format. Source code in anta/custom_types.py def validate_regex(value: str) -> str:\n \"\"\"Validate that the input value is a valid regex format.\"\"\"\n try:\n re.compile(value)\n except re.error as e:\n msg = f\"Invalid regex: {e}\"\n raise ValueError(msg) from e\n return value\n"},{"location":"cli/check/","title":"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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n --help Show this message and exit.\n"},{"location":"cli/debug/","title":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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":"Usage: 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.\n\n 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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"},{"location":"cli/debug/#example-of-multiple-arguments","title":"Example of multiple arguments","text":"Warning If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters. 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":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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_2024-04-09_15_56_19]\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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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 [DEPRECATED] Ensure devices have 'aaa authorization\n exec default local' configured (required for SCP on\n EOS). THIS WILL CHANGE THE CONFIGURATION OF YOUR\n 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. Warning ANTA can automatically configure aaa authorization exec default local using the anta exec collect-tech-support --configure option but this option is deprecated and will be removed in ANTA 2.0.0. 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":"The ANTA CLI offers multiple commands to access data from your local inventory."},{"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 (visit this page to learn more about tags). The --connected option allows to display only the devices where a connection has been established."},{"location":"cli/get-inventory-information/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 By default, anta get inventory only provides information that doesn\u2019t rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, use the --connected option."},{"location":"cli/get-inventory-information/#example","title":"Example","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 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={'DC1-SPINE1', 'DC1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC1-SPINE2': AsyncEOSDevice(\n name='DC1-SPINE2',\n tags={'DC1', 'SPINE', 'DC1-SPINE2'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE1': AsyncEOSDevice(\n name='DC2-SPINE1',\n tags={'DC2', 'DC2-SPINE1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE2': AsyncEOSDevice(\n name='DC2-SPINE2',\n tags={'DC2', 'DC2-SPINE2', 'SPINE'},\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=False,\n insecure=False\n )\n}\n"},{"location":"cli/inv-from-ansible/","title":"Inventory from Ansible","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\n NOTE: This command does not support inline vaulted variables. Make sure to\n comment them out.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var:\n ANTA_INVENTORY; required]\n --overwrite Do not prompt when overriding current inventory\n [env var: ANTA_GET_FROM_ANSIBLE_OVERWRITE]\n -g, --ansible-group TEXT Ansible group to filter\n --ansible-inventory FILE Path to your ansible inventory file to read\n [required]\n --help Show this message and exit.\n Warnings anta get from-ansible does not support inline vaulted variables, comment them out to generate your inventory. If the vaulted variable is necessary to build the inventory (e.g. ansible_host), it needs to be unvaulted for from-ansible command to work.\u201d The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritance 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: ---\nall:\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 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"},{"location":"cli/inv-from-cvp/","title":"Inventory from CVP","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. Info The current implementation only works with on-premises CloudVision instances, not with CloudVision as a Service (CVaaS)."},{"location":"cli/inv-from-cvp/#command-overview","title":"Command overview","text":"Usage: anta get from-cvp [OPTIONS]\n\n Build ANTA inventory from CloudVision.\n\n NOTE: Only username/password authentication is supported for on-premises CloudVision instances.\n Token authentication for both on-premises and CloudVision as a Service (CVaaS) is not supported.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var: ANTA_INVENTORY;\n required]\n --overwrite Do not prompt when overriding current inventory [env\n var: ANTA_GET_FROM_CVP_OVERWRITE]\n -host, --host TEXT CloudVision instance FQDN or IP [required]\n -u, --username TEXT CloudVision username [required]\n -p, --password TEXT CloudVision password [required]\n -c, --container TEXT CloudVision container where devices are configured\n --ignore-cert By default connection to CV will use HTTPS\n certificate, set this flag to disable it [env var:\n ANTA_GET_FROM_CVP_IGNORE_CERT]\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":"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 report Table report JSON report Custom template report CSV report Markdown report "},{"location":"cli/nrfu/#nrfu-command-overview","title":"NRFU Command overview","text":"Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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. Refer to the dedicated page for more information."},{"location":"cli/nrfu/#device-and-test-filtering","title":"Device and test filtering","text":"Options --device and --test can be used to target one or multiple devices and/or tests to run in your environment. The options can be repeated. Example: anta nrfu --device leaf1a --device leaf1b --test VerifyUptime --test VerifyReloadCause."},{"location":"cli/nrfu/#hide-results","title":"Hide results","text":"Option --hide can be used to hide test results in the output or report file based on their status. The option can be repeated. Example: anta nrfu --hide error --hide skipped."},{"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":"Usage: anta nrfu text [OPTIONS]\n\n ANTA command to check network states with text result.\n\nOptions:\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example","title":"Example","text":"anta nrfu --device DC1-LEAF1A text\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":"Usage: anta nrfu table [OPTIONS]\n\n ANTA command to check network states with table result.\n\nOptions:\n --group-by [device|test] Group result by test or device.\n --help Show this message and exit.\n 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 --device spine1 table\n anta nrfu --test VerifyZeroTouch table\n "},{"location":"cli/nrfu/#performing-nrfu-with-json-rendering","title":"Performing NRFU with JSON rendering","text":"The JSON rendering command in NRFU testing will generate an output of all test results in JSON format."},{"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 JSON 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. If specified, no output will be displayed in the terminal. This is useful for further processing or integration with other tools."},{"location":"cli/nrfu/#example_1","title":"Example","text":"anta nrfu --tags LEAF json\n"},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-csv-file","title":"Performing NRFU and saving results in a CSV file","text":"The csv command in NRFU testing is useful for generating a CSV file with all tests result. This file can be easily analyzed and filtered by operator for reporting purposes."},{"location":"cli/nrfu/#command-overview_3","title":"Command overview","text":"anta nrfu csv --help\nUsage: anta nrfu csv [OPTIONS]\n\n ANTA command to check network states with CSV result.\n\nOptions:\n --csv-output FILE Path to save report as a CSV file [env var:\n ANTA_NRFU_CSV_CSV_OUTPUT]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_2","title":"Example","text":""},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-markdown-file","title":"Performing NRFU and saving results in a Markdown file","text":"The md-report command in NRFU testing generates a comprehensive Markdown report containing various sections, including detailed statistics for devices and test categories."},{"location":"cli/nrfu/#command-overview_4","title":"Command overview","text":"anta nrfu md-report --help\n\nUsage: anta nrfu md-report [OPTIONS]\n\n ANTA command to check network state with Markdown report.\n\nOptions:\n --md-output FILE Path to save the report as a Markdown file [env var:\n ANTA_NRFU_MD_REPORT_MD_OUTPUT; required]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_3","title":"Example","text":""},{"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_5","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_4","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/nrfu/#dry-run-mode","title":"Dry-run mode","text":"It is possible to run anta nrfu --dry-run to execute ANTA up to the point where it should communicate with the network to execute the tests. When using --dry-run, all inventory devices are assumed to be online. This can be useful to check how many tests would be run using the catalog and inventory. "},{"location":"cli/overview/","title":"Overview","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."},{"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 selected inventory devices.\n"},{"location":"cli/overview/#anta-environment-variables","title":"ANTA environment 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_CATALOG=tests.yml\n Then, run the CLI without options: anta nrfu\n Note All environment variables may not be needed for every commands. Refer to <command> --help for the comprehensive environment variables names. Below are the environment 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":"ANTA uses tags to define test-to-device mappings (tests run on devices with matching tags) and the --tags CLI option acts as a filter to execute specific test/device combinations."},{"location":"cli/tag-management/#defining-tags","title":"Defining tags","text":""},{"location":"cli/tag-management/#device-tags","title":"Device tags","text":"Device tags can be defined in the inventory: anta_inventory:\n hosts:\n - name: leaf1\n host: leaf1.anta.arista.com\n tags: [\"leaf\"]\n - name: leaf2\n host: leaf2.anta.arista.com\n tags: [\"leaf\"]\n - name: spine1\n host: spine1.anta.arista.com\n tags: [\"spine\"]\n Each device also has its own name automatically added as a tag: $ anta get inventory\nCurrent inventory content is:\n{\n 'leaf1': AsyncEOSDevice(\n name='leaf1',\n tags={'leaf', 'leaf1'}, <--\n [...]\n host='leaf1.anta.arista.com',\n [...]\n ),\n 'leaf2': AsyncEOSDevice(\n name='leaf2',\n tags={'leaf', 'leaf2'}, <--\n [...]\n host='leaf2.anta.arista.com',\n [...]\n ),\n 'spine1': AsyncEOSDevice(\n name='spine1',\n tags={'spine1', 'spine'}, <--\n [...]\n host='spine1.anta.arista.com',\n [...]\n )\n}\n"},{"location":"cli/tag-management/#test-tags","title":"Test tags","text":"Tags can be defined in the test catalog to restrict tests to tagged devices: anta.tests.system:\n - VerifyUptime:\n minimum: 10\n filters:\n tags: ['spine']\n - VerifyUptime:\n minimum: 9\n filters:\n tags: ['leaf']\n - VerifyReloadCause:\n filters:\n tags: ['spine', 'leaf']\n - VerifyCoredump:\n - VerifyAgentLogs:\n - VerifyCPUUtilization:\n - VerifyMemoryUtilization:\n - VerifyFileSystemUtilization:\n - VerifyNTP:\n\nanta.tests.mlag:\n - VerifyMlagStatus:\n filters:\n tags: ['leaf']\n\nanta.tests.interfaces:\n - VerifyL3MTU:\n mtu: 1500\n filters:\n tags: ['spine']\n A tag used to filter a test can also be a device name Use different input values for a specific test Leverage tags to define different input values for a specific test. See the VerifyUptime example above."},{"location":"cli/tag-management/#using-tags","title":"Using tags","text":"Command Description No --tags option Run all tests on all devices according to the tag definitions in your inventory and test catalog. Tests without tags are executed on all devices. --tags leaf Run all tests marked with the leaf tag on all devices configured with the leaf tag. All other tests are ignored. --tags leaf,spine Run all tests marked with the leaf tag on all devices configured with the leaf tag.Run all tests marked with the spine tag on all devices configured with the spine tag. All other tests are ignored."},{"location":"cli/tag-management/#examples","title":"Examples","text":"The following examples use the inventory and test catalog defined above."},{"location":"cli/tag-management/#no-tags-option","title":"No --tags option","text":"Tests without tags are run on all devices. Tests with tags will only run on devices with matching tags. $ anta nrfu table --group-by device\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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 27\n---------------------------------\n Summary per device\n\u250f\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\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 leaf1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 leaf2 \u2502 7 \u2502 1 \u2502 1 \u2502 0 \u2502 VerifyAgentLogs \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 spine1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u2514\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/#single-tag","title":"Single tag","text":"With a tag specified, only tests matching this tag will be run on matching devices. $ anta nrfu --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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (2 established)\nTotal number of selected tests: 6\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\n In this case, only leaf devices defined in the inventory are used to run tests marked with the leaf in the test catalog."},{"location":"cli/tag-management/#multiple-tags","title":"Multiple tags","text":"It is possible to use multiple tags using the --tags tag1,tag2 syntax. $ anta nrfu --tags leaf,spine 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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 15\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyL3MTU :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyL3MTU :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyReloadCause :: SUCCESS\nspine1 :: VerifyMlagStatus :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyL3MTU :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\n"},{"location":"cli/tag-management/#obtaining-all-configured-tags","title":"Obtaining all configured tags","text":"As most ANTA commands accommodate tag filtering, this command is useful for enumerating all tags configured in the inventory. Running the anta get tags command will return a list of all tags configured in the inventory."},{"location":"cli/tag-management/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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/tag-management/#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 \"leaf\",\n \"leaf1\",\n \"leaf2\",\n \"spine\",\n \"spine1\"\n]\n"}]} \ No newline at end of file diff --git a/main/sitemap.xml.gz b/main/sitemap.xml.gz index cf5ee0d0e..74b57bcd3 100644 Binary files a/main/sitemap.xml.gz and b/main/sitemap.xml.gz differ diff --git a/main/troubleshooting/index.html b/main/troubleshooting/index.html index d8d155e45..d8db962e8 100644 --- a/main/troubleshooting/index.html +++ b/main/troubleshooting/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2418,7 +2418,7 @@ Troubleshooting on EOS - + @@ -2429,7 +2429,7 @@ Troubleshooting on EOS - + @@ -2440,7 +2440,7 @@ Troubleshooting on EOS - + diff --git a/main/usage-inventory-catalog/index.html b/main/usage-inventory-catalog/index.html index e066d66c8..272bea518 100644 --- a/main/usage-inventory-catalog/index.html +++ b/main/usage-inventory-catalog/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2879,7 +2879,7 @@ Example script to merge catalogs - + @@ -2890,7 +2890,7 @@ Example script to merge catalogs - + @@ -2901,7 +2901,7 @@ Example script to merge catalogs - +
anta/tests/field_notices.py
127 + 123 +124 +125 +126 +127 128 129 130 @@ -2666,11 +2662,7 @@ 190 191 192 -193 -194 -195 -196 -197class VerifyFieldNotice72Resolution(AntaTest): +193class VerifyFieldNotice72Resolution(AntaTest): """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 @@ -2834,7 +2826,7 @@ - + @@ -2845,7 +2837,7 @@ - + @@ -2856,7 +2848,7 @@ - + diff --git a/main/api/tests.flow_tracking/index.html b/main/api/tests.flow_tracking/index.html index 0a196333b..97726c936 100644 --- a/main/api/tests.flow_tracking/index.html +++ b/main/api/tests.flow_tracking/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3322,7 +3322,7 @@ - + @@ -3333,7 +3333,7 @@ - + @@ -3344,7 +3344,7 @@ - + diff --git a/main/api/tests.greent/index.html b/main/api/tests.greent/index.html index b1ea4a557..6b86338ef 100644 --- a/main/api/tests.greent/index.html +++ b/main/api/tests.greent/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2593,7 +2593,7 @@ - + @@ -2604,7 +2604,7 @@ - + @@ -2615,7 +2615,7 @@ - + diff --git a/main/api/tests.hardware/index.html b/main/api/tests.hardware/index.html index 3762debb9..1ce9ab109 100644 --- a/main/api/tests.hardware/index.html +++ b/main/api/tests.hardware/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3611,7 +3611,7 @@ - + @@ -3622,7 +3622,7 @@ - + @@ -3633,7 +3633,7 @@ - + diff --git a/main/api/tests.interfaces/index.html b/main/api/tests.interfaces/index.html index 2d767ea9f..d2162dfa9 100644 --- a/main/api/tests.interfaces/index.html +++ b/main/api/tests.interfaces/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -6967,7 +6967,7 @@ - + @@ -6978,7 +6978,7 @@ - + @@ -6989,7 +6989,7 @@ - + diff --git a/main/api/tests.lanz/index.html b/main/api/tests.lanz/index.html index 63ed4a717..bd63b81b9 100644 --- a/main/api/tests.lanz/index.html +++ b/main/api/tests.lanz/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2449,7 +2449,7 @@ - + @@ -2460,7 +2460,7 @@ - + @@ -2471,7 +2471,7 @@ - + diff --git a/main/api/tests.logging/index.html b/main/api/tests.logging/index.html index 307bf9315..5efda247a 100644 --- a/main/api/tests.logging/index.html +++ b/main/api/tests.logging/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4005,7 +4005,7 @@ - + @@ -4016,7 +4016,7 @@ - + @@ -4027,7 +4027,7 @@ - + diff --git a/main/api/tests.mlag/index.html b/main/api/tests.mlag/index.html index d8987f555..f2a748e95 100644 --- a/main/api/tests.mlag/index.html +++ b/main/api/tests.mlag/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3607,7 +3607,7 @@ - + @@ -3618,7 +3618,7 @@ - + @@ -3629,7 +3629,7 @@ - + diff --git a/main/api/tests.multicast/index.html b/main/api/tests.multicast/index.html index 84d512a14..ec75770d8 100644 --- a/main/api/tests.multicast/index.html +++ b/main/api/tests.multicast/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2767,7 +2767,7 @@ Inputs - + @@ -2778,7 +2778,7 @@ Inputs - + @@ -2789,7 +2789,7 @@ Inputs - + diff --git a/main/api/tests.path_selection/index.html b/main/api/tests.path_selection/index.html index 10781ddf6..1e996847e 100644 --- a/main/api/tests.path_selection/index.html +++ b/main/api/tests.path_selection/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2944,7 +2944,7 @@ RouterPath - + @@ -2955,7 +2955,7 @@ RouterPath - + @@ -2966,7 +2966,7 @@ RouterPath - + diff --git a/main/api/tests.profiles/index.html b/main/api/tests.profiles/index.html index ad7134d4f..d2e832279 100644 --- a/main/api/tests.profiles/index.html +++ b/main/api/tests.profiles/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2759,7 +2759,7 @@ Inputs< - + @@ -2770,7 +2770,7 @@ Inputs< - + @@ -2781,7 +2781,7 @@ Inputs< - + diff --git a/main/api/tests.ptp/index.html b/main/api/tests.ptp/index.html index 85ef5a5ff..73c2d28af 100644 --- a/main/api/tests.ptp/index.html +++ b/main/api/tests.ptp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3210,7 +3210,7 @@ - + @@ -3221,7 +3221,7 @@ - + @@ -3232,7 +3232,7 @@ - + diff --git a/main/api/tests.routing.bgp/index.html b/main/api/tests.routing.bgp/index.html index dca68a1fc..64de1e0b6 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -9227,7 +9227,7 @@ - + @@ -9238,7 +9238,7 @@ - + @@ -9249,7 +9249,7 @@ - + diff --git a/main/api/tests.routing.generic/index.html b/main/api/tests.routing.generic/index.html index a97dfb4fb..d80640fbf 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3190,7 +3190,7 @@ Inputs - + @@ -3201,7 +3201,7 @@ Inputs - + @@ -3212,7 +3212,7 @@ Inputs - + diff --git a/main/api/tests.routing.isis/index.html b/main/api/tests.routing.isis/index.html index 817e5f85b..1e2e5cf07 100644 --- a/main/api/tests.routing.isis/index.html +++ b/main/api/tests.routing.isis/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -5077,7 +5077,7 @@ Vias - + @@ -5088,7 +5088,7 @@ Vias - + @@ -5099,7 +5099,7 @@ Vias - + diff --git a/main/api/tests.routing.ospf/index.html b/main/api/tests.routing.ospf/index.html index e29e71dc0..db70676e7 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2858,7 +2858,7 @@ - + @@ -2869,7 +2869,7 @@ - + @@ -2880,7 +2880,7 @@ - + diff --git a/main/api/tests.security/index.html b/main/api/tests.security/index.html index a51964fbb..747288d15 100644 --- a/main/api/tests.security/index.html +++ b/main/api/tests.security/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -6422,7 +6422,7 @@ - + @@ -6433,7 +6433,7 @@ - + @@ -6444,7 +6444,7 @@ - + diff --git a/main/api/tests.services/index.html b/main/api/tests.services/index.html index 7b20d6acd..cc27eb5bb 100644 --- a/main/api/tests.services/index.html +++ b/main/api/tests.services/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3749,7 +3749,7 @@ - + @@ -3760,7 +3760,7 @@ - + @@ -3771,7 +3771,7 @@ - + diff --git a/main/api/tests.snmp/index.html b/main/api/tests.snmp/index.html index 3e6e32650..dc4d855b3 100644 --- a/main/api/tests.snmp/index.html +++ b/main/api/tests.snmp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4048,7 +4048,7 @@ Inputs - + @@ -4059,7 +4059,7 @@ Inputs - + @@ -4070,7 +4070,7 @@ Inputs - + diff --git a/main/api/tests.software/index.html b/main/api/tests.software/index.html index 549116e1b..8f7935376 100644 --- a/main/api/tests.software/index.html +++ b/main/api/tests.software/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2935,7 +2935,7 @@ Inputs - + @@ -2946,7 +2946,7 @@ Inputs - + @@ -2957,7 +2957,7 @@ Inputs - + diff --git a/main/api/tests.stp/index.html b/main/api/tests.stp/index.html index e5d902b6f..1024cd4a6 100644 --- a/main/api/tests.stp/index.html +++ b/main/api/tests.stp/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3696,7 +3696,7 @@ Inputs - + @@ -3707,7 +3707,7 @@ Inputs - + @@ -3718,7 +3718,7 @@ Inputs - + diff --git a/main/api/tests.stun/index.html b/main/api/tests.stun/index.html index 747f2d3a3..2ea88167b 100644 --- a/main/api/tests.stun/index.html +++ b/main/api/tests.stun/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2906,7 +2906,7 @@ - + @@ -2917,7 +2917,7 @@ - + @@ -2928,7 +2928,7 @@ - + diff --git a/main/api/tests.system/index.html b/main/api/tests.system/index.html index 406a5497b..6bfaa38b6 100644 --- a/main/api/tests.system/index.html +++ b/main/api/tests.system/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4154,7 +4154,7 @@ - + @@ -4165,7 +4165,7 @@ - + @@ -4176,7 +4176,7 @@ - + diff --git a/main/api/tests.vlan/index.html b/main/api/tests.vlan/index.html index 00feb26d7..0fd71d0dd 100644 --- a/main/api/tests.vlan/index.html +++ b/main/api/tests.vlan/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2594,7 +2594,7 @@ Inputs - + @@ -2605,7 +2605,7 @@ Inputs - + @@ -2616,7 +2616,7 @@ Inputs - + diff --git a/main/api/tests.vxlan/index.html b/main/api/tests.vxlan/index.html index b55df5d03..eccd46cb2 100644 --- a/main/api/tests.vxlan/index.html +++ b/main/api/tests.vxlan/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3435,7 +3435,7 @@ Inputs - + @@ -3446,7 +3446,7 @@ Inputs - + @@ -3457,7 +3457,7 @@ Inputs - + diff --git a/main/api/tests/index.html b/main/api/tests/index.html index 837937a96..486addd84 100644 --- a/main/api/tests/index.html +++ b/main/api/tests/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2356,7 +2356,7 @@ Using the Tests - + @@ -2367,7 +2367,7 @@ Using the Tests - + @@ -2378,7 +2378,7 @@ Using the Tests - + diff --git a/main/api/types/index.html b/main/api/types/index.html index 3fa39ceb0..ec45d8303 100644 --- a/main/api/types/index.html +++ b/main/api/types/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -4867,7 +4867,7 @@ - + @@ -4878,7 +4878,7 @@ - + @@ -4889,7 +4889,7 @@ - + diff --git a/main/assets/stylesheets/main.0253249f.min.css b/main/assets/stylesheets/main.0253249f.min.css deleted file mode 100644 index 9a7a6982d..000000000 --- a/main/assets/stylesheets/main.0253249f.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}.md-typeset ul[type]{list-style-type:revert-layer}[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}.md-typeset ol ol ol ol,.md-typeset ul ol ol ol{list-style-type:upper-alpha}.md-typeset ol ol ol ol ol,.md-typeset ul ol ol ol ol{list-style-type:upper-roman}.md-typeset ol[type],.md-typeset ul[type]{list-style-type:revert-layer}[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:-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{border-radius:100%;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)}.md-typeset .md-code__content{display:grid}@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)}@media print{.md-feedback{display:none}}.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;position:relative}.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{-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-style:normal;font-weight:400;outline:none;text-align:initial;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}:root{--md-tooltip-width:20rem;--md-tooltip-tail:0.3rem}.md-tooltip2{-webkit-backface-visibility:hidden;backface-visibility:hidden;color:var(--md-default-fg-color);font-family:var(--md-text-font-family);opacity:0;pointer-events:none;position:absolute;top:calc(var(--md-tooltip-host-y) + var(--md-tooltip-y));transform:translateY(-.4rem);transform-origin:calc(var(--md-tooltip-host-x) + var(--md-tooltip-x)) 0;transition:transform 0ms .25s,opacity .25s,z-index .25s;width:100%;z-index:0}.md-tooltip2:before{border-left:var(--md-tooltip-tail) solid #0000;border-right:var(--md-tooltip-tail) solid #0000;content:"";display:block;left:clamp(1.5 * .8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-tail),100vw - 2 * var(--md-tooltip-tail) - 1.5 * .8rem);position:absolute;z-index:1}.md-tooltip2--top:before{border-top:var(--md-tooltip-tail) solid var(--md-default-bg-color);bottom:calc(var(--md-tooltip-tail)*-1 + .025rem);filter:drop-shadow(0 1px 0 hsla(0,0%,0%,.05))}.md-tooltip2--bottom:before{border-bottom:var(--md-tooltip-tail) solid var(--md-default-bg-color);filter:drop-shadow(0 -1px 0 hsla(0,0%,0%,.05));top:calc(var(--md-tooltip-tail)*-1 + .025rem)}.md-tooltip2--active{opacity:1;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.5,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip2__inner{scrollbar-gutter:stable;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);max-height:40vh;max-width:calc(100vw - 1.6rem);position:relative;scrollbar-width:thin}.md-tooltip2__inner::-webkit-scrollbar{height:.2rem;width:.2rem}.md-tooltip2__inner::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-tooltip2__inner::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}[role=tooltip]>.md-tooltip2__inner{font-size:.5rem;font-weight:700;left:clamp(.8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-width)/2,100vw - var(--md-tooltip-width) - .8rem);max-width:min(100vw - 2 * .8rem,400px);padding:.2rem .4rem;-webkit-user-select:none;user-select:none;width:-moz-fit-content;width:fit-content}.md-tooltip2__inner.md-typeset>:first-child{margin-top:0}.md-tooltip2__inner.md-typeset>:last-child{margin-bottom:0}[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__alias{margin-left:.3rem;opacity:.7}.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: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]{display:initial}.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)}@media print{.giscus,[id=__comments]{display:none}}: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.0253249f.min.css.map b/main/assets/stylesheets/main.0253249f.min.css.map deleted file mode 100644 index 748194709..000000000 --- a/main/assets/stylesheets/main.0253249f.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/_code.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/_tooltip2.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/_giscus.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,gBCyyCF,CCvzCA,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,wNAAA,CACA,gNAAA,CACA,iNJgIF,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,qBAWE,kCAAA,CAAA,0BAAA,CADA,eAAA,CATA,aAAA,CAEA,QAAA,CAMA,uCAAA,CALA,aAAA,CAFA,oCAAA,CAKA,yDAAA,CACA,oBAAA,CAFA,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,CIxDI,qBACE,4BJ0DN,CIrDE,kDAGE,kBJuDJ,CI1DE,kDAGE,mBJuDJ,CI1DE,8BAEE,SJwDJ,CIpDI,0DACE,iBJuDN,CInDI,oCACE,2BJsDN,CInDM,0CACE,2BJsDR,CInDQ,gDACE,2BJsDV,CInDU,sDACE,2BJsDZ,CI9CI,0CACE,4BJiDN,CI7CI,wDACE,kBJiDN,CIlDI,wDACE,mBJiDN,CIlDI,oCAEE,kBJgDN,CI7CM,kGAEE,aJiDR,CI7CM,0DACE,eJgDR,CI5CM,4HAEE,kBJ+CR,CIjDM,4HAEE,mBJ+CR,CIjDM,oFACE,kBAAA,CAAA,eJgDR,CIzCE,yBAEE,mBJ2CJ,CI7CE,yBAEE,oBJ2CJ,CI7CE,eACE,mBAAA,CAAA,cJ4CJ,CIvCE,kDAIE,WAAA,CADA,cJ0CJ,CIlCI,4BAEE,oBJoCN,CIhCI,6BAEE,oBJkCN,CI9BI,kCACE,YJgCN,CI3BE,mBACE,iBAAA,CAGA,eAAA,CADA,cAAA,CAEA,iBAAA,CAHA,sBAAA,CAAA,iBJgCJ,CI1BI,uBACE,aAAA,CACA,aJ4BN,CIvBE,uBAGE,iBAAA,CADA,eAAA,CADA,eJ2BJ,CIrBE,mBACE,cJuBJ,CInBE,+BAME,2CAAA,CACA,iDAAA,CACA,mBAAA,CAPA,oBAAA,CAGA,gBAAA,CAFA,cAAA,CACA,aAAA,CAEA,iBJwBJ,CIlBI,aAXF,+BAYI,aJqBJ,CACF,CIhBI,iCACE,gBJkBN,CIXM,8FACE,YJaR,CITM,4FACE,eJWR,CINI,8FACE,eJQN,CILM,kHACE,gBJOR,CIFI,kCAGE,eAAA,CAFA,cAAA,CACA,sBAAA,CAEA,kBJIN,CIAI,kCAGE,qDAAA,CAFA,sBAAA,CACA,kBJGN,CIEI,wCACE,iCJAN,CIGM,8CACE,qDAAA,CACA,sDJDR,CIMI,iCACE,iBJJN,CISE,wCACE,cJPJ,CIUI,wDAIE,gBJFN,CIFI,wDAIE,iBJFN,CIFI,8CAME,UAAA,CALA,oBAAA,CAEA,YAAA,CAIA,oDAAA,CAAA,4CAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CALA,0BAAA,CAHA,WJAN,CIYI,oDACE,oDJVN,CIcI,mEACE,kDAAA,CACA,yDAAA,CAAA,iDJZN,CIgBI,oEACE,kDAAA,CACA,0DAAA,CAAA,kDJdN,CImBE,wBACE,iBAAA,CACA,eAAA,CACA,iBJjBJ,CIqBE,mBACE,oBAAA,CAEA,kBAAA,CADA,eJlBJ,CIsBI,aANF,mBAOI,aJnBJ,CACF,CIsBI,8BACE,aAAA,CAEA,QAAA,CACA,eAAA,CAFA,UJlBN,CKlWI,0CDmYF,uBACE,iBJ7BF,CIgCE,4BACE,eJ9BJ,CACF,CMjiBE,uBAOE,kBAAA,CALA,aAAA,CACA,aAAA,CAEA,aAAA,CACA,eAAA,CALA,iBAAA,CAOA,sCACE,CALF,YNuiBJ,CM9hBI,2BACE,aNgiBN,CM5hBI,6BAME,+CAAA,CAFA,yCAAA,CAHA,eAAA,CACA,eAAA,CACA,kBAAA,CAEA,iBN+hBN,CM1hBI,6BAEE,aAAA,CADA,YN6hBN,CMvhBE,wBACE,kBNyhBJ,CMthBI,4BAIE,kBAAA,CAHA,mCAAA,CAIA,uBNshBN,CMlhBI,4DAEE,oBAAA,CADA,SNqhBN,CMjhBM,oEACE,mBNmhBR,CO5kBA,WAGE,0CAAA,CADA,+BAAA,CADA,aPilBF,CO5kBE,aANF,WAOI,YP+kBF,CACF,CO5kBE,oBAEE,2CAAA,CADA,gCP+kBJ,CO1kBE,kBAGE,eAAA,CADA,iBAAA,CADA,eP8kBJ,COxkBE,6BACE,WP6kBJ,CO9kBE,6BACE,UP6kBJ,CO9kBE,mBAEE,aAAA,CACA,cAAA,CACA,uBP0kBJ,COvkBI,0BACE,YPykBN,COrkBI,yBACE,UPukBN,CQ5mBA,KASE,cAAA,CARA,WAAA,CACA,iBRgnBF,CK5cI,oCGtKJ,KAaI,gBRymBF,CACF,CKjdI,oCGtKJ,KAkBI,cRymBF,CACF,CQpmBA,KASE,2CAAA,CAPA,YAAA,CACA,qBAAA,CAKA,eAAA,CAHA,eAAA,CAJA,iBAAA,CAGA,UR0mBF,CQlmBE,aAZF,KAaI,aRqmBF,CACF,CKldI,0CGhJF,yBAII,cRkmBJ,CACF,CQzlBA,SAEE,gBAAA,CAAA,iBAAA,CADA,eR6lBF,CQxlBA,cACE,YAAA,CAEA,qBAAA,CADA,WR4lBF,CQxlBE,aANF,cAOI,aR2lBF,CACF,CQvlBA,SACE,WR0lBF,CQvlBE,gBACE,YAAA,CACA,WAAA,CACA,iBRylBJ,CQplBA,aACE,eAAA,CACA,sBRulBF,CQ9kBA,WACE,YRilBF,CQ5kBA,WAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,ORilBF,CQ5kBE,uCACE,aR8kBJ,CQ1kBE,+BAEE,uCAAA,CADA,kBR6kBJ,CQvkBA,SASE,2CAAA,CACA,mBAAA,CAFA,gCAAA,CADA,gBAAA,CADA,YAAA,CAMA,SAAA,CADA,uCAAA,CANA,mBAAA,CAJA,cAAA,CAYA,2BAAA,CATA,URilBF,CQrkBE,eAEE,SAAA,CAIA,uBAAA,CAHA,oEACE,CAHF,UR0kBJ,CQ5jBA,MACE,WR+jBF,CSxtBA,MACE,6PT0tBF,CSptBA,cASE,mBAAA,CAFA,0CAAA,CACA,cAAA,CAFA,YAAA,CAIA,uCAAA,CACA,oBAAA,CAVA,iBAAA,CAEA,UAAA,CADA,QAAA,CAUA,qBAAA,CAPA,WAAA,CADA,ST+tBF,CSptBE,aAfF,cAgBI,YTutBF,CACF,CSptBE,kCAEE,uCAAA,CADA,YTutBJ,CSltBE,qBACE,uCTotBJ,CShtBE,wCACE,+BTktBJ,CS7sBE,oBAME,6BAAA,CADA,UAAA,CAJA,aAAA,CAEA,cAAA,CACA,aAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,aTutBJ,CS3sBE,sBACE,cT6sBJ,CS1sBI,2BACE,2CT4sBN,CStsBI,kEAEE,uDAAA,CADA,+BTysBN,CU3wBE,8BACE,YV8wBJ,CWnxBA,mBACE,GACE,SAAA,CACA,0BXsxBF,CWnxBA,GACE,SAAA,CACA,uBXqxBF,CACF,CWjxBA,mBACE,GACE,SXmxBF,CWhxBA,GACE,SXkxBF,CACF,CWvwBE,qBASE,2BAAA,CAFA,mCAAA,CAAA,2BAAA,CADA,0BAAA,CADA,WAAA,CAGA,SAAA,CAPA,cAAA,CACA,KAAA,CAEA,UAAA,CADA,SX+wBJ,CWrwBE,mBAcE,mDAAA,CANA,2CAAA,CACA,QAAA,CACA,mBAAA,CARA,QAAA,CASA,kDACE,CAPF,eAAA,CAEA,aAAA,CADA,SAAA,CALA,cAAA,CAGA,UAAA,CADA,SXgxBJ,CWjwBE,kBACE,aXmwBJ,CW/vBE,sBACE,YAAA,CACA,YXiwBJ,CW9vBI,oCACE,aXgwBN,CW3vBE,sBACE,mBX6vBJ,CW1vBI,6CACE,cX4vBN,CKtpBI,0CMvGA,6CAKI,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,UX8vBN,CACF,CWvvBE,kBACE,cXyvBJ,CY11BA,YACE,WAAA,CAIA,WZ01BF,CYv1BE,mBAEE,qBAAA,CADA,iBZ01BJ,CK7rBI,sCOtJE,4EACE,kBZs1BN,CYl1BI,0JACE,mBZo1BN,CYr1BI,8EACE,kBZo1BN,CACF,CY/0BI,0BAGE,UAAA,CAFA,aAAA,CACA,YZk1BN,CY70BI,+BACE,eZ+0BN,CYz0BE,8BACE,WZ80BJ,CY/0BE,8BACE,UZ80BJ,CY/0BE,8BAIE,iBZ20BJ,CY/0BE,8BAIE,kBZ20BJ,CY/0BE,oBAGE,cAAA,CADA,SZ60BJ,CYx0BI,aAPF,oBAQI,YZ20BJ,CACF,CYx0BI,gCACE,yCZ00BN,CYt0BI,wBACE,cAAA,CACA,kBZw0BN,CYr0BM,kCACE,oBZu0BR,Cax4BA,qBAEE,Wbs5BF,Cax5BA,qBAEE,Ubs5BF,Cax5BA,WAQE,2CAAA,CACA,mBAAA,CANA,YAAA,CAOA,8BAAA,CALA,iBAAA,CAMA,SAAA,CALA,mBAAA,CACA,mBAAA,CANA,cAAA,CAcA,0BAAA,CAHA,wCACE,CATF,Sbo5BF,Cat4BE,aAlBF,WAmBI,Yby4BF,CACF,Cat4BE,mBAEE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,kEby4BJ,Cal4BE,kBAEE,gCAAA,CADA,ebq4BJ,Ccv6BA,aACE,gBAAA,CACA,iBd06BF,Ccv6BE,sBAGE,WAAA,CADA,QAAA,CADA,Sd26BJ,Ccr6BE,oBAEE,eAAA,CADA,edw6BJ,Ccn6BE,oBACE,iBdq6BJ,Ccj6BE,mBAEE,YAAA,CACA,cAAA,CACA,6BAAA,CAHA,iBds6BJ,Cch6BI,iDACE,yCdk6BN,Cc95BI,6BACE,iBdg6BN,Cc35BE,mBAGE,uCAAA,CACA,cAAA,CAHA,aAAA,CACA,cAAA,CAGA,sBd65BJ,Cc15BI,gDACE,+Bd45BN,Ccx5BI,4BACE,0CAAA,CACA,mBd05BN,Ccr5BE,mBAEE,SAAA,CADA,iBAAA,CAKA,2BAAA,CAHA,8Ddw5BJ,Ccl5BI,qBAEE,aAAA,CADA,edq5BN,Cch5BI,6BACE,SAAA,CACA,uBdk5BN,Cc74BE,aAnFF,aAoFI,Ydg5BF,CACF,Cer+BA,WAEE,0CAAA,CADA,+Bfy+BF,Cer+BE,aALF,WAMI,Yfw+BF,CACF,Cer+BE,kBACE,6BAAA,CAEA,aAAA,CADA,afw+BJ,Cep+BI,gCACE,Yfs+BN,Cej+BE,iBAOE,eAAA,CANA,YAAA,CAKA,cAAA,CAGA,mBAAA,CAAA,eAAA,CADA,cAAA,CAGA,uCAAA,CADA,eAAA,CAEA,uBf+9BJ,Ce59BI,8CACE,Uf89BN,Ce19BI,+BACE,oBf49BN,CK90BI,0CUvIE,uBACE,afw9BN,Cer9BM,yCACE,Yfu9BR,CACF,Cel9BI,iCACE,gBfq9BN,Cet9BI,iCACE,iBfq9BN,Cet9BI,uBAEE,gBfo9BN,Cej9BM,iCACE,efm9BR,Ce78BE,kBACE,WAAA,CAIA,eAAA,CADA,mBAAA,CAFA,6BAAA,CACA,cAAA,CAGA,kBf+8BJ,Ce38BE,mBAEE,YAAA,CADA,af88BJ,Cez8BE,sBACE,gBAAA,CACA,Uf28BJ,Cet8BA,gBACE,gDfy8BF,Cet8BE,uBACE,YAAA,CACA,cAAA,CACA,6BAAA,CACA,afw8BJ,Cep8BE,kCACE,sCfs8BJ,Cen8BI,gFACE,+Bfq8BN,Ce77BA,cAKE,wCAAA,CADA,gBAAA,CADA,iBAAA,CADA,eAAA,CADA,Ufo8BF,CKx5BI,mCU7CJ,cASI,Ufg8BF,CACF,Ce57BE,yBACE,sCf87BJ,Cev7BA,WACE,mBAAA,CACA,SAAA,CAEA,cAAA,CADA,qBf27BF,CKv6BI,mCUvBJ,WAQI,ef07BF,CACF,Cev7BE,iBACE,oBAAA,CAEA,aAAA,CACA,iBAAA,CAFA,Yf27BJ,Cet7BI,wBACE,efw7BN,Cep7BI,qBAGE,iBAAA,CAFA,gBAAA,CACA,mBfu7BN,CgB7lCE,uBAME,kBAAA,CACA,mBAAA,CAHA,gCAAA,CACA,cAAA,CAJA,oBAAA,CAEA,eAAA,CADA,kBAAA,CAMA,gEhBgmCJ,CgB1lCI,gCAEE,2CAAA,CACA,uCAAA,CAFA,gChB8lCN,CgBxlCI,0DAEE,0CAAA,CACA,sCAAA,CAFA,+BhB4lCN,CgBrlCE,gCAKE,4BhB0lCJ,CgB/lCE,gEAME,6BhBylCJ,CgB/lCE,gCAME,4BhBylCJ,CgB/lCE,sBAIE,6DAAA,CAGA,8BAAA,CAJA,eAAA,CAFA,aAAA,CACA,eAAA,CAMA,sChBulCJ,CgBllCI,wDACE,6CAAA,CACA,8BhBolCN,CgBhlCI,+BACE,UhBklCN,CiBroCA,WAOE,2CAAA,CAGA,8CACE,CALF,gCAAA,CADA,aAAA,CAHA,MAAA,CADA,eAAA,CACA,OAAA,CACA,KAAA,CACA,SjB4oCF,CiBjoCE,aAfF,WAgBI,YjBooCF,CACF,CiBjoCE,mBAIE,2BAAA,CAHA,iEjBooCJ,CiB7nCE,mBACE,kDACE,CAEF,kEjB6nCJ,CiBvnCE,kBAEE,kBAAA,CADA,YAAA,CAEA,ejBynCJ,CiBrnCE,mBAKE,kBAAA,CAEA,cAAA,CAHA,YAAA,CAIA,uCAAA,CALA,aAAA,CAFA,iBAAA,CAQA,uBAAA,CAHA,qBAAA,CAJA,SjB8nCJ,CiBpnCI,yBACE,UjBsnCN,CiBlnCI,iCACE,oBjBonCN,CiBhnCI,uCAEE,uCAAA,CADA,YjBmnCN,CiB9mCI,2BAEE,YAAA,CADA,ajBinCN,CKngCI,0CY/GA,2BAMI,YjBgnCN,CACF,CiB7mCM,8DAIE,iBAAA,CAHA,aAAA,CAEA,aAAA,CADA,UjBinCR,CKjiCI,mCYzEA,iCAII,YjB0mCN,CACF,CiBvmCM,wCACE,YjBymCR,CiBrmCM,+CACE,oBjBumCR,CK5iCI,sCYtDA,iCAII,YjBkmCN,CACF,CiB7lCE,kBAEE,YAAA,CACA,cAAA,CAFA,iBAAA,CAIA,8DACE,CAFF,kBjBgmCJ,CiB1lCI,oCAGE,SAAA,CADA,mBAAA,CAKA,6BAAA,CAHA,8DACE,CAJF,UjBgmCN,CiBvlCM,8CACE,8BjBylCR,CiBplCI,8BACE,ejBslCN,CiBjlCE,4BAGE,gBAAA,CAAA,kBjBqlCJ,CiBxlCE,4BAGE,iBAAA,CAAA,iBjBqlCJ,CiBxlCE,kBACE,WAAA,CAGA,eAAA,CAFA,aAAA,CAGA,kBjBmlCJ,CiBhlCI,4CAGE,SAAA,CADA,mBAAA,CAKA,8BAAA,CAHA,8DACE,CAJF,UjBslCN,CiB7kCM,sDACE,6BjB+kCR,CiB3kCM,8DAGE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,8DACE,CAJF,SjBilCR,CiBtkCI,uCAGE,WAAA,CAFA,iBAAA,CACA,UjBykCN,CiBnkCE,mBACE,YAAA,CACA,aAAA,CACA,cAAA,CAEA,+CACE,CAFF,kBjBskCJ,CiBhkCI,8DACE,WAAA,CACA,SAAA,CACA,oCjBkkCN,CiBzjCI,yBACE,QjB2jCN,CiBtjCE,mBACE,YjBwjCJ,CKpnCI,mCY2DF,6BAQI,gBjBwjCJ,CiBhkCA,6BAQI,iBjBwjCJ,CiBhkCA,mBAKI,aAAA,CAEA,iBAAA,CADA,ajB0jCJ,CACF,CK5nCI,sCY2DF,6BAaI,kBjBwjCJ,CiBrkCA,6BAaI,mBjBwjCJ,CACF,CDvyCA,SAGE,uCAAA,CAFA,eAAA,CACA,eC2yCF,CDvyCE,eACE,mBAAA,CACA,cAAA,CAGA,eAAA,CADA,QAAA,CADA,SC2yCJ,CDryCE,sCAEE,WAAA,CADA,iBAAA,CAAA,kBCwyCJ,CDnyCE,eACE,+BCqyCJ,CDlyCI,0CACE,+BCoyCN,CD9xCA,UAKE,wBmBaa,CnBZb,oBAAA,CAFA,UAAA,CAHA,oBAAA,CAEA,eAAA,CADA,0BAAA,CAAA,2BCqyCF,CmBv0CA,MACE,uMAAA,CACA,sLAAA,CACA,iNnB00CF,CmBp0CA,QACE,eAAA,CACA,enBu0CF,CmBp0CE,eAKE,uCAAA,CAJA,aAAA,CAGA,eAAA,CADA,eAAA,CADA,eAAA,CAIA,sBnBs0CJ,CmBn0CI,+BACE,YnBq0CN,CmBl0CM,mCAEE,WAAA,CADA,UnBq0CR,CmB7zCQ,sFAME,iBAAA,CALA,aAAA,CAGA,aAAA,CADA,cAAA,CAEA,kBAAA,CAHA,UnBm0CV,CmBxzCE,cAGE,eAAA,CADA,QAAA,CADA,SnB4zCJ,CmBtzCE,cAGE,sBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBAAA,CACA,uBAAA,CACA,sBnBwzCJ,CmBrzCI,sBACE,uCnBuzCN,CmBhzCM,6EAEE,+BnBkzCR,CmB7yCI,2BAIE,iBnB4yCN,CmBxyCI,4CACE,gBnB0yCN,CmB3yCI,4CACE,iBnB0yCN,CmBtyCI,kBAME,iBAAA,CAFA,aAAA,CACA,YAAA,CAFA,iBnByyCN,CmBlyCI,sGACE,+BAAA,CACA,cnBoyCN,CmBhyCI,4BACE,uCAAA,CACA,oBnBkyCN,CmB9xCI,0CACE,YnBgyCN,CmB7xCM,yDAIE,6BAAA,CAHA,aAAA,CAEA,WAAA,CAEA,qCAAA,CAAA,6BAAA,CAHA,UnBkyCR,CmB3xCM,kDACE,YnB6xCR,CmBvxCE,iCACE,YnByxCJ,CmBtxCI,6CACE,WAAA,CAGA,WnBsxCN,CmBjxCE,cACE,anBmxCJ,CmB/wCE,gBACE,YnBixCJ,CKlvCI,0CcxBA,0CASE,2CAAA,CAHA,YAAA,CACA,qBAAA,CACA,WAAA,CALA,MAAA,CADA,iBAAA,CACA,OAAA,CACA,KAAA,CACA,SnBgxCJ,CmBrwCI,+DACE,eAAA,CACA,enBuwCN,CmBnwCI,gCAQE,qDAAA,CAHA,uCAAA,CAEA,cAAA,CALA,aAAA,CAEA,kBAAA,CADA,wBAAA,CAFA,iBAAA,CAKA,kBnBuwCN,CmBlwCM,wDAEE,UnBywCR,CmB3wCM,wDAEE,WnBywCR,CmB3wCM,8CAIE,aAAA,CAEA,aAAA,CACA,YAAA,CANA,iBAAA,CAEA,SAAA,CAEA,YnBswCR,CmBjwCQ,oDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnB0wCV,CmB9vCM,8CAIE,2CAAA,CACA,gEACE,CALF,eAAA,CAEA,4BAAA,CADA,kBnBmwCR,CmB5vCQ,2DACE,YnB8vCV,CmBzvCM,8CAGE,2CAAA,CADA,gCAAA,CADA,enB6vCR,CmBvvCM,yCAIE,aAAA,CAFA,UAAA,CAIA,YAAA,CADA,aAAA,CAJA,iBAAA,CACA,WAAA,CACA,SnB4vCR,CmBpvCI,+BACE,MnBsvCN,CmBlvCI,+BACE,4DnBovCN,CmBjvCM,qDACE,+BnBmvCR,CmBhvCQ,sHACE,+BnBkvCV,CmB5uCI,+BAEE,YAAA,CADA,mBnB+uCN,CmB3uCM,mCACE,enB6uCR,CmBzuCM,6CACE,SnB2uCR,CmBvuCM,uDAGE,mBnB0uCR,CmB7uCM,uDAGE,kBnB0uCR,CmB7uCM,6CAIE,gBAAA,CAFA,aAAA,CADA,YnB4uCR,CmBtuCQ,mDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnB+uCV,CmB/tCM,+CACE,mBnBiuCR,CmBztCM,4CAEE,wBAAA,CADA,enB4tCR,CmBxtCQ,oEACE,mBnB0tCV,CmB3tCQ,oEACE,oBnB0tCV,CmBttCQ,4EACE,iBnBwtCV,CmBztCQ,4EACE,kBnBwtCV,CmBptCQ,oFACE,mBnBstCV,CmBvtCQ,oFACE,oBnBstCV,CmBltCQ,4FACE,mBnBotCV,CmBrtCQ,4FACE,oBnBotCV,CmB7sCE,mBACE,wBnB+sCJ,CmB3sCE,wBACE,YAAA,CACA,SAAA,CAIA,0BAAA,CAHA,oEnB8sCJ,CmBxsCI,kCACE,2BnB0sCN,CmBrsCE,gCACE,SAAA,CAIA,uBAAA,CAHA,qEnBwsCJ,CmBlsCI,8CAEE,kCAAA,CAAA,0BnBmsCN,CACF,CKr4CI,0Cc0MA,0CACE,YnB8rCJ,CmB3rCI,yDACE,UnB6rCN,CmBzrCI,wDACE,YnB2rCN,CmBvrCI,kDACE,YnByrCN,CmBprCE,gBAIE,iDAAA,CADA,gCAAA,CAFA,aAAA,CACA,enBwrCJ,CACF,CKl8CM,+DcmRF,6CACE,YnBkrCJ,CmB/qCI,4DACE,UnBirCN,CmB7qCI,2DACE,YnB+qCN,CmB3qCI,qDACE,YnB6qCN,CACF,CK17CI,mCc7JJ,QAgbI,oBnB2qCF,CmBrqCI,kCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnBuqCN,CmBlqCM,6CACE,uBnBoqCR,CmBhqCM,gDACE,YnBkqCR,CmB7pCI,2CACE,kBnBgqCN,CmBjqCI,2CACE,mBnBgqCN,CmBjqCI,iCAEE,oBnB+pCN,CmBxpCI,yDACE,kBnB0pCN,CmB3pCI,yDACE,iBnB0pCN,CACF,CKn9CI,sCc7JJ,QA4dI,oBAAA,CACA,oDnBwpCF,CmBlpCI,gCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnBopCN,CmB/oCM,8CACE,uBnBipCR,CmB7oCM,8CACE,YnB+oCR,CmB1oCI,yCACE,kBnB6oCN,CmB9oCI,yCACE,mBnB6oCN,CmB9oCI,+BAEE,oBnB4oCN,CmBroCI,uDACE,kBnBuoCN,CmBxoCI,uDACE,iBnBuoCN,CmBloCE,wBACE,YAAA,CACA,sBAAA,CAEA,SAAA,CACA,6FACE,CAHF,mBnBsoCJ,CmB9nCI,sCACE,enBgoCN,CmB3nCE,iFACE,sBAAA,CAEA,SAAA,CACA,4FACE,CAHF,kBnB+nCJ,CmBtnCE,iDACE,enBwnCJ,CmBpnCE,6CACE,YnBsnCJ,CmBlnCE,uBACE,aAAA,CACA,enBonCJ,CmBjnCI,kCACE,enBmnCN,CmB/mCI,qCACE,enBinCN,CmB9mCM,0CACE,uCnBgnCR,CmB5mCM,6DACE,mBnB8mCR,CmB1mCM,yFAEE,YnB4mCR,CmBvmCI,yCAEE,kBnB2mCN,CmB7mCI,yCAEE,mBnB2mCN,CmB7mCI,+BACE,aAAA,CAGA,SAAA,CADA,kBnB0mCN,CmBtmCM,2DACE,SnBwmCR,CmBlmCE,cAGE,kBAAA,CADA,YAAA,CAEA,gCAAA,CAHA,WnBumCJ,CmBjmCI,oBACE,uDnBmmCN,CmB/lCI,oBAME,6BAAA,CACA,kBAAA,CAFA,UAAA,CAJA,oBAAA,CAEA,WAAA,CAKA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,yBAAA,CARA,qBAAA,CAFA,UnB2mCN,CmB9lCM,8BACE,wBnBgmCR,CmB5lCM,kKAEE,uBnB6lCR,CmB/kCI,2EACE,YnBolCN,CmBjlCM,oDACE,anBmlCR,CmBhlCQ,kEAKE,qCAAA,CACA,qDAAA,CAFA,YAAA,CAHA,eAAA,CACA,KAAA,CACA,SnBqlCV,CmB/kCU,0FACE,mBnBilCZ,CmB5kCQ,0EACE,QnB8kCV,CmBzkCM,sFACE,kBnB2kCR,CmB5kCM,sFACE,mBnB2kCR,CmBvkCM,kDACE,uCnBykCR,CmBnkCI,2CACE,sBAAA,CAEA,SAAA,CADA,kBnBskCN,CmB7jCI,qFAIE,mDnBgkCN,CmBpkCI,qFAIE,oDnBgkCN,CmBpkCI,2EACE,aAAA,CACA,oBAAA,CAGA,SAAA,CAFA,kBnBikCN,CmB5jCM,yFAEE,gBAAA,CADA,gBnB+jCR,CmB1jCM,0FACE,YnB4jCR,CACF,CoBnxDA,eAKE,eAAA,CACA,eAAA,CAJA,SpB0xDF,CoBnxDE,gCANA,kBAAA,CAFA,YAAA,CAGA,sBpBiyDF,CoB5xDE,iBAOE,mBAAA,CAFA,aAAA,CADA,gBAAA,CAEA,iBpBsxDJ,CoBjxDE,wBAEE,qDAAA,CADA,uCpBoxDJ,CoB/wDE,qBACE,6CpBixDJ,CoB5wDI,sDAEE,uDAAA,CADA,+BpB+wDN,CoB3wDM,8DACE,+BpB6wDR,CoBxwDI,mCACE,uCAAA,CACA,oBpB0wDN,CoBtwDI,yBAKE,iBAAA,CADA,yCAAA,CAHA,aAAA,CAEA,eAAA,CADA,YpB2wDN,CqB3zDE,eAGE,+DAAA,CADA,oBAAA,CADA,qBrBg0DJ,CK3oDI,0CgBtLF,eAOI,YrB8zDJ,CACF,CqBxzDM,6BACE,oBrB0zDR,CqBpzDE,kBACE,YAAA,CACA,qBAAA,CACA,SAAA,CACA,qBrBszDJ,CqB/yDI,0BACE,sBrBizDN,CqB9yDM,gEACE,+BrBgzDR,CqB1yDE,gBAEE,uCAAA,CADA,erB6yDJ,CqBxyDE,kBACE,oBrB0yDJ,CqBvyDI,mCAGE,kBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBrByyDN,CqBryDI,oCAIE,kBAAA,CAHA,mBAAA,CACA,kBAAA,CACA,SAAA,CAGA,QAAA,CADA,iBrBwyDN,CqBnyDI,0DACE,kBrBqyDN,CqBtyDI,0DACE,iBrBqyDN,CqBjyDI,iDACE,uBAAA,CAEA,YrBkyDN,CqB7xDE,4BACE,YrB+xDJ,CqBxxDA,YAGE,kBAAA,CAFA,YAAA,CAIA,eAAA,CAHA,SAAA,CAIA,eAAA,CAFA,UrB6xDF,CqBxxDE,yBACE,WrB0xDJ,CqBnxDA,kBACE,YrBsxDF,CK9sDI,0CgBzEJ,kBAKI,wBrBsxDF,CACF,CqBnxDE,qCACE,WrBqxDJ,CKzuDI,sCgB7CF,+CAKI,kBrBqxDJ,CqB1xDA,+CAKI,mBrBqxDJ,CACF,CK3tDI,0CgBrDJ,6BAMI,SAAA,CAFA,eAAA,CACA,UrBkxDF,CqB/wDE,qDACE,gBrBixDJ,CqB9wDE,gDACE,SrBgxDJ,CqB7wDE,4CACE,iBAAA,CAAA,kBrB+wDJ,CqB5wDE,2CAEE,WAAA,CADA,crB+wDJ,CqB3wDE,2CACE,mBAAA,CACA,cAAA,CACA,SAAA,CACA,oBAAA,CAAA,iBrB6wDJ,CqB1wDE,2CACE,SrB4wDJ,CqBzwDE,qCAEE,WAAA,CACA,eAAA,CAFA,erB6wDJ,CACF,CsBv7DA,MACE,qBAAA,CACA,yBtB07DF,CsBp7DA,aAME,qCAAA,CADA,cAAA,CAEA,0FACE,CAPF,cAAA,CACA,KAAA,CAaA,mDAAA,CACA,qBAAA,CAJA,wFACE,CATF,UAAA,CADA,StB87DF,CuBz8DA,MACE,mfvB48DF,CuBt8DA,WACE,iBvBy8DF,CK3yDI,mCkB/JJ,WAKI,evBy8DF,CACF,CuBt8DE,kBACE,YvBw8DJ,CuBp8DE,oBAEE,SAAA,CADA,SvBu8DJ,CKpyDI,0CkBpKF,8BAOI,YvB+8DJ,CuBt9DA,8BAOI,avB+8DJ,CuBt9DA,oBAaI,2CAAA,CACA,kBAAA,CAJA,WAAA,CACA,eAAA,CACA,mBAAA,CANA,iBAAA,CAEA,SAAA,CAUA,uBAAA,CAHA,4CACE,CAPF,UvB68DJ,CuBj8DI,+DACE,SAAA,CACA,oCvBm8DN,CACF,CK10DI,mCkBjJF,8BAgCI,MvBs8DJ,CuBt+DA,8BAgCI,OvBs8DJ,CuBt+DA,oBAqCI,0BAAA,CADA,cAAA,CADA,QAAA,CAJA,cAAA,CAEA,KAAA,CAKA,sDACE,CALF,OvBo8DJ,CuB17DI,+DAME,YAAA,CACA,SAAA,CACA,4CACE,CARF,UvB+7DN,CACF,CKz0DI,0CkBxGA,+DAII,mBvBi7DN,CACF,CKv3DM,+DkB/DF,+DASI,mBvBi7DN,CACF,CK53DM,+DkB/DF,+DAcI,mBvBi7DN,CACF,CuB56DE,kBAEE,kCAAA,CAAA,0BvB66DJ,CK31DI,0CkBpFF,4BAOI,MvBq7DJ,CuB57DA,4BAOI,OvBq7DJ,CuB57DA,kBAWI,QAAA,CAEA,SAAA,CADA,eAAA,CANA,cAAA,CAEA,KAAA,CAWA,wBAAA,CALA,qGACE,CALF,OAAA,CADA,SvBm7DJ,CuBt6DI,4BACE,yBvBw6DN,CuBp6DI,6DAEE,WAAA,CACA,SAAA,CAMA,uBAAA,CALA,sGACE,CAJF,UvB06DN,CACF,CKt4DI,mCkBjEF,4BA2CI,WvBo6DJ,CuB/8DA,4BA2CI,UvBo6DJ,CuB/8DA,kBA6CI,eAAA,CAHA,iBAAA,CAIA,8CAAA,CAFA,avBm6DJ,CACF,CKr6DM,+DkBOF,6DAII,avB85DN,CACF,CKp5DI,sCkBfA,6DASI,avB85DN,CACF,CuBz5DE,iBAIE,2CAAA,CACA,0BAAA,CAFA,aAAA,CAFA,iBAAA,CAKA,2CACE,CALF,SvB+5DJ,CKj6DI,mCkBAF,iBAaI,0BAAA,CACA,mBAAA,CAFA,avB25DJ,CuBt5DI,uBACE,0BvBw5DN,CACF,CuBp5DI,4DAEE,2CAAA,CACA,6BAAA,CACA,8BAAA,CAHA,gCvBy5DN,CuBj5DE,4BAKE,mBAAA,CAAA,oBvBs5DJ,CuB35DE,4BAKE,mBAAA,CAAA,oBvBs5DJ,CuB35DE,kBAQE,gBAAA,CAFA,eAAA,CAFA,WAAA,CAHA,iBAAA,CAMA,sBAAA,CAJA,UAAA,CADA,SvBy5DJ,CuBh5DI,+BACE,qBvBk5DN,CuB94DI,kEAEE,uCvB+4DN,CuB34DI,6BACE,YvB64DN,CKj7DI,0CkBaF,kBA8BI,eAAA,CADA,aAAA,CADA,UvB84DJ,CACF,CK38DI,mCkBgCF,4BAmCI,mBvB84DJ,CuBj7DA,4BAmCI,oBvB84DJ,CuBj7DA,kBAqCI,aAAA,CADA,evB64DJ,CuBz4DI,+BACE,uCvB24DN,CuBv4DI,mCACE,gCvBy4DN,CuBr4DI,6DACE,kBvBu4DN,CuBp4DM,8EACE,uCvBs4DR,CuBl4DM,0EACE,WvBo4DR,CACF,CuB93DE,iBAIE,cAAA,CAHA,oBAAA,CAEA,aAAA,CAEA,kCACE,CAJF,YvBm4DJ,CuB33DI,uBACE,UvB63DN,CuBz3DI,yCAEE,UvB63DN,CuB/3DI,yCAEE,WvB63DN,CuB/3DI,+BACE,iBAAA,CAEA,SAAA,CACA,SvB23DN,CuBx3DM,6CACE,oBvB03DR,CKj+DI,0CkB+FA,yCAaI,UvB03DN,CuBv4DE,yCAaI,WvB03DN,CuBv4DE,+BAcI,SvBy3DN,CuBt3DM,+CACE,YvBw3DR,CACF,CK7/DI,mCkBkHA,+BAwBI,mBvBu3DN,CuBp3DM,8CACE,YvBs3DR,CACF,CuBh3DE,8BAEE,WvBq3DJ,CuBv3DE,8BAEE,UvBq3DJ,CuBv3DE,oBAKE,mBAAA,CAJA,iBAAA,CAEA,SAAA,CACA,SvBm3DJ,CKz/DI,0CkBkIF,8BASI,WvBm3DJ,CuB53DA,8BASI,UvBm3DJ,CuB53DA,oBAUI,SvBk3DJ,CACF,CuB/2DI,uCACE,iBvBq3DN,CuBt3DI,uCACE,kBvBq3DN,CuBt3DI,6BAEE,uCAAA,CACA,SAAA,CAIA,oBAAA,CAHA,+DvBk3DN,CuB52DM,iDAEE,uCAAA,CADA,YvB+2DR,CuB12DM,gGAGE,SAAA,CADA,mBAAA,CAEA,kBvB22DR,CuBx2DQ,sGACE,UvB02DV,CuBn2DE,8BAOE,mBAAA,CAAA,oBvB02DJ,CuBj3DE,8BAOE,mBAAA,CAAA,oBvB02DJ,CuBj3DE,oBAIE,kBAAA,CAKA,yCAAA,CANA,YAAA,CAKA,eAAA,CAFA,WAAA,CAKA,SAAA,CAVA,iBAAA,CACA,KAAA,CAUA,uBAAA,CAFA,kBAAA,CALA,UvB42DJ,CKnjEI,mCkBkMF,8BAgBI,mBvBs2DJ,CuBt3DA,8BAgBI,oBvBs2DJ,CuBt3DA,oBAiBI,evBq2DJ,CACF,CuBl2DI,+DACE,SAAA,CACA,0BvBo2DN,CuB/1DE,6BAKE,+BvBk2DJ,CuBv2DE,0DAME,gCvBi2DJ,CuBv2DE,6BAME,+BvBi2DJ,CuBv2DE,mBAIE,eAAA,CAHA,iBAAA,CAEA,UAAA,CADA,SvBq2DJ,CKljEI,0CkB2MF,mBAWI,QAAA,CADA,UvBk2DJ,CACF,CK3kEI,mCkB8NF,mBAiBI,SAAA,CADA,UAAA,CAEA,sBvBi2DJ,CuB91DI,8DACE,8BAAA,CACA,SvBg2DN,CACF,CuB31DE,uBASE,kCAAA,CAAA,0BAAA,CAFA,2CAAA,CANA,WAAA,CACA,eAAA,CAIA,kBvB41DJ,CuBt1DI,iEAZF,uBAaI,uBvBy1DJ,CACF,CKxnEM,+DkBiRJ,uBAkBI,avBy1DJ,CACF,CKvmEI,sCkB2PF,uBAuBI,avBy1DJ,CACF,CK5mEI,mCkB2PF,uBA4BI,YAAA,CACA,yDAAA,CACA,oBvBy1DJ,CuBt1DI,kEACE,evBw1DN,CuBp1DI,6BACE,+CvBs1DN,CuBl1DI,0CAEE,YAAA,CADA,WvBq1DN,CuBh1DI,gDACE,oDvBk1DN,CuB/0DM,sDACE,0CvBi1DR,CACF,CuB10DA,kBACE,gCAAA,CACA,qBvB60DF,CuB10DE,wBAME,qDAAA,CAFA,uCAAA,CAFA,gBAAA,CACA,kBAAA,CAFA,eAAA,CAIA,uBvB60DJ,CKhpEI,mCkB8TF,kCAUI,mBvB40DJ,CuBt1DA,kCAUI,oBvB40DJ,CACF,CuBx0DE,wBAGE,eAAA,CADA,QAAA,CADA,SAAA,CAIA,wBAAA,CAAA,gBvBy0DJ,CuBr0DE,wBACE,yDvBu0DJ,CuBp0DI,oCACE,evBs0DN,CuBj0DE,wBACE,aAAA,CAEA,YAAA,CADA,uBAAA,CAEA,gCvBm0DJ,CuBh0DI,4DACE,uDvBk0DN,CuB9zDI,gDACE,mBvBg0DN,CuB3zDE,gCAKE,cAAA,CADA,aAAA,CAGA,YAAA,CANA,eAAA,CAKA,uBAAA,CAJA,KAAA,CACA,SvBi0DJ,CuB1zDI,wCACE,YvB4zDN,CuBvzDI,wDACE,YvByzDN,CuBrzDI,oCAGE,+BAAA,CADA,gBAAA,CADA,mBAAA,CAGA,2CvBuzDN,CKlsEI,mCkBuYA,8CAUI,mBvBqzDN,CuB/zDE,8CAUI,oBvBqzDN,CACF,CuBjzDI,oFAEE,uDAAA,CADA,+BvBozDN,CuB9yDE,sCACE,2CvBgzDJ,CuB3yDE,2BAGE,eAAA,CADA,eAAA,CADA,iBvB+yDJ,CKntEI,mCkBmaF,qCAOI,mBvB6yDJ,CuBpzDA,qCAOI,oBvB6yDJ,CACF,CuBzyDE,kCAEE,MvB+yDJ,CuBjzDE,kCAEE,OvB+yDJ,CuBjzDE,wBAME,uCAAA,CAFA,aAAA,CACA,YAAA,CAJA,iBAAA,CAEA,YvB8yDJ,CK7sEI,0CkB4ZF,wBAUI,YvB2yDJ,CACF,CuBxyDI,8BAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,+CAAA,CAAA,uCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UvBizDN,CuBvyDM,wCACE,oBvByyDR,CuBnyDE,8BAGE,uCAAA,CAFA,gBAAA,CACA,evBsyDJ,CuBlyDI,iCAKE,gCAAA,CAHA,eAAA,CACA,eAAA,CACA,eAAA,CAHA,evBwyDN,CuBjyDM,sCACE,oBvBmyDR,CuB9xDI,iCAKE,gCAAA,CAHA,gBAAA,CACA,eAAA,CACA,eAAA,CAHA,avBoyDN,CuB7xDM,sCACE,oBvB+xDR,CuBzxDE,yBAKE,gCAAA,CAJA,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,avB8xDJ,CuBvxDE,uBAGE,wBAAA,CAFA,+BAAA,CACA,yBvB0xDJ,CwB97EA,WACE,iBAAA,CACA,SxBi8EF,CwB97EE,kBAOE,2CAAA,CACA,mBAAA,CACA,8BAAA,CAHA,gCAAA,CAHA,QAAA,CAEA,gBAAA,CADA,YAAA,CAMA,SAAA,CATA,iBAAA,CACA,sBAAA,CAaA,mCAAA,CAJA,oExBi8EJ,CwB17EI,6EACE,gBAAA,CACA,SAAA,CAKA,+BAAA,CAJA,8ExB67EN,CwBr7EI,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,OxB87EN,CwBl7EE,iBAOE,mBAAA,CAFA,eAAA,CACA,oBAAA,CAHA,QAAA,CAFA,kBAAA,CAGA,aAAA,CAFA,SxBy7EJ,CwBh7EE,iBACE,kBxBk7EJ,CwB96EE,2BAGE,kBAAA,CAAA,oBxBo7EJ,CwBv7EE,2BAGE,mBAAA,CAAA,mBxBo7EJ,CwBv7EE,iBAIE,cAAA,CAHA,aAAA,CAKA,YAAA,CADA,uBAAA,CAEA,2CACE,CANF,UxBq7EJ,CwB36EI,8CACE,+BxB66EN,CwBz6EI,uBACE,qDxB26EN,CyB//EA,YAIE,qBAAA,CADA,aAAA,CAGA,gBAAA,CALA,eAAA,CACA,UAAA,CAGA,azBmgFF,CyB//EE,aATF,YAUI,YzBkgFF,CACF,CKp1EI,0CoB3KF,+BAKI,azBugFJ,CyB5gFA,+BAKI,czBugFJ,CyB5gFA,qBAWI,2CAAA,CAHA,aAAA,CAEA,WAAA,CANA,cAAA,CAEA,KAAA,CASA,uBAAA,CAHA,iEACE,CAJF,aAAA,CAFA,SzBqgFJ,CyB1/EI,mEACE,8BAAA,CACA,6BzB4/EN,CyBz/EM,6EACE,8BzB2/ER,CyBt/EI,6CAEE,QAAA,CAAA,MAAA,CACA,QAAA,CACA,eAAA,CAHA,iBAAA,CACA,OAAA,CAGA,qBAAA,CAHA,KzB2/EN,CACF,CKn4EI,sCoBtKJ,YAuDI,QzBs/EF,CyBn/EE,mBACE,WzBq/EJ,CyBj/EE,6CACE,UzBm/EJ,CACF,CyB/+EE,uBACE,YAAA,CACA,OzBi/EJ,CKl5EI,mCoBjGF,uBAMI,QzBi/EJ,CyB9+EI,8BACE,WzBg/EN,CyB5+EI,qCACE,azB8+EN,CyB1+EI,+CACE,kBzB4+EN,CACF,CyBv+EE,wBAIE,uBAAA,CAOA,kCAAA,CAAA,0BAAA,CAVA,cAAA,CACA,eAAA,CACA,yDAAA,CAMA,oBzBs+EJ,CyBj+EI,2CAEE,YAAA,CADA,WzBo+EN,CyB/9EI,mEACE,+CzBi+EN,CyB99EM,qHACE,oDzBg+ER,CyB79EQ,iIACE,0CzB+9EV,CyBh9EE,wCAGE,wBACE,qBzBg9EJ,CyB58EE,6BACE,kCzB88EJ,CyB/8EE,6BACE,iCzB88EJ,CACF,CK16EI,0CoB5BF,YAME,0BAAA,CADA,QAAA,CAEA,SAAA,CANA,cAAA,CACA,KAAA,CAMA,sDACE,CALF,OAAA,CADA,SzB+8EF,CyBp8EE,4CAEE,WAAA,CACA,SAAA,CACA,4CACE,CAJF,UzBy8EJ,CACF,C0BtnFA,iBACE,GACE,Q1BwnFF,C0BrnFA,GACE,a1BunFF,CACF,C0BnnFA,gBACE,GACE,SAAA,CACA,0B1BqnFF,C0BlnFA,IACE,S1BonFF,C0BjnFA,GACE,SAAA,CACA,uB1BmnFF,CACF,C0B3mFA,MACE,2eAAA,CACA,+fAAA,CACA,0lBAAA,CACA,kf1B6mFF,C0BvmFA,WAOE,kCAAA,CAAA,0BAAA,CANA,aAAA,CACA,gBAAA,CACA,eAAA,CAEA,uCAAA,CAGA,uBAAA,CAJA,kB1B6mFF,C0BtmFE,iBACE,U1BwmFJ,C0BpmFE,iBACE,oBAAA,CAEA,aAAA,CACA,qBAAA,CAFA,U1BwmFJ,C0BnmFI,+BACE,iB1BsmFN,C0BvmFI,+BACE,kB1BsmFN,C0BvmFI,qBAEE,gB1BqmFN,C0BjmFI,kDACE,iB1BomFN,C0BrmFI,kDACE,kB1BomFN,C0BrmFI,kDAEE,iB1BmmFN,C0BrmFI,kDAEE,kB1BmmFN,C0B9lFE,iCAGE,iB1BmmFJ,C0BtmFE,iCAGE,kB1BmmFJ,C0BtmFE,uBACE,oBAAA,CACA,6BAAA,CAEA,eAAA,CACA,sBAAA,CACA,qB1BgmFJ,C0B5lFE,kBACE,YAAA,CAMA,gBAAA,CALA,SAAA,CAMA,oBAAA,CAHA,gBAAA,CAIA,WAAA,CAHA,eAAA,CAFA,SAAA,CADA,U1BomFJ,C0B3lFI,iDACE,4B1B6lFN,C0BxlFE,iBACE,eAAA,CACA,sB1B0lFJ,C0BvlFI,gDACE,2B1BylFN,C0BrlFI,kCAIE,kB1B6lFN,C0BjmFI,kCAIE,iB1B6lFN,C0BjmFI,wBAOE,6BAAA,CADA,UAAA,CALA,oBAAA,CAEA,YAAA,CAMA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CALA,uBAAA,CAHA,W1B+lFN,C0BnlFI,iCACE,a1BqlFN,C0BjlFI,iCACE,gDAAA,CAAA,wC1BmlFN,C0B/kFI,+BACE,8CAAA,CAAA,sC1BilFN,C0B7kFI,+BACE,8CAAA,CAAA,sC1B+kFN,C0B3kFI,sCACE,qDAAA,CAAA,6C1B6kFN,C0BvkFA,gBACE,Y1B0kFF,C0BvkFE,gCAIE,kB1B2kFJ,C0B/kFE,gCAIE,iB1B2kFJ,C0B/kFE,sBAGE,kBAAA,CAGA,uCAAA,CALA,mBAAA,CAIA,gBAAA,CAHA,S1B6kFJ,C0BtkFI,+BACE,aAAA,CACA,oB1BwkFN,C0BpkFI,2CACE,U1BukFN,C0BxkFI,2CACE,W1BukFN,C0BxkFI,iCAEE,kB1BskFN,C0BlkFI,0BACE,W1BokFN,C2B3vFA,MACE,iSAAA,CACA,4UAAA,CACA,+NAAA,CACA,gZ3B8vFF,C2BrvFE,iBAME,kDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,cAAA,CAIA,mCAAA,CAAA,2BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CANA,0BAAA,CAFA,a3BgwFJ,C2BpvFE,uBACE,6B3BsvFJ,C2BlvFE,sBACE,wCAAA,CAAA,gC3BovFJ,C2BhvFE,6BACE,+CAAA,CAAA,uC3BkvFJ,C2B9uFE,4BACE,8CAAA,CAAA,sC3BgvFJ,C4B3xFA,SASE,2CAAA,CADA,gCAAA,CAJA,aAAA,CAGA,eAAA,CADA,aAAA,CADA,UAAA,CAFA,S5BkyFF,C4BzxFE,aAZF,SAaI,Y5B4xFF,CACF,CKjnFI,0CuBzLJ,SAkBI,Y5B4xFF,CACF,C4BzxFE,iBACE,mB5B2xFJ,C4BvxFE,yBAIE,iB5B8xFJ,C4BlyFE,yBAIE,kB5B8xFJ,C4BlyFE,eAQE,eAAA,CAPA,YAAA,CAMA,eAAA,CAJA,QAAA,CAEA,aAAA,CAHA,SAAA,CAWA,oBAAA,CAPA,kB5B4xFJ,C4BlxFI,kCACE,Y5BoxFN,C4B/wFE,eACE,aAAA,CACA,kBAAA,CAAA,mB5BixFJ,C4B9wFI,sCACE,aAAA,CACA,S5BgxFN,C4B1wFE,eAOE,kCAAA,CAAA,0BAAA,CANA,YAAA,CAEA,eAAA,CADA,gBAAA,CAMA,UAAA,CAJA,uCAAA,CACA,oBAAA,CAIA,8D5B2wFJ,C4BtwFI,0CACE,aAAA,CACA,S5BwwFN,C4BpwFI,6BAEE,kB5BuwFN,C4BzwFI,6BAEE,iB5BuwFN,C4BzwFI,mBAGE,iBAAA,CAFA,Y5BwwFN,C4BjwFM,2CACE,qB5BmwFR,C4BpwFM,2CACE,qB5BswFR,C4BvwFM,2CACE,qB5BywFR,C4B1wFM,2CACE,qB5B4wFR,C4B7wFM,2CACE,oB5B+wFR,C4BhxFM,2CACE,qB5BkxFR,C4BnxFM,2CACE,qB5BqxFR,C4BtxFM,2CACE,qB5BwxFR,C4BzxFM,4CACE,qB5B2xFR,C4B5xFM,4CACE,oB5B8xFR,C4B/xFM,4CACE,qB5BiyFR,C4BlyFM,4CACE,qB5BoyFR,C4BryFM,4CACE,qB5BuyFR,C4BxyFM,4CACE,qB5B0yFR,C4B3yFM,4CACE,oB5B6yFR,C4BvyFI,gCACE,SAAA,CAIA,yBAAA,CAHA,wC5B0yFN,C6B74FA,MACE,mS7Bg5FF,C6Bv4FE,mCACE,mBAAA,CACA,cAAA,CACA,QAAA,CAEA,mBAAA,CADA,kB7B24FJ,C6Bt4FE,oBAGE,kBAAA,CAOA,+CAAA,CACA,oBAAA,CAVA,mBAAA,CAIA,gBAAA,CACA,0BAAA,CACA,eAAA,CALA,QAAA,CAOA,qBAAA,CADA,eAAA,CAJA,wB7B+4FJ,C6Br4FI,0BAGE,uCAAA,CAFA,aAAA,CACA,YAAA,CAEA,6C7Bu4FN,C6Bl4FM,gEAEE,0CAAA,CADA,+B7Bq4FR,C6B/3FI,yBACE,uB7Bi4FN,C6Bz3FI,gCAME,oDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,qCAAA,CAAA,6BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CAPA,0BAAA,CAFA,W7Bo4FN,C6Bv3FI,wFACE,0C7By3FN,C8Bn8FA,iBACE,GACE,oB9Bs8FF,C8Bn8FA,IACE,kB9Bq8FF,C8Bl8FA,GACE,oB9Bo8FF,CACF,C8B57FA,MACE,yNAAA,CACA,sP9B+7FF,C8Bx7FA,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,S9B47FF,C8B16FE,oBAEE,SAAA,CAKA,uBAAA,CAJA,2EACE,CAHF,S9B+6FJ,C8Br6FE,oBAEE,eAAA,CACA,wBAAA,CAAA,gBAAA,CAFA,U9By6FJ,C8Bp6FI,6CACE,qC9Bs6FN,C8Bl6FI,uCAEE,eAAA,CADA,mB9Bq6FN,C8B/5FI,6BACE,Y9Bi6FN,C8B55FE,8CACE,sC9B85FJ,C8B15FE,mBAEE,gBAAA,CADA,a9B65FJ,C8Bz5FI,2CACE,Y9B25FN,C8Bv5FI,0CACE,e9By5FN,C8Bj5FA,eACE,iBAAA,CACA,eAAA,CAIA,YAAA,CAHA,kBAAA,CAEA,0BAAA,CADA,kB9Bs5FF,C8Bj5FE,yBACE,a9Bm5FJ,C8B/4FE,oBACE,sCAAA,CACA,iB9Bi5FJ,C8B74FE,6BACE,oBAAA,CAGA,gB9B64FJ,C8Bz4FE,sBAYE,mBAAA,CANA,cAAA,CAHA,oBAAA,CACA,gBAAA,CAAA,iBAAA,CAIA,YAAA,CAGA,eAAA,CAVA,iBAAA,CAMA,wBAAA,CAAA,gBAAA,CAFA,uBAAA,CAHA,S9Bm5FJ,C8Br4FI,qCACE,uB9Bu4FN,C8Bn4FI,cArBF,sBAsBI,W9Bs4FJ,C8Bn4FI,wCACE,2B9Bq4FN,C8Bj4FI,6BAOE,qCAAA,CACA,+CAAA,CAAA,uC9Bs4FN,C8B53FI,yDAZE,UAAA,CADA,YAAA,CAKA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CACA,SAAA,CAEA,WAAA,CADA,U9B05FN,C8B34FI,4BAOE,oDAAA,CACA,4CAAA,CAAA,oCAAA,CAQA,uBAAA,CAJA,+C9B+3FN,C8Bx3FM,gDACE,uB9B03FR,C8Bt3FM,mFACE,0C9Bw3FR,CACF,C8Bn3FI,0CAGE,2BAAA,CADA,uBAAA,CADA,S9Bu3FN,C8Bj3FI,8CACE,oB9Bm3FN,C8Bh3FM,aAJF,8CASI,8CAAA,CACA,iBAAA,CAHA,gCAAA,CADA,eAAA,CADA,cAAA,CAGA,kB9Bq3FN,C8Bh3FM,oDACE,mC9Bk3FR,CACF,C8Bt2FE,gCAEE,iBAAA,CADA,e9B02FJ,C8Bt2FI,mCACE,iB9Bw2FN,C8Br2FM,oDAEE,a9Bo3FR,C8Bt3FM,oDAEE,c9Bo3FR,C8Bt3FM,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,CARA,S9Bm3FR,C+BnoGA,MACE,wBAAA,CACA,wB/BsoGF,C+BhoGA,aA+BE,kCAAA,CAAA,0BAAA,CAjBA,gCAAA,CADA,sCAAA,CAGA,SAAA,CADA,mBAAA,CAdA,iBAAA,CAGA,wDACE,CAgBF,4BAAA,CAGA,uEACE,CARF,uDACE,CANF,UAAA,CADA,S/BooGF,C+B7mGE,oBAuBE,8CAAA,CAAA,+CAAA,CADA,UAAA,CADA,aAAA,CAfA,gJACE,CANF,iBAAA,CAmBA,S/BimGJ,C+B1lGE,yBAGE,kEAAA,CAFA,gDAAA,CACA,6C/B6lGJ,C+BxlGE,4BAGE,qEAAA,CADA,8CAAA,CADA,6C/B4lGJ,C+BtlGE,qBAEE,SAAA,CAKA,uBAAA,CAJA,wEACE,CAHF,S/B2lGJ,C+BjlGE,oBAqBE,uBAAA,CAEA,2CAAA,CACA,mBAAA,CACA,8BAAA,CAnBA,0FACE,CAaF,eAAA,CADA,8BAAA,CAlBA,iBAAA,CAqBA,oB/BskGJ,C+BhkGI,uCAEE,YAAA,CADA,W/BmkGN,C+B9jGI,6CACE,oD/BgkGN,C+B7jGM,mDACE,0C/B+jGR,C+BvjGI,mCAwBE,eAAA,CACA,eAAA,CAxBA,oIACE,CAgBF,sCACE,CAIF,mBAAA,CAKA,wBAAA,CAAA,gBAAA,CAbA,sBAAA,CAAA,iB/BijGN,C+BhiGI,4CACE,Y/BkiGN,C+B9hGI,2CACE,e/BgiGN,CgCntGA,kBAME,ehC+tGF,CgCruGA,kBAME,gBhC+tGF,CgCruGA,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,ShCkuGF,CgC/sGE,aAtBF,QAuBI,YhCktGF,CACF,CgC/sGE,kBACE,wBhCitGJ,CgC7sGE,gBAEE,SAAA,CADA,mBAAA,CAGA,+BAAA,CADA,uBhCgtGJ,CgC5sGI,0BACE,8BhC8sGN,CgCzsGE,4BAEE,0CAAA,CADA,+BhC4sGJ,CgCvsGE,YACE,oBAAA,CACA,oBhCysGJ,CiC9vGA,oBACE,GACE,mBjCiwGF,CACF,CiCzvGA,MACE,wfjC2vGF,CiCrvGA,YACE,aAAA,CAEA,eAAA,CADA,ajCyvGF,CiCrvGE,+BAOE,kBAAA,CAAA,kBjCsvGJ,CiC7vGE,+BAOE,iBAAA,CAAA,mBjCsvGJ,CiC7vGE,qBAQE,aAAA,CACA,cAAA,CACA,YAAA,CATA,iBAAA,CAKA,UjCuvGJ,CiChvGI,qCAIE,iBjCwvGN,CiC5vGI,qCAIE,kBjCwvGN,CiC5vGI,2BAME,6BAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,WjC0vGN,CiC7uGE,mBACE,iBAAA,CACA,UjC+uGJ,CiC3uGE,kBAWE,2CAAA,CACA,mBAAA,CACA,8BAAA,CALA,gCAAA,CACA,oBAAA,CAHA,kBAAA,CAFA,YAAA,CAUA,SAAA,CAPA,aAAA,CAFA,SAAA,CAJA,iBAAA,CASA,4BAAA,CARA,UAAA,CAaA,+CACE,CAbF,SjCyvGJ,CiCxuGI,+EACE,gBAAA,CACA,SAAA,CACA,sCjC0uGN,CiCpuGI,qCAEE,oCACE,gCjCquGN,CiCjuGI,2CACE,cjCmuGN,CACF,CiC9tGE,kBACE,kBjCguGJ,CiC5tGE,4BAGE,kBAAA,CAAA,oBjCmuGJ,CiCtuGE,4BAGE,mBAAA,CAAA,mBjCmuGJ,CiCtuGE,kBAKE,cAAA,CAJA,aAAA,CAMA,YAAA,CADA,uBAAA,CAEA,2CACE,CALF,kBAAA,CAFA,UjCouGJ,CiCztGI,gDACE,+BjC2tGN,CiCvtGI,wBACE,qDjCytGN,CkC/zGA,MAEI,6VAAA,CAAA,uWAAA,CAAA,qPAAA,CAAA,2xBAAA,CAAA,qMAAA,CAAA,+aAAA,CAAA,2LAAA,CAAA,yPAAA,CAAA,2TAAA,CAAA,oaAAA,CAAA,2SAAA,CAAA,2LlCw1GJ,CkC50GE,4CAME,8CAAA,CACA,4BAAA,CACA,mBAAA,CACA,8BAAA,CAJA,mCAAA,CAJA,iBAAA,CAGA,gBAAA,CADA,iBAAA,CADA,eAAA,CASA,uBAAA,CADA,2BlCg1GJ,CkC50GI,aAdF,4CAeI,elC+0GJ,CACF,CkC50GI,sEACE,gClC80GN,CkCz0GI,gDACE,qBlC20GN,CkCv0GI,gIAEE,iBAAA,CADA,clC00GN,CkCr0GI,4FACE,iBlCu0GN,CkCn0GI,kFACE,elCq0GN,CkCj0GI,0FACE,YlCm0GN,CkC/zGI,8EACE,mBlCi0GN,CkC5zGE,sEAGE,iBAAA,CAAA,mBlCs0GJ,CkCz0GE,sEAGE,kBAAA,CAAA,kBlCs0GJ,CkCz0GE,sEASE,uBlCg0GJ,CkCz0GE,sEASE,wBlCg0GJ,CkCz0GE,sEAUE,4BlC+zGJ,CkCz0GE,4IAWE,6BlC8zGJ,CkCz0GE,sEAWE,4BlC8zGJ,CkCz0GE,kDAOE,0BAAA,CACA,WAAA,CAFA,eAAA,CADA,eAAA,CAHA,oBAAA,CAAA,iBAAA,CADA,iBlCw0GJ,CkC3zGI,kFACE,elC6zGN,CkCzzGI,oFAEE,UlCo0GN,CkCt0GI,oFAEE,WlCo0GN,CkCt0GI,gEAOE,wBhBiIU,CgBlIV,UAAA,CADA,WAAA,CAGA,kDAAA,CAAA,0CAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CACA,UlCk0GN,CkCvzGI,4DACE,4DlCyzGN,CkC3yGE,sDACE,oBlC8yGJ,CkC3yGI,gFACE,gClC6yGN,CkCxyGE,8DACE,0BlC2yGJ,CkCxyGI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClC0yGN,CkCtyGI,0EACE,alCwyGN,CkC7zGE,8DACE,oBlCg0GJ,CkC7zGI,wFACE,gClC+zGN,CkC1zGE,sEACE,0BlC6zGJ,CkC1zGI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClC4zGN,CkCxzGI,kFACE,alC0zGN,CkC/0GE,sDACE,oBlCk1GJ,CkC/0GI,gFACE,gClCi1GN,CkC50GE,8DACE,0BlC+0GJ,CkC50GI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClC80GN,CkC10GI,0EACE,alC40GN,CkCj2GE,oDACE,oBlCo2GJ,CkCj2GI,8EACE,gClCm2GN,CkC91GE,4DACE,0BlCi2GJ,CkC91GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClCg2GN,CkC51GI,wEACE,alC81GN,CkCn3GE,4DACE,oBlCs3GJ,CkCn3GI,sFACE,gClCq3GN,CkCh3GE,oEACE,0BlCm3GJ,CkCh3GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCk3GN,CkC92GI,gFACE,alCg3GN,CkCr4GE,8DACE,oBlCw4GJ,CkCr4GI,wFACE,gClCu4GN,CkCl4GE,sEACE,0BlCq4GJ,CkCl4GI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClCo4GN,CkCh4GI,kFACE,alCk4GN,CkCv5GE,4DACE,oBlC05GJ,CkCv5GI,sFACE,gClCy5GN,CkCp5GE,oEACE,0BlCu5GJ,CkCp5GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCs5GN,CkCl5GI,gFACE,alCo5GN,CkCz6GE,4DACE,oBlC46GJ,CkCz6GI,sFACE,gClC26GN,CkCt6GE,oEACE,0BlCy6GJ,CkCt6GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCw6GN,CkCp6GI,gFACE,alCs6GN,CkC37GE,0DACE,oBlC87GJ,CkC37GI,oFACE,gClC67GN,CkCx7GE,kEACE,0BlC27GJ,CkCx7GI,gFACE,wBAlBG,CAmBH,oDAAA,CAAA,4ClC07GN,CkCt7GI,8EACE,alCw7GN,CkC78GE,oDACE,oBlCg9GJ,CkC78GI,8EACE,gClC+8GN,CkC18GE,4DACE,0BlC68GJ,CkC18GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClC48GN,CkCx8GI,wEACE,alC08GN,CkC/9GE,4DACE,oBlCk+GJ,CkC/9GI,sFACE,gClCi+GN,CkC59GE,oEACE,0BlC+9GJ,CkC59GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClC89GN,CkC19GI,gFACE,alC49GN,CkCj/GE,wDACE,oBlCo/GJ,CkCj/GI,kFACE,gClCm/GN,CkC9+GE,gEACE,0BlCi/GJ,CkC9+GI,8EACE,wBAlBG,CAmBH,mDAAA,CAAA,2ClCg/GN,CkC5+GI,4EACE,alC8+GN,CmClpHA,MACE,qMnCqpHF,CmC5oHE,sBAEE,uCAAA,CADA,gBnCgpHJ,CmC5oHI,mCACE,anC8oHN,CmC/oHI,mCACE,cnC8oHN,CmC1oHM,4BACE,sBnC4oHR,CmCzoHQ,mCACE,gCnC2oHV,CmCvoHQ,2DACE,SAAA,CAEA,uBAAA,CADA,enC0oHV,CmCroHQ,yGACE,SAAA,CACA,uBnCuoHV,CmCnoHQ,yCACE,YnCqoHV,CmC9nHE,0BACE,eAAA,CACA,enCgoHJ,CmC7nHI,+BACE,oBnC+nHN,CmC1nHE,gDACE,YnC4nHJ,CmCxnHE,8BAIE,+BAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,SAAA,CAKA,4BAAA,CAJA,4DACE,CAHF,0BnC4nHJ,CmCnnHI,aAdF,8BAeI,+BAAA,CACA,SAAA,CACA,uBnCsnHJ,CACF,CmCnnHI,wCACE,6BnCqnHN,CmCjnHI,oCACE,+BnCmnHN,CmC/mHI,qCAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,YAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,WnCwnHN,CmC3mHQ,mDACE,oBnC6mHV,CoC3tHE,kCAEE,iBpCiuHJ,CoCnuHE,kCAEE,kBpCiuHJ,CoCnuHE,wBAGE,yCAAA,CAFA,oBAAA,CAGA,SAAA,CACA,mCpC8tHJ,CoCztHI,aAVF,wBAWI,YpC4tHJ,CACF,CoCxtHE,6FAEE,SAAA,CACA,mCpC0tHJ,CoCptHE,4FAEE,+BpCstHJ,CoCltHE,oBACE,yBAAA,CACA,uBAAA,CAGA,yEpCktHJ,CKnlHI,sC+BrHE,qDACE,uBpC2sHN,CACF,CoCtsHE,kEACE,yBpCwsHJ,CoCpsHE,sBACE,0BpCssHJ,CqCjwHE,2BACE,arCowHJ,CK/kHI,0CgCtLF,2BAKI,erCowHJ,CqCjwHI,6BACE,iBrCmwHN,CACF,CqC/vHI,6BAEE,0BAAA,CAAA,2BAAA,CADA,eAAA,CAEA,iBrCiwHN,CqC9vHM,2CACE,kBrCgwHR,CqC1vHI,6CACE,QrC4vHN,CsCxxHE,uBACE,4CtC4xHJ,CsCvxHE,8CAJE,kCAAA,CAAA,0BtC+xHJ,CsC3xHE,uBACE,4CtC0xHJ,CsCrxHE,4BAEE,kCAAA,CAAA,0BAAA,CADA,qCtCwxHJ,CsCpxHI,mCACE,atCsxHN,CsClxHI,kCACE,atCoxHN,CsC/wHE,0BAKE,eAAA,CAJA,aAAA,CAEA,YAAA,CACA,aAAA,CAFA,kBAAA,CAAA,mBtCoxHJ,CsC9wHI,uCACE,etCgxHN,CsC5wHI,sCACE,kBtC8wHN,CuC3zHA,MACE,oLvC8zHF,CuCrzHE,oBAGE,iBAAA,CAEA,gBAAA,CADA,avCuzHJ,CuCnzHI,wCACE,uBvCqzHN,CuCjzHI,gCAEE,eAAA,CADA,gBvCozHN,CuC7yHM,wCACE,mBvC+yHR,CuCzyHE,8BAKE,oBvC6yHJ,CuClzHE,8BAKE,mBvC6yHJ,CuClzHE,8BAUE,4BvCwyHJ,CuClzHE,4DAWE,6BvCuyHJ,CuClzHE,8BAWE,4BvCuyHJ,CuClzHE,oBASE,cAAA,CANA,aAAA,CACA,eAAA,CAIA,evC0yHJ,CuCpyHI,kCACE,uCAAA,CACA,oBvCsyHN,CuClyHI,wCAEE,uCAAA,CADA,YvCqyHN,CuChyHI,oCAEE,WvC6yHN,CuC/yHI,oCAEE,UvC6yHN,CuC/yHI,0BAOE,6BAAA,CADA,UAAA,CADA,WAAA,CAGA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CAUA,sBAAA,CADA,yBAAA,CARA,UvC2yHN,CuC/xHM,oCACE,wBvCiyHR,CuC5xHI,4BACE,YvC8xHN,CuCzxHI,4CACE,YvC2xHN,CwCr3HE,+DACE,sBAAA,CAEA,mBAAA,CACA,0BAAA,CACA,uBxCu3HJ,CwCp3HI,2EAGE,iBAAA,CADA,eAAA,CADA,yBxCw3HN,CwCj3HE,mEACE,0BxCm3HJ,CwC/2HE,oBACE,qBxCi3HJ,CwC72HE,gBACE,oBxC+2HJ,CwC32HE,gBACE,qBxC62HJ,CwCz2HE,iBACE,kBxC22HJ,CwCv2HE,kBACE,kBxCy2HJ,CyCl5HE,6BACE,sCzCq5HJ,CyCl5HE,cACE,yCzCo5HJ,CyCx4HE,sIACE,oCzC04HJ,CyCl4HE,2EACE,qCzCo4HJ,CyC13HE,wGACE,oCzC43HJ,CyCn3HE,yFACE,qCzCq3HJ,CyCh3HE,6BACE,kCzCk3HJ,CyC52HE,6CACE,sCzC82HJ,CyCv2HE,4DACE,sCzCy2HJ,CyCl2HE,4DACE,qCzCo2HJ,CyC31HE,yFACE,qCzC61HJ,CyCr1HE,2EACE,sCzCu1HJ,CyC50HE,wHACE,qCzC80HJ,CyCz0HE,8BAGE,mBAAA,CADA,gBAAA,CADA,gBzC60HJ,CyCx0HE,eACE,4CzC00HJ,CyCv0HE,eACE,4CzCy0HJ,CyCr0HE,gBAIE,+CAAA,CACA,kDAAA,CAJA,aAAA,CAEA,wBAAA,CADA,wBzC00HJ,CyCn0HE,yBAOE,wCAAA,CACA,+DAAA,CACA,4BAAA,CACA,6BAAA,CARA,iBAAA,CAGA,eAAA,CACA,eAAA,CAFA,cAAA,CADA,oCAAA,CAFA,iBzC80HJ,CyCl0HI,6BACE,YzCo0HN,CyCj0HM,kCACE,wBAAA,CACA,yBzCm0HR,CyC7zHE,iCAaE,wCAAA,CACA,+DAAA,CAJA,uCAAA,CACA,0BAAA,CALA,UAAA,CAJA,oBAAA,CAOA,2BAAA,CADA,2BAAA,CADA,2BAAA,CANA,eAAA,CAWA,wBAAA,CAAA,gBAAA,CAPA,SzCs0HJ,CyCpzHE,sBACE,iBAAA,CACA,iBzCszHJ,CyCjzHE,iCAKE,ezC+yHJ,CyC5yHI,sCACE,gBzC8yHN,CyC1yHI,gDACE,YzC4yHN,CyClyHA,gBACE,iBzCqyHF,CyCjyHE,yCACE,aAAA,CACA,SzCmyHJ,CyC9xHE,mBACE,YzCgyHJ,CyC3xHE,oBACE,QzC6xHJ,CyCzxHE,4BACE,WAAA,CACA,SAAA,CACA,ezC2xHJ,CyCxxHI,0CACE,YzC0xHN,CyCpxHE,yBAKE,wCAAA,CAEA,+BAAA,CADA,4BAAA,CAHA,eAAA,CADA,oDAAA,CAEA,wBAAA,CAAA,gBzCyxHJ,CyClxHE,2BAEE,+DAAA,CADA,2BzCqxHJ,CyCjxHI,+BACE,uCAAA,CACA,gBzCmxHN,CyC9wHE,sBACE,MAAA,CACA,WzCgxHJ,CyC3wHA,aACE,azC8wHF,CyCpwHE,4BAEE,aAAA,CADA,YzCwwHJ,CyCpwHI,wDAEE,2BAAA,CADA,wBzCuwHN,CyCjwHE,+BAKE,2CAAA,CAEA,+BAAA,CADA,gCAAA,CADA,sBAAA,CAHA,mBAAA,CACA,gBAAA,CAFA,azCywHJ,CyChwHI,qCAEE,UAAA,CACA,UAAA,CAFA,azCowHN,CK34HI,0CoCsJF,8BACE,iBzCyvHF,CyC/uHE,wSAGE,ezCqvHJ,CyCjvHE,sCAEE,mBAAA,CACA,eAAA,CADA,oBAAA,CADA,kBAAA,CAAA,mBzCqvHJ,CACF,C0CllII,yDAIE,+BAAA,CACA,8BAAA,CAFA,aAAA,CADA,QAAA,CADA,iB1CwlIN,C0ChlII,uBAEE,uCAAA,CADA,c1CmlIN,C0C9hIM,iHAEE,WAlDkB,CAiDlB,kB1CyiIR,C0C1iIM,6HAEE,WAlDkB,CAiDlB,kB1CqjIR,C0CtjIM,6HAEE,WAlDkB,CAiDlB,kB1CikIR,C0ClkIM,oHAEE,WAlDkB,CAiDlB,kB1C6kIR,C0C9kIM,0HAEE,WAlDkB,CAiDlB,kB1CylIR,C0C1lIM,uHAEE,WAlDkB,CAiDlB,kB1CqmIR,C0CtmIM,uHAEE,WAlDkB,CAiDlB,kB1CinIR,C0ClnIM,6HAEE,WAlDkB,CAiDlB,kB1C6nIR,C0C9nIM,yCAEE,WAlDkB,CAiDlB,kB1CioIR,C0CloIM,yCAEE,WAlDkB,CAiDlB,kB1CqoIR,C0CtoIM,0CAEE,WAlDkB,CAiDlB,kB1CyoIR,C0C1oIM,uCAEE,WAlDkB,CAiDlB,kB1C6oIR,C0C9oIM,wCAEE,WAlDkB,CAiDlB,kB1CipIR,C0ClpIM,sCAEE,WAlDkB,CAiDlB,kB1CqpIR,C0CtpIM,wCAEE,WAlDkB,CAiDlB,kB1CypIR,C0C1pIM,oCAEE,WAlDkB,CAiDlB,kB1C6pIR,C0C9pIM,2CAEE,WAlDkB,CAiDlB,kB1CiqIR,C0ClqIM,qCAEE,WAlDkB,CAiDlB,kB1CqqIR,C0CtqIM,oCAEE,WAlDkB,CAiDlB,kB1CyqIR,C0C1qIM,kCAEE,WAlDkB,CAiDlB,kB1C6qIR,C0C9qIM,qCAEE,WAlDkB,CAiDlB,kB1CirIR,C0ClrIM,mCAEE,WAlDkB,CAiDlB,kB1CqrIR,C0CtrIM,qCAEE,WAlDkB,CAiDlB,kB1CyrIR,C0C1rIM,wCAEE,WAlDkB,CAiDlB,kB1C6rIR,C0C9rIM,sCAEE,WAlDkB,CAiDlB,kB1CisIR,C0ClsIM,2CAEE,WAlDkB,CAiDlB,kB1CqsIR,C0C1rIM,iCAEE,WAPkB,CAMlB,iB1C6rIR,C0C9rIM,uCAEE,WAPkB,CAMlB,iB1CisIR,C0ClsIM,mCAEE,WAPkB,CAMlB,iB1CqsIR,C2CvxIA,MACE,2LAAA,CACA,yL3C0xIF,C2CjxIE,wBAKE,mBAAA,CAHA,YAAA,CACA,qBAAA,CACA,YAAA,CAHA,iB3CwxIJ,C2C9wII,8BAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,O3CkxIN,C2C7wIM,qCACE,0B3C+wIR,C2ClvIM,kEACE,0C3CovIR,C2C9uIE,2BAME,uBAAA,CADA,+DAAA,CAJA,YAAA,CACA,cAAA,CACA,aAAA,CACA,oB3CkvIJ,C2C7uII,aATF,2BAUI,gB3CgvIJ,CACF,C2C7uII,cAGE,+BACE,iB3C6uIN,C2C1uIM,sCAQE,qCAAA,CANA,QAAA,CAKA,UAAA,CAHA,aAAA,CAEA,UAAA,CAHA,MAAA,CAFA,iBAAA,CAaA,2CAAA,CALA,2DACE,CAGF,kDAAA,CARA,+B3CkvIR,CACF,C2CpuII,8CACE,Y3CsuIN,C2CluII,iCAUE,+BAAA,CACA,6BAAA,CALA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,gBAAA,CACA,eAAA,CAFA,8BAAA,CAMA,+BAAA,CAGA,2CACE,CANF,kBAAA,CALA,U3C8uIN,C2C/tIM,aAII,6CACE,O3C8tIV,C2C/tIQ,8CACE,O3CiuIV,C2CluIQ,8CACE,O3CouIV,C2CruIQ,8CACE,O3CuuIV,C2CxuIQ,8CACE,O3C0uIV,C2C3uIQ,8CACE,O3C6uIV,C2C9uIQ,8CACE,O3CgvIV,C2CjvIQ,8CACE,O3CmvIV,C2CpvIQ,8CACE,O3CsvIV,C2CvvIQ,+CACE,Q3CyvIV,C2C1vIQ,+CACE,Q3C4vIV,C2C7vIQ,+CACE,Q3C+vIV,C2ChwIQ,+CACE,Q3CkwIV,C2CnwIQ,+CACE,Q3CqwIV,C2CtwIQ,+CACE,Q3CwwIV,C2CzwIQ,+CACE,Q3C2wIV,C2C5wIQ,+CACE,Q3C8wIV,C2C/wIQ,+CACE,Q3CixIV,C2ClxIQ,+CACE,Q3CoxIV,C2CrxIQ,+CACE,Q3CuxIV,CACF,C2ClxIM,uCACE,gC3CoxIR,C2ChxIM,oDACE,a3CkxIR,C2C7wII,yCACE,S3C+wIN,C2C3wIM,2CACE,aAAA,CACA,8B3C6wIR,C2CvwIE,4BACE,U3CywIJ,C2CtwII,aAJF,4BAKI,gB3CywIJ,CACF,C2CrwIE,0BACE,Y3CuwIJ,C2CpwII,aAJF,0BAKI,a3CuwIJ,C2CnwIM,sCACE,O3CqwIR,C2CtwIM,uCACE,O3CwwIR,C2CzwIM,uCACE,O3C2wIR,C2C5wIM,uCACE,O3C8wIR,C2C/wIM,uCACE,O3CixIR,C2ClxIM,uCACE,O3CoxIR,C2CrxIM,uCACE,O3CuxIR,C2CxxIM,uCACE,O3C0xIR,C2C3xIM,uCACE,O3C6xIR,C2C9xIM,wCACE,Q3CgyIR,C2CjyIM,wCACE,Q3CmyIR,C2CpyIM,wCACE,Q3CsyIR,C2CvyIM,wCACE,Q3CyyIR,C2C1yIM,wCACE,Q3C4yIR,C2C7yIM,wCACE,Q3C+yIR,C2ChzIM,wCACE,Q3CkzIR,C2CnzIM,wCACE,Q3CqzIR,C2CtzIM,wCACE,Q3CwzIR,C2CzzIM,wCACE,Q3C2zIR,C2C5zIM,wCACE,Q3C8zIR,CACF,C2CxzII,+FAEE,Q3C0zIN,C2CvzIM,yGACE,wBAAA,CACA,yB3C0zIR,C2CjzIM,2DAEE,wBAAA,CACA,yBAAA,CAFA,Q3CqzIR,C2C9yIM,iEACE,Q3CgzIR,C2C7yIQ,qLAGE,wBAAA,CACA,yBAAA,CAFA,Q3CizIV,C2C3yIQ,6FACE,wBAAA,CACA,yB3C6yIV,C2CxyIM,yDACE,kB3C0yIR,C2CryII,sCACE,Q3CuyIN,C2ClyIE,2BAEE,iBAAA,CAOA,kBAAA,CAHA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,YAAA,CACA,gBAAA,CAEA,mBAAA,CAGA,gCAAA,CAPA,W3C2yIJ,C2CjyII,iCAEE,uDAAA,CADA,+B3CoyIN,C2C/xII,iCAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,8CAAA,CAAA,sCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,+CACE,CATF,U3CyyIN,C2C1xIE,4BAOE,yEACE,CANF,YAAA,CAGA,aAAA,CAFA,qBAAA,CAGA,mBAAA,CALA,iBAAA,CAYA,wBAAA,CATA,Y3CgyIJ,C2CpxII,sCACE,wB3CsxIN,C2ClxII,oCACE,S3CoxIN,C2ChxII,kCAGE,wEACE,CAFF,mBAAA,CADA,O3CoxIN,C2C1wIM,uDACE,8CAAA,CAAA,sC3C4wIR,CKn5II,0CsCqJF,wDAEE,kB3CowIF,C2CtwIA,wDAEE,mB3CowIF,C2CtwIA,8CAGE,eAAA,CAFA,eAAA,CAGA,iC3CkwIF,C2C9vIE,8DACE,mB3CiwIJ,C2ClwIE,8DACE,kB3CiwIJ,C2ClwIE,oDAEE,U3CgwIJ,C2C5vIE,8EAEE,kB3C+vIJ,C2CjwIE,8EAEE,mB3C+vIJ,C2CjwIE,8EAGE,kB3C8vIJ,C2CjwIE,8EAGE,mB3C8vIJ,C2CjwIE,oEACE,U3CgwIJ,C2C1vIE,8EAEE,mB3C6vIJ,C2C/vIE,8EAEE,kB3C6vIJ,C2C/vIE,8EAGE,mB3C4vIJ,C2C/vIE,8EAGE,kB3C4vIJ,C2C/vIE,oEACE,U3C8vIJ,CACF,C2ChvIE,cAHF,olDAII,gC3CmvIF,C2ChvIE,g8GACE,uC3CkvIJ,CACF,C2C7uIA,4sDACE,+B3CgvIF,C2C5uIA,wmDACE,a3C+uIF,C4CnnJA,MACE,qWAAA,CACA,8W5CsnJF,C4C7mJE,4BAEE,oBAAA,CADA,iB5CinJJ,C4C5mJI,sDAEE,S5C+mJN,C4CjnJI,sDAEE,U5C+mJN,C4CjnJI,4CACE,iBAAA,CAEA,S5C8mJN,C4CzmJE,+CAEE,SAAA,CADA,U5C4mJJ,C4CvmJE,kDAEE,W5CknJJ,C4CpnJE,kDAEE,Y5CknJJ,C4CpnJE,wCAOE,qDAAA,CADA,UAAA,CADA,aAAA,CAGA,0CAAA,CAAA,kCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,SAAA,CACA,Y5CgnJJ,C4CrmJE,gEACE,wB1B2Wa,C0B1Wb,mDAAA,CAAA,2C5CumJJ,C6CvpJA,aAQE,wBACE,Y7CspJF,CACF,C8ChqJA,QACE,8DAAA,CAGA,+CAAA,CACA,iEAAA,CACA,oDAAA,CACA,sDAAA,CACA,mDAAA,CAGA,qEAAA,CACA,qEAAA,CACA,wEAAA,CACA,0EAAA,CACA,wEAAA,CACA,yEAAA,CACA,kEAAA,CACA,+DAAA,CACA,oEAAA,CACA,oEAAA,CACA,mEAAA,CACA,gEAAA,CACA,uEAAA,CACA,mEAAA,CACA,qEAAA,CACA,oEAAA,CACA,gEAAA,CACA,wEAAA,CACA,qEAAA,CACA,+D9C8pJF,C8CxpJA,SAEE,kBAAA,CADA,Y9C4pJF,C+C9rJE,kBAUE,cAAA,CATA,YAAA,CACA,kEACE,CAQF,Y/C0rJJ,C+CtrJI,sDACE,gB/CwrJN,C+ClrJI,oFAKE,wDAAA,CACA,mBAAA,CAJA,aAAA,CAEA,QAAA,CADA,aAAA,CAIA,sC/CorJN,C+C/qJM,iOACE,kBAAA,CACA,8B/CkrJR,C+C9qJM,6FACE,iBAAA,CAAA,c/CirJR,C+C7qJM,2HACE,Y/CgrJR,C+C5qJM,wHACE,e/C+qJR,C+ChqJI,yMAGE,eAAA,CAAA,Y/CwqJN,C+C1pJI,ybAOE,W/CgqJN,C+C5pJI,8BACE,eAAA,CAAA,Y/C8pJN,CK1lJI,mC2ChKA,8BACE,UhDkwJJ,CgDnwJE,8BACE,WhDkwJJ,CgDnwJE,8BAGE,kBhDgwJJ,CgDnwJE,8BAGE,iBhDgwJJ,CgDnwJE,oBAKE,mBAAA,CADA,YAAA,CAFA,ahDiwJJ,CgD3vJI,kCACE,WhD8vJN,CgD/vJI,kCACE,UhD8vJN,CgD/vJI,kCAEE,iBAAA,CAAA,chD6vJN,CgD/vJI,kCAEE,aAAA,CAAA,kBhD6vJN,CACF","file":"main.css"} \ No newline at end of file diff --git a/main/assets/stylesheets/main.6f8fc17f.min.css b/main/assets/stylesheets/main.6f8fc17f.min.css new file mode 100644 index 000000000..a0d06b0eb --- /dev/null +++ b/main/assets/stylesheets/main.6f8fc17f.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 h5 code{text-transform:none}.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}.md-typeset ul[type]{list-style-type:revert-layer}[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}.md-typeset ol ol ol ol,.md-typeset ul ol ol ol{list-style-type:upper-alpha}.md-typeset ol ol ol ol ol,.md-typeset ul ol ol ol ol{list-style-type:upper-roman}.md-typeset ol[type],.md-typeset ul[type]{list-style-type:revert-layer}[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:-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{border-radius:100%;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)}.md-typeset .md-code__content{display:grid}@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)}@media print{.md-feedback{display:none}}.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;position:relative}.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{-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-style:normal;font-weight:400;outline:none;text-align:initial;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}:root{--md-tooltip-width:20rem;--md-tooltip-tail:0.3rem}.md-tooltip2{-webkit-backface-visibility:hidden;backface-visibility:hidden;color:var(--md-default-fg-color);font-family:var(--md-text-font-family);opacity:0;pointer-events:none;position:absolute;top:calc(var(--md-tooltip-host-y) + var(--md-tooltip-y));transform:translateY(-.4rem);transform-origin:calc(var(--md-tooltip-host-x) + var(--md-tooltip-x)) 0;transition:transform 0ms .25s,opacity .25s,z-index .25s;width:100%;z-index:0}.md-tooltip2:before{border-left:var(--md-tooltip-tail) solid #0000;border-right:var(--md-tooltip-tail) solid #0000;content:"";display:block;left:clamp(1.5 * .8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-tail),100vw - 2 * var(--md-tooltip-tail) - 1.5 * .8rem);position:absolute;z-index:1}.md-tooltip2--top:before{border-top:var(--md-tooltip-tail) solid var(--md-default-bg-color);bottom:calc(var(--md-tooltip-tail)*-1 + .025rem);filter:drop-shadow(0 1px 0 hsla(0,0%,0%,.05))}.md-tooltip2--bottom:before{border-bottom:var(--md-tooltip-tail) solid var(--md-default-bg-color);filter:drop-shadow(0 -1px 0 hsla(0,0%,0%,.05));top:calc(var(--md-tooltip-tail)*-1 + .025rem)}.md-tooltip2--active{opacity:1;transform:translateY(0);transition:transform .4s cubic-bezier(0,1,.5,1),opacity .25s,z-index 0ms;z-index:2}.md-tooltip2__inner{scrollbar-gutter:stable;background-color:var(--md-default-bg-color);border-radius:.1rem;box-shadow:var(--md-shadow-z2);left:clamp(.8rem,var(--md-tooltip-host-x) - .8rem,100vw - var(--md-tooltip-width) - .8rem);max-height:40vh;max-width:calc(100vw - 1.6rem);position:relative;scrollbar-width:thin}.md-tooltip2__inner::-webkit-scrollbar{height:.2rem;width:.2rem}.md-tooltip2__inner::-webkit-scrollbar-thumb{background-color:var(--md-default-fg-color--lighter)}.md-tooltip2__inner::-webkit-scrollbar-thumb:hover{background-color:var(--md-accent-fg-color)}[role=tooltip]>.md-tooltip2__inner{font-size:.5rem;font-weight:700;left:clamp(.8rem,var(--md-tooltip-host-x) + var(--md-tooltip-x) - var(--md-tooltip-width)/2,100vw - var(--md-tooltip-width) - .8rem);max-width:min(100vw - 2 * .8rem,400px);padding:.2rem .4rem;-webkit-user-select:none;user-select:none;width:-moz-fit-content;width:fit-content}.md-tooltip2__inner.md-typeset>:first-child{margin-top:0}.md-tooltip2__inner.md-typeset>:last-child{margin-bottom:0}[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__alias{margin-left:.3rem;opacity:.7}.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: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]{display:initial}.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)}@media print{.giscus,[id=__comments]{display:none}}: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.6f8fc17f.min.css.map b/main/assets/stylesheets/main.6f8fc17f.min.css.map new file mode 100644 index 000000000..8ba5ce39e --- /dev/null +++ b/main/assets/stylesheets/main.6f8fc17f.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/_code.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/_tooltip2.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/_giscus.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,gBC4yCF,CC1zCA,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,wNAAA,CACA,gNAAA,CACA,iNJgIF,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,CIzGI,oBACE,mBJ2GN,CItGE,eAGE,+DAAA,CAFA,iBAAA,CACA,cJyGJ,CIpGE,cACE,+BAAA,CACA,qBJsGJ,CInGI,mCAEE,sBJoGN,CIhGI,wCACE,+BJkGN,CI/FM,kDACE,uDJiGR,CI5FI,mBACE,kBAAA,CACA,iCJ8FN,CI1FI,4BACE,uCAAA,CACA,oBJ4FN,CIvFE,iDAIE,6BAAA,CACA,aAAA,CAFA,2BJ2FJ,CItFI,aARF,iDASI,oBJ2FJ,CACF,CIvFE,iBAIE,wCAAA,CACA,mBAAA,CACA,kCAAA,CAAA,0BAAA,CAJA,eAAA,CADA,uBAAA,CAEA,qBJ4FJ,CItFI,qCAEE,uCAAA,CADA,YJyFN,CInFE,gBAEE,iBAAA,CACA,eAAA,CAFA,iBJuFJ,CIlFI,qBAWE,kCAAA,CAAA,0BAAA,CADA,eAAA,CATA,aAAA,CAEA,QAAA,CAMA,uCAAA,CALA,aAAA,CAFA,oCAAA,CAKA,yDAAA,CACA,oBAAA,CAFA,iBAAA,CADA,iBJ0FN,CIjFM,2BACE,+CJmFR,CI/EM,wCAEE,YAAA,CADA,WJkFR,CI7EM,8CACE,oDJ+ER,CI5EQ,oDACE,0CJ8EV,CIvEE,gBAOE,4CAAA,CACA,mBAAA,CACA,mKACE,CANF,gCAAA,CAHA,oBAAA,CAEA,eAAA,CADA,uBAAA,CAIA,uBAAA,CADA,qBJ6EJ,CIlEE,iBAGE,6CAAA,CACA,kCAAA,CAAA,0BAAA,CAHA,aAAA,CACA,qBJsEJ,CIhEE,iBAGE,6DAAA,CADA,WAAA,CADA,oBJoEJ,CI9DE,kBACE,WJgEJ,CI5DE,oDAEE,qBJ8DJ,CIhEE,oDAEE,sBJ8DJ,CI1DE,iCACE,kBJ+DJ,CIhEE,iCACE,mBJ+DJ,CIhEE,iCAIE,2DJ4DJ,CIhEE,iCAIE,4DJ4DJ,CIhEE,uBAGE,uCAAA,CADA,aAAA,CAAA,cJ8DJ,CIxDE,eACE,oBJ0DJ,CItDI,qBACE,4BJwDN,CInDE,kDAGE,kBJqDJ,CIxDE,kDAGE,mBJqDJ,CIxDE,8BAEE,SJsDJ,CIlDI,0DACE,iBJqDN,CIjDI,oCACE,2BJoDN,CIjDM,0CACE,2BJoDR,CIjDQ,gDACE,2BJoDV,CIjDU,sDACE,2BJoDZ,CI5CI,0CACE,4BJ+CN,CI3CI,wDACE,kBJ+CN,CIhDI,wDACE,mBJ+CN,CIhDI,oCAEE,kBJ8CN,CI3CM,kGAEE,aJ+CR,CI3CM,0DACE,eJ8CR,CI1CM,4HAEE,kBJ6CR,CI/CM,4HAEE,mBJ6CR,CI/CM,oFACE,kBAAA,CAAA,eJ8CR,CIvCE,yBAEE,mBJyCJ,CI3CE,yBAEE,oBJyCJ,CI3CE,eACE,mBAAA,CAAA,cJ0CJ,CIrCE,kDAIE,WAAA,CADA,cJwCJ,CIhCI,4BAEE,oBJkCN,CI9BI,6BAEE,oBJgCN,CI5BI,kCACE,YJ8BN,CIzBE,mBACE,iBAAA,CAGA,eAAA,CADA,cAAA,CAEA,iBAAA,CAHA,sBAAA,CAAA,iBJ8BJ,CIxBI,uBACE,aAAA,CACA,aJ0BN,CIrBE,uBAGE,iBAAA,CADA,eAAA,CADA,eJyBJ,CInBE,mBACE,cJqBJ,CIjBE,+BAME,2CAAA,CACA,iDAAA,CACA,mBAAA,CAPA,oBAAA,CAGA,gBAAA,CAFA,cAAA,CACA,aAAA,CAEA,iBJsBJ,CIhBI,aAXF,+BAYI,aJmBJ,CACF,CIdI,iCACE,gBJgBN,CITM,8FACE,YJWR,CIPM,4FACE,eJSR,CIJI,8FACE,eJMN,CIHM,kHACE,gBJKR,CIAI,kCAGE,eAAA,CAFA,cAAA,CACA,sBAAA,CAEA,kBJEN,CIEI,kCAGE,qDAAA,CAFA,sBAAA,CACA,kBJCN,CIII,wCACE,iCJFN,CIKM,8CACE,qDAAA,CACA,sDJHR,CIQI,iCACE,iBJNN,CIWE,wCACE,cJTJ,CIYI,wDAIE,gBJJN,CIAI,wDAIE,iBJJN,CIAI,8CAME,UAAA,CALA,oBAAA,CAEA,YAAA,CAIA,oDAAA,CAAA,4CAAA,CACA,6BAAA,CAAA,qBAAA,CACA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CALA,0BAAA,CAHA,WJFN,CIcI,oDACE,oDJZN,CIgBI,mEACE,kDAAA,CACA,yDAAA,CAAA,iDJdN,CIkBI,oEACE,kDAAA,CACA,0DAAA,CAAA,kDJhBN,CIqBE,wBACE,iBAAA,CACA,eAAA,CACA,iBJnBJ,CIuBE,mBACE,oBAAA,CAEA,kBAAA,CADA,eJpBJ,CIwBI,aANF,mBAOI,aJrBJ,CACF,CIwBI,8BACE,aAAA,CAEA,QAAA,CACA,eAAA,CAFA,UJpBN,CKrWI,0CDwYF,uBACE,iBJ/BF,CIkCE,4BACE,eJhCJ,CACF,CMpiBE,uBAOE,kBAAA,CALA,aAAA,CACA,aAAA,CAEA,aAAA,CACA,eAAA,CALA,iBAAA,CAOA,sCACE,CALF,YN0iBJ,CMjiBI,2BACE,aNmiBN,CM/hBI,6BAME,+CAAA,CAFA,yCAAA,CAHA,eAAA,CACA,eAAA,CACA,kBAAA,CAEA,iBNkiBN,CM7hBI,6BAEE,aAAA,CADA,YNgiBN,CM1hBE,wBACE,kBN4hBJ,CMzhBI,4BAIE,kBAAA,CAHA,mCAAA,CAIA,uBNyhBN,CMrhBI,4DAEE,oBAAA,CADA,SNwhBN,CMphBM,oEACE,mBNshBR,CO/kBA,WAGE,0CAAA,CADA,+BAAA,CADA,aPolBF,CO/kBE,aANF,WAOI,YPklBF,CACF,CO/kBE,oBAEE,2CAAA,CADA,gCPklBJ,CO7kBE,kBAGE,eAAA,CADA,iBAAA,CADA,ePilBJ,CO3kBE,6BACE,WPglBJ,COjlBE,6BACE,UPglBJ,COjlBE,mBAEE,aAAA,CACA,cAAA,CACA,uBP6kBJ,CO1kBI,0BACE,YP4kBN,COxkBI,yBACE,UP0kBN,CQ/mBA,KASE,cAAA,CARA,WAAA,CACA,iBRmnBF,CK/cI,oCGtKJ,KAaI,gBR4mBF,CACF,CKpdI,oCGtKJ,KAkBI,cR4mBF,CACF,CQvmBA,KASE,2CAAA,CAPA,YAAA,CACA,qBAAA,CAKA,eAAA,CAHA,eAAA,CAJA,iBAAA,CAGA,UR6mBF,CQrmBE,aAZF,KAaI,aRwmBF,CACF,CKrdI,0CGhJF,yBAII,cRqmBJ,CACF,CQ5lBA,SAEE,gBAAA,CAAA,iBAAA,CADA,eRgmBF,CQ3lBA,cACE,YAAA,CAEA,qBAAA,CADA,WR+lBF,CQ3lBE,aANF,cAOI,aR8lBF,CACF,CQ1lBA,SACE,WR6lBF,CQ1lBE,gBACE,YAAA,CACA,WAAA,CACA,iBR4lBJ,CQvlBA,aACE,eAAA,CACA,sBR0lBF,CQjlBA,WACE,YRolBF,CQ/kBA,WAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,ORolBF,CQ/kBE,uCACE,aRilBJ,CQ7kBE,+BAEE,uCAAA,CADA,kBRglBJ,CQ1kBA,SASE,2CAAA,CACA,mBAAA,CAFA,gCAAA,CADA,gBAAA,CADA,YAAA,CAMA,SAAA,CADA,uCAAA,CANA,mBAAA,CAJA,cAAA,CAYA,2BAAA,CATA,URolBF,CQxkBE,eAEE,SAAA,CAIA,uBAAA,CAHA,oEACE,CAHF,UR6kBJ,CQ/jBA,MACE,WRkkBF,CS3tBA,MACE,6PT6tBF,CSvtBA,cASE,mBAAA,CAFA,0CAAA,CACA,cAAA,CAFA,YAAA,CAIA,uCAAA,CACA,oBAAA,CAVA,iBAAA,CAEA,UAAA,CADA,QAAA,CAUA,qBAAA,CAPA,WAAA,CADA,STkuBF,CSvtBE,aAfF,cAgBI,YT0tBF,CACF,CSvtBE,kCAEE,uCAAA,CADA,YT0tBJ,CSrtBE,qBACE,uCTutBJ,CSntBE,wCACE,+BTqtBJ,CShtBE,oBAME,6BAAA,CADA,UAAA,CAJA,aAAA,CAEA,cAAA,CACA,aAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,aT0tBJ,CS9sBE,sBACE,cTgtBJ,CS7sBI,2BACE,2CT+sBN,CSzsBI,kEAEE,uDAAA,CADA,+BT4sBN,CU9wBE,8BACE,YVixBJ,CWtxBA,mBACE,GACE,SAAA,CACA,0BXyxBF,CWtxBA,GACE,SAAA,CACA,uBXwxBF,CACF,CWpxBA,mBACE,GACE,SXsxBF,CWnxBA,GACE,SXqxBF,CACF,CW1wBE,qBASE,2BAAA,CAFA,mCAAA,CAAA,2BAAA,CADA,0BAAA,CADA,WAAA,CAGA,SAAA,CAPA,cAAA,CACA,KAAA,CAEA,UAAA,CADA,SXkxBJ,CWxwBE,mBAcE,mDAAA,CANA,2CAAA,CACA,QAAA,CACA,mBAAA,CARA,QAAA,CASA,kDACE,CAPF,eAAA,CAEA,aAAA,CADA,SAAA,CALA,cAAA,CAGA,UAAA,CADA,SXmxBJ,CWpwBE,kBACE,aXswBJ,CWlwBE,sBACE,YAAA,CACA,YXowBJ,CWjwBI,oCACE,aXmwBN,CW9vBE,sBACE,mBXgwBJ,CW7vBI,6CACE,cX+vBN,CKzpBI,0CMvGA,6CAKI,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,UXiwBN,CACF,CW1vBE,kBACE,cX4vBJ,CY71BA,YACE,WAAA,CAIA,WZ61BF,CY11BE,mBAEE,qBAAA,CADA,iBZ61BJ,CKhsBI,sCOtJE,4EACE,kBZy1BN,CYr1BI,0JACE,mBZu1BN,CYx1BI,8EACE,kBZu1BN,CACF,CYl1BI,0BAGE,UAAA,CAFA,aAAA,CACA,YZq1BN,CYh1BI,+BACE,eZk1BN,CY50BE,8BACE,WZi1BJ,CYl1BE,8BACE,UZi1BJ,CYl1BE,8BAIE,iBZ80BJ,CYl1BE,8BAIE,kBZ80BJ,CYl1BE,oBAGE,cAAA,CADA,SZg1BJ,CY30BI,aAPF,oBAQI,YZ80BJ,CACF,CY30BI,gCACE,yCZ60BN,CYz0BI,wBACE,cAAA,CACA,kBZ20BN,CYx0BM,kCACE,oBZ00BR,Ca34BA,qBAEE,Wby5BF,Ca35BA,qBAEE,Uby5BF,Ca35BA,WAQE,2CAAA,CACA,mBAAA,CANA,YAAA,CAOA,8BAAA,CALA,iBAAA,CAMA,SAAA,CALA,mBAAA,CACA,mBAAA,CANA,cAAA,CAcA,0BAAA,CAHA,wCACE,CATF,Sbu5BF,Caz4BE,aAlBF,WAmBI,Yb44BF,CACF,Caz4BE,mBAEE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,kEb44BJ,Car4BE,kBAEE,gCAAA,CADA,ebw4BJ,Cc16BA,aACE,gBAAA,CACA,iBd66BF,Cc16BE,sBAGE,WAAA,CADA,QAAA,CADA,Sd86BJ,Ccx6BE,oBAEE,eAAA,CADA,ed26BJ,Cct6BE,oBACE,iBdw6BJ,Ccp6BE,mBAEE,YAAA,CACA,cAAA,CACA,6BAAA,CAHA,iBdy6BJ,Ccn6BI,iDACE,yCdq6BN,Ccj6BI,6BACE,iBdm6BN,Cc95BE,mBAGE,uCAAA,CACA,cAAA,CAHA,aAAA,CACA,cAAA,CAGA,sBdg6BJ,Cc75BI,gDACE,+Bd+5BN,Cc35BI,4BACE,0CAAA,CACA,mBd65BN,Ccx5BE,mBAEE,SAAA,CADA,iBAAA,CAKA,2BAAA,CAHA,8Dd25BJ,Ccr5BI,qBAEE,aAAA,CADA,edw5BN,Ccn5BI,6BACE,SAAA,CACA,uBdq5BN,Cch5BE,aAnFF,aAoFI,Ydm5BF,CACF,Cex+BA,WAEE,0CAAA,CADA,+Bf4+BF,Cex+BE,aALF,WAMI,Yf2+BF,CACF,Cex+BE,kBACE,6BAAA,CAEA,aAAA,CADA,af2+BJ,Cev+BI,gCACE,Yfy+BN,Cep+BE,iBAOE,eAAA,CANA,YAAA,CAKA,cAAA,CAGA,mBAAA,CAAA,eAAA,CADA,cAAA,CAGA,uCAAA,CADA,eAAA,CAEA,uBfk+BJ,Ce/9BI,8CACE,Ufi+BN,Ce79BI,+BACE,oBf+9BN,CKj1BI,0CUvIE,uBACE,af29BN,Cex9BM,yCACE,Yf09BR,CACF,Cer9BI,iCACE,gBfw9BN,Cez9BI,iCACE,iBfw9BN,Cez9BI,uBAEE,gBfu9BN,Cep9BM,iCACE,efs9BR,Ceh9BE,kBACE,WAAA,CAIA,eAAA,CADA,mBAAA,CAFA,6BAAA,CACA,cAAA,CAGA,kBfk9BJ,Ce98BE,mBAEE,YAAA,CADA,afi9BJ,Ce58BE,sBACE,gBAAA,CACA,Uf88BJ,Cez8BA,gBACE,gDf48BF,Cez8BE,uBACE,YAAA,CACA,cAAA,CACA,6BAAA,CACA,af28BJ,Cev8BE,kCACE,sCfy8BJ,Cet8BI,gFACE,+Bfw8BN,Ceh8BA,cAKE,wCAAA,CADA,gBAAA,CADA,iBAAA,CADA,eAAA,CADA,Ufu8BF,CK35BI,mCU7CJ,cASI,Ufm8BF,CACF,Ce/7BE,yBACE,sCfi8BJ,Ce17BA,WACE,mBAAA,CACA,SAAA,CAEA,cAAA,CADA,qBf87BF,CK16BI,mCUvBJ,WAQI,ef67BF,CACF,Ce17BE,iBACE,oBAAA,CAEA,aAAA,CACA,iBAAA,CAFA,Yf87BJ,Cez7BI,wBACE,ef27BN,Cev7BI,qBAGE,iBAAA,CAFA,gBAAA,CACA,mBf07BN,CgBhmCE,uBAME,kBAAA,CACA,mBAAA,CAHA,gCAAA,CACA,cAAA,CAJA,oBAAA,CAEA,eAAA,CADA,kBAAA,CAMA,gEhBmmCJ,CgB7lCI,gCAEE,2CAAA,CACA,uCAAA,CAFA,gChBimCN,CgB3lCI,0DAEE,0CAAA,CACA,sCAAA,CAFA,+BhB+lCN,CgBxlCE,gCAKE,4BhB6lCJ,CgBlmCE,gEAME,6BhB4lCJ,CgBlmCE,gCAME,4BhB4lCJ,CgBlmCE,sBAIE,6DAAA,CAGA,8BAAA,CAJA,eAAA,CAFA,aAAA,CACA,eAAA,CAMA,sChB0lCJ,CgBrlCI,wDACE,6CAAA,CACA,8BhBulCN,CgBnlCI,+BACE,UhBqlCN,CiBxoCA,WAOE,2CAAA,CAGA,8CACE,CALF,gCAAA,CADA,aAAA,CAHA,MAAA,CADA,eAAA,CACA,OAAA,CACA,KAAA,CACA,SjB+oCF,CiBpoCE,aAfF,WAgBI,YjBuoCF,CACF,CiBpoCE,mBAIE,2BAAA,CAHA,iEjBuoCJ,CiBhoCE,mBACE,kDACE,CAEF,kEjBgoCJ,CiB1nCE,kBAEE,kBAAA,CADA,YAAA,CAEA,ejB4nCJ,CiBxnCE,mBAKE,kBAAA,CAEA,cAAA,CAHA,YAAA,CAIA,uCAAA,CALA,aAAA,CAFA,iBAAA,CAQA,uBAAA,CAHA,qBAAA,CAJA,SjBioCJ,CiBvnCI,yBACE,UjBynCN,CiBrnCI,iCACE,oBjBunCN,CiBnnCI,uCAEE,uCAAA,CADA,YjBsnCN,CiBjnCI,2BAEE,YAAA,CADA,ajBonCN,CKtgCI,0CY/GA,2BAMI,YjBmnCN,CACF,CiBhnCM,8DAIE,iBAAA,CAHA,aAAA,CAEA,aAAA,CADA,UjBonCR,CKpiCI,mCYzEA,iCAII,YjB6mCN,CACF,CiB1mCM,wCACE,YjB4mCR,CiBxmCM,+CACE,oBjB0mCR,CK/iCI,sCYtDA,iCAII,YjBqmCN,CACF,CiBhmCE,kBAEE,YAAA,CACA,cAAA,CAFA,iBAAA,CAIA,8DACE,CAFF,kBjBmmCJ,CiB7lCI,oCAGE,SAAA,CADA,mBAAA,CAKA,6BAAA,CAHA,8DACE,CAJF,UjBmmCN,CiB1lCM,8CACE,8BjB4lCR,CiBvlCI,8BACE,ejBylCN,CiBplCE,4BAGE,gBAAA,CAAA,kBjBwlCJ,CiB3lCE,4BAGE,iBAAA,CAAA,iBjBwlCJ,CiB3lCE,kBACE,WAAA,CAGA,eAAA,CAFA,aAAA,CAGA,kBjBslCJ,CiBnlCI,4CAGE,SAAA,CADA,mBAAA,CAKA,8BAAA,CAHA,8DACE,CAJF,UjBylCN,CiBhlCM,sDACE,6BjBklCR,CiB9kCM,8DAGE,SAAA,CADA,mBAAA,CAKA,uBAAA,CAHA,8DACE,CAJF,SjBolCR,CiBzkCI,uCAGE,WAAA,CAFA,iBAAA,CACA,UjB4kCN,CiBtkCE,mBACE,YAAA,CACA,aAAA,CACA,cAAA,CAEA,+CACE,CAFF,kBjBykCJ,CiBnkCI,8DACE,WAAA,CACA,SAAA,CACA,oCjBqkCN,CiB5jCI,yBACE,QjB8jCN,CiBzjCE,mBACE,YjB2jCJ,CKvnCI,mCY2DF,6BAQI,gBjB2jCJ,CiBnkCA,6BAQI,iBjB2jCJ,CiBnkCA,mBAKI,aAAA,CAEA,iBAAA,CADA,ajB6jCJ,CACF,CK/nCI,sCY2DF,6BAaI,kBjB2jCJ,CiBxkCA,6BAaI,mBjB2jCJ,CACF,CD1yCA,SAGE,uCAAA,CAFA,eAAA,CACA,eC8yCF,CD1yCE,eACE,mBAAA,CACA,cAAA,CAGA,eAAA,CADA,QAAA,CADA,SC8yCJ,CDxyCE,sCAEE,WAAA,CADA,iBAAA,CAAA,kBC2yCJ,CDtyCE,eACE,+BCwyCJ,CDryCI,0CACE,+BCuyCN,CDjyCA,UAKE,wBmBaa,CnBZb,oBAAA,CAFA,UAAA,CAHA,oBAAA,CAEA,eAAA,CADA,0BAAA,CAAA,2BCwyCF,CmB10CA,MACE,uMAAA,CACA,sLAAA,CACA,iNnB60CF,CmBv0CA,QACE,eAAA,CACA,enB00CF,CmBv0CE,eAKE,uCAAA,CAJA,aAAA,CAGA,eAAA,CADA,eAAA,CADA,eAAA,CAIA,sBnBy0CJ,CmBt0CI,+BACE,YnBw0CN,CmBr0CM,mCAEE,WAAA,CADA,UnBw0CR,CmBh0CQ,sFAME,iBAAA,CALA,aAAA,CAGA,aAAA,CADA,cAAA,CAEA,kBAAA,CAHA,UnBs0CV,CmB3zCE,cAGE,eAAA,CADA,QAAA,CADA,SnB+zCJ,CmBzzCE,cAGE,sBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBAAA,CACA,uBAAA,CACA,sBnB2zCJ,CmBxzCI,sBACE,uCnB0zCN,CmBnzCM,6EAEE,+BnBqzCR,CmBhzCI,2BAIE,iBnB+yCN,CmB3yCI,4CACE,gBnB6yCN,CmB9yCI,4CACE,iBnB6yCN,CmBzyCI,kBAME,iBAAA,CAFA,aAAA,CACA,YAAA,CAFA,iBnB4yCN,CmBryCI,sGACE,+BAAA,CACA,cnBuyCN,CmBnyCI,4BACE,uCAAA,CACA,oBnBqyCN,CmBjyCI,0CACE,YnBmyCN,CmBhyCM,yDAIE,6BAAA,CAHA,aAAA,CAEA,WAAA,CAEA,qCAAA,CAAA,6BAAA,CAHA,UnBqyCR,CmB9xCM,kDACE,YnBgyCR,CmB1xCE,iCACE,YnB4xCJ,CmBzxCI,6CACE,WAAA,CAGA,WnByxCN,CmBpxCE,cACE,anBsxCJ,CmBlxCE,gBACE,YnBoxCJ,CKrvCI,0CcxBA,0CASE,2CAAA,CAHA,YAAA,CACA,qBAAA,CACA,WAAA,CALA,MAAA,CADA,iBAAA,CACA,OAAA,CACA,KAAA,CACA,SnBmxCJ,CmBxwCI,+DACE,eAAA,CACA,enB0wCN,CmBtwCI,gCAQE,qDAAA,CAHA,uCAAA,CAEA,cAAA,CALA,aAAA,CAEA,kBAAA,CADA,wBAAA,CAFA,iBAAA,CAKA,kBnB0wCN,CmBrwCM,wDAEE,UnB4wCR,CmB9wCM,wDAEE,WnB4wCR,CmB9wCM,8CAIE,aAAA,CAEA,aAAA,CACA,YAAA,CANA,iBAAA,CAEA,SAAA,CAEA,YnBywCR,CmBpwCQ,oDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnB6wCV,CmBjwCM,8CAIE,2CAAA,CACA,gEACE,CALF,eAAA,CAEA,4BAAA,CADA,kBnBswCR,CmB/vCQ,2DACE,YnBiwCV,CmB5vCM,8CAGE,2CAAA,CADA,gCAAA,CADA,enBgwCR,CmB1vCM,yCAIE,aAAA,CAFA,UAAA,CAIA,YAAA,CADA,aAAA,CAJA,iBAAA,CACA,WAAA,CACA,SnB+vCR,CmBvvCI,+BACE,MnByvCN,CmBrvCI,+BACE,4DnBuvCN,CmBpvCM,qDACE,+BnBsvCR,CmBnvCQ,sHACE,+BnBqvCV,CmB/uCI,+BAEE,YAAA,CADA,mBnBkvCN,CmB9uCM,mCACE,enBgvCR,CmB5uCM,6CACE,SnB8uCR,CmB1uCM,uDAGE,mBnB6uCR,CmBhvCM,uDAGE,kBnB6uCR,CmBhvCM,6CAIE,gBAAA,CAFA,aAAA,CADA,YnB+uCR,CmBzuCQ,mDAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UnBkvCV,CmBluCM,+CACE,mBnBouCR,CmB5tCM,4CAEE,wBAAA,CADA,enB+tCR,CmB3tCQ,oEACE,mBnB6tCV,CmB9tCQ,oEACE,oBnB6tCV,CmBztCQ,4EACE,iBnB2tCV,CmB5tCQ,4EACE,kBnB2tCV,CmBvtCQ,oFACE,mBnBytCV,CmB1tCQ,oFACE,oBnBytCV,CmBrtCQ,4FACE,mBnButCV,CmBxtCQ,4FACE,oBnButCV,CmBhtCE,mBACE,wBnBktCJ,CmB9sCE,wBACE,YAAA,CACA,SAAA,CAIA,0BAAA,CAHA,oEnBitCJ,CmB3sCI,kCACE,2BnB6sCN,CmBxsCE,gCACE,SAAA,CAIA,uBAAA,CAHA,qEnB2sCJ,CmBrsCI,8CAEE,kCAAA,CAAA,0BnBssCN,CACF,CKx4CI,0Cc0MA,0CACE,YnBisCJ,CmB9rCI,yDACE,UnBgsCN,CmB5rCI,wDACE,YnB8rCN,CmB1rCI,kDACE,YnB4rCN,CmBvrCE,gBAIE,iDAAA,CADA,gCAAA,CAFA,aAAA,CACA,enB2rCJ,CACF,CKr8CM,+DcmRF,6CACE,YnBqrCJ,CmBlrCI,4DACE,UnBorCN,CmBhrCI,2DACE,YnBkrCN,CmB9qCI,qDACE,YnBgrCN,CACF,CK77CI,mCc7JJ,QAgbI,oBnB8qCF,CmBxqCI,kCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnB0qCN,CmBrqCM,6CACE,uBnBuqCR,CmBnqCM,gDACE,YnBqqCR,CmBhqCI,2CACE,kBnBmqCN,CmBpqCI,2CACE,mBnBmqCN,CmBpqCI,iCAEE,oBnBkqCN,CmB3pCI,yDACE,kBnB6pCN,CmB9pCI,yDACE,iBnB6pCN,CACF,CKt9CI,sCc7JJ,QA4dI,oBAAA,CACA,oDnB2pCF,CmBrpCI,gCAME,qCAAA,CACA,qDAAA,CANA,eAAA,CACA,KAAA,CAGA,SnBupCN,CmBlpCM,8CACE,uBnBopCR,CmBhpCM,8CACE,YnBkpCR,CmB7oCI,yCACE,kBnBgpCN,CmBjpCI,yCACE,mBnBgpCN,CmBjpCI,+BAEE,oBnB+oCN,CmBxoCI,uDACE,kBnB0oCN,CmB3oCI,uDACE,iBnB0oCN,CmBroCE,wBACE,YAAA,CACA,sBAAA,CAEA,SAAA,CACA,6FACE,CAHF,mBnByoCJ,CmBjoCI,sCACE,enBmoCN,CmB9nCE,iFACE,sBAAA,CAEA,SAAA,CACA,4FACE,CAHF,kBnBkoCJ,CmBznCE,iDACE,enB2nCJ,CmBvnCE,6CACE,YnBynCJ,CmBrnCE,uBACE,aAAA,CACA,enBunCJ,CmBpnCI,kCACE,enBsnCN,CmBlnCI,qCACE,enBonCN,CmBjnCM,0CACE,uCnBmnCR,CmB/mCM,6DACE,mBnBinCR,CmB7mCM,yFAEE,YnB+mCR,CmB1mCI,yCAEE,kBnB8mCN,CmBhnCI,yCAEE,mBnB8mCN,CmBhnCI,+BACE,aAAA,CAGA,SAAA,CADA,kBnB6mCN,CmBzmCM,2DACE,SnB2mCR,CmBrmCE,cAGE,kBAAA,CADA,YAAA,CAEA,gCAAA,CAHA,WnB0mCJ,CmBpmCI,oBACE,uDnBsmCN,CmBlmCI,oBAME,6BAAA,CACA,kBAAA,CAFA,UAAA,CAJA,oBAAA,CAEA,WAAA,CAKA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,yBAAA,CARA,qBAAA,CAFA,UnB8mCN,CmBjmCM,8BACE,wBnBmmCR,CmB/lCM,kKAEE,uBnBgmCR,CmBllCI,2EACE,YnBulCN,CmBplCM,oDACE,anBslCR,CmBnlCQ,kEAKE,qCAAA,CACA,qDAAA,CAFA,YAAA,CAHA,eAAA,CACA,KAAA,CACA,SnBwlCV,CmBllCU,0FACE,mBnBolCZ,CmB/kCQ,0EACE,QnBilCV,CmB5kCM,sFACE,kBnB8kCR,CmB/kCM,sFACE,mBnB8kCR,CmB1kCM,kDACE,uCnB4kCR,CmBtkCI,2CACE,sBAAA,CAEA,SAAA,CADA,kBnBykCN,CmBhkCI,qFAIE,mDnBmkCN,CmBvkCI,qFAIE,oDnBmkCN,CmBvkCI,2EACE,aAAA,CACA,oBAAA,CAGA,SAAA,CAFA,kBnBokCN,CmB/jCM,yFAEE,gBAAA,CADA,gBnBkkCR,CmB7jCM,0FACE,YnB+jCR,CACF,CoBtxDA,eAKE,eAAA,CACA,eAAA,CAJA,SpB6xDF,CoBtxDE,gCANA,kBAAA,CAFA,YAAA,CAGA,sBpBoyDF,CoB/xDE,iBAOE,mBAAA,CAFA,aAAA,CADA,gBAAA,CAEA,iBpByxDJ,CoBpxDE,wBAEE,qDAAA,CADA,uCpBuxDJ,CoBlxDE,qBACE,6CpBoxDJ,CoB/wDI,sDAEE,uDAAA,CADA,+BpBkxDN,CoB9wDM,8DACE,+BpBgxDR,CoB3wDI,mCACE,uCAAA,CACA,oBpB6wDN,CoBzwDI,yBAKE,iBAAA,CADA,yCAAA,CAHA,aAAA,CAEA,eAAA,CADA,YpB8wDN,CqB9zDE,eAGE,+DAAA,CADA,oBAAA,CADA,qBrBm0DJ,CK9oDI,0CgBtLF,eAOI,YrBi0DJ,CACF,CqB3zDM,6BACE,oBrB6zDR,CqBvzDE,kBACE,YAAA,CACA,qBAAA,CACA,SAAA,CACA,qBrByzDJ,CqBlzDI,0BACE,sBrBozDN,CqBjzDM,gEACE,+BrBmzDR,CqB7yDE,gBAEE,uCAAA,CADA,erBgzDJ,CqB3yDE,kBACE,oBrB6yDJ,CqB1yDI,mCAGE,kBAAA,CAFA,YAAA,CACA,SAAA,CAEA,iBrB4yDN,CqBxyDI,oCAIE,kBAAA,CAHA,mBAAA,CACA,kBAAA,CACA,SAAA,CAGA,QAAA,CADA,iBrB2yDN,CqBtyDI,0DACE,kBrBwyDN,CqBzyDI,0DACE,iBrBwyDN,CqBpyDI,iDACE,uBAAA,CAEA,YrBqyDN,CqBhyDE,4BACE,YrBkyDJ,CqB3xDA,YAGE,kBAAA,CAFA,YAAA,CAIA,eAAA,CAHA,SAAA,CAIA,eAAA,CAFA,UrBgyDF,CqB3xDE,yBACE,WrB6xDJ,CqBtxDA,kBACE,YrByxDF,CKjtDI,0CgBzEJ,kBAKI,wBrByxDF,CACF,CqBtxDE,qCACE,WrBwxDJ,CK5uDI,sCgB7CF,+CAKI,kBrBwxDJ,CqB7xDA,+CAKI,mBrBwxDJ,CACF,CK9tDI,0CgBrDJ,6BAMI,SAAA,CAFA,eAAA,CACA,UrBqxDF,CqBlxDE,qDACE,gBrBoxDJ,CqBjxDE,gDACE,SrBmxDJ,CqBhxDE,4CACE,iBAAA,CAAA,kBrBkxDJ,CqB/wDE,2CAEE,WAAA,CADA,crBkxDJ,CqB9wDE,2CACE,mBAAA,CACA,cAAA,CACA,SAAA,CACA,oBAAA,CAAA,iBrBgxDJ,CqB7wDE,2CACE,SrB+wDJ,CqB5wDE,qCAEE,WAAA,CACA,eAAA,CAFA,erBgxDJ,CACF,CsB17DA,MACE,qBAAA,CACA,yBtB67DF,CsBv7DA,aAME,qCAAA,CADA,cAAA,CAEA,0FACE,CAPF,cAAA,CACA,KAAA,CAaA,mDAAA,CACA,qBAAA,CAJA,wFACE,CATF,UAAA,CADA,StBi8DF,CuB58DA,MACE,mfvB+8DF,CuBz8DA,WACE,iBvB48DF,CK9yDI,mCkB/JJ,WAKI,evB48DF,CACF,CuBz8DE,kBACE,YvB28DJ,CuBv8DE,oBAEE,SAAA,CADA,SvB08DJ,CKvyDI,0CkBpKF,8BAOI,YvBk9DJ,CuBz9DA,8BAOI,avBk9DJ,CuBz9DA,oBAaI,2CAAA,CACA,kBAAA,CAJA,WAAA,CACA,eAAA,CACA,mBAAA,CANA,iBAAA,CAEA,SAAA,CAUA,uBAAA,CAHA,4CACE,CAPF,UvBg9DJ,CuBp8DI,+DACE,SAAA,CACA,oCvBs8DN,CACF,CK70DI,mCkBjJF,8BAgCI,MvBy8DJ,CuBz+DA,8BAgCI,OvBy8DJ,CuBz+DA,oBAqCI,0BAAA,CADA,cAAA,CADA,QAAA,CAJA,cAAA,CAEA,KAAA,CAKA,sDACE,CALF,OvBu8DJ,CuB77DI,+DAME,YAAA,CACA,SAAA,CACA,4CACE,CARF,UvBk8DN,CACF,CK50DI,0CkBxGA,+DAII,mBvBo7DN,CACF,CK13DM,+DkB/DF,+DASI,mBvBo7DN,CACF,CK/3DM,+DkB/DF,+DAcI,mBvBo7DN,CACF,CuB/6DE,kBAEE,kCAAA,CAAA,0BvBg7DJ,CK91DI,0CkBpFF,4BAOI,MvBw7DJ,CuB/7DA,4BAOI,OvBw7DJ,CuB/7DA,kBAWI,QAAA,CAEA,SAAA,CADA,eAAA,CANA,cAAA,CAEA,KAAA,CAWA,wBAAA,CALA,qGACE,CALF,OAAA,CADA,SvBs7DJ,CuBz6DI,4BACE,yBvB26DN,CuBv6DI,6DAEE,WAAA,CACA,SAAA,CAMA,uBAAA,CALA,sGACE,CAJF,UvB66DN,CACF,CKz4DI,mCkBjEF,4BA2CI,WvBu6DJ,CuBl9DA,4BA2CI,UvBu6DJ,CuBl9DA,kBA6CI,eAAA,CAHA,iBAAA,CAIA,8CAAA,CAFA,avBs6DJ,CACF,CKx6DM,+DkBOF,6DAII,avBi6DN,CACF,CKv5DI,sCkBfA,6DASI,avBi6DN,CACF,CuB55DE,iBAIE,2CAAA,CACA,0BAAA,CAFA,aAAA,CAFA,iBAAA,CAKA,2CACE,CALF,SvBk6DJ,CKp6DI,mCkBAF,iBAaI,0BAAA,CACA,mBAAA,CAFA,avB85DJ,CuBz5DI,uBACE,0BvB25DN,CACF,CuBv5DI,4DAEE,2CAAA,CACA,6BAAA,CACA,8BAAA,CAHA,gCvB45DN,CuBp5DE,4BAKE,mBAAA,CAAA,oBvBy5DJ,CuB95DE,4BAKE,mBAAA,CAAA,oBvBy5DJ,CuB95DE,kBAQE,gBAAA,CAFA,eAAA,CAFA,WAAA,CAHA,iBAAA,CAMA,sBAAA,CAJA,UAAA,CADA,SvB45DJ,CuBn5DI,+BACE,qBvBq5DN,CuBj5DI,kEAEE,uCvBk5DN,CuB94DI,6BACE,YvBg5DN,CKp7DI,0CkBaF,kBA8BI,eAAA,CADA,aAAA,CADA,UvBi5DJ,CACF,CK98DI,mCkBgCF,4BAmCI,mBvBi5DJ,CuBp7DA,4BAmCI,oBvBi5DJ,CuBp7DA,kBAqCI,aAAA,CADA,evBg5DJ,CuB54DI,+BACE,uCvB84DN,CuB14DI,mCACE,gCvB44DN,CuBx4DI,6DACE,kBvB04DN,CuBv4DM,8EACE,uCvBy4DR,CuBr4DM,0EACE,WvBu4DR,CACF,CuBj4DE,iBAIE,cAAA,CAHA,oBAAA,CAEA,aAAA,CAEA,kCACE,CAJF,YvBs4DJ,CuB93DI,uBACE,UvBg4DN,CuB53DI,yCAEE,UvBg4DN,CuBl4DI,yCAEE,WvBg4DN,CuBl4DI,+BACE,iBAAA,CAEA,SAAA,CACA,SvB83DN,CuB33DM,6CACE,oBvB63DR,CKp+DI,0CkB+FA,yCAaI,UvB63DN,CuB14DE,yCAaI,WvB63DN,CuB14DE,+BAcI,SvB43DN,CuBz3DM,+CACE,YvB23DR,CACF,CKhgEI,mCkBkHA,+BAwBI,mBvB03DN,CuBv3DM,8CACE,YvBy3DR,CACF,CuBn3DE,8BAEE,WvBw3DJ,CuB13DE,8BAEE,UvBw3DJ,CuB13DE,oBAKE,mBAAA,CAJA,iBAAA,CAEA,SAAA,CACA,SvBs3DJ,CK5/DI,0CkBkIF,8BASI,WvBs3DJ,CuB/3DA,8BASI,UvBs3DJ,CuB/3DA,oBAUI,SvBq3DJ,CACF,CuBl3DI,uCACE,iBvBw3DN,CuBz3DI,uCACE,kBvBw3DN,CuBz3DI,6BAEE,uCAAA,CACA,SAAA,CAIA,oBAAA,CAHA,+DvBq3DN,CuB/2DM,iDAEE,uCAAA,CADA,YvBk3DR,CuB72DM,gGAGE,SAAA,CADA,mBAAA,CAEA,kBvB82DR,CuB32DQ,sGACE,UvB62DV,CuBt2DE,8BAOE,mBAAA,CAAA,oBvB62DJ,CuBp3DE,8BAOE,mBAAA,CAAA,oBvB62DJ,CuBp3DE,oBAIE,kBAAA,CAKA,yCAAA,CANA,YAAA,CAKA,eAAA,CAFA,WAAA,CAKA,SAAA,CAVA,iBAAA,CACA,KAAA,CAUA,uBAAA,CAFA,kBAAA,CALA,UvB+2DJ,CKtjEI,mCkBkMF,8BAgBI,mBvBy2DJ,CuBz3DA,8BAgBI,oBvBy2DJ,CuBz3DA,oBAiBI,evBw2DJ,CACF,CuBr2DI,+DACE,SAAA,CACA,0BvBu2DN,CuBl2DE,6BAKE,+BvBq2DJ,CuB12DE,0DAME,gCvBo2DJ,CuB12DE,6BAME,+BvBo2DJ,CuB12DE,mBAIE,eAAA,CAHA,iBAAA,CAEA,UAAA,CADA,SvBw2DJ,CKrjEI,0CkB2MF,mBAWI,QAAA,CADA,UvBq2DJ,CACF,CK9kEI,mCkB8NF,mBAiBI,SAAA,CADA,UAAA,CAEA,sBvBo2DJ,CuBj2DI,8DACE,8BAAA,CACA,SvBm2DN,CACF,CuB91DE,uBASE,kCAAA,CAAA,0BAAA,CAFA,2CAAA,CANA,WAAA,CACA,eAAA,CAIA,kBvB+1DJ,CuBz1DI,iEAZF,uBAaI,uBvB41DJ,CACF,CK3nEM,+DkBiRJ,uBAkBI,avB41DJ,CACF,CK1mEI,sCkB2PF,uBAuBI,avB41DJ,CACF,CK/mEI,mCkB2PF,uBA4BI,YAAA,CACA,yDAAA,CACA,oBvB41DJ,CuBz1DI,kEACE,evB21DN,CuBv1DI,6BACE,+CvBy1DN,CuBr1DI,0CAEE,YAAA,CADA,WvBw1DN,CuBn1DI,gDACE,oDvBq1DN,CuBl1DM,sDACE,0CvBo1DR,CACF,CuB70DA,kBACE,gCAAA,CACA,qBvBg1DF,CuB70DE,wBAME,qDAAA,CAFA,uCAAA,CAFA,gBAAA,CACA,kBAAA,CAFA,eAAA,CAIA,uBvBg1DJ,CKnpEI,mCkB8TF,kCAUI,mBvB+0DJ,CuBz1DA,kCAUI,oBvB+0DJ,CACF,CuB30DE,wBAGE,eAAA,CADA,QAAA,CADA,SAAA,CAIA,wBAAA,CAAA,gBvB40DJ,CuBx0DE,wBACE,yDvB00DJ,CuBv0DI,oCACE,evBy0DN,CuBp0DE,wBACE,aAAA,CAEA,YAAA,CADA,uBAAA,CAEA,gCvBs0DJ,CuBn0DI,4DACE,uDvBq0DN,CuBj0DI,gDACE,mBvBm0DN,CuB9zDE,gCAKE,cAAA,CADA,aAAA,CAGA,YAAA,CANA,eAAA,CAKA,uBAAA,CAJA,KAAA,CACA,SvBo0DJ,CuB7zDI,wCACE,YvB+zDN,CuB1zDI,wDACE,YvB4zDN,CuBxzDI,oCAGE,+BAAA,CADA,gBAAA,CADA,mBAAA,CAGA,2CvB0zDN,CKrsEI,mCkBuYA,8CAUI,mBvBwzDN,CuBl0DE,8CAUI,oBvBwzDN,CACF,CuBpzDI,oFAEE,uDAAA,CADA,+BvBuzDN,CuBjzDE,sCACE,2CvBmzDJ,CuB9yDE,2BAGE,eAAA,CADA,eAAA,CADA,iBvBkzDJ,CKttEI,mCkBmaF,qCAOI,mBvBgzDJ,CuBvzDA,qCAOI,oBvBgzDJ,CACF,CuB5yDE,kCAEE,MvBkzDJ,CuBpzDE,kCAEE,OvBkzDJ,CuBpzDE,wBAME,uCAAA,CAFA,aAAA,CACA,YAAA,CAJA,iBAAA,CAEA,YvBizDJ,CKhtEI,0CkB4ZF,wBAUI,YvB8yDJ,CACF,CuB3yDI,8BAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,+CAAA,CAAA,uCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,UvBozDN,CuB1yDM,wCACE,oBvB4yDR,CuBtyDE,8BAGE,uCAAA,CAFA,gBAAA,CACA,evByyDJ,CuBryDI,iCAKE,gCAAA,CAHA,eAAA,CACA,eAAA,CACA,eAAA,CAHA,evB2yDN,CuBpyDM,sCACE,oBvBsyDR,CuBjyDI,iCAKE,gCAAA,CAHA,gBAAA,CACA,eAAA,CACA,eAAA,CAHA,avBuyDN,CuBhyDM,sCACE,oBvBkyDR,CuB5xDE,yBAKE,gCAAA,CAJA,aAAA,CAEA,gBAAA,CACA,iBAAA,CAFA,avBiyDJ,CuB1xDE,uBAGE,wBAAA,CAFA,+BAAA,CACA,yBvB6xDJ,CwBj8EA,WACE,iBAAA,CACA,SxBo8EF,CwBj8EE,kBAOE,2CAAA,CACA,mBAAA,CACA,8BAAA,CAHA,gCAAA,CAHA,QAAA,CAEA,gBAAA,CADA,YAAA,CAMA,SAAA,CATA,iBAAA,CACA,sBAAA,CAaA,mCAAA,CAJA,oExBo8EJ,CwB77EI,6EACE,gBAAA,CACA,SAAA,CAKA,+BAAA,CAJA,8ExBg8EN,CwBx7EI,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,OxBi8EN,CwBr7EE,iBAOE,mBAAA,CAFA,eAAA,CACA,oBAAA,CAHA,QAAA,CAFA,kBAAA,CAGA,aAAA,CAFA,SxB47EJ,CwBn7EE,iBACE,kBxBq7EJ,CwBj7EE,2BAGE,kBAAA,CAAA,oBxBu7EJ,CwB17EE,2BAGE,mBAAA,CAAA,mBxBu7EJ,CwB17EE,iBAIE,cAAA,CAHA,aAAA,CAKA,YAAA,CADA,uBAAA,CAEA,2CACE,CANF,UxBw7EJ,CwB96EI,8CACE,+BxBg7EN,CwB56EI,uBACE,qDxB86EN,CyBlgFA,YAIE,qBAAA,CADA,aAAA,CAGA,gBAAA,CALA,eAAA,CACA,UAAA,CAGA,azBsgFF,CyBlgFE,aATF,YAUI,YzBqgFF,CACF,CKv1EI,0CoB3KF,+BAKI,azB0gFJ,CyB/gFA,+BAKI,czB0gFJ,CyB/gFA,qBAWI,2CAAA,CAHA,aAAA,CAEA,WAAA,CANA,cAAA,CAEA,KAAA,CASA,uBAAA,CAHA,iEACE,CAJF,aAAA,CAFA,SzBwgFJ,CyB7/EI,mEACE,8BAAA,CACA,6BzB+/EN,CyB5/EM,6EACE,8BzB8/ER,CyBz/EI,6CAEE,QAAA,CAAA,MAAA,CACA,QAAA,CACA,eAAA,CAHA,iBAAA,CACA,OAAA,CAGA,qBAAA,CAHA,KzB8/EN,CACF,CKt4EI,sCoBtKJ,YAuDI,QzBy/EF,CyBt/EE,mBACE,WzBw/EJ,CyBp/EE,6CACE,UzBs/EJ,CACF,CyBl/EE,uBACE,YAAA,CACA,OzBo/EJ,CKr5EI,mCoBjGF,uBAMI,QzBo/EJ,CyBj/EI,8BACE,WzBm/EN,CyB/+EI,qCACE,azBi/EN,CyB7+EI,+CACE,kBzB++EN,CACF,CyB1+EE,wBAIE,uBAAA,CAOA,kCAAA,CAAA,0BAAA,CAVA,cAAA,CACA,eAAA,CACA,yDAAA,CAMA,oBzBy+EJ,CyBp+EI,2CAEE,YAAA,CADA,WzBu+EN,CyBl+EI,mEACE,+CzBo+EN,CyBj+EM,qHACE,oDzBm+ER,CyBh+EQ,iIACE,0CzBk+EV,CyBn9EE,wCAGE,wBACE,qBzBm9EJ,CyB/8EE,6BACE,kCzBi9EJ,CyBl9EE,6BACE,iCzBi9EJ,CACF,CK76EI,0CoB5BF,YAME,0BAAA,CADA,QAAA,CAEA,SAAA,CANA,cAAA,CACA,KAAA,CAMA,sDACE,CALF,OAAA,CADA,SzBk9EF,CyBv8EE,4CAEE,WAAA,CACA,SAAA,CACA,4CACE,CAJF,UzB48EJ,CACF,C0BznFA,iBACE,GACE,Q1B2nFF,C0BxnFA,GACE,a1B0nFF,CACF,C0BtnFA,gBACE,GACE,SAAA,CACA,0B1BwnFF,C0BrnFA,IACE,S1BunFF,C0BpnFA,GACE,SAAA,CACA,uB1BsnFF,CACF,C0B9mFA,MACE,2eAAA,CACA,+fAAA,CACA,0lBAAA,CACA,kf1BgnFF,C0B1mFA,WAOE,kCAAA,CAAA,0BAAA,CANA,aAAA,CACA,gBAAA,CACA,eAAA,CAEA,uCAAA,CAGA,uBAAA,CAJA,kB1BgnFF,C0BzmFE,iBACE,U1B2mFJ,C0BvmFE,iBACE,oBAAA,CAEA,aAAA,CACA,qBAAA,CAFA,U1B2mFJ,C0BtmFI,+BACE,iB1BymFN,C0B1mFI,+BACE,kB1BymFN,C0B1mFI,qBAEE,gB1BwmFN,C0BpmFI,kDACE,iB1BumFN,C0BxmFI,kDACE,kB1BumFN,C0BxmFI,kDAEE,iB1BsmFN,C0BxmFI,kDAEE,kB1BsmFN,C0BjmFE,iCAGE,iB1BsmFJ,C0BzmFE,iCAGE,kB1BsmFJ,C0BzmFE,uBACE,oBAAA,CACA,6BAAA,CAEA,eAAA,CACA,sBAAA,CACA,qB1BmmFJ,C0B/lFE,kBACE,YAAA,CAMA,gBAAA,CALA,SAAA,CAMA,oBAAA,CAHA,gBAAA,CAIA,WAAA,CAHA,eAAA,CAFA,SAAA,CADA,U1BumFJ,C0B9lFI,iDACE,4B1BgmFN,C0B3lFE,iBACE,eAAA,CACA,sB1B6lFJ,C0B1lFI,gDACE,2B1B4lFN,C0BxlFI,kCAIE,kB1BgmFN,C0BpmFI,kCAIE,iB1BgmFN,C0BpmFI,wBAOE,6BAAA,CADA,UAAA,CALA,oBAAA,CAEA,YAAA,CAMA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CALA,uBAAA,CAHA,W1BkmFN,C0BtlFI,iCACE,a1BwlFN,C0BplFI,iCACE,gDAAA,CAAA,wC1BslFN,C0BllFI,+BACE,8CAAA,CAAA,sC1BolFN,C0BhlFI,+BACE,8CAAA,CAAA,sC1BklFN,C0B9kFI,sCACE,qDAAA,CAAA,6C1BglFN,C0B1kFA,gBACE,Y1B6kFF,C0B1kFE,gCAIE,kB1B8kFJ,C0BllFE,gCAIE,iB1B8kFJ,C0BllFE,sBAGE,kBAAA,CAGA,uCAAA,CALA,mBAAA,CAIA,gBAAA,CAHA,S1BglFJ,C0BzkFI,+BACE,aAAA,CACA,oB1B2kFN,C0BvkFI,2CACE,U1B0kFN,C0B3kFI,2CACE,W1B0kFN,C0B3kFI,iCAEE,kB1BykFN,C0BrkFI,0BACE,W1BukFN,C2B9vFA,MACE,iSAAA,CACA,4UAAA,CACA,+NAAA,CACA,gZ3BiwFF,C2BxvFE,iBAME,kDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,cAAA,CAIA,mCAAA,CAAA,2BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CANA,0BAAA,CAFA,a3BmwFJ,C2BvvFE,uBACE,6B3ByvFJ,C2BrvFE,sBACE,wCAAA,CAAA,gC3BuvFJ,C2BnvFE,6BACE,+CAAA,CAAA,uC3BqvFJ,C2BjvFE,4BACE,8CAAA,CAAA,sC3BmvFJ,C4B9xFA,SASE,2CAAA,CADA,gCAAA,CAJA,aAAA,CAGA,eAAA,CADA,aAAA,CADA,UAAA,CAFA,S5BqyFF,C4B5xFE,aAZF,SAaI,Y5B+xFF,CACF,CKpnFI,0CuBzLJ,SAkBI,Y5B+xFF,CACF,C4B5xFE,iBACE,mB5B8xFJ,C4B1xFE,yBAIE,iB5BiyFJ,C4BryFE,yBAIE,kB5BiyFJ,C4BryFE,eAQE,eAAA,CAPA,YAAA,CAMA,eAAA,CAJA,QAAA,CAEA,aAAA,CAHA,SAAA,CAWA,oBAAA,CAPA,kB5B+xFJ,C4BrxFI,kCACE,Y5BuxFN,C4BlxFE,eACE,aAAA,CACA,kBAAA,CAAA,mB5BoxFJ,C4BjxFI,sCACE,aAAA,CACA,S5BmxFN,C4B7wFE,eAOE,kCAAA,CAAA,0BAAA,CANA,YAAA,CAEA,eAAA,CADA,gBAAA,CAMA,UAAA,CAJA,uCAAA,CACA,oBAAA,CAIA,8D5B8wFJ,C4BzwFI,0CACE,aAAA,CACA,S5B2wFN,C4BvwFI,6BAEE,kB5B0wFN,C4B5wFI,6BAEE,iB5B0wFN,C4B5wFI,mBAGE,iBAAA,CAFA,Y5B2wFN,C4BpwFM,2CACE,qB5BswFR,C4BvwFM,2CACE,qB5BywFR,C4B1wFM,2CACE,qB5B4wFR,C4B7wFM,2CACE,qB5B+wFR,C4BhxFM,2CACE,oB5BkxFR,C4BnxFM,2CACE,qB5BqxFR,C4BtxFM,2CACE,qB5BwxFR,C4BzxFM,2CACE,qB5B2xFR,C4B5xFM,4CACE,qB5B8xFR,C4B/xFM,4CACE,oB5BiyFR,C4BlyFM,4CACE,qB5BoyFR,C4BryFM,4CACE,qB5BuyFR,C4BxyFM,4CACE,qB5B0yFR,C4B3yFM,4CACE,qB5B6yFR,C4B9yFM,4CACE,oB5BgzFR,C4B1yFI,gCACE,SAAA,CAIA,yBAAA,CAHA,wC5B6yFN,C6Bh5FA,MACE,mS7Bm5FF,C6B14FE,mCACE,mBAAA,CACA,cAAA,CACA,QAAA,CAEA,mBAAA,CADA,kB7B84FJ,C6Bz4FE,oBAGE,kBAAA,CAOA,+CAAA,CACA,oBAAA,CAVA,mBAAA,CAIA,gBAAA,CACA,0BAAA,CACA,eAAA,CALA,QAAA,CAOA,qBAAA,CADA,eAAA,CAJA,wB7Bk5FJ,C6Bx4FI,0BAGE,uCAAA,CAFA,aAAA,CACA,YAAA,CAEA,6C7B04FN,C6Br4FM,gEAEE,0CAAA,CADA,+B7Bw4FR,C6Bl4FI,yBACE,uB7Bo4FN,C6B53FI,gCAME,oDAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,qCAAA,CAAA,6BAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,iCAAA,CAPA,0BAAA,CAFA,W7Bu4FN,C6B13FI,wFACE,0C7B43FN,C8Bt8FA,iBACE,GACE,oB9By8FF,C8Bt8FA,IACE,kB9Bw8FF,C8Br8FA,GACE,oB9Bu8FF,CACF,C8B/7FA,MACE,yNAAA,CACA,sP9Bk8FF,C8B37FA,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,S9B+7FF,C8B76FE,oBAEE,SAAA,CAKA,uBAAA,CAJA,2EACE,CAHF,S9Bk7FJ,C8Bx6FE,oBAEE,eAAA,CACA,wBAAA,CAAA,gBAAA,CAFA,U9B46FJ,C8Bv6FI,6CACE,qC9By6FN,C8Br6FI,uCAEE,eAAA,CADA,mB9Bw6FN,C8Bl6FI,6BACE,Y9Bo6FN,C8B/5FE,8CACE,sC9Bi6FJ,C8B75FE,mBAEE,gBAAA,CADA,a9Bg6FJ,C8B55FI,2CACE,Y9B85FN,C8B15FI,0CACE,e9B45FN,C8Bp5FA,eACE,iBAAA,CACA,eAAA,CAIA,YAAA,CAHA,kBAAA,CAEA,0BAAA,CADA,kB9By5FF,C8Bp5FE,yBACE,a9Bs5FJ,C8Bl5FE,oBACE,sCAAA,CACA,iB9Bo5FJ,C8Bh5FE,6BACE,oBAAA,CAGA,gB9Bg5FJ,C8B54FE,sBAYE,mBAAA,CANA,cAAA,CAHA,oBAAA,CACA,gBAAA,CAAA,iBAAA,CAIA,YAAA,CAGA,eAAA,CAVA,iBAAA,CAMA,wBAAA,CAAA,gBAAA,CAFA,uBAAA,CAHA,S9Bs5FJ,C8Bx4FI,qCACE,uB9B04FN,C8Bt4FI,cArBF,sBAsBI,W9By4FJ,C8Bt4FI,wCACE,2B9Bw4FN,C8Bp4FI,6BAOE,qCAAA,CACA,+CAAA,CAAA,uC9By4FN,C8B/3FI,yDAZE,UAAA,CADA,YAAA,CAKA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CACA,SAAA,CAEA,WAAA,CADA,U9B65FN,C8B94FI,4BAOE,oDAAA,CACA,4CAAA,CAAA,oCAAA,CAQA,uBAAA,CAJA,+C9Bk4FN,C8B33FM,gDACE,uB9B63FR,C8Bz3FM,mFACE,0C9B23FR,CACF,C8Bt3FI,0CAGE,2BAAA,CADA,uBAAA,CADA,S9B03FN,C8Bp3FI,8CACE,oB9Bs3FN,C8Bn3FM,aAJF,8CASI,8CAAA,CACA,iBAAA,CAHA,gCAAA,CADA,eAAA,CADA,cAAA,CAGA,kB9Bw3FN,C8Bn3FM,oDACE,mC9Bq3FR,CACF,C8Bz2FE,gCAEE,iBAAA,CADA,e9B62FJ,C8Bz2FI,mCACE,iB9B22FN,C8Bx2FM,oDAEE,a9Bu3FR,C8Bz3FM,oDAEE,c9Bu3FR,C8Bz3FM,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,CARA,S9Bs3FR,C+BtoGA,MACE,wBAAA,CACA,wB/ByoGF,C+BnoGA,aA+BE,kCAAA,CAAA,0BAAA,CAjBA,gCAAA,CADA,sCAAA,CAGA,SAAA,CADA,mBAAA,CAdA,iBAAA,CAGA,wDACE,CAgBF,4BAAA,CAGA,uEACE,CARF,uDACE,CANF,UAAA,CADA,S/BuoGF,C+BhnGE,oBAuBE,8CAAA,CAAA,+CAAA,CADA,UAAA,CADA,aAAA,CAfA,gJACE,CANF,iBAAA,CAmBA,S/BomGJ,C+B7lGE,yBAGE,kEAAA,CAFA,gDAAA,CACA,6C/BgmGJ,C+B3lGE,4BAGE,qEAAA,CADA,8CAAA,CADA,6C/B+lGJ,C+BzlGE,qBAEE,SAAA,CAKA,uBAAA,CAJA,wEACE,CAHF,S/B8lGJ,C+BplGE,oBAqBE,uBAAA,CAEA,2CAAA,CACA,mBAAA,CACA,8BAAA,CAnBA,0FACE,CAaF,eAAA,CADA,8BAAA,CAlBA,iBAAA,CAqBA,oB/BykGJ,C+BnkGI,uCAEE,YAAA,CADA,W/BskGN,C+BjkGI,6CACE,oD/BmkGN,C+BhkGM,mDACE,0C/BkkGR,C+B1jGI,mCAwBE,eAAA,CACA,eAAA,CAxBA,oIACE,CAgBF,sCACE,CAIF,mBAAA,CAKA,wBAAA,CAAA,gBAAA,CAbA,sBAAA,CAAA,iB/BojGN,C+BniGI,4CACE,Y/BqiGN,C+BjiGI,2CACE,e/BmiGN,CgCttGA,kBAME,ehCkuGF,CgCxuGA,kBAME,gBhCkuGF,CgCxuGA,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,ShCquGF,CgCltGE,aAtBF,QAuBI,YhCqtGF,CACF,CgCltGE,kBACE,wBhCotGJ,CgChtGE,gBAEE,SAAA,CADA,mBAAA,CAGA,+BAAA,CADA,uBhCmtGJ,CgC/sGI,0BACE,8BhCitGN,CgC5sGE,4BAEE,0CAAA,CADA,+BhC+sGJ,CgC1sGE,YACE,oBAAA,CACA,oBhC4sGJ,CiCjwGA,oBACE,GACE,mBjCowGF,CACF,CiC5vGA,MACE,wfjC8vGF,CiCxvGA,YACE,aAAA,CAEA,eAAA,CADA,ajC4vGF,CiCxvGE,+BAOE,kBAAA,CAAA,kBjCyvGJ,CiChwGE,+BAOE,iBAAA,CAAA,mBjCyvGJ,CiChwGE,qBAQE,aAAA,CACA,cAAA,CACA,YAAA,CATA,iBAAA,CAKA,UjC0vGJ,CiCnvGI,qCAIE,iBjC2vGN,CiC/vGI,qCAIE,kBjC2vGN,CiC/vGI,2BAME,6BAAA,CADA,UAAA,CAJA,oBAAA,CAEA,YAAA,CAIA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CARA,WjC6vGN,CiChvGE,mBACE,iBAAA,CACA,UjCkvGJ,CiC9uGE,kBAWE,2CAAA,CACA,mBAAA,CACA,8BAAA,CALA,gCAAA,CACA,oBAAA,CAHA,kBAAA,CAFA,YAAA,CAUA,SAAA,CAPA,aAAA,CAFA,SAAA,CAJA,iBAAA,CASA,4BAAA,CARA,UAAA,CAaA,+CACE,CAbF,SjC4vGJ,CiC3uGI,+EACE,gBAAA,CACA,SAAA,CACA,sCjC6uGN,CiCvuGI,qCAEE,oCACE,gCjCwuGN,CiCpuGI,2CACE,cjCsuGN,CACF,CiCjuGE,kBACE,kBjCmuGJ,CiC/tGE,4BAGE,kBAAA,CAAA,oBjCsuGJ,CiCzuGE,4BAGE,mBAAA,CAAA,mBjCsuGJ,CiCzuGE,kBAKE,cAAA,CAJA,aAAA,CAMA,YAAA,CADA,uBAAA,CAEA,2CACE,CALF,kBAAA,CAFA,UjCuuGJ,CiC5tGI,gDACE,+BjC8tGN,CiC1tGI,wBACE,qDjC4tGN,CkCl0GA,MAEI,6VAAA,CAAA,uWAAA,CAAA,qPAAA,CAAA,2xBAAA,CAAA,qMAAA,CAAA,+aAAA,CAAA,2LAAA,CAAA,yPAAA,CAAA,2TAAA,CAAA,oaAAA,CAAA,2SAAA,CAAA,2LlC21GJ,CkC/0GE,4CAME,8CAAA,CACA,4BAAA,CACA,mBAAA,CACA,8BAAA,CAJA,mCAAA,CAJA,iBAAA,CAGA,gBAAA,CADA,iBAAA,CADA,eAAA,CASA,uBAAA,CADA,2BlCm1GJ,CkC/0GI,aAdF,4CAeI,elCk1GJ,CACF,CkC/0GI,sEACE,gClCi1GN,CkC50GI,gDACE,qBlC80GN,CkC10GI,gIAEE,iBAAA,CADA,clC60GN,CkCx0GI,4FACE,iBlC00GN,CkCt0GI,kFACE,elCw0GN,CkCp0GI,0FACE,YlCs0GN,CkCl0GI,8EACE,mBlCo0GN,CkC/zGE,sEAGE,iBAAA,CAAA,mBlCy0GJ,CkC50GE,sEAGE,kBAAA,CAAA,kBlCy0GJ,CkC50GE,sEASE,uBlCm0GJ,CkC50GE,sEASE,wBlCm0GJ,CkC50GE,sEAUE,4BlCk0GJ,CkC50GE,4IAWE,6BlCi0GJ,CkC50GE,sEAWE,4BlCi0GJ,CkC50GE,kDAOE,0BAAA,CACA,WAAA,CAFA,eAAA,CADA,eAAA,CAHA,oBAAA,CAAA,iBAAA,CADA,iBlC20GJ,CkC9zGI,kFACE,elCg0GN,CkC5zGI,oFAEE,UlCu0GN,CkCz0GI,oFAEE,WlCu0GN,CkCz0GI,gEAOE,wBhBiIU,CgBlIV,UAAA,CADA,WAAA,CAGA,kDAAA,CAAA,0CAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CACA,UlCq0GN,CkC1zGI,4DACE,4DlC4zGN,CkC9yGE,sDACE,oBlCizGJ,CkC9yGI,gFACE,gClCgzGN,CkC3yGE,8DACE,0BlC8yGJ,CkC3yGI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClC6yGN,CkCzyGI,0EACE,alC2yGN,CkCh0GE,8DACE,oBlCm0GJ,CkCh0GI,wFACE,gClCk0GN,CkC7zGE,sEACE,0BlCg0GJ,CkC7zGI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClC+zGN,CkC3zGI,kFACE,alC6zGN,CkCl1GE,sDACE,oBlCq1GJ,CkCl1GI,gFACE,gClCo1GN,CkC/0GE,8DACE,0BlCk1GJ,CkC/0GI,4EACE,wBAlBG,CAmBH,kDAAA,CAAA,0ClCi1GN,CkC70GI,0EACE,alC+0GN,CkCp2GE,oDACE,oBlCu2GJ,CkCp2GI,8EACE,gClCs2GN,CkCj2GE,4DACE,0BlCo2GJ,CkCj2GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClCm2GN,CkC/1GI,wEACE,alCi2GN,CkCt3GE,4DACE,oBlCy3GJ,CkCt3GI,sFACE,gClCw3GN,CkCn3GE,oEACE,0BlCs3GJ,CkCn3GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCq3GN,CkCj3GI,gFACE,alCm3GN,CkCx4GE,8DACE,oBlC24GJ,CkCx4GI,wFACE,gClC04GN,CkCr4GE,sEACE,0BlCw4GJ,CkCr4GI,oFACE,wBAlBG,CAmBH,sDAAA,CAAA,8ClCu4GN,CkCn4GI,kFACE,alCq4GN,CkC15GE,4DACE,oBlC65GJ,CkC15GI,sFACE,gClC45GN,CkCv5GE,oEACE,0BlC05GJ,CkCv5GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCy5GN,CkCr5GI,gFACE,alCu5GN,CkC56GE,4DACE,oBlC+6GJ,CkC56GI,sFACE,gClC86GN,CkCz6GE,oEACE,0BlC46GJ,CkCz6GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClC26GN,CkCv6GI,gFACE,alCy6GN,CkC97GE,0DACE,oBlCi8GJ,CkC97GI,oFACE,gClCg8GN,CkC37GE,kEACE,0BlC87GJ,CkC37GI,gFACE,wBAlBG,CAmBH,oDAAA,CAAA,4ClC67GN,CkCz7GI,8EACE,alC27GN,CkCh9GE,oDACE,oBlCm9GJ,CkCh9GI,8EACE,gClCk9GN,CkC78GE,4DACE,0BlCg9GJ,CkC78GI,0EACE,wBAlBG,CAmBH,iDAAA,CAAA,yClC+8GN,CkC38GI,wEACE,alC68GN,CkCl+GE,4DACE,oBlCq+GJ,CkCl+GI,sFACE,gClCo+GN,CkC/9GE,oEACE,0BlCk+GJ,CkC/9GI,kFACE,wBAlBG,CAmBH,qDAAA,CAAA,6ClCi+GN,CkC79GI,gFACE,alC+9GN,CkCp/GE,wDACE,oBlCu/GJ,CkCp/GI,kFACE,gClCs/GN,CkCj/GE,gEACE,0BlCo/GJ,CkCj/GI,8EACE,wBAlBG,CAmBH,mDAAA,CAAA,2ClCm/GN,CkC/+GI,4EACE,alCi/GN,CmCrpHA,MACE,qMnCwpHF,CmC/oHE,sBAEE,uCAAA,CADA,gBnCmpHJ,CmC/oHI,mCACE,anCipHN,CmClpHI,mCACE,cnCipHN,CmC7oHM,4BACE,sBnC+oHR,CmC5oHQ,mCACE,gCnC8oHV,CmC1oHQ,2DACE,SAAA,CAEA,uBAAA,CADA,enC6oHV,CmCxoHQ,yGACE,SAAA,CACA,uBnC0oHV,CmCtoHQ,yCACE,YnCwoHV,CmCjoHE,0BACE,eAAA,CACA,enCmoHJ,CmChoHI,+BACE,oBnCkoHN,CmC7nHE,gDACE,YnC+nHJ,CmC3nHE,8BAIE,+BAAA,CAHA,oBAAA,CAEA,WAAA,CAGA,SAAA,CAKA,4BAAA,CAJA,4DACE,CAHF,0BnC+nHJ,CmCtnHI,aAdF,8BAeI,+BAAA,CACA,SAAA,CACA,uBnCynHJ,CACF,CmCtnHI,wCACE,6BnCwnHN,CmCpnHI,oCACE,+BnCsnHN,CmClnHI,qCAKE,6BAAA,CADA,UAAA,CAHA,oBAAA,CAEA,YAAA,CAGA,2CAAA,CAAA,mCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAPA,WnC2nHN,CmC9mHQ,mDACE,oBnCgnHV,CoC9tHE,kCAEE,iBpCouHJ,CoCtuHE,kCAEE,kBpCouHJ,CoCtuHE,wBAGE,yCAAA,CAFA,oBAAA,CAGA,SAAA,CACA,mCpCiuHJ,CoC5tHI,aAVF,wBAWI,YpC+tHJ,CACF,CoC3tHE,6FAEE,SAAA,CACA,mCpC6tHJ,CoCvtHE,4FAEE,+BpCytHJ,CoCrtHE,oBACE,yBAAA,CACA,uBAAA,CAGA,yEpCqtHJ,CKtlHI,sC+BrHE,qDACE,uBpC8sHN,CACF,CoCzsHE,kEACE,yBpC2sHJ,CoCvsHE,sBACE,0BpCysHJ,CqCpwHE,2BACE,arCuwHJ,CKllHI,0CgCtLF,2BAKI,erCuwHJ,CqCpwHI,6BACE,iBrCswHN,CACF,CqClwHI,6BAEE,0BAAA,CAAA,2BAAA,CADA,eAAA,CAEA,iBrCowHN,CqCjwHM,2CACE,kBrCmwHR,CqC7vHI,6CACE,QrC+vHN,CsC3xHE,uBACE,4CtC+xHJ,CsC1xHE,8CAJE,kCAAA,CAAA,0BtCkyHJ,CsC9xHE,uBACE,4CtC6xHJ,CsCxxHE,4BAEE,kCAAA,CAAA,0BAAA,CADA,qCtC2xHJ,CsCvxHI,mCACE,atCyxHN,CsCrxHI,kCACE,atCuxHN,CsClxHE,0BAKE,eAAA,CAJA,aAAA,CAEA,YAAA,CACA,aAAA,CAFA,kBAAA,CAAA,mBtCuxHJ,CsCjxHI,uCACE,etCmxHN,CsC/wHI,sCACE,kBtCixHN,CuC9zHA,MACE,oLvCi0HF,CuCxzHE,oBAGE,iBAAA,CAEA,gBAAA,CADA,avC0zHJ,CuCtzHI,wCACE,uBvCwzHN,CuCpzHI,gCAEE,eAAA,CADA,gBvCuzHN,CuChzHM,wCACE,mBvCkzHR,CuC5yHE,8BAKE,oBvCgzHJ,CuCrzHE,8BAKE,mBvCgzHJ,CuCrzHE,8BAUE,4BvC2yHJ,CuCrzHE,4DAWE,6BvC0yHJ,CuCrzHE,8BAWE,4BvC0yHJ,CuCrzHE,oBASE,cAAA,CANA,aAAA,CACA,eAAA,CAIA,evC6yHJ,CuCvyHI,kCACE,uCAAA,CACA,oBvCyyHN,CuCryHI,wCAEE,uCAAA,CADA,YvCwyHN,CuCnyHI,oCAEE,WvCgzHN,CuClzHI,oCAEE,UvCgzHN,CuClzHI,0BAOE,6BAAA,CADA,UAAA,CADA,WAAA,CAGA,yCAAA,CAAA,iCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,UAAA,CAUA,sBAAA,CADA,yBAAA,CARA,UvC8yHN,CuClyHM,oCACE,wBvCoyHR,CuC/xHI,4BACE,YvCiyHN,CuC5xHI,4CACE,YvC8xHN,CwCx3HE,+DACE,sBAAA,CAEA,mBAAA,CACA,0BAAA,CACA,uBxC03HJ,CwCv3HI,2EAGE,iBAAA,CADA,eAAA,CADA,yBxC23HN,CwCp3HE,mEACE,0BxCs3HJ,CwCl3HE,oBACE,qBxCo3HJ,CwCh3HE,gBACE,oBxCk3HJ,CwC92HE,gBACE,qBxCg3HJ,CwC52HE,iBACE,kBxC82HJ,CwC12HE,kBACE,kBxC42HJ,CyCr5HE,6BACE,sCzCw5HJ,CyCr5HE,cACE,yCzCu5HJ,CyC34HE,sIACE,oCzC64HJ,CyCr4HE,2EACE,qCzCu4HJ,CyC73HE,wGACE,oCzC+3HJ,CyCt3HE,yFACE,qCzCw3HJ,CyCn3HE,6BACE,kCzCq3HJ,CyC/2HE,6CACE,sCzCi3HJ,CyC12HE,4DACE,sCzC42HJ,CyCr2HE,4DACE,qCzCu2HJ,CyC91HE,yFACE,qCzCg2HJ,CyCx1HE,2EACE,sCzC01HJ,CyC/0HE,wHACE,qCzCi1HJ,CyC50HE,8BAGE,mBAAA,CADA,gBAAA,CADA,gBzCg1HJ,CyC30HE,eACE,4CzC60HJ,CyC10HE,eACE,4CzC40HJ,CyCx0HE,gBAIE,+CAAA,CACA,kDAAA,CAJA,aAAA,CAEA,wBAAA,CADA,wBzC60HJ,CyCt0HE,yBAOE,wCAAA,CACA,+DAAA,CACA,4BAAA,CACA,6BAAA,CARA,iBAAA,CAGA,eAAA,CACA,eAAA,CAFA,cAAA,CADA,oCAAA,CAFA,iBzCi1HJ,CyCr0HI,6BACE,YzCu0HN,CyCp0HM,kCACE,wBAAA,CACA,yBzCs0HR,CyCh0HE,iCAaE,wCAAA,CACA,+DAAA,CAJA,uCAAA,CACA,0BAAA,CALA,UAAA,CAJA,oBAAA,CAOA,2BAAA,CADA,2BAAA,CADA,2BAAA,CANA,eAAA,CAWA,wBAAA,CAAA,gBAAA,CAPA,SzCy0HJ,CyCvzHE,sBACE,iBAAA,CACA,iBzCyzHJ,CyCpzHE,iCAKE,ezCkzHJ,CyC/yHI,sCACE,gBzCizHN,CyC7yHI,gDACE,YzC+yHN,CyCryHA,gBACE,iBzCwyHF,CyCpyHE,yCACE,aAAA,CACA,SzCsyHJ,CyCjyHE,mBACE,YzCmyHJ,CyC9xHE,oBACE,QzCgyHJ,CyC5xHE,4BACE,WAAA,CACA,SAAA,CACA,ezC8xHJ,CyC3xHI,0CACE,YzC6xHN,CyCvxHE,yBAKE,wCAAA,CAEA,+BAAA,CADA,4BAAA,CAHA,eAAA,CADA,oDAAA,CAEA,wBAAA,CAAA,gBzC4xHJ,CyCrxHE,2BAEE,+DAAA,CADA,2BzCwxHJ,CyCpxHI,+BACE,uCAAA,CACA,gBzCsxHN,CyCjxHE,sBACE,MAAA,CACA,WzCmxHJ,CyC9wHA,aACE,azCixHF,CyCvwHE,4BAEE,aAAA,CADA,YzC2wHJ,CyCvwHI,wDAEE,2BAAA,CADA,wBzC0wHN,CyCpwHE,+BAKE,2CAAA,CAEA,+BAAA,CADA,gCAAA,CADA,sBAAA,CAHA,mBAAA,CACA,gBAAA,CAFA,azC4wHJ,CyCnwHI,qCAEE,UAAA,CACA,UAAA,CAFA,azCuwHN,CK94HI,0CoCsJF,8BACE,iBzC4vHF,CyClvHE,wSAGE,ezCwvHJ,CyCpvHE,sCAEE,mBAAA,CACA,eAAA,CADA,oBAAA,CADA,kBAAA,CAAA,mBzCwvHJ,CACF,C0CrlII,yDAIE,+BAAA,CACA,8BAAA,CAFA,aAAA,CADA,QAAA,CADA,iB1C2lIN,C0CnlII,uBAEE,uCAAA,CADA,c1CslIN,C0CjiIM,iHAEE,WAlDkB,CAiDlB,kB1C4iIR,C0C7iIM,6HAEE,WAlDkB,CAiDlB,kB1CwjIR,C0CzjIM,6HAEE,WAlDkB,CAiDlB,kB1CokIR,C0CrkIM,oHAEE,WAlDkB,CAiDlB,kB1CglIR,C0CjlIM,0HAEE,WAlDkB,CAiDlB,kB1C4lIR,C0C7lIM,uHAEE,WAlDkB,CAiDlB,kB1CwmIR,C0CzmIM,uHAEE,WAlDkB,CAiDlB,kB1ConIR,C0CrnIM,6HAEE,WAlDkB,CAiDlB,kB1CgoIR,C0CjoIM,yCAEE,WAlDkB,CAiDlB,kB1CooIR,C0CroIM,yCAEE,WAlDkB,CAiDlB,kB1CwoIR,C0CzoIM,0CAEE,WAlDkB,CAiDlB,kB1C4oIR,C0C7oIM,uCAEE,WAlDkB,CAiDlB,kB1CgpIR,C0CjpIM,wCAEE,WAlDkB,CAiDlB,kB1CopIR,C0CrpIM,sCAEE,WAlDkB,CAiDlB,kB1CwpIR,C0CzpIM,wCAEE,WAlDkB,CAiDlB,kB1C4pIR,C0C7pIM,oCAEE,WAlDkB,CAiDlB,kB1CgqIR,C0CjqIM,2CAEE,WAlDkB,CAiDlB,kB1CoqIR,C0CrqIM,qCAEE,WAlDkB,CAiDlB,kB1CwqIR,C0CzqIM,oCAEE,WAlDkB,CAiDlB,kB1C4qIR,C0C7qIM,kCAEE,WAlDkB,CAiDlB,kB1CgrIR,C0CjrIM,qCAEE,WAlDkB,CAiDlB,kB1CorIR,C0CrrIM,mCAEE,WAlDkB,CAiDlB,kB1CwrIR,C0CzrIM,qCAEE,WAlDkB,CAiDlB,kB1C4rIR,C0C7rIM,wCAEE,WAlDkB,CAiDlB,kB1CgsIR,C0CjsIM,sCAEE,WAlDkB,CAiDlB,kB1CosIR,C0CrsIM,2CAEE,WAlDkB,CAiDlB,kB1CwsIR,C0C7rIM,iCAEE,WAPkB,CAMlB,iB1CgsIR,C0CjsIM,uCAEE,WAPkB,CAMlB,iB1CosIR,C0CrsIM,mCAEE,WAPkB,CAMlB,iB1CwsIR,C2C1xIA,MACE,2LAAA,CACA,yL3C6xIF,C2CpxIE,wBAKE,mBAAA,CAHA,YAAA,CACA,qBAAA,CACA,YAAA,CAHA,iB3C2xIJ,C2CjxII,8BAGE,QAAA,CACA,SAAA,CAHA,iBAAA,CACA,O3CqxIN,C2ChxIM,qCACE,0B3CkxIR,C2CrvIM,kEACE,0C3CuvIR,C2CjvIE,2BAME,uBAAA,CADA,+DAAA,CAJA,YAAA,CACA,cAAA,CACA,aAAA,CACA,oB3CqvIJ,C2ChvII,aATF,2BAUI,gB3CmvIJ,CACF,C2ChvII,cAGE,+BACE,iB3CgvIN,C2C7uIM,sCAQE,qCAAA,CANA,QAAA,CAKA,UAAA,CAHA,aAAA,CAEA,UAAA,CAHA,MAAA,CAFA,iBAAA,CAaA,2CAAA,CALA,2DACE,CAGF,kDAAA,CARA,+B3CqvIR,CACF,C2CvuII,8CACE,Y3CyuIN,C2CruII,iCAUE,+BAAA,CACA,6BAAA,CALA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,gBAAA,CACA,eAAA,CAFA,8BAAA,CAMA,+BAAA,CAGA,2CACE,CANF,kBAAA,CALA,U3CivIN,C2CluIM,aAII,6CACE,O3CiuIV,C2CluIQ,8CACE,O3CouIV,C2CruIQ,8CACE,O3CuuIV,C2CxuIQ,8CACE,O3C0uIV,C2C3uIQ,8CACE,O3C6uIV,C2C9uIQ,8CACE,O3CgvIV,C2CjvIQ,8CACE,O3CmvIV,C2CpvIQ,8CACE,O3CsvIV,C2CvvIQ,8CACE,O3CyvIV,C2C1vIQ,+CACE,Q3C4vIV,C2C7vIQ,+CACE,Q3C+vIV,C2ChwIQ,+CACE,Q3CkwIV,C2CnwIQ,+CACE,Q3CqwIV,C2CtwIQ,+CACE,Q3CwwIV,C2CzwIQ,+CACE,Q3C2wIV,C2C5wIQ,+CACE,Q3C8wIV,C2C/wIQ,+CACE,Q3CixIV,C2ClxIQ,+CACE,Q3CoxIV,C2CrxIQ,+CACE,Q3CuxIV,C2CxxIQ,+CACE,Q3C0xIV,CACF,C2CrxIM,uCACE,gC3CuxIR,C2CnxIM,oDACE,a3CqxIR,C2ChxII,yCACE,S3CkxIN,C2C9wIM,2CACE,aAAA,CACA,8B3CgxIR,C2C1wIE,4BACE,U3C4wIJ,C2CzwII,aAJF,4BAKI,gB3C4wIJ,CACF,C2CxwIE,0BACE,Y3C0wIJ,C2CvwII,aAJF,0BAKI,a3C0wIJ,C2CtwIM,sCACE,O3CwwIR,C2CzwIM,uCACE,O3C2wIR,C2C5wIM,uCACE,O3C8wIR,C2C/wIM,uCACE,O3CixIR,C2ClxIM,uCACE,O3CoxIR,C2CrxIM,uCACE,O3CuxIR,C2CxxIM,uCACE,O3C0xIR,C2C3xIM,uCACE,O3C6xIR,C2C9xIM,uCACE,O3CgyIR,C2CjyIM,wCACE,Q3CmyIR,C2CpyIM,wCACE,Q3CsyIR,C2CvyIM,wCACE,Q3CyyIR,C2C1yIM,wCACE,Q3C4yIR,C2C7yIM,wCACE,Q3C+yIR,C2ChzIM,wCACE,Q3CkzIR,C2CnzIM,wCACE,Q3CqzIR,C2CtzIM,wCACE,Q3CwzIR,C2CzzIM,wCACE,Q3C2zIR,C2C5zIM,wCACE,Q3C8zIR,C2C/zIM,wCACE,Q3Ci0IR,CACF,C2C3zII,+FAEE,Q3C6zIN,C2C1zIM,yGACE,wBAAA,CACA,yB3C6zIR,C2CpzIM,2DAEE,wBAAA,CACA,yBAAA,CAFA,Q3CwzIR,C2CjzIM,iEACE,Q3CmzIR,C2ChzIQ,qLAGE,wBAAA,CACA,yBAAA,CAFA,Q3CozIV,C2C9yIQ,6FACE,wBAAA,CACA,yB3CgzIV,C2C3yIM,yDACE,kB3C6yIR,C2CxyII,sCACE,Q3C0yIN,C2CryIE,2BAEE,iBAAA,CAOA,kBAAA,CAHA,uCAAA,CAEA,cAAA,CAPA,aAAA,CAGA,YAAA,CACA,gBAAA,CAEA,mBAAA,CAGA,gCAAA,CAPA,W3C8yIJ,C2CpyII,iCAEE,uDAAA,CADA,+B3CuyIN,C2ClyII,iCAKE,6BAAA,CADA,UAAA,CAHA,aAAA,CAEA,WAAA,CAGA,8CAAA,CAAA,sCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CACA,+CACE,CATF,U3C4yIN,C2C7xIE,4BAOE,yEACE,CANF,YAAA,CAGA,aAAA,CAFA,qBAAA,CAGA,mBAAA,CALA,iBAAA,CAYA,wBAAA,CATA,Y3CmyIJ,C2CvxII,sCACE,wB3CyxIN,C2CrxII,oCACE,S3CuxIN,C2CnxII,kCAGE,wEACE,CAFF,mBAAA,CADA,O3CuxIN,C2C7wIM,uDACE,8CAAA,CAAA,sC3C+wIR,CKt5II,0CsCqJF,wDAEE,kB3CuwIF,C2CzwIA,wDAEE,mB3CuwIF,C2CzwIA,8CAGE,eAAA,CAFA,eAAA,CAGA,iC3CqwIF,C2CjwIE,8DACE,mB3CowIJ,C2CrwIE,8DACE,kB3CowIJ,C2CrwIE,oDAEE,U3CmwIJ,C2C/vIE,8EAEE,kB3CkwIJ,C2CpwIE,8EAEE,mB3CkwIJ,C2CpwIE,8EAGE,kB3CiwIJ,C2CpwIE,8EAGE,mB3CiwIJ,C2CpwIE,oEACE,U3CmwIJ,C2C7vIE,8EAEE,mB3CgwIJ,C2ClwIE,8EAEE,kB3CgwIJ,C2ClwIE,8EAGE,mB3C+vIJ,C2ClwIE,8EAGE,kB3C+vIJ,C2ClwIE,oEACE,U3CiwIJ,CACF,C2CnvIE,cAHF,olDAII,gC3CsvIF,C2CnvIE,g8GACE,uC3CqvIJ,CACF,C2ChvIA,4sDACE,+B3CmvIF,C2C/uIA,wmDACE,a3CkvIF,C4CtnJA,MACE,qWAAA,CACA,8W5CynJF,C4ChnJE,4BAEE,oBAAA,CADA,iB5ConJJ,C4C/mJI,sDAEE,S5CknJN,C4CpnJI,sDAEE,U5CknJN,C4CpnJI,4CACE,iBAAA,CAEA,S5CinJN,C4C5mJE,+CAEE,SAAA,CADA,U5C+mJJ,C4C1mJE,kDAEE,W5CqnJJ,C4CvnJE,kDAEE,Y5CqnJJ,C4CvnJE,wCAOE,qDAAA,CADA,UAAA,CADA,aAAA,CAGA,0CAAA,CAAA,kCAAA,CAEA,4BAAA,CAAA,oBAAA,CADA,6BAAA,CAAA,qBAAA,CAEA,yBAAA,CAAA,iBAAA,CAVA,iBAAA,CAEA,SAAA,CACA,Y5CmnJJ,C4CxmJE,gEACE,wB1B2Wa,C0B1Wb,mDAAA,CAAA,2C5C0mJJ,C6C1pJA,aAQE,wBACE,Y7CypJF,CACF,C8CnqJA,QACE,8DAAA,CAGA,+CAAA,CACA,iEAAA,CACA,oDAAA,CACA,sDAAA,CACA,mDAAA,CAGA,qEAAA,CACA,qEAAA,CACA,wEAAA,CACA,0EAAA,CACA,wEAAA,CACA,yEAAA,CACA,kEAAA,CACA,+DAAA,CACA,oEAAA,CACA,oEAAA,CACA,mEAAA,CACA,gEAAA,CACA,uEAAA,CACA,mEAAA,CACA,qEAAA,CACA,oEAAA,CACA,gEAAA,CACA,wEAAA,CACA,qEAAA,CACA,+D9CiqJF,C8C3pJA,SAEE,kBAAA,CADA,Y9C+pJF,C+CjsJE,kBAUE,cAAA,CATA,YAAA,CACA,kEACE,CAQF,Y/C6rJJ,C+CzrJI,sDACE,gB/C2rJN,C+CrrJI,oFAKE,wDAAA,CACA,mBAAA,CAJA,aAAA,CAEA,QAAA,CADA,aAAA,CAIA,sC/CurJN,C+ClrJM,iOACE,kBAAA,CACA,8B/CqrJR,C+CjrJM,6FACE,iBAAA,CAAA,c/CorJR,C+ChrJM,2HACE,Y/CmrJR,C+C/qJM,wHACE,e/CkrJR,C+CnqJI,yMAGE,eAAA,CAAA,Y/C2qJN,C+C7pJI,ybAOE,W/CmqJN,C+C/pJI,8BACE,eAAA,CAAA,Y/CiqJN,CK7lJI,mC2ChKA,8BACE,UhDqwJJ,CgDtwJE,8BACE,WhDqwJJ,CgDtwJE,8BAGE,kBhDmwJJ,CgDtwJE,8BAGE,iBhDmwJJ,CgDtwJE,oBAKE,mBAAA,CADA,YAAA,CAFA,ahDowJJ,CgD9vJI,kCACE,WhDiwJN,CgDlwJI,kCACE,UhDiwJN,CgDlwJI,kCAEE,iBAAA,CAAA,chDgwJN,CgDlwJI,kCAEE,aAAA,CAAA,kBhDgwJN,CACF","file":"main.css"} \ No newline at end of file diff --git a/main/cli/check/index.html b/main/cli/check/index.html index 72225126a..5da64616d 100644 --- a/main/cli/check/index.html +++ b/main/cli/check/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2320,7 +2320,7 @@ Checking the catalog - + @@ -2331,7 +2331,7 @@ Checking the catalog - + @@ -2342,7 +2342,7 @@ Checking the catalog - + diff --git a/main/cli/debug/index.html b/main/cli/debug/index.html index ae66b40d0..c90e0d693 100644 --- a/main/cli/debug/index.html +++ b/main/cli/debug/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2632,7 +2632,7 @@ Example of multiple arguments - + @@ -2643,7 +2643,7 @@ Example of multiple arguments - + @@ -2654,7 +2654,7 @@ Example of multiple arguments - + diff --git a/main/cli/exec/index.html b/main/cli/exec/index.html index 6e5bb22f4..3302bc93a 100644 --- a/main/cli/exec/index.html +++ b/main/cli/exec/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2825,7 +2825,7 @@ Example - + @@ -2836,7 +2836,7 @@ Example - + @@ -2847,7 +2847,7 @@ Example - + diff --git a/main/cli/get-inventory-information/index.html b/main/cli/get-inventory-information/index.html index 66f981d71..8522e13c2 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2517,7 +2517,7 @@ Example - + @@ -2528,7 +2528,7 @@ Example - + @@ -2539,7 +2539,7 @@ Example - + diff --git a/main/cli/inv-from-ansible/index.html b/main/cli/inv-from-ansible/index.html index c36ec8a8b..c32b4d88e 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2385,7 +2385,7 @@ Command output - + @@ -2396,7 +2396,7 @@ Command output - + @@ -2407,7 +2407,7 @@ Command output - + diff --git a/main/cli/inv-from-cvp/index.html b/main/cli/inv-from-cvp/index.html index db12c0fee..58ea6abbe 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 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2386,7 +2386,7 @@ Creating an inventory fr - + @@ -2397,7 +2397,7 @@ Creating an inventory fr - + @@ -2408,7 +2408,7 @@ Creating an inventory fr - + diff --git a/main/cli/nrfu/index.html b/main/cli/nrfu/index.html index af83ed585..e4814d923 100644 --- a/main/cli/nrfu/index.html +++ b/main/cli/nrfu/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -3185,7 +3185,7 @@ Dry-run mode - + @@ -3196,7 +3196,7 @@ Dry-run mode - + @@ -3207,7 +3207,7 @@ Dry-run mode - + diff --git a/main/cli/overview/index.html b/main/cli/overview/index.html index 7ae34083d..de7dfc32e 100644 --- a/main/cli/overview/index.html +++ b/main/cli/overview/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2500,7 +2500,7 @@ Shell Completion - + @@ -2511,7 +2511,7 @@ Shell Completion - + @@ -2522,7 +2522,7 @@ Shell Completion - + diff --git a/main/cli/tag-management/index.html b/main/cli/tag-management/index.html index 0bd503e15..e4c5b1edd 100644 --- a/main/cli/tag-management/index.html +++ b/main/cli/tag-management/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2841,7 +2841,7 @@ Example - + @@ -2852,7 +2852,7 @@ Example - + @@ -2863,7 +2863,7 @@ Example - + diff --git a/main/contribution/index.html b/main/contribution/index.html index 818cbe7a9..184709092 100644 --- a/main/contribution/index.html +++ b/main/contribution/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2812,7 +2812,7 @@ Continuous Integration - + @@ -2823,7 +2823,7 @@ Continuous Integration - + @@ -2834,7 +2834,7 @@ Continuous Integration - + diff --git a/main/faq/index.html b/main/faq/index.html index 9e540805e..45f0c4469 100644 --- a/main/faq/index.html +++ b/main/faq/index.html @@ -18,7 +18,7 @@ - + @@ -26,7 +26,7 @@ - + @@ -131,7 +131,7 @@ - + @@ -228,7 +228,7 @@ - + ANTA on Github @@ -264,7 +264,7 @@ - + Arista Network Test Automation - ANTA @@ -274,7 +274,7 @@ - + ANTA on Github @@ -2588,7 +2588,7 @@ Still facing issues? - + @@ -2599,7 +2599,7 @@ Still facing issues? - + @@ -2610,7 +2610,7 @@ Still facing issues? - + diff --git a/main/getting-started/index.html b/main/getting-started/index.html index 1d2129f24..3a3fe708b 100644 --- a/main/getting-started/index.html +++ b/main/getting-started/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2968,7 +2968,7 @@ Basic usage in a Python script - + @@ -2979,7 +2979,7 @@ Basic usage in a Python script - + @@ -2990,7 +2990,7 @@ Basic usage in a Python script - + diff --git a/main/index.html b/main/index.html index 673ccba42..29105cdbb 100644 --- a/main/index.html +++ b/main/index.html @@ -18,7 +18,7 @@ - + @@ -26,7 +26,7 @@ - + @@ -131,7 +131,7 @@ - + @@ -228,7 +228,7 @@ - + ANTA on Github @@ -264,7 +264,7 @@ - + Arista Network Test Automation - ANTA @@ -274,7 +274,7 @@ - + ANTA on Github @@ -2526,7 +2526,7 @@ Credits - + @@ -2537,7 +2537,7 @@ Credits - + @@ -2548,7 +2548,7 @@ Credits - + diff --git a/main/requirements-and-installation/index.html b/main/requirements-and-installation/index.html index 9705edfd0..1e081fc3c 100644 --- a/main/requirements-and-installation/index.html +++ b/main/requirements-and-installation/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2583,7 +2583,7 @@ EOS Requirements - + @@ -2594,7 +2594,7 @@ EOS Requirements - + @@ -2605,7 +2605,7 @@ EOS Requirements - + diff --git a/main/search/search_index.json b/main/search/search_index.json index 4ff562a6b..c53e00eff 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: As a Python library in your own application The ANTA CLI "},{"location":"#install-anta-library","title":"Install ANTA library","text":"The library will NOT install the necessary dependencies for the CLI. # Install ANTA as a library\npip install anta\n"},{"location":"#install-anta-cli","title":"Install ANTA CLI","text":"If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx is a tool to install and run python applications in isolated environments. Refer to pipx instructions to install on your system. pipx installs ANTA in an isolated python environment and makes it available globally. This is not recommended if you plan to contribute to ANTA # Install ANTA CLI with pipx\n$ pipx install anta[cli]\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 You can also still choose to install it with directly with pip: pip install anta[cli]\n"},{"location":"#documentation","title":"Documentation","text":"The documentation is published on ANTA package website."},{"location":"#contribution-guide","title":"Contribution guide","text":"Contributions are welcome. Please refer to the contribution guide"},{"location":"#credits","title":"Credits","text":"Thank you to Jeremy Schulman for aio-eapi. 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":"Contribution model is based on a fork-model. Don\u2019t push to aristanetworks/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-environment","title":"Create a development environment","text":"Run the following commands to create an ANTA development environment: # Clone repository\n$ git clone https://github.com/aristanetworks/anta.git\n$ cd anta\n\n# Install ANTA in editable mode and its development tools\n$ pip install -e .[dev]\n# To also install the CLI\n$ pip install -e .[dev,cli]\n\n# Verify installation\n$ pip list -e\nPackage Version Editable project location\n------- ------- -------------------------\nanta 1.1.0 /mnt/lab/projects/anta\n Then, tox is configured 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\npy39 -> Run pytest with py39\npy310 -> Run pytest with py310\npy311 -> Run pytest with py311\npy312 -> Run pytest with py312\nreport -> Generate coverage report\n"},{"location":"contribution/#code-linting","title":"Code linting","text":"tox -e lint\n[...]\nlint: commands[0]> ruff check .\nAll checks passed!\nlint: commands[1]> ruff format . --check\n158 files already formatted\nlint: commands[2]> pylint anta\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\nlint: commands[3]> pylint tests\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\n lint: OK (22.69=setup[2.19]+cmd[0.02,0.02,9.71,10.75] seconds)\n congratulations :) (22.72 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 68 source files\ntype: commands[1]> mypy --config-file=pyproject.toml tests\nSuccess: no issues found in 82 source files\n type: OK (31.15=setup[14.62]+cmd[6.05,10.48] seconds)\n congratulations :) (31.18 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.units.anta_tests 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.units.anta_tests import test\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 pre-commit\ntrim trailing whitespace.................................................Passed\nfix end of files.........................................................Passed\ncheck for added large files..............................................Passed\ncheck for merge conflicts................................................Passed\nCheck and insert license on Python files.................................Passed\nCheck and insert license on Markdown files...............................Passed\nRun Ruff linter..........................................................Passed\nRun Ruff formatter.......................................................Passed\nCheck code style with pylint.............................................Passed\nChecks for common misspellings in text files.............................Passed\nCheck typing with mypy...................................................Passed\nCheck Markdown files style...............................................Passed\n"},{"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. The results can be viewed here."},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#a-local-os-error-occurred-while-connecting-to-a-device","title":"A local OS error occurred while connecting to a device","text":"A local OS error occurred while connecting to a device When running ANTA, you can receive A local OS error occurred while connecting to <device> errors. The underlying OSError exception can have various reasons: [Errno 24] Too many open files or [Errno 16] Device or resource busy. This usually means that the operating system refused to open a new file descriptor (or socket) for the ANTA process. This might be due to the hard limit for open file descriptors currently set for the ANTA process. At startup, ANTA sets the soft limit of its process to the hard limit up to 16384. This is because the soft limit is usually 1024 and the hard limit is usually higher (depends on the system). If the hard limit of the ANTA process is still lower than the number of selected tests in ANTA, the ANTA process may request to the operating system too many file descriptors and get an error, a WARNING is displayed at startup if this is the case."},{"location":"faq/#solution","title":"Solution","text":"One solution could be to raise the hard limit for the user starting the ANTA process. You can get the current hard limit for a user using the command ulimit -n -H while logged in. Create the file /etc/security/limits.d/10-anta.conf with the following content: <user> hard nofile <value>\n The user is the one with which the ANTA process is started. The value is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again."},{"location":"faq/#timeout-error-in-the-logs","title":"Timeout error in the logs","text":"Timeout error in the logs When running ANTA, you can receive <Foo>Timeout errors in the logs (could be ReadTimeout, WriteTimeout, ConnectTimeout or PoolTimeout). More details on the timeouts of the underlying library are available here: https://www.python-httpx.org/advanced/timeouts. This might be due to the time the host on which ANTA is run takes to reach the target devices (for instance if going through firewalls, NATs, \u2026) or when a lot of tests are being run at the same time on a device (eAPI has a queue mechanism to avoid exhausting EOS resources because of a high number of simultaneous eAPI requests)."},{"location":"faq/#solution_1","title":"Solution","text":"Use the timeout option. As an example for the nrfu command: anta nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml --timeout 50 text\n The previous command set a couple of options for ANTA NRFU, one them being the timeout command, by default, when running ANTA from CLI, it is set to 30s. The timeout is increased to 50s to allow ANTA to wait for API calls a little longer."},{"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_2","title":"Solution","text":" 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 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_3","title":"Solution","text":" 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_4","title":"Solution","text":" 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 follow the troubleshooting instructions and report the issue in our GitHub repository."},{"location":"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 install ANTA package is to run Python (>=3.9) and its pip package to install: pip install anta[cli]\n For more details about how to install package, please see the requirements and installation 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: s1-spine1\n tags: ['fabric', 'spine']\n - host: 192.168.0.11\n name: s1-spine2\n tags: ['fabric', 'spine']\n - host: 192.168.0.12\n name: s1-leaf1\n tags: ['fabric', 'leaf']\n - host: 192.168.0.13\n name: s1-leaf2\n tags: ['fabric', 'leaf']\n - host: 192.168.0.14\n name: s1-leaf3\n tags: ['fabric', 'leaf']\n - host: 192.168.0.15\n name: s1-leaf3\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: ---\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\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":""},{"location":"getting-started/#cli","title":"CLI","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. Usage: 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 selected inventory devices.\n Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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 Note The following examples shows how to pass all the CLI options. See how to use environment variables instead in the CLI overview"},{"location":"getting-started/#default-report-using-table","title":"Default report using table","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n `# table is default if not provided` \\\n table\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:01] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.058. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.069. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:02] INFO Running ANTA tests completed in: 0:00:00.969. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 s1-spine1 \u2502 VerifyMlagConfigSanity \u2502 skipped \u2502 MLAG is disabled \u2502 Verifies there are no MLAG config-sanity \u2502 MLAG \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502 inconsistencies. \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-spine1 \u2502 VerifyEOSVersion \u2502 failure \u2502 device is running version \u2502 Verifies the EOS version of the device. \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 \"4.32.2F-38195967.4322F (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)\" not in expected versions: \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['4.25.4M', '4.26.1F', \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 '4.28.3M-28837868.4283M (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n[...]\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyTerminAttrVersion \u2502 failure \u2502 device is running TerminAttr version \u2502 Verifies the TerminAttr version of the \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 v1.34.0 and is not in the allowed list: \u2502 device. \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['v1.22.1'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyZeroTouch \u2502 success \u2502 \u2502 Verifies ZeroTouch is disabled \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\u2500\u2500\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\u2500\u2500\u2500\u2500\u2500\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 arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n 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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:52:39] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.057. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.068. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:52:40] INFO Running ANTA tests completed in: 0:00:00.863. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\n\ns1-spine1 :: VerifyEOSVersion :: FAILURE(device is running version \"4.32.2F-38195967.4322F (engineering build)\" not in expected versions: ['4.25.4M', '4.26.1F',\n'4.28.3M-28837868.4283M (engineering build)'])\ns1-spine1 :: VerifyTerminAttrVersion :: FAILURE(device is running TerminAttr version v1.34.0 and is not in the allowed list: ['v1.22.1'])\ns1-spine1 :: VerifyZeroTouch :: SUCCESS()\ns1-spine1 :: VerifyMlagConfigSanity :: SKIPPED(MLAG is disabled)\n"},{"location":"getting-started/#report-in-json-format","title":"Report in JSON format","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable `\\\n `# --enable-password <password> `\\\n --catalog ./catalog.yml \\\n json\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:11] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.053. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.065. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:12] INFO Running ANTA tests completed in: 0:00:00.857. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \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\": \"s1-spine1\",\n \"test\": \"VerifyNTP\",\n \"categories\": [\n \"system\"\n ],\n \"description\": \"Verifies if NTP is synchronised.\",\n \"result\": \"success\",\n \"messages\": [],\n \"custom_field\": null\n },\n {\n \"name\": \"s1-spine1\",\n \"test\": \"VerifyMlagConfigSanity\",\n \"categories\": [\n \"mlag\"\n ],\n \"description\": \"Verifies there are no MLAG config-sanity inconsistencies.\",\n \"result\": \"skipped\",\n \"messages\": [\n \"MLAG is disabled\"\n ],\n \"custom_field\": null\n },\n [...]\n"},{"location":"getting-started/#basic-usage-in-a-python-script","title":"Basic usage in a Python script","text":"# 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\"\"\"Example script for ANTA.\n\nusage:\n\npython anta_runner.py\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport sys\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.cli.nrfu.utils import anta_progress_bar\nfrom anta.inventory import AntaInventory\nfrom anta.logger import Log, setup_logging\nfrom anta.models import AntaTest\nfrom anta.result_manager import ResultManager\nfrom anta.runner import main as anta_runner\n\n# setup logging\nsetup_logging(Log.INFO, Path(\"/tmp/anta.log\"))\nLOGGER = logging.getLogger()\nSCRIPT_LOG_PREFIX = \"[bold magenta][ANTA RUNNER SCRIPT][/] \" # For convenience purpose - there are nicer way to do this.\n\n\n# NOTE: The inventory and catalog files are not delivered with this script\nUSERNAME = \"admin\"\nPASSWORD = \"admin\"\nCATALOG_PATH = Path(\"/tmp/anta_catalog.yml\")\nINVENTORY_PATH = Path(\"/tmp/anta_inventory.yml\")\n\n# Load catalog file\ntry:\n catalog = AntaCatalog.parse(CATALOG_PATH)\nexcept Exception:\n LOGGER.exception(\"%s Catalog failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Catalog loaded!\", SCRIPT_LOG_PREFIX)\n\n# Load inventory\ntry:\n inventory = AntaInventory.parse(INVENTORY_PATH, username=USERNAME, password=PASSWORD)\nexcept Exception:\n LOGGER.exception(\"%s Inventory failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Inventory loaded!\", SCRIPT_LOG_PREFIX)\n\n# Create result manager object\nmanager = ResultManager()\n\n# Launch ANTA\nLOGGER.info(\"%s Starting ANTA runner...\", SCRIPT_LOG_PREFIX)\nwith anta_progress_bar() as AntaTest.progress:\n # Set dry_run to True to avoid connecting to the devices\n asyncio.run(anta_runner(manager, inventory, catalog, dry_run=False))\n\nLOGGER.info(\"%s ANTA run completed!\", SCRIPT_LOG_PREFIX)\n\n# Manipulate the test result object\nfor test_result in manager.results:\n LOGGER.info(\"%s %s:%s:%s\", SCRIPT_LOG_PREFIX, test_result.name, test_result.test, test_result.result)\n"},{"location":"requirements-and-installation/","title":"Installation","text":""},{"location":"requirements-and-installation/#python-version","title":"Python version","text":"Python 3 (>=3.9) is required: python --version\nPython 3.11.8\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-library-from-pypi-server","title":"Install library from Pypi server","text":"pip install anta\n Warning This command alone will not install the ANTA CLI requirements. "},{"location":"requirements-and-installation/#install-anta-cli-as-an-application-with-pipx","title":"Install ANTA CLI as an application with pipx","text":"pipx is a tool to install and run python applications in isolated environments. If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx installs ANTA in an isolated python environment and makes it available globally. pipx install anta[cli]\n Info Please take the time to read through the installation instructions of pipx before getting started."},{"location":"requirements-and-installation/#install-cli-from-pypi-server","title":"Install CLI from Pypi server","text":"Alternatively, pip install with cli extra is enough to install the ANTA CLI. pip install anta[cli]\n"},{"location":"requirements-and-installation/#install-anta-from-github","title":"Install ANTA from github","text":"pip install git+https://github.com/aristanetworks/anta.git\npip install git+https://github.com/aristanetworks/anta.git#egg=anta[cli]\n\n# You can even specify the branch, tag or commit:\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>#egg=anta[cli]\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 v1.1.0\n"},{"location":"requirements-and-installation/#eos-requirements","title":"EOS Requirements","text":"To get ANTA working, the targeted Arista EOS devices must have eAPI enabled. They need to use 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":"troubleshooting/","title":"Troubleshooting ANTA","text":"A couple of things to check when hitting an issue with ANTA: flowchart LR\n A>Hitting an issue with ANTA] --> B{Is my issue <br >listed in the FAQ?}\n B -- Yes --> C{Does the FAQ solution<br />works for me?}\n C -- Yes --> V(((Victory)))\n B -->|No| E{Is my problem<br />mentioned in one<br />of the open issues?}\n C -->|No| E\n E -- Yes --> F{Has the issue been<br />fixed in a newer<br />release or in main?}\n F -- Yes --> U[Upgrade]\n E -- No ---> H((Follow the steps below<br />and open a Github issue))\n U --> I{Did it fix<br /> your problem}\n I -- Yes --> V\n I -- No --> H\n F -- No ----> G((Add a comment on the <br />issue indicating you<br >are hitting this and<br />describing your setup<br /> and adding your logs.))\n\n click B \"../faq\" \"FAQ\"\n click E \"https://github.com/aristanetworks/anta/issues\"\n click H \"https://github.com/aristanetworks/anta/issues\"\n style A stroke:#f00,stroke-width:2px"},{"location":"troubleshooting/#capturing-logs","title":"Capturing logs","text":"To help document the issue in Github, it is important to capture some logs so the developers can understand what is affecting your system. No logs mean that the first question asked on the issue will probably be \u201cCan you share some logs please?\u201d. ANTA provides very verbose logs when using the DEBUG level. When using DEBUG log level with a log file, the DEBUG logging level is not sent to stdout, but only to the file. Danger On real deployments, do not use DEBUG logging level without setting a log file at the same time. To save the logs to a file called anta.log, use the following flags: # Where ANTA_COMMAND is one of nrfu, debug, get, exec, check\nanta -l DEBUG \u2013log-file anta.log <ANTA_COMMAND>\n See anta --help for more information. These have to precede the nrfu cmd. Tip Remember that in ANTA, each level of command has its own options and they can only be set at this level. so the -l and --log-file MUST be between anta and the ANTA_COMMAND. similarly, all the nrfu options MUST be set between the nrfu and the ANTA_NRFU_SUBCOMMAND (json, text, table or tpl-report). As an example, for the nrfu command, it would look like: anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#anta_debug-environment-variable","title":"ANTA_DEBUG environment variable","text":"Warning Do not use this if you do not know why. This produces a lot of logs and can create confusion if you do not know what to look for. The environment variable ANTA_DEBUG=true enable ANTA Debug Mode. This flag is used by various functions in ANTA: when set to true, the function will display or log more information. In particular, when an Exception occurs in the code and this variable is set, the logging function used by ANTA is different to also produce the Python traceback for debugging. This typically needs to be done when opening a GitHub issue and an Exception is seen at runtime. Example: ANTA_DEBUG=true anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#troubleshooting-on-eos","title":"Troubleshooting on EOS","text":"ANTA is using a specific ID in eAPI requests towards EOS. This allows for easier eAPI requests debugging on the device using EOS configuration trace CapiApp setting UwsgiRequestContext/4,CapiUwsgiServer/4 to set up CapiApp agent logs. Then, you can view agent logs using: bash tail -f /var/log/agents/CapiApp-*\n\n2024-05-15 15:32:54.056166 1429 UwsgiRequestContext 4 request content b'{\"jsonrpc\": \"2.0\", \"method\": \"runCmds\", \"params\": {\"version\": \"latest\", \"cmds\": [{\"cmd\": \"show ip route vrf default 10.255.0.3\", \"revision\": 4}], \"format\": \"json\", \"autoComplete\": false, \"expandAliases\": false}, \"id\": \"ANTA-VerifyRoutingTableEntry-132366530677328\"}'\n"},{"location":"usage-inventory-catalog/","title":"Inventory and Test catalog","text":"The ANTA framework needs 2 important inputs from the user to run: A device inventory 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 accessible via eAPI ranges: scan a range for devices accessible 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 in either YAML or JSON: ---\n<Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\n {\n \"<Python module>\": [\n {\n \"<AntaTest subclass>\": <AntaTest.Input compliant dictionary>\n }\n ]\n}\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 or equivalent in JSON: {\n \"anta.tests.connectivity\": [\n {\n \"VerifyReachability\": {\n \"result_overwrite\": {\n \"description\": \"Test with overwritten description\",\n \"categories\": [\n \"Overwritten category 1\"\n ],\n \"custom_field\": \"Test run by John Doe\"\n },\n \"filters\": {\n \"tags\": [\n \"leaf\"\n ]\n },\n \"hosts\": [\n {\n \"destination\": \"1.1.1.1\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n },\n {\n \"destination\": \"8.8.8.8\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n }\n ]\n }\n }\n ]\n}\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 or JSON file: anta.tests.software:\n - VerifyEOSVersion:\n # List of allowed EOS versions.\n versions:\n - 4.25.4M\n - 4.26.1F\n {\n \"anta.tests.software\": [\n {\n \"VerifyEOSVersion\": {\n \"versions\": [\n \"4.25.4M\",\n \"4.31.1F\"\n ]\n }\n }\n ]\n}\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 custom.tests.system Python module, the test catalog will be: custom.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":"usage-inventory-catalog/#example-script-to-merge-catalogs","title":"Example script to merge catalogs","text":"The following script reads all the files in intended/test_catalogs/ with names <device_name>-catalog.yml and merge them together inside one big catalog anta-catalog.yml using the new AntaCatalog.merge_catalogs() class method. # Copyright (c) 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\"\"\"Script that merge a collection of catalogs into one AntaCatalog.\"\"\"\n\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.models import AntaTest\n\nCATALOG_SUFFIX = \"-catalog.yml\"\nCATALOG_DIR = \"intended/test_catalogs/\"\n\nif __name__ == \"__main__\":\n catalogs = []\n for file in Path(CATALOG_DIR).glob(\"*\" + CATALOG_SUFFIX):\n device = str(file).removesuffix(CATALOG_SUFFIX).removeprefix(CATALOG_DIR)\n print(f\"Loading test catalog for device {device}\")\n catalog = AntaCatalog.parse(file)\n # Add the device name as a tag to all tests in the catalog\n for test in catalog.tests:\n test.inputs.filters = AntaTest.Input.Filters(tags={device})\n catalogs.append(catalog)\n\n # Merge all catalogs\n merged_catalog = AntaCatalog.merge_catalogs(catalogs)\n\n # Save the merged catalog to a file\n with Path(\"anta-catalog.yml\").open(\"w\") as f:\n f.write(merged_catalog.dump().yaml())\n Warning The AntaCatalog.merge() method is deprecated and will be removed in ANTA v2.0. Please use the AntaCatalog.merge_catalogs() class method instead."},{"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. "},{"location":"advanced_usages/as-python-lib/#examples","title":"Examples","text":""},{"location":"advanced_usages/as-python-lib/#parse-an-anta-inventory-file","title":"Parse an ANTA inventory file","text":"# Copyright (c) 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\"\"\"Script that parses an ANTA inventory file, connects to devices and print their status.\"\"\"\n\nimport asyncio\n\nfrom anta.inventory import AntaInventory\n\n\nasync def main(inv: AntaInventory) -> None:\n \"\"\"Read an AntaInventory and try to connect to every device in the inventory.\n\n 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\n\nif __name__ == \"__main__\":\n # Create the AntaInventory instance\n inventory = AntaInventory.parse(\n filename=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\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."},{"location":"advanced_usages/as-python-lib/#run-eos-commands","title":"Run EOS commands","text":"# Copyright (c) 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\"\"\"Script that runs a list of EOS commands on reachable devices.\"\"\"\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 \"\"\"Run a list of commands against each valid device in the inventory.\n\n Take an AntaInventory and a list of commands as string\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 -------\n dict[str, list[AntaCommand]]\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=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\n )\n\n # Create a list of commands with json output\n command_list = [\"show version\", \"show ip bgp summary\"]\n\n # Run the main asyncio entry point\n res = asyncio.run(main(inventory, command_list))\n\n pprint(res)\n"},{"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: 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 Caching can be disabled per device, network or range by setting the disable_cache key to True when defining 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. For tests developers, 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: set = None) -> None:\n super().__init__(name, tags, disable_cache=True)\n"},{"location":"advanced_usages/custom-tests/","title":"Developing ANTA tests","text":"Info 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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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":"Full AntaTest API documentation is available in the API documentation section"},{"location":"advanced_usages/custom-tests/#class-attributes","title":"Class Attributes","text":" name (str, optional): Name of the test. Used during reporting. By default set to the Class name. description (str, optional): A human readable description of your test. By default set to the first line of the docstring. 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":"Attributes: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. 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":"Full Input model documentation is available in API documentation section Attributes: Name Type Description result_overwrite ResultOverwrite | None Define fields to overwrite in the TestResult object."},{"location":"advanced_usages/custom-tests/#resultoverwrite-model","title":"ResultOverwrite model","text":"Full ResultOverwrite model documentation is available in API documentation section Attributes: Name Type Description description str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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 occurrence 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: 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. 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. 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. 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. 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, the first line is used as description of the test by default>\n \"\"\"\n\n # name = <override> # uncomment to override default behavior of name=Class Name\n # description = <override> # uncomment to override default behavior of description=first line of docstring\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 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 values 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, integrations with existing tools are not broken. This is done by using by default version=1 in eAPI calls. By default, ANTA uses version=\"latest\" in AntaCommand, but when developing tests, the revision MUST be provided when the outformat of the command is json. As explained earlier, this is to ensure that the eAPI always returns the same output model and that the test remains always valid from the day it was created. For some commands, you may also want to run them with a different revision or version. For instance, the VerifyBFDPeersHealth 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/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: Parse the command outputs using the self.instance_commands instance attribute. If needed, access the test inputs using the self.inputs instance attribute and write your conditional logic. 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_custom and the test is defined in anta_custom.dc_project Python module, the test catalog would look like: anta_custom.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(\n tests: list[AntaTestDefinition] | None = None,\n filename: str | Path | None = None,\n)\n Class representing an ANTA Catalog. It can be instantiated using its constructor or one of the static methods: parse(), from_list() or from_dict() Parameters: Name Type Description Default tests list[AntaTestDefinition] | None A list of AntaTestDefinition instances. None filename str | Path | None The path from which the catalog is loaded. None"},{"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.build_indexes","title":"build_indexes","text":"build_indexes(\n filtered_tests: set[str] | None = None,\n) -> None\n Indexes tests by their tags for quick access during filtering operations. If a filtered_tests set is provided, only the tests in this set will be indexed. This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests. Once the indexes are built, the indexes_built attribute is set to True. Source code in anta/catalog.py def build_indexes(self, filtered_tests: set[str] | None = None) -> None:\n \"\"\"Indexes tests by their tags for quick access during filtering operations.\n\n If a `filtered_tests` set is provided, only the tests in this set will be indexed.\n\n This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests.\n\n Once the indexes are built, the `indexes_built` attribute is set to True.\n \"\"\"\n for test in self.tests:\n # Skip tests that are not in the specified filtered_tests set\n if filtered_tests and test.test.name not in filtered_tests:\n continue\n\n # Indexing by tag\n if test.inputs.filters and (test_tags := test.inputs.filters.tags):\n for tag in test_tags:\n self.tag_to_tests[tag].add(test)\n else:\n self.tag_to_tests[None].add(test)\n\n self.indexes_built = True\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.clear_indexes","title":"clear_indexes","text":"clear_indexes() -> None\n Clear this AntaCatalog instance indexes. Source code in anta/catalog.py def clear_indexes(self) -> None:\n \"\"\"Clear this AntaCatalog instance indexes.\"\"\"\n self._init_indexes()\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.dump","title":"dump","text":"dump() -> AntaCatalogFile\n Return an AntaCatalogFile instance from this AntaCatalog instance. Returns: Type Description AntaCatalogFile An AntaCatalogFile instance containing tests of this AntaCatalog instance. Source code in anta/catalog.py def dump(self) -> AntaCatalogFile:\n \"\"\"Return an AntaCatalogFile instance from this AntaCatalog instance.\n\n Returns\n -------\n AntaCatalogFile\n An AntaCatalogFile instance containing tests of this AntaCatalog instance.\n \"\"\"\n root: dict[ImportString[Any], list[AntaTestDefinition]] = {}\n for test in self.tests:\n # Cannot use AntaTest.module property as the class is not instantiated\n root.setdefault(test.test.__module__, []).append(test)\n return AntaCatalogFile(root=root)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.from_dict","title":"from_dict staticmethod","text":"from_dict(\n data: RawCatalogInput,\n filename: str | Path | None = None,\n) -> 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. Parameters: Name Type Description Default data RawCatalogInput Python dictionary used to instantiate the AntaCatalog instance. required filename str | Path | None value to be set as AntaCatalog instance attribute None Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 dictionary content. 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 Parameters\n ----------\n data\n Python dictionary used to instantiate the AntaCatalog instance.\n filename\n value to be set as AntaCatalog instance attribute\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' dictionary content.\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. Parameters: Name Type Description Default data ListAntaTestTuples Python list used to instantiate the AntaCatalog instance. required Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 list content. 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 Parameters\n ----------\n data\n Python list used to instantiate the AntaCatalog instance.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' list content.\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(\n tags: set[str], *, strict: bool = False\n) -> set[AntaTestDefinition]\n Return all tests that match a given set of tags, according to the specified strictness. Parameters: Name Type Description Default tags set[str] The tags to filter tests by. If empty, return all tests without tags. required strict bool If True, returns only tests that contain all specified tags (intersection). If False, returns tests that contain any of the specified tags (union). False Returns: Type Description set[AntaTestDefinition] A set of tests that match the given tags. Raises: Type Description ValueError If the indexes have not been built prior to method call. Source code in anta/catalog.py def get_tests_by_tags(self, tags: set[str], *, strict: bool = False) -> set[AntaTestDefinition]:\n \"\"\"Return all tests that match a given set of tags, according to the specified strictness.\n\n Parameters\n ----------\n tags\n The tags to filter tests by. If empty, return all tests without tags.\n strict\n If True, returns only tests that contain all specified tags (intersection).\n If False, returns tests that contain any of the specified tags (union).\n\n Returns\n -------\n set[AntaTestDefinition]\n A set of tests that match the given tags.\n\n Raises\n ------\n ValueError\n If the indexes have not been built prior to method call.\n \"\"\"\n if not self.indexes_built:\n msg = \"Indexes have not been built yet. Call build_indexes() first.\"\n raise ValueError(msg)\n if not tags:\n return self.tag_to_tests[None]\n\n filtered_sets = [self.tag_to_tests[tag] for tag in tags if tag in self.tag_to_tests]\n if not filtered_sets:\n return set()\n\n if strict:\n return set.intersection(*filtered_sets)\n return set.union(*filtered_sets)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge","title":"merge","text":"merge(catalog: AntaCatalog) -> AntaCatalog\n Merge two AntaCatalog instances. Warning This method is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead. Parameters: Name Type Description Default catalog AntaCatalog AntaCatalog instance to merge to this instance. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of the two instances. Source code in anta/catalog.py def merge(self, catalog: AntaCatalog) -> AntaCatalog:\n \"\"\"Merge two AntaCatalog instances.\n\n Warning\n -------\n This method is deprecated and will be removed in ANTA v2.0. Use `AntaCatalog.merge_catalogs()` instead.\n\n Parameters\n ----------\n catalog\n AntaCatalog instance to merge to this instance.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of the two instances.\n \"\"\"\n # TODO: Use a decorator to deprecate this method instead. See https://github.com/aristanetworks/anta/issues/754\n warn(\n message=\"AntaCatalog.merge() is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n return self.merge_catalogs([self, catalog])\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge_catalogs","title":"merge_catalogs classmethod","text":"merge_catalogs(catalogs: list[AntaCatalog]) -> AntaCatalog\n Merge multiple AntaCatalog instances. Parameters: Name Type Description Default catalogs list[AntaCatalog] A list of AntaCatalog instances to merge. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of all the input catalogs. Source code in anta/catalog.py @classmethod\ndef merge_catalogs(cls, catalogs: list[AntaCatalog]) -> AntaCatalog:\n \"\"\"Merge multiple AntaCatalog instances.\n\n Parameters\n ----------\n catalogs\n A list of AntaCatalog instances to merge.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of all the input catalogs.\n \"\"\"\n combined_tests = list(chain(*(catalog.tests for catalog in catalogs)))\n return cls(tests=combined_tests)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.parse","title":"parse staticmethod","text":"parse(\n filename: str | Path,\n file_format: Literal[\"yaml\", \"json\"] = \"yaml\",\n) -> AntaCatalog\n Create an AntaCatalog instance from a test catalog file. Parameters: Name Type Description Default filename str | Path Path to test catalog YAML or JSON file. required file_format Literal['yaml', 'json'] Format of the file, either \u2018yaml\u2019 or \u2018json\u2019. 'yaml' Returns: Type Description AntaCatalog An AntaCatalog populated with the file content. Source code in anta/catalog.py @staticmethod\ndef parse(filename: str | Path, file_format: Literal[\"yaml\", \"json\"] = \"yaml\") -> AntaCatalog:\n \"\"\"Create an AntaCatalog instance from a test catalog file.\n\n Parameters\n ----------\n filename\n Path to test catalog YAML or JSON file.\n file_format\n Format of the file, either 'yaml' or 'json'.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the file content.\n \"\"\"\n if file_format not in [\"yaml\", \"json\"]:\n message = f\"'{file_format}' is not a valid format for an AntaCatalog file. Only 'yaml' and 'json' are supported.\"\n raise ValueError(message)\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) if file_format == \"yaml\" else json_load(f)\n except (TypeError, YAMLError, OSError, ValueError) 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(\n **data: (\n type[AntaTest]\n | AntaTest.Input\n | dict[str, Any]\n | None\n )\n)\n Bases: BaseModel Define a test with its associated inputs. Attributes: Name Type Description test type[AntaTest] An AntaTest concrete subclass. inputs Input The associated AntaTest.Input subclass instance. https://docs.pydantic.dev/2.0/usage/validators/#using-validation-context-with-basemodel-initialization."},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.check_inputs","title":"check_inputs","text":"check_inputs() -> Self\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) -> Self:\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(\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. 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\"Could not instantiate inputs as type {type(data).__name__} is not valid\"\n raise ValueError(msg)\n"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.serialize_model","title":"serialize_model","text":"serialize_model() -> dict[str, AntaTest.Input]\n Serialize the AntaTestDefinition model. The dictionary representing the model will be look like: <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n Returns: Type Description dict A dictionary representing the model. Source code in anta/catalog.py @model_serializer()\ndef serialize_model(self) -> dict[str, AntaTest.Input]:\n \"\"\"Serialize the AntaTestDefinition model.\n\n The dictionary representing the model will be look like:\n ```\n <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n ```\n\n Returns\n -------\n dict\n A dictionary representing the model.\n \"\"\"\n return {self.test.__name__: self.inputs}\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: <Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\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 if not data:\n return data\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 isinstance(test_definition, AntaTestDefinition):\n test_definitions.append(test_definition)\n continue\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 return data\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.flatten_modules","title":"flatten_modules staticmethod","text":"flatten_modules(\n data: dict[str, Any], package: str | None = None\n) -> dict[ModuleType, list[Any]]\n Allow the user to provide a data structure with nested Python modules. Example anta.tests.routing:\n generic:\n - <AntaTestDefinition>\n bgp:\n - <AntaTestDefinition>\n anta.tests.routing.generic and anta.tests.routing.bgp are importable Python modules. 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:\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 elif isinstance(tests, list):\n # This is a list of AntaTestDefinition\n modules[module] = tests\n else:\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 return modules\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.to_json","title":"to_json","text":"to_json() -> str\n Return a JSON representation string of this model. Returns: Type Description str The JSON representation string of this model. Source code in anta/catalog.py def to_json(self) -> str:\n \"\"\"Return a JSON representation string of this model.\n\n Returns\n -------\n str\n The JSON representation string of this model.\n \"\"\"\n return self.model_dump_json(serialize_as_any=True, exclude_unset=True, indent=2)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/catalog.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return safe_dump(safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/csv_reporter/","title":"CSV reporter","text":"CSV Report management for ANTA."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv","title":"ReportCsv","text":"Build a CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_name: str = \"Test Name\",\n test_status: str = \"Test Status\",\n messages: str = \"Message(s)\",\n description: str = \"Test description\",\n categories: str = \"Test category\",\n)\n Headers for the CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.convert_to_list","title":"convert_to_list classmethod","text":"convert_to_list(result: TestResult) -> list[str]\n Convert a TestResult into a list of string for creating file content. Parameters: Name Type Description Default result TestResult A TestResult to convert into list. required Returns: Type Description list[str] TestResult converted into a list. Source code in anta/reporter/csv_reporter.py @classmethod\ndef convert_to_list(cls, result: TestResult) -> list[str]:\n \"\"\"Convert a TestResult into a list of string for creating file content.\n\n Parameters\n ----------\n result\n A TestResult to convert into list.\n\n Returns\n -------\n list[str]\n TestResult converted into a list.\n \"\"\"\n message = cls.split_list_to_txt_list(result.messages) if len(result.messages) > 0 else \"\"\n categories = cls.split_list_to_txt_list(convert_categories(result.categories)) if len(result.categories) > 0 else \"None\"\n return [\n str(result.name),\n result.test,\n result.result,\n message,\n result.description,\n categories,\n ]\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.generate","title":"generate classmethod","text":"generate(\n results: ResultManager, csv_filename: pathlib.Path\n) -> None\n Build CSV flle with tests results. Parameters: Name Type Description Default results ResultManager A ResultManager instance. required csv_filename Path File path where to save CSV data. required Raises: Type Description OSError if any is raised while writing the CSV file. Source code in anta/reporter/csv_reporter.py @classmethod\ndef generate(cls, results: ResultManager, csv_filename: pathlib.Path) -> None:\n \"\"\"Build CSV flle with tests results.\n\n Parameters\n ----------\n results\n A ResultManager instance.\n csv_filename\n File path where to save CSV data.\n\n Raises\n ------\n OSError\n if any is raised while writing the CSV file.\n \"\"\"\n headers = [\n cls.Headers.device,\n cls.Headers.test_name,\n cls.Headers.test_status,\n cls.Headers.messages,\n cls.Headers.description,\n cls.Headers.categories,\n ]\n\n try:\n with csv_filename.open(mode=\"w\", encoding=\"utf-8\") as csvfile:\n csvwriter = csv.writer(\n csvfile,\n delimiter=\",\",\n )\n csvwriter.writerow(headers)\n for entry in results.results:\n csvwriter.writerow(cls.convert_to_list(entry))\n except OSError as exc:\n message = f\"OSError caught while writing the CSV file '{csv_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.split_list_to_txt_list","title":"split_list_to_txt_list classmethod","text":"split_list_to_txt_list(\n usr_list: list[str], delimiter: str = \" - \"\n) -> str\n Split list to multi-lines string. Parameters: Name Type Description Default usr_list list[str] List of string to concatenate. required delimiter str A delimiter to use to start string. Defaults to None. ' - ' Returns: Type Description str Multi-lines string. Source code in anta/reporter/csv_reporter.py @classmethod\ndef split_list_to_txt_list(cls, usr_list: list[str], delimiter: str = \" - \") -> str:\n \"\"\"Split list to multi-lines string.\n\n Parameters\n ----------\n usr_list\n List of string to concatenate.\n delimiter\n A delimiter to use to start string. Defaults to None.\n\n Returns\n -------\n str\n Multi-lines string.\n\n \"\"\"\n return f\"{delimiter}\".join(f\"{line}\" for line in usr_list)\n"},{"location":"api/device/","title":"Device","text":""},{"location":"api/device/#antadevice-base-class","title":"AntaDevice base class","text":""},{"location":"api/device/#anta.device.AntaDevice","title":"AntaDevice","text":"AntaDevice(\n name: str,\n tags: set[str] | None = None,\n *,\n disable_cache: bool = False\n)\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 str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. cache Cache | None In-memory cache from aiocache library for this device (None if cache is disabled). cache_locks dict Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled. Parameters: Name Type Description Default name str Device name. required tags set[str] | None Tags for this device. None disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AntaDevice.cache_statistics","title":"cache_statistics property","text":"cache_statistics: dict[str, Any] | None\n Return 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.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AntaDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AntaDevice.\"\"\"\n return (\n f\"AntaDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AntaDevice._collect","title":"_collect abstractmethod async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output. This abstract coroutine can be used to implement any command collection method for a device in ANTA. The _collect() implementation needs to populate the output attribute of the AntaCommand object passed as argument. If a failure occurs, the _collect() implementation is expected to catch the exception and implement proper logging, the output attribute of the AntaCommand object passed as argument would be None in this case. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py @abstractmethod\nasync def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None:\n \"\"\"Collect device command output.\n\n This abstract coroutine can be used to implement any command collection method\n for a device in ANTA.\n\n The `_collect()` implementation needs to populate the `output` attribute\n of the `AntaCommand` object passed as argument.\n\n If a failure occurs, the `_collect()` implementation is expected to catch the\n exception and implement proper logging, the `output` attribute of the\n `AntaCommand` object passed as argument would be `None` in this case.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n"},{"location":"api/device/#anta.device.AntaDevice.collect","title":"collect async","text":"collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> 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. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect(self, command: AntaCommand, *, collection_id: str | None = None) -> 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 Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\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, collection_id=collection_id)\n await self.cache.set(command.uid, command.output) # pylint: disable=no-member\n else:\n await self._collect(command=command, collection_id=collection_id)\n"},{"location":"api/device/#anta.device.AntaDevice.collect_commands","title":"collect_commands async","text":"collect_commands(\n commands: list[AntaCommand],\n *,\n collection_id: str | None = None\n) -> None\n Collect multiple commands. Parameters: Name Type Description Default commands list[AntaCommand] The commands to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect_commands(self, commands: list[AntaCommand], *, collection_id: str | None = None) -> None:\n \"\"\"Collect multiple commands.\n\n Parameters\n ----------\n commands\n The commands to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n await asyncio.gather(*(self.collect(command=command, collection_id=collection_id) for command in commands))\n"},{"location":"api/device/#anta.device.AntaDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device, usually through SCP. It is not mandatory to implement this for a valid AntaDevice subclass. Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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\n - `is_online`: When the device IP is reachable and a port can be open.\n\n - `established`: When a command execution succeeds.\n\n - `hw_model`: The hardware model of the device.\n \"\"\"\n"},{"location":"api/device/#async-eos-device-class","title":"Async EOS device class","text":""},{"location":"api/device/#anta.device.AsyncEOSDevice","title":"AsyncEOSDevice","text":"AsyncEOSDevice(\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: set[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)\n Bases: AntaDevice Implementation of AntaDevice for EOS using aio-eapi. Attributes: Name Type Description name str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. Parameters: Name Type Description Default host str Device FQDN or IP. required username str Username to connect to eAPI and SSH. required password str Password to connect to eAPI and SSH. required name str | None Device name. None enable bool Collect commands using privileged mode. False enable_password str | None Password used to gain privileged access on EOS. None port int | None eAPI port. Defaults to 80 is proto is \u2018http\u2019 or 443 if proto is \u2018https\u2019. None ssh_port int | None SSH port. 22 tags set[str] | None Tags for this device. None timeout float | None Timeout value in seconds for outgoing API calls. None insecure bool Disable SSH Host Key validation. False proto Literal['http', 'https'] eAPI protocol. Value can be \u2018http\u2019 or \u2018https\u2019. 'https' disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AsyncEOSDevice.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AsyncEOSDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AsyncEOSDevice.\"\"\"\n return (\n f\"AsyncEOSDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r}, \"\n f\"host={self._session.host!r}, \"\n f\"eapi_port={self._session.port!r}, \"\n f\"username={self._ssh_opts.username!r}, \"\n f\"enable={self.enable!r}, \"\n f\"insecure={self._ssh_opts.known_hosts is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AsyncEOSDevice._collect","title":"_collect async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output from EOS using aio-eapi. Supports outformat json and text as output structure. Gain privileged access using the enable_password attribute of the AntaDevice instance if populated. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None: # noqa: C901 function is too complex - because of many required except blocks\n \"\"\"Collect device command output from EOS using aio-eapi.\n\n Supports outformat `json` and `text` as output structure.\n Gain privileged access using the `enable_password` attribute\n of the `AntaDevice` instance if populated.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n commands: list[dict[str, str | int]] = []\n if self.enable and self._enable_password is not None:\n commands.append(\n {\n \"cmd\": \"enable\",\n \"input\": str(self._enable_password),\n },\n )\n elif self.enable:\n # No password\n commands.append({\"cmd\": \"enable\"})\n commands += [{\"cmd\": command.command, \"revision\": command.revision}] if command.revision else [{\"cmd\": command.command}]\n try:\n response: list[dict[str, Any] | str] = await self._session.cli(\n commands=commands,\n ofmt=command.ofmt,\n version=command.version,\n req_id=f\"ANTA-{collection_id}-{id(command)}\" if collection_id else f\"ANTA-{id(command)}\",\n ) # type: ignore[assignment] # multiple commands returns a list\n # Do not keep response of 'enable' command\n command.output = response[-1]\n except asynceapi.EapiCommandError as e:\n # This block catches exceptions related to EOS issuing an error.\n command.errors = e.errors\n if command.requires_privileges:\n logger.error(\n \"Command '%s' requires privileged mode on %s. Verify user permissions and if the `enable` option is required.\", command.command, self.name\n )\n if command.supported:\n logger.error(\"Command '%s' failed on %s: %s\", command.command, self.name, e.errors[0] if len(e.errors) == 1 else e.errors)\n else:\n logger.debug(\"Command '%s' is not supported on '%s' (%s)\", command.command, self.name, self.hw_model)\n except TimeoutException as e:\n # This block catches Timeout exceptions.\n command.errors = [exc_to_str(e)]\n timeouts = self._session.timeout.as_dict()\n logger.error(\n \"%s occurred while sending a command to %s. Consider increasing the timeout.\\nCurrent timeouts: Connect: %s | Read: %s | Write: %s | Pool: %s\",\n exc_to_str(e),\n self.name,\n timeouts[\"connect\"],\n timeouts[\"read\"],\n timeouts[\"write\"],\n timeouts[\"pool\"],\n )\n except (ConnectError, OSError) as e:\n # This block catches OSError and socket issues related exceptions.\n command.errors = [exc_to_str(e)]\n if (isinstance(exc := e.__cause__, httpcore.ConnectError) and isinstance(os_error := exc.__context__, OSError)) or isinstance(os_error := e, OSError): # pylint: disable=no-member\n if isinstance(os_error.__cause__, OSError):\n os_error = os_error.__cause__\n logger.error(\"A local OS error occurred while connecting to %s: %s.\", self.name, os_error)\n else:\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n except HTTPError as e:\n # This block catches most of the httpx Exceptions and logs a general message.\n command.errors = [exc_to_str(e)]\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n logger.debug(\"%s: %s\", self.name, command)\n"},{"location":"api/device/#anta.device.AsyncEOSDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device using asyncssh.scp(). Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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() function 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 = AntaCommand(command=\"show version\")\n await self._collect(show_version)\n if not show_version.collected:\n logger.warning(\"Cannot get hardware information from device %s\", self.name)\n else:\n self.hw_model = show_version.json_output.get(\"modelName\", None)\n if self.hw_model is None:\n logger.critical(\"Cannot parse 'show version' returned by device %s\", self.name)\n # in some cases it is possible that 'modelName' comes back empty\n # and it is nice to get a meaninfule error message\n elif self.hw_model == \"\":\n logger.critical(\"Got an empty 'modelName' in the 'show version' returned by device %s\", self.name)\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.devices","title":"devices property","text":"devices: list[AntaDevice]\n List of AntaDevice in this inventory."},{"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. Parameters: Name Type Description Default device AntaDevice Device object to be added. required Source code in anta/inventory/__init__.py def add_device(self, device: AntaDevice) -> None:\n \"\"\"Add a device to final inventory.\n\n Parameters\n ----------\n device\n 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(\n *,\n established_only: bool = False,\n tags: set[str] | None = None,\n devices: set[str] | None = None\n) -> AntaInventory\n Return a filtered inventory. Parameters: Name Type Description Default established_only bool Whether or not to include only established devices. False tags set[str] | None Tags to filter devices. None devices set[str] | None Names to filter devices. None 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: set[str] | None = None, devices: set[str] | None = None) -> AntaInventory:\n \"\"\"Return a filtered inventory.\n\n Parameters\n ----------\n established_only\n Whether or not to include only established devices.\n tags\n Tags to filter devices.\n devices\n Names to filter devices.\n\n Returns\n -------\n AntaInventory\n An inventory with filtered AntaDevice objects.\n \"\"\"\n\n def _filter_devices(device: AntaDevice) -> bool:\n \"\"\"Select the devices based on the inputs `tags`, `devices` and `established_only`.\"\"\"\n if tags is not None and all(tag not in tags for tag in device.tags):\n return False\n if devices is None or device.name in devices:\n return bool(not established_only or device.established)\n return False\n\n filtered_devices: list[AntaDevice] = list(filter(_filter_devices, self.values()))\n result = AntaInventory()\n for device in filtered_devices:\n result.add_device(device)\n return result\n"},{"location":"api/inventory/#anta.inventory.AntaInventory.parse","title":"parse staticmethod","text":"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. The inventory devices are AsyncEOSDevice instances. Parameters: Name Type Description Default filename str | Path Path to device inventory YAML file. required username str Username to use to connect to devices. required password str Password to use to connect to devices. required enable_password str | None Enable password to use if required. None timeout float | None Timeout value in seconds for outgoing API calls. None enable bool Whether or not the commands need to be run in enable mode towards the devices. False insecure bool Disable SSH Host Key validation. False disable_cache bool Disable cache globally. False 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 Parameters\n ----------\n filename\n Path to device inventory YAML file.\n username\n Username to use to connect to devices.\n password\n Password to use to connect to devices.\n enable_password\n Enable password to use if required.\n timeout\n Timeout value in seconds for outgoing API calls.\n enable\n Whether or not the commands need to be run in enable mode towards the devices.\n insecure\n Disable SSH Host Key validation.\n disable_cache\n Disable cache globally.\n\n Raises\n ------\n InventoryRootKeyError\n Root key of inventory is missing.\n InventoryIncorrectSchemaError\n 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 Device inventory input model."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryInput.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/inventory/models.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return yaml.safe_dump(yaml.safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryHost","title":"AntaInventoryHost","text":" Bases: BaseModel Host entry of AntaInventoryInput. Attributes: Name Type Description host Hostname | IPvAnyAddress IP Address or FQDN of the device. port Port | None Custom eAPI port to use. name str | None Custom name of the device. tags set[str] Tags of the device. disable_cache bool Disable cache for this device."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryNetwork","title":"AntaInventoryNetwork","text":" Bases: BaseModel Network entry of AntaInventoryInput. Attributes: Name Type Description network IPvAnyNetwork Subnet to use for scanning. tags set[str] Tags of the devices in this network. disable_cache bool Disable cache for all devices in this network."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryRange","title":"AntaInventoryRange","text":" Bases: BaseModel IP Range entry of AntaInventoryInput. Attributes: Name Type Description start IPvAnyAddress IPv4 or IPv6 address for the beginning of the range. stop IPvAnyAddress IPv4 or IPv6 address for the end of the range. tags set[str] Tags of the devices in this IP range. disable_cache bool Disable cache for all devices in this IP range."},{"location":"api/md_reporter/","title":"Markdown reporter","text":"Markdown report generator for ANTA test results."},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport","title":"ANTAReport","text":"ANTAReport(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generate the # ANTA Report section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the # ANTA Report section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `# ANTA Report` section of the markdown report.\"\"\"\n self.write_heading(heading_level=1)\n toc = MD_REPORT_TOC\n self.mdfile.write(toc + \"\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase","title":"MDReportBase","text":"MDReportBase(mdfile: TextIOWrapper, results: ResultManager)\n Bases: ABC Base class for all sections subclasses. Every subclasses must implement the generate_section method that uses the ResultManager object to generate and write content to the provided markdown file. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_heading_name","title":"generate_heading_name","text":"generate_heading_name() -> str\n Generate a formatted heading name based on the class name. Returns: Type Description str Formatted header name. Example ANTAReport will become ANTA Report. TestResultsSummary will become Test Results Summary. Source code in anta/reporter/md_reporter.py def generate_heading_name(self) -> str:\n \"\"\"Generate a formatted heading name based on the class name.\n\n Returns\n -------\n str\n Formatted header name.\n\n Example\n -------\n - `ANTAReport` will become `ANTA Report`.\n - `TestResultsSummary` will become `Test Results Summary`.\n \"\"\"\n class_name = self.__class__.__name__\n\n # Split the class name into words, keeping acronyms together\n words = re.findall(r\"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\\d|\\W|$)|\\d+\", class_name)\n\n # Capitalize each word, but keep acronyms in all caps\n formatted_words = [word if word.isupper() else word.capitalize() for word in words]\n\n return \" \".join(formatted_words)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of a markdown table for a specific report section. Subclasses can implement this method to generate the content of the table rows. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of a markdown table for a specific report section.\n\n Subclasses can implement this method to generate the content of the table rows.\n \"\"\"\n msg = \"Subclasses should implement this method\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_section","title":"generate_section abstractmethod","text":"generate_section() -> None\n Abstract method to generate a specific section of the markdown report. Must be implemented by subclasses. Source code in anta/reporter/md_reporter.py @abstractmethod\ndef generate_section(self) -> None:\n \"\"\"Abstract method to generate a specific section of the markdown report.\n\n Must be implemented by subclasses.\n \"\"\"\n msg = \"Must be implemented by subclasses\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.safe_markdown","title":"safe_markdown","text":"safe_markdown(text: str | None) -> str\n Escape markdown characters in the text to prevent markdown rendering issues. Parameters: Name Type Description Default text str | None The text to escape markdown characters from. required Returns: Type Description str The text with escaped markdown characters. Source code in anta/reporter/md_reporter.py def safe_markdown(self, text: str | None) -> str:\n \"\"\"Escape markdown characters in the text to prevent markdown rendering issues.\n\n Parameters\n ----------\n text\n The text to escape markdown characters from.\n\n Returns\n -------\n str\n The text with escaped markdown characters.\n \"\"\"\n # Custom field from a TestResult object can be None\n if text is None:\n return \"\"\n\n # Replace newlines with spaces to keep content on one line\n text = text.replace(\"\\n\", \" \")\n\n # Replace backticks with single quotes\n return text.replace(\"`\", \"'\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_heading","title":"write_heading","text":"write_heading(heading_level: int) -> None\n Write a markdown heading to the markdown file. The heading name used is the class name. Parameters: Name Type Description Default heading_level int The level of the heading (1-6). required Example ## Test Results Summary Source code in anta/reporter/md_reporter.py def write_heading(self, heading_level: int) -> None:\n \"\"\"Write a markdown heading to the markdown file.\n\n The heading name used is the class name.\n\n Parameters\n ----------\n heading_level\n The level of the heading (1-6).\n\n Example\n -------\n `## Test Results Summary`\n \"\"\"\n # Ensure the heading level is within the valid range of 1 to 6\n heading_level = max(1, min(heading_level, 6))\n heading_name = self.generate_heading_name()\n heading = \"#\" * heading_level + \" \" + heading_name\n self.mdfile.write(f\"{heading}\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_table","title":"write_table","text":"write_table(\n table_heading: list[str], *, last_table: bool = False\n) -> None\n Write a markdown table with a table heading and multiple rows to the markdown file. Parameters: Name Type Description Default table_heading list[str] List of strings to join for the table heading. required last_table bool Flag to determine if it\u2019s the last table of the markdown file to avoid unnecessary new line. Defaults to False. False Source code in anta/reporter/md_reporter.py def write_table(self, table_heading: list[str], *, last_table: bool = False) -> None:\n \"\"\"Write a markdown table with a table heading and multiple rows to the markdown file.\n\n Parameters\n ----------\n table_heading\n List of strings to join for the table heading.\n last_table\n Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.\n \"\"\"\n self.mdfile.write(\"\\n\".join(table_heading) + \"\\n\")\n for row in self.generate_rows():\n self.mdfile.write(row)\n if not last_table:\n self.mdfile.write(\"\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator","title":"MDReportGenerator","text":"Class responsible for generating a Markdown report based on the provided ResultManager object. It aggregates different report sections, each represented by a subclass of MDReportBase, and sequentially generates their content into a markdown file. The generate class method will loop over all the section subclasses and call their generate_section method. The final report will be generated in the same order as the sections list of the method."},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator.generate","title":"generate classmethod","text":"generate(results: ResultManager, md_filename: Path) -> None\n Generate and write the various sections of the markdown report. Parameters: Name Type Description Default results ResultManager The ResultsManager instance containing all test results. required md_filename Path The path to the markdown file to write the report into. required Source code in anta/reporter/md_reporter.py @classmethod\ndef generate(cls, results: ResultManager, md_filename: Path) -> None:\n \"\"\"Generate and write the various sections of the markdown report.\n\n Parameters\n ----------\n results\n The ResultsManager instance containing all test results.\n md_filename\n The path to the markdown file to write the report into.\n \"\"\"\n try:\n with md_filename.open(\"w\", encoding=\"utf-8\") as mdfile:\n sections: list[MDReportBase] = [\n ANTAReport(mdfile, results),\n TestResultsSummary(mdfile, results),\n SummaryTotals(mdfile, results),\n SummaryTotalsDeviceUnderTest(mdfile, results),\n SummaryTotalsPerCategory(mdfile, results),\n TestResults(mdfile, results),\n ]\n for section in sections:\n section.generate_section()\n except OSError as exc:\n message = f\"OSError caught while writing the Markdown file '{md_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals","title":"SummaryTotals","text":"SummaryTotals(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals table.\"\"\"\n yield (\n f\"| {self.results.get_total_results()} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SUCCESS})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SKIPPED})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.FAILURE})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.ERROR})} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest","title":"SummaryTotalsDeviceUnderTest","text":"SummaryTotalsDeviceUnderTest(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Devices Under Tests section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals device under test table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals device under test table.\"\"\"\n for device, stat in self.results.device_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n categories_skipped = \", \".join(sorted(convert_categories(list(stat.categories_skipped))))\n categories_failed = \", \".join(sorted(convert_categories(list(stat.categories_failed))))\n yield (\n f\"| {device} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} | {stat.tests_error_count} \"\n f\"| {categories_skipped or '-'} | {categories_failed or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Devices Under Tests section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Devices Under Tests` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory","title":"SummaryTotalsPerCategory","text":"SummaryTotalsPerCategory(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Per Category section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals per category table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals per category table.\"\"\"\n for category, stat in self.results.sorted_category_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n yield (\n f\"| {category} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} \"\n f\"| {stat.tests_error_count} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Per Category section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Per Category` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults","title":"TestResults","text":"TestResults(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generates the ## Test Results section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the all test results table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the all test results table.\"\"\"\n for result in self.results.get_results(sort_by=[\"name\", \"test\"]):\n messages = self.safe_markdown(\", \".join(result.messages))\n categories = \", \".join(convert_categories(result.categories))\n yield (\n f\"| {result.name or '-'} | {categories or '-'} | {result.test or '-'} \"\n f\"| {result.description or '-'} | {self.safe_markdown(result.custom_field) or '-'} | {result.result or '-'} | {messages or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n self.write_table(table_heading=self.TABLE_HEADING, last_table=True)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary","title":"TestResultsSummary","text":"TestResultsSummary(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ## Test Results Summary section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results Summary section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results Summary` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n"},{"location":"api/models/","title":"Test models","text":""},{"location":"api/models/#test-definition","title":"Test definition","text":""},{"location":"api/models/#anta.models.AntaTest","title":"AntaTest","text":"AntaTest(\n device: AntaDevice,\n inputs: dict[str, Any] | AntaTest.Input | None = None,\n eos_data: list[dict[Any, Any] | str] | None = None,\n)\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 '''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 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: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. Parameters: Name Type Description Default device AntaDevice AntaDevice instance on which the test will be run. required inputs dict[str, Any] | Input | None Dictionary of attributes used to instantiate the AntaTest.Input instance. None eos_data list[dict[Any, Any] | str] | None Populate outputs of the test commands instead of collecting from devices. This list must have the same length and order than the instance_commands instance attribute. None"},{"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 Return 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 Return a list of all the commands that have failed."},{"location":"api/models/#anta.models.AntaTest.module","title":"module property","text":"module: str\n Return the Python module in which this AntaTest class is defined."},{"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: Name Type Description result_overwrite ResultOverwrite | None 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 set[str] | None Tag of devices on which to run 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 str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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(\n function: F,\n) -> Callable[..., Coroutine[Any, Any, TestResult]]\n Decorate the test() method in child classes. This decorator implements (in this order): Instantiate the command outputs if eos_data is provided to the test() method Collect the commands from the device Run the test() method 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 Parameters\n ----------\n self\n The test instance.\n eos_data\n 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\n Any keyword argument to pass to the test.\n\n Returns\n -------\n TestResult\n The TestResult instance attribute populated with error status if any.\n\n \"\"\"\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 AntaTest.update_progress()\n return self.result\n\n if cmds := self.failed_commands:\n unsupported_commands = [f\"'{c.command}' is not supported on {self.device.hw_model}\" for c in cmds if not c.supported]\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 else:\n self.result.is_error(message=\"\\n\".join([f\"{c.command} has failed: {', '.join(c.errors)}\" for c in cmds]))\n AntaTest.update_progress()\n return self.result\n\n try:\n function(self, **kwargs)\n except Exception as e: # noqa: BLE001\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 # TODO: find a correct way to time test execution\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, collection_id=self.name)\n except Exception as e: # noqa: BLE001\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]:\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 _ = template\n msg = f\"AntaTemplate are provided but render() method has not been implemented for {self.module}.{self.__class__.__name__}\"\n raise NotImplementedError(msg)\n"},{"location":"api/models/#anta.models.AntaTest.save_commands_data","title":"save_commands_data","text":"save_commands_data(\n eos_data: list[dict[str, Any] | str]\n) -> 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 reason\")\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 reason\")\n ```\n\n \"\"\"\n"},{"location":"api/models/#command-definition","title":"Command definition","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 str Device command. version Literal[1, 'latest'] eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision | None eAPI revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt Literal['json', 'text'] eAPI output - json or text. output dict[str, Any] | str | None Output of the command. Only defined if there was no errors. template AntaTemplate | None AntaTemplate object used to render this command. errors list[str] If the command execution fails, eAPI returns a list of strings detailing the error(s). params AntaParamsBaseModel Pydantic Model containing the variables values used to render the template. use_cache bool Enable or disable caching for this AntaCommand if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaCommand.collected","title":"collected property","text":"collected: bool\n Return True if the command has been collected, False otherwise. A command that has not been collected could have returned an error. See error property."},{"location":"api/models/#anta.models.AntaCommand.error","title":"error property","text":"error: bool\n Return True if the command returned an error, False otherwise."},{"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.requires_privileges","title":"requires_privileges property","text":"requires_privileges: bool\n Return True if the command requires privileged mode, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"location":"api/models/#anta.models.AntaCommand.supported","title":"supported property","text":"supported: bool\n Return True if the command is supported on the device hardware platform, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"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/#anta.models.AntaTemplate","title":"AntaTemplate","text":"AntaTemplate(\n template: str,\n version: Literal[1, \"latest\"] = \"latest\",\n revision: Revision | None = None,\n ofmt: Literal[\"json\", \"text\"] = \"json\",\n *,\n use_cache: bool = True\n)\n 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: \u2018show vlan {vlan_id}\u2019. version eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt eAPI output - json or text. use_cache Enable or disable caching for this AntaTemplate if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaTemplate.__repr__","title":"__repr__","text":"__repr__() -> str\n Return the representation of the class. Copying pydantic model style, excluding params_schema Source code in anta/models.py def __repr__(self) -> str:\n \"\"\"Return the representation of the class.\n\n Copying pydantic model style, excluding `params_schema`\n \"\"\"\n return \" \".join(f\"{a}={v!r}\" for a, v in vars(self).items() if a != \"params_schema\")\n"},{"location":"api/models/#anta.models.AntaTemplate.render","title":"render","text":"render(**params: str | int | bool) -> AntaCommand\n Render an AntaCommand from an AntaTemplate instance. Keep the parameters used in the AntaTemplate instance. Parameters: Name Type Description Default params str | int | bool Dictionary of variables with string values to render the Python f-string. {} Returns: Type Description AntaCommand The rendered AntaCommand. This AntaCommand instance have a template attribute that references this AntaTemplate instance. Raises: Type Description AntaTemplateRenderError If a parameter is missing to render the AntaTemplate instance. Source code in anta/models.py def render(self, **params: str | int | bool) -> AntaCommand:\n \"\"\"Render an AntaCommand from an AntaTemplate instance.\n\n Keep the parameters used in the AntaTemplate instance.\n\n Parameters\n ----------\n params\n Dictionary of variables with string values to render the Python f-string.\n\n Returns\n -------\n AntaCommand\n The rendered AntaCommand.\n This AntaCommand instance have a template attribute that references this\n AntaTemplate instance.\n\n Raises\n ------\n AntaTemplateRenderError\n If a parameter is missing to render the AntaTemplate instance.\n \"\"\"\n try:\n command = self.template.format(**params)\n except (KeyError, SyntaxError) as e:\n raise AntaTemplateRenderError(self, e.args[0]) from e\n return AntaCommand(\n command=command,\n ofmt=self.ofmt,\n version=self.version,\n revision=self.revision,\n template=self,\n params=self.params_schema(**params),\n use_cache=self.use_cache,\n )\n"},{"location":"api/reporters/","title":"Other reporters","text":"Report management for ANTA."},{"location":"api/reporters/#anta.reporter.ReportJinja","title":"ReportJinja","text":"ReportJinja(template_path: pathlib.Path)\n Report builder based on a Jinja2 template."},{"location":"api/reporters/#anta.reporter.ReportJinja.render","title":"render","text":"render(\n data: list[dict[str, Any]],\n *,\n trim_blocks: bool = True,\n lstrip_blocks: bool = True\n) -> str\n Build a report based on a Jinja2 template. Report is built based on a J2 template provided by user. Data structure sent to template is: Example >>> print(ResultManager.json)\n[\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n]\n Parameters: Name Type Description Default data list[dict[str, Any]] List of results from ResultManager.results. required trim_blocks bool enable trim_blocks for J2 rendering. True lstrip_blocks bool enable lstrip_blocks for J2 rendering. True Returns: Type Description str Rendered template Source code in anta/reporter/__init__.py def render(self, data: list[dict[str, Any]], *, trim_blocks: bool = True, lstrip_blocks: bool = True) -> str:\n \"\"\"Build a report based on a Jinja2 template.\n\n Report is built based on a J2 template provided by user.\n Data structure sent to template is:\n\n Example\n -------\n ```\n >>> print(ResultManager.json)\n [\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n ]\n ```\n\n Parameters\n ----------\n data\n List of results from `ResultManager.results`.\n trim_blocks\n enable trim_blocks for J2 rendering.\n lstrip_blocks\n enable lstrip_blocks for J2 rendering.\n\n Returns\n -------\n str\n Rendered template\n\n \"\"\"\n with self.template_path.open(encoding=\"utf-8\") as file_:\n template = Template(file_.read(), trim_blocks=trim_blocks, lstrip_blocks=lstrip_blocks)\n\n return template.render({\"data\": data})\n"},{"location":"api/reporters/#anta.reporter.ReportTable","title":"ReportTable","text":"TableReport Generate a Table based on TestResult."},{"location":"api/reporters/#anta.reporter.ReportTable.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_case: str = \"Test Name\",\n number_of_success: str = \"# of success\",\n number_of_failure: str = \"# of failure\",\n number_of_skipped: str = \"# of skipped\",\n number_of_errors: str = \"# of errors\",\n list_of_error_nodes: str = \"List of failed or error nodes\",\n list_of_error_tests: str = \"List of failed or error test cases\",\n)\n Headers for the table report."},{"location":"api/reporters/#anta.reporter.ReportTable.report_all","title":"report_all","text":"report_all(\n manager: ResultManager, title: str = \"All tests results\"\n) -> Table\n Create a table report with all tests for one or all devices. Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required title str Title for the report. Defaults to \u2018All tests results\u2019. 'All tests results' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_all(self, manager: ResultManager, title: str = \"All tests results\") -> Table:\n \"\"\"Create a table report with all tests for one or all devices.\n\n Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n title\n Title for the report. Defaults to 'All tests results'.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\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 def add_line(result: TestResult) -> None:\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(convert_categories(result.categories))\n table.add_row(str(result.name), result.test, state, message, result.description, categories)\n\n for result in manager.results:\n add_line(result)\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_devices","title":"report_summary_devices","text":"report_summary_devices(\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table\n Create a table report with result aggregated per device. Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required devices list[str] | None List of device names to include. None to select all devices. None title str Title of the report. 'Summary per device' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_devices(\n self,\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per device.\n\n Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n devices\n List of device names to include. None to select all devices.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.device,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_tests,\n ]\n table = self._build_headers(headers=headers, table=table)\n for device, stats in sorted(manager.device_stats.items()):\n if devices is None or device in devices:\n table.add_row(\n device,\n str(stats.tests_success_count),\n str(stats.tests_skipped_count),\n str(stats.tests_failure_count),\n str(stats.tests_error_count),\n \", \".join(stats.tests_failure),\n )\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_tests","title":"report_summary_tests","text":"report_summary_tests(\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table\n Create a table report with result aggregated per test. Create table with full output: Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required tests list[str] | None List of test names to include. None to select all tests. None title str Title of the report. 'Summary per test' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_tests(\n self,\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per test.\n\n Create table with full output:\n Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n tests\n List of test names to include. None to select all tests.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.test_case,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_nodes,\n ]\n table = self._build_headers(headers=headers, table=table)\n for test, stats in sorted(manager.test_stats.items()):\n if tests is None or test in tests:\n table.add_row(\n test,\n str(stats.devices_success_count),\n str(stats.devices_skipped_count),\n str(stats.devices_failure_count),\n str(stats.devices_error_count),\n \", \".join(stats.devices_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":" options:\n filters: [\"!^_[^_]\", \"!^__len__\"]\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager","title":"ResultManager","text":"ResultManager()\n Helper to manage Test Results and generate reports. Examples Create Inventory: inventory_anta = AntaInventory.parse(\n filename='examples/inventory.yml',\n username='ansible',\n password='ansible',\n)\n Create Result Manager: manager = ResultManager()\n Run tests for all connected devices: for device in inventory_anta.get_inventory().devices:\n manager.add(\n VerifyNTP(device=device).test()\n )\n manager.add(\n VerifyEOSVersion(device=device).test(version='4.28.3M')\n )\n Print result in native format: manager.results\n[\n TestResult(\n name=\"pf1\",\n test=\"VerifyZeroTouch\",\n categories=[\"configuration\"],\n description=\"Verifies ZeroTouch is disabled\",\n result=\"success\",\n messages=[],\n custom_field=None,\n ),\n TestResult(\n name=\"pf1\",\n test='VerifyNTP',\n categories=[\"software\"],\n categories=['system'],\n description='Verifies if NTP is synchronised.',\n result='failure',\n messages=[\"The device is not synchronized with the configured NTP server(s): 'NTP is disabled.'\"],\n custom_field=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."},{"location":"api/result_manager/#anta.result_manager.ResultManager.json","title":"json property","text":"json: str\n Get a JSON representation of the results."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results","title":"results property writable","text":"results: list[TestResult]\n Get the list of TestResult."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results_by_status","title":"results_by_status cached property","text":"results_by_status: dict[AntaTestStatus, list[TestResult]]\n A cached property that returns the results grouped by status."},{"location":"api/result_manager/#anta.result_manager.ResultManager.sorted_category_stats","title":"sorted_category_stats property","text":"sorted_category_stats: dict[str, CategoryStats]\n A property that returns the category_stats dictionary sorted by key name."},{"location":"api/result_manager/#anta.result_manager.ResultManager.__len__","title":"__len__","text":"__len__() -> int\n Implement len method to count number of results. Source code in anta/result_manager/__init__.py def __len__(self) -> int:\n \"\"\"Implement __len__ method to count number of results.\"\"\"\n return len(self._result_entries)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.add","title":"add","text":"add(result: TestResult) -> None\n Add a result to the ResultManager instance. The result is added to the internal list of results and the overall status of the ResultManager instance is updated based on the added test status. Parameters: Name Type Description Default result TestResult TestResult to add to the ResultManager instance. required Source code in anta/result_manager/__init__.py def add(self, result: TestResult) -> None:\n \"\"\"Add a result to the ResultManager instance.\n\n The result is added to the internal list of results and the overall status\n of the ResultManager instance is updated based on the added test status.\n\n Parameters\n ----------\n result\n TestResult to add to the ResultManager instance.\n \"\"\"\n self._result_entries.append(result)\n self._update_status(result.result)\n self._update_stats(result)\n\n # Every time a new result is added, we need to clear the cached property\n self.__dict__.pop(\"results_by_status\", None)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter","title":"filter","text":"filter(hide: set[AntaTestStatus]) -> ResultManager\n Get a filtered ResultManager based on test status. Parameters: Name Type Description Default hide set[AntaTestStatus] Set of AntaTestStatus enum members to select tests to hide based on their status. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter(self, hide: set[AntaTestStatus]) -> ResultManager:\n \"\"\"Get a filtered ResultManager based on test status.\n\n Parameters\n ----------\n hide\n Set of AntaTestStatus enum members to select tests to hide based on their status.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n possible_statuses = set(AntaTestStatus)\n manager = ResultManager()\n manager.results = self.get_results(possible_statuses - hide)\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_devices","title":"filter_by_devices","text":"filter_by_devices(devices: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific devices. Parameters: Name Type Description Default devices set[str] Set of device names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_devices(self, devices: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific devices.\n\n Parameters\n ----------\n devices\n Set of device names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.name in devices]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_tests","title":"filter_by_tests","text":"filter_by_tests(tests: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific tests. Parameters: Name Type Description Default tests set[str] Set of test names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_tests(self, tests: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific tests.\n\n Parameters\n ----------\n tests\n Set of test names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.test in tests]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_devices","title":"get_devices","text":"get_devices() -> set[str]\n Get the set of all the device names. Returns: Type Description set[str] Set of device names. Source code in anta/result_manager/__init__.py def get_devices(self) -> set[str]:\n \"\"\"Get the set of all the device names.\n\n Returns\n -------\n set[str]\n Set of device names.\n \"\"\"\n return {str(result.name) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_results","title":"get_results","text":"get_results(\n status: set[AntaTestStatus] | None = None,\n sort_by: list[str] | None = None,\n) -> list[TestResult]\n Get the results, optionally filtered by status and sorted by TestResult fields. If no status is provided, all results are returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None sort_by list[str] | None Optional list of TestResult fields to sort the results. None Returns: Type Description list[TestResult] List of results. Source code in anta/result_manager/__init__.py def get_results(self, status: set[AntaTestStatus] | None = None, sort_by: list[str] | None = None) -> list[TestResult]:\n \"\"\"Get the results, optionally filtered by status and sorted by TestResult fields.\n\n If no status is provided, all results are returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n sort_by\n Optional list of TestResult fields to sort the results.\n\n Returns\n -------\n list[TestResult]\n List of results.\n \"\"\"\n # Return all results if no status is provided, otherwise return results for multiple statuses\n results = self._result_entries if status is None else list(chain.from_iterable(self.results_by_status.get(status, []) for status in status))\n\n if sort_by:\n accepted_fields = TestResult.model_fields.keys()\n if not set(sort_by).issubset(set(accepted_fields)):\n msg = f\"Invalid sort_by fields: {sort_by}. Accepted fields are: {list(accepted_fields)}\"\n raise ValueError(msg)\n results = sorted(results, key=lambda result: [getattr(result, field) for field in sort_by])\n\n return results\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_tests","title":"get_tests","text":"get_tests() -> set[str]\n Get the set of all the test names. Returns: Type Description set[str] Set of test names. Source code in anta/result_manager/__init__.py def get_tests(self) -> set[str]:\n \"\"\"Get the set of all the test names.\n\n Returns\n -------\n set[str]\n Set of test names.\n \"\"\"\n return {str(result.test) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_total_results","title":"get_total_results","text":"get_total_results(\n status: set[AntaTestStatus] | None = None,\n) -> int\n Get the total number of results, optionally filtered by status. If no status is provided, the total number of results is returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None Returns: Type Description int Total number of results. Source code in anta/result_manager/__init__.py def get_total_results(self, status: set[AntaTestStatus] | None = None) -> int:\n \"\"\"Get the total number of results, optionally filtered by status.\n\n If no status is provided, the total number of results is returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n\n Returns\n -------\n int\n Total number of results.\n \"\"\"\n if status is None:\n # Return the total number of results\n return sum(len(results) for results in self.results_by_status.values())\n\n # Return the total number of results for multiple statuses\n return sum(len(self.results_by_status.get(status, [])) for status in status)\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/#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 str Name of the device where the test was run. test str Name of the test run on the device. categories list[str] List of categories the TestResult belongs to. Defaults to the AntaTest categories. description str Description of the TestResult. Defaults to the AntaTest description. result AntaTestStatus Result of the test. Must be one of the AntaTestStatus Enum values: unset, success, failure, error or skipped. messages list[str] Messages to report after the test, if any. custom_field str | None 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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_error(self, message: str | None = None) -> None:\n \"\"\"Set status to error.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_failure(self, message: str | None = None) -> None:\n \"\"\"Set status to failure.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_skipped(self, message: str | None = None) -> None:\n \"\"\"Set status to skipped.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_success(self, message: str | None = None) -> None:\n \"\"\"Set status to success.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.SUCCESS, message)\n"},{"location":"api/runner/","title":"Runner","text":""},{"location":"api/runner/#anta.runner","title":"runner","text":"ANTA runner function."},{"location":"api/runner/#anta.runner.adjust_rlimit_nofile","title":"adjust_rlimit_nofile","text":"adjust_rlimit_nofile() -> tuple[int, int]\n Adjust the maximum number of open file descriptors for the ANTA process. The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable. If the ANTA_NOFILE environment variable is not set or is invalid, DEFAULT_NOFILE is used. Returns: Type Description tuple[int, int] The new soft and hard limits for open file descriptors. Source code in anta/runner.py def adjust_rlimit_nofile() -> tuple[int, int]:\n \"\"\"Adjust the maximum number of open file descriptors for the ANTA process.\n\n The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable.\n\n If the `ANTA_NOFILE` environment variable is not set or is invalid, `DEFAULT_NOFILE` is used.\n\n Returns\n -------\n tuple[int, int]\n The new soft and hard limits for open file descriptors.\n \"\"\"\n try:\n nofile = int(os.environ.get(\"ANTA_NOFILE\", DEFAULT_NOFILE))\n except ValueError as exception:\n logger.warning(\"The ANTA_NOFILE environment variable value is invalid: %s\\nDefault to %s.\", exc_to_str(exception), DEFAULT_NOFILE)\n nofile = DEFAULT_NOFILE\n\n limits = resource.getrlimit(resource.RLIMIT_NOFILE)\n logger.debug(\"Initial limit numbers for open file descriptors for the current ANTA process: Soft Limit: %s | Hard Limit: %s\", limits[0], limits[1])\n nofile = min(limits[1], nofile)\n logger.debug(\"Setting soft limit for open file descriptors for the current ANTA process to %s\", nofile)\n resource.setrlimit(resource.RLIMIT_NOFILE, (nofile, limits[1]))\n return resource.getrlimit(resource.RLIMIT_NOFILE)\n"},{"location":"api/runner/#anta.runner.get_coroutines","title":"get_coroutines","text":"get_coroutines(\n selected_tests: defaultdict[\n AntaDevice, set[AntaTestDefinition]\n ],\n manager: ResultManager,\n) -> list[Coroutine[Any, Any, TestResult]]\n Get the coroutines for the ANTA run. Parameters: Name Type Description Default selected_tests defaultdict[AntaDevice, set[AntaTestDefinition]] A mapping of devices to the tests to run. The selected tests are generated by the prepare_tests function. required manager ResultManager A ResultManager required Returns: Type Description list[Coroutine[Any, Any, TestResult]] The list of coroutines to run. Source code in anta/runner.py def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]], manager: ResultManager) -> list[Coroutine[Any, Any, TestResult]]:\n \"\"\"Get the coroutines for the ANTA run.\n\n Parameters\n ----------\n selected_tests\n A mapping of devices to the tests to run. The selected tests are generated by the `prepare_tests` function.\n manager\n A ResultManager\n\n Returns\n -------\n list[Coroutine[Any, Any, TestResult]]\n The list of coroutines to run.\n \"\"\"\n coros = []\n for device, test_definitions in selected_tests.items():\n for test in test_definitions:\n try:\n test_instance = test.test(device=device, inputs=test.inputs)\n manager.add(test_instance.result)\n coros.append(test_instance.test())\n except Exception as e: # noqa: PERF203, BLE001\n # An AntaTest instance is potentially user-defined code.\n # We need to catch everything and exit gracefully with an error message.\n message = \"\\n\".join(\n [\n f\"There is an error when creating test {test.test.__module__}.{test.test.__name__}.\",\n f\"If this is not a custom test implementation: {GITHUB_SUGGESTION}\",\n ],\n )\n anta_log_exception(e, message, logger)\n return coros\n"},{"location":"api/runner/#anta.runner.log_cache_statistics","title":"log_cache_statistics","text":"log_cache_statistics(devices: list[AntaDevice]) -> None\n Log cache statistics for each device in the inventory. Parameters: Name Type Description Default devices list[AntaDevice] List of devices in the inventory. required Source code in anta/runner.py def log_cache_statistics(devices: list[AntaDevice]) -> None:\n \"\"\"Log cache statistics for each device in the inventory.\n\n Parameters\n ----------\n devices\n List of devices in the inventory.\n \"\"\"\n for device in devices:\n if device.cache_statistics is not None:\n msg = (\n f\"Cache statistics for '{device.name}': \"\n f\"{device.cache_statistics['cache_hits']} hits / {device.cache_statistics['total_commands_sent']} \"\n f\"command(s) ({device.cache_statistics['cache_hit_ratio']})\"\n )\n logger.info(msg)\n else:\n logger.info(\"Caching is not enabled on %s\", device.name)\n"},{"location":"api/runner/#anta.runner.main","title":"main async","text":"main(\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False\n) -> None\n Run ANTA. Use this as an entrypoint to the test framework in your script. ResultManager object gets updated with the test results. Parameters: Name Type Description Default manager ResultManager ResultManager object to populate with the test results. required inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required devices set[str] | None Devices on which to run tests. None means all devices. These may come from the --device / -d CLI option in NRFU. None tests set[str] | None Tests to run against devices. None means all tests. These may come from the --test / -t CLI option in NRFU. None tags set[str] | None Tags to filter devices from the inventory. These may come from the --tags CLI option in NRFU. None established_only bool Include only established device(s). True dry_run bool Build the list of coroutine to run and stop before test execution. False Source code in anta/runner.py @cprofile()\nasync def main( # noqa: PLR0913\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False,\n) -> None:\n \"\"\"Run ANTA.\n\n Use this as an entrypoint to the test framework in your script.\n ResultManager object gets updated with the test results.\n\n Parameters\n ----------\n manager\n ResultManager object to populate with the test results.\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n devices\n Devices on which to run tests. None means all devices. These may come from the `--device / -d` CLI option in NRFU.\n tests\n Tests to run against devices. None means all tests. These may come from the `--test / -t` CLI option in NRFU.\n tags\n Tags to filter devices from the inventory. These may come from the `--tags` CLI option in NRFU.\n established_only\n Include only established device(s).\n dry_run\n Build the list of coroutine to run and stop before test execution.\n \"\"\"\n # Adjust the maximum number of open file descriptors for the ANTA process\n limits = adjust_rlimit_nofile()\n\n if not catalog.tests:\n logger.info(\"The list of tests is empty, exiting\")\n return\n\n with Catchtime(logger=logger, message=\"Preparing ANTA NRFU Run\"):\n # Setup the inventory\n selected_inventory = inventory if dry_run else await setup_inventory(inventory, tags, devices, established_only=established_only)\n if selected_inventory is None:\n return\n\n with Catchtime(logger=logger, message=\"Preparing the tests\"):\n selected_tests = prepare_tests(selected_inventory, catalog, tests, tags)\n if selected_tests is None:\n return\n final_tests_count = sum(len(tests) for tests in selected_tests.values())\n\n run_info = (\n \"--- ANTA NRFU Run Information ---\\n\"\n f\"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\\n\"\n f\"Total number of selected tests: {final_tests_count}\\n\"\n f\"Maximum number of open file descriptors for the current ANTA process: {limits[0]}\\n\"\n \"---------------------------------\"\n )\n\n logger.info(run_info)\n\n if final_tests_count > limits[0]:\n logger.warning(\n \"The number of concurrent tests is higher than the open file descriptors limit for this ANTA process.\\n\"\n \"Errors may occur while running the tests.\\n\"\n \"Please consult the ANTA FAQ.\"\n )\n\n coroutines = get_coroutines(selected_tests, manager)\n\n if dry_run:\n logger.info(\"Dry-run mode, exiting before running the tests.\")\n for coro in coroutines:\n coro.close()\n return\n\n if AntaTest.progress is not None:\n AntaTest.nrfu_task = AntaTest.progress.add_task(\"Running NRFU Tests...\", total=len(coroutines))\n\n with Catchtime(logger=logger, message=\"Running ANTA tests\"):\n await asyncio.gather(*coroutines)\n\n log_cache_statistics(selected_inventory.devices)\n"},{"location":"api/runner/#anta.runner.prepare_tests","title":"prepare_tests","text":"prepare_tests(\n inventory: AntaInventory,\n catalog: AntaCatalog,\n tests: set[str] | None,\n tags: set[str] | None,\n) -> (\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n)\n Prepare the tests to run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required tests set[str] | None Tests to run against devices. None means all tests. required tags set[str] | None Tags to filter devices from the inventory. required Returns: Type Description defaultdict[AntaDevice, set[AntaTestDefinition]] | None A mapping of devices to the tests to run or None if there are no tests to run. Source code in anta/runner.py def prepare_tests(\n inventory: AntaInventory, catalog: AntaCatalog, tests: set[str] | None, tags: set[str] | None\n) -> defaultdict[AntaDevice, set[AntaTestDefinition]] | None:\n \"\"\"Prepare the tests to run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n tests\n Tests to run against devices. None means all tests.\n tags\n Tags to filter devices from the inventory.\n\n Returns\n -------\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n A mapping of devices to the tests to run or None if there are no tests to run.\n \"\"\"\n # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests\n catalog.build_indexes(filtered_tests=tests)\n\n # Using a set to avoid inserting duplicate tests\n device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)\n\n total_test_count = 0\n\n # Create the device to tests mapping from the tags\n for device in inventory.devices:\n if tags:\n # If there are CLI tags, execute tests with matching tags for this device\n if not (matching_tags := tags.intersection(device.tags)):\n # The device does not have any selected tag, skipping\n continue\n device_to_tests[device].update(catalog.get_tests_by_tags(matching_tags))\n else:\n # If there is no CLI tags, execute all tests that do not have any tags\n device_to_tests[device].update(catalog.tag_to_tests[None])\n\n # Then add the tests with matching tags from device tags\n device_to_tests[device].update(catalog.get_tests_by_tags(device.tags))\n\n total_test_count += len(device_to_tests[device])\n\n if total_test_count == 0:\n msg = (\n f\"There are no tests{f' matching the tags {tags} ' if tags else ' '}to run in the current test catalog and device inventory, please verify your inputs.\"\n )\n logger.warning(msg)\n return None\n\n return device_to_tests\n"},{"location":"api/runner/#anta.runner.setup_inventory","title":"setup_inventory async","text":"setup_inventory(\n inventory: AntaInventory,\n tags: set[str] | None,\n devices: set[str] | None,\n *,\n established_only: bool\n) -> AntaInventory | None\n Set up the inventory for the ANTA run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required tags set[str] | None Tags to filter devices from the inventory. required devices set[str] | None Devices on which to run tests. None means all devices. required established_only bool If True use return only devices where a connection is established. required Returns: Type Description AntaInventory | None The filtered inventory or None if there are no devices to run tests on. Source code in anta/runner.py async def setup_inventory(inventory: AntaInventory, tags: set[str] | None, devices: set[str] | None, *, established_only: bool) -> AntaInventory | None:\n \"\"\"Set up the inventory for the ANTA run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n tags\n Tags to filter devices from the inventory.\n devices\n Devices on which to run tests. None means all devices.\n established_only\n If True use return only devices where a connection is established.\n\n Returns\n -------\n AntaInventory | None\n The filtered inventory or None if there are no devices to run tests on.\n \"\"\"\n if len(inventory) == 0:\n logger.info(\"The inventory is empty, exiting\")\n return None\n\n # Filter the inventory based on the CLI provided tags and devices if any\n selected_inventory = inventory.get_inventory(tags=tags, devices=devices) if tags or devices else inventory\n\n with Catchtime(logger=logger, message=\"Connecting to devices\"):\n # Connect to the devices\n await selected_inventory.connect_inventory()\n\n # Remove devices that are unreachable\n selected_inventory = selected_inventory.get_inventory(established_only=established_only)\n\n # If there are no devices in the inventory after filtering, exit\n if not selected_inventory.devices:\n msg = f'No reachable device {f\"matching the tags {tags} \" if tags else \"\"}was found.{f\" Selected devices: {devices} \" if devices is not None else \"\"}'\n logger.warning(msg)\n return None\n\n return selected_inventory\n"},{"location":"api/test.cvx/","title":"Test.cvx","text":""},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX","title":"VerifyManagementCVX","text":"Verifies the management CVX global status. Expected Results Success: The test will pass if the management CVX global status matches the expected status. Failure: The test will fail if the management CVX global status does not match the expected status. Examples anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n Source code in anta/tests/cvx.py class VerifyManagementCVX(AntaTest):\n \"\"\"Verifies the management CVX global status.\n\n Expected Results\n ----------------\n * Success: The test will pass if the management CVX global status matches the expected status.\n * Failure: The test will fail if the management CVX global status does not match the expected status.\n\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyManagementCVX test.\"\"\"\n\n enabled: bool\n \"\"\"Whether management CVX must be enabled (True) or disabled (False).\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyManagementCVX.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n cluster_status = command_output[\"clusterStatus\"]\n if (cluster_state := cluster_status.get(\"enabled\")) != self.inputs.enabled:\n self.result.is_failure(f\"Management CVX status is not valid: {cluster_state}\")\n"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX-attributes","title":"Inputs","text":"Name Type Description Default enabled bool Whether management CVX must be enabled (True) or disabled (False). -"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyMcsClientMounts","title":"VerifyMcsClientMounts","text":"Verify if all MCS client mounts are in mountStateMountComplete. Expected Results Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete. Failure: The test will fail even if one switch\u2019s MCS client mount status is not mountStateMountComplete. Examples anta.tests.cvx:\n- VerifyMcsClientMounts:\n Source code in anta/tests/cvx.py class VerifyMcsClientMounts(AntaTest):\n \"\"\"Verify if all MCS client mounts are in mountStateMountComplete.\n\n Expected Results\n ----------------\n * Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete.\n * Failure: The test will fail even if one switch's MCS client mount status is not mountStateMountComplete.\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyMcsClientMounts:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx mounts\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyMcsClientMounts.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n mount_states = command_output[\"mountStates\"]\n mcs_mount_state_detected = False\n for mount_state in mount_states:\n if not mount_state[\"type\"].startswith(\"Mcs\"):\n continue\n mcs_mount_state_detected = True\n if (state := mount_state[\"state\"]) != \"mountStateMountComplete\":\n self.result.is_failure(f\"MCS Client mount states are not valid: {state}\")\n\n if not mcs_mount_state_detected:\n self.result.is_failure(\"MCS Client mount states are not present\")\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authentication\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authorization\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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.avt/","title":"Adaptive Virtual Topology","text":""},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTPathHealth","title":"VerifyAVTPathHealth","text":"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs. Expected Results Success: The test will pass if all AVT paths for all VRFs are active and valid. Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid. Examples anta.tests.avt:\n - VerifyAVTPathHealth:\n Source code in anta/tests/avt.py class VerifyAVTPathHealth(AntaTest):\n \"\"\"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for all VRFs are active and valid.\n * Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTPathHealth:\n ```\n \"\"\"\n\n description = \"Verifies the status of all AVT paths for all VRFs.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTPathHealth.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output.get(\"vrfs\", {})\n\n # Check if AVT is configured\n if not command_output:\n self.result.is_failure(\"Adaptive virtual topology paths are not configured.\")\n return\n\n # Iterate over each VRF\n for vrf, vrf_data in command_output.items():\n # Iterate over each AVT path\n for profile, avt_path in vrf_data.get(\"avts\", {}).items():\n for path, flags in avt_path.get(\"avtPaths\", {}).items():\n # Get the status of the AVT path\n valid = flags[\"flags\"][\"valid\"]\n active = flags[\"flags\"][\"active\"]\n\n # Check the status of the AVT path\n if not valid and not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid and not active.\")\n elif not valid:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid.\")\n elif not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is not active.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole","title":"VerifyAVTRole","text":"Verifies the Adaptive Virtual Topology (AVT) role of a device. Expected Results Success: The test will pass if the AVT role of the device matches the expected role. Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role. Examples anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n Source code in anta/tests/avt.py class VerifyAVTRole(AntaTest):\n \"\"\"Verifies the Adaptive Virtual Topology (AVT) role of a device.\n\n Expected Results\n ----------------\n * Success: The test will pass if the AVT role of the device matches the expected role.\n * Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n ```\n \"\"\"\n\n description = \"Verifies the AVT role of a device.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTRole test.\"\"\"\n\n role: str\n \"\"\"Expected AVT role of the device.\"\"\"\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTRole.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output\n\n # Check if the AVT role matches the expected role\n if self.inputs.role != command_output.get(\"role\"):\n self.result.is_failure(f\"Expected AVT role as `{self.inputs.role}`, but found `{command_output.get('role')}` instead.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole-attributes","title":"Inputs","text":"Name Type Description Default role str Expected AVT role of the device. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath","title":"VerifyAVTSpecificPath","text":"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF. Expected Results Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided. If multiple paths are configured, the test will pass only if all the paths are valid and active. Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid, or does not match the specified type. Examples anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n Source code in anta/tests/avt.py class VerifyAVTSpecificPath(AntaTest):\n \"\"\"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided.\n If multiple paths are configured, the test will pass only if all the paths are valid and active.\n * Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid,\n or does not match the specified type.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n ```\n \"\"\"\n\n description = \"Verifies the status and type of an AVT path for a specified VRF.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show adaptive-virtual-topology path vrf {vrf} avt {avt_name} destination {destination}\")\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTSpecificPath test.\"\"\"\n\n avt_paths: list[AVTPaths]\n \"\"\"List of AVT paths to verify.\"\"\"\n\n class AVTPaths(BaseModel):\n \"\"\"Model for the details of AVT paths.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"The VRF for the AVT path. Defaults to 'default' if not provided.\"\"\"\n avt_name: str\n \"\"\"Name of the adaptive virtual topology.\"\"\"\n destination: IPv4Address\n \"\"\"The IPv4 address of the AVT peer.\"\"\"\n next_hop: IPv4Address\n \"\"\"The IPv4 address of the next hop for the AVT peer.\"\"\"\n path_type: str | None = None\n \"\"\"The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input AVT path/peer.\"\"\"\n return [template.render(vrf=path.vrf, avt_name=path.avt_name, destination=path.destination) for path in self.inputs.avt_paths]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTSpecificPath.\"\"\"\n # Assume the test is successful until a failure is detected\n self.result.is_success()\n\n # Process each command in the instance\n for command, input_avt in zip(self.instance_commands, self.inputs.avt_paths):\n # Extract the command output and parameters\n vrf = command.params.vrf\n avt_name = command.params.avt_name\n peer = str(command.params.destination)\n\n command_output = command.json_output.get(\"vrfs\", {})\n\n # If no AVT is configured, mark the test as failed and skip to the next command\n if not command_output:\n self.result.is_failure(f\"AVT configuration for peer '{peer}' under topology '{avt_name}' in VRF '{vrf}' is not found.\")\n continue\n\n # Extract the AVT paths\n avt_paths = get_value(command_output, f\"{vrf}.avts.{avt_name}.avtPaths\")\n next_hop, input_path_type = str(input_avt.next_hop), input_avt.path_type\n\n nexthop_path_found = path_type_found = False\n\n # Check each AVT path\n for path, path_data in avt_paths.items():\n # If the path does not match the expected next hop, skip to the next path\n if path_data.get(\"nexthopAddr\") != next_hop:\n continue\n\n nexthop_path_found = True\n path_type = \"direct\" if get_value(path_data, \"flags.directPath\") else \"multihop\"\n\n # If the path type does not match the expected path type, skip to the next path\n if input_path_type and path_type != input_path_type:\n continue\n\n path_type_found = True\n valid = get_value(path_data, \"flags.valid\")\n active = get_value(path_data, \"flags.active\")\n\n # Check the path status and type against the expected values\n if not all([valid, active]):\n failure_reasons = []\n if not get_value(path_data, \"flags.active\"):\n failure_reasons.append(\"inactive\")\n if not get_value(path_data, \"flags.valid\"):\n failure_reasons.append(\"invalid\")\n # Construct the failure message prefix\n failed_log = f\"AVT path '{path}' for topology '{avt_name}' in VRF '{vrf}'\"\n self.result.is_failure(f\"{failed_log} is {', '.join(failure_reasons)}.\")\n\n # If no matching next hop or path type was found, mark the test as failed\n if not nexthop_path_found or not path_type_found:\n self.result.is_failure(\n f\"No '{input_path_type}' path found with next-hop address '{next_hop}' for AVT peer '{peer}' under topology '{avt_name}' in VRF '{vrf}'.\"\n )\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"Inputs","text":"Name Type Description Default avt_paths list[AVTPaths] List of AVT paths to verify. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"AVTPaths","text":"Name Type Description Default vrf str The VRF for the AVT path. Defaults to 'default' if not provided. 'default' avt_name str Name of the adaptive virtual topology. - destination IPv4Address The IPv4 address of the AVT peer. - next_hop IPv4Address The IPv4 address of the next hop for the AVT peer. - path_type str | None The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered. None"},{"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 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\", revision=1),\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 categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\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 tx_interval = bfd_peers.tx_interval\n rx_interval = bfd_peers.rx_interval\n multiplier = bfd_peers.multiplier\n\n # Check if BFD peer configured\n bfd_output = get_value(\n self.instance_commands[0].json_output,\n f\"vrfs..{vrf}..ipv4Neighbors..{peer}..peerStats..\",\n separator=\"..\",\n )\n if not bfd_output:\n failures[peer] = {vrf: \"Not Configured\"}\n continue\n\n # Convert interval timer(s) into milliseconds to be consistent with the inputs.\n bfd_details = bfd_output.get(\"peerStatsDetail\", {})\n op_tx_interval = bfd_details.get(\"operTxInterval\") // 1000\n op_rx_interval = bfd_details.get(\"operRxInterval\") // 1000\n detect_multiplier = bfd_details.get(\"detectMult\")\n intervals_ok = op_tx_interval == tx_interval and op_rx_interval == rx_interval and detect_multiplier == multiplier\n\n # Check timers of BFD peer\n if not intervals_ok:\n failures[peer] = {\n vrf: {\n \"tx_interval\": op_tx_interval,\n \"rx_interval\": op_rx_interval,\n \"multiplier\": detect_multiplier,\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.VerifyBFDPeersRegProtocols","title":"VerifyBFDPeersRegProtocols","text":"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered. Expected Results Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s). Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s). Examples anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n Source code in anta/tests/bfd.py class VerifyBFDPeersRegProtocols(AntaTest):\n \"\"\"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered.\n\n Expected Results\n ----------------\n * Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s).\n * Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s).\n\n Examples\n --------\n ```yaml\n anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBFDPeersRegProtocols 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 protocols: list[BfdProtocol]\n \"\"\"List of protocols to be verified.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBFDPeersRegProtocols.\"\"\"\n # Initialize failure messages\n failures: dict[Any, Any] = {}\n\n # Iterating over BFD peers, extract the parameters and command output\n for bfd_peer in self.inputs.bfd_peers:\n peer = str(bfd_peer.peer_address)\n vrf = bfd_peer.vrf\n protocols = bfd_peer.protocols\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 registered protocols\n difference = set(protocols) - set(get_value(bfd_output, \"peerStatsDetail.apps\"))\n\n if difference:\n failures[peer] = {vrf: sorted(difference)}\n\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BFD peers are not configured or have non-registered protocol(s):\\n{failures}\")\n"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-attributes","title":"Inputs","text":"Name Type Description Default bfd_peers list[BFDPeer] List of IPv4 BFD peers. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-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' protocols list[BfdProtocol] List of protocols to be verified. -"},{"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 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\", revision=1)]\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 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.VerifyRunningConfigLines","title":"VerifyRunningConfigLines","text":"Verifies the given regular expression patterns are present in the running-config. Warning Since this uses regular expression searches on the whole running-config, it can drastically impact performance and should only be used if no other test is available. If possible, try using another ANTA test that is more specific. Expected Results Success: The test will pass if all the patterns are found in the running-config. Failure: The test will fail if any of the patterns are NOT found in the running-config. Examples anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n Source code in anta/tests/configuration.py class VerifyRunningConfigLines(AntaTest):\n \"\"\"Verifies the given regular expression patterns are present in the running-config.\n\n !!! warning\n Since this uses regular expression searches on the whole running-config, it can\n drastically impact performance and should only be used if no other test is available.\n\n If possible, try using another ANTA test that is more specific.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the patterns are found in the running-config.\n * Failure: The test will fail if any of the patterns are NOT found in the running-config.\n\n Examples\n --------\n ```yaml\n anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n ```\n \"\"\"\n\n description = \"Search the Running-Config for the given RegEx patterns.\"\n categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show running-config\", ofmt=\"text\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyRunningConfigLines test.\"\"\"\n\n regex_patterns: list[RegexString]\n \"\"\"List of regular expressions.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRunningConfigLines.\"\"\"\n failure_msgs = []\n command_output = self.instance_commands[0].text_output\n\n for pattern in self.inputs.regex_patterns:\n re_search = re.compile(pattern, flags=re.MULTILINE)\n\n if not re_search.search(command_output):\n failure_msgs.append(f\"'{pattern}'\")\n\n if not failure_msgs:\n self.result.is_success()\n else:\n self.result.is_failure(\"Following patterns were not found: \" + \",\".join(failure_msgs))\n"},{"location":"api/tests.configuration/#anta.tests.configuration.VerifyRunningConfigLines-attributes","title":"Inputs","text":"Name Type Description Default regex_patterns list[RegexString] List of regular expressions. -"},{"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 categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show zerotouch\", revision=1)]\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/#tests","title":"Tests","text":""},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors","title":"VerifyLLDPNeighbors","text":"Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. This test performs the following checks for each specified LLDP neighbor: Confirming matching ports on both local and neighboring devices. Ensuring compatibility of device names and interface identifiers. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored. Expected Results Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table. The system name or port of the LLDP neighbor does not match the expected 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 the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.\n\n This test performs the following checks for each specified LLDP neighbor:\n\n 1. Confirming matching ports on both local and neighboring devices.\n 2. Ensuring compatibility of device names and interface identifiers.\n 3. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table.\n - The system name or port of the LLDP neighbor does not match the expected 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 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\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLLDPNeighbors test.\"\"\"\n\n neighbors: list[LLDPNeighbor]\n \"\"\"List of LLDP neighbors.\"\"\"\n Neighbor: ClassVar[type[Neighbor]] = Neighbor\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLLDPNeighbors.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output[\"lldpNeighbors\"]\n for neighbor in self.inputs.neighbors:\n if neighbor.port not in output:\n self.result.is_failure(f\"{neighbor} - Port not found\")\n continue\n\n if len(lldp_neighbor_info := output[neighbor.port][\"lldpNeighborInfo\"]) == 0:\n self.result.is_failure(f\"{neighbor} - No LLDP neighbors\")\n continue\n\n # Check if the system name and neighbor port matches\n match_found = any(\n info[\"systemName\"] == neighbor.neighbor_device and info[\"neighborInterfaceInfo\"][\"interfaceId_v2\"] == neighbor.neighbor_port\n for info in lldp_neighbor_info\n )\n if not match_found:\n failure_msg = [f\"{info['systemName']}/{info['neighborInterfaceInfo']['interfaceId_v2']}\" for info in lldp_neighbor_info]\n self.result.is_failure(f\"{neighbor} - Wrong LLDP neighbors: {', '.join(failure_msg)}\")\n"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors-attributes","title":"Inputs","text":"Name Type Description Default neighbors list[LLDPNeighbor] List of LLDP neighbors. -"},{"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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"connectivity\"]\n # Template uses '{size}{df_bit}' without space since df_bit includes leading space when enabled\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"ping vrf {vrf} {destination} source {source} size {size}{df_bit} repeat {repeat}\", revision=1)\n ]\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 Host: ClassVar[type[Host]] = Host\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each host in the input list.\"\"\"\n commands = []\n for host in self.inputs.hosts:\n # df_bit includes leading space when enabled, empty string when disabled\n df_bit = \" df-bit\" if host.df_bit else \"\"\n command = template.render(destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat, size=host.size, df_bit=df_bit)\n commands.append(command)\n return commands\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyReachability.\"\"\"\n self.result.is_success()\n\n for command, host in zip(self.instance_commands, self.inputs.hosts):\n if f\"{host.repeat} received\" not in command.json_output[\"messages\"][0]:\n self.result.is_failure(f\"{host} - Unreachable\")\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/#input-models","title":"Input models","text":""},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Host","title":"Host","text":"Model for a remote host to ping. 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 size int Specify datagram size. Defaults to 100. 100 df_bit bool Enable do not fragment bit in IP header. Defaults to False. False Source code in anta/input_models/connectivity.py class Host(BaseModel):\n \"\"\"Model for a remote host to ping.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\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 size: int = 100\n \"\"\"Specify datagram size. Defaults to 100.\"\"\"\n df_bit: bool = False\n \"\"\"Enable do not fragment bit in IP header. Defaults to False.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the Host for reporting.\n\n Examples\n --------\n Host 10.1.1.1 (src: 10.2.2.2, vrf: mgmt, size: 100B, repeat: 2)\n\n \"\"\"\n df_status = \", df-bit: enabled\" if self.df_bit else \"\"\n return f\"Host {self.destination} (src: {self.source}, vrf: {self.vrf}, size: {self.size}B, repeat: {self.repeat}{df_status})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.LLDPNeighbor","title":"LLDPNeighbor","text":"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information. Name Type Description Default port Interface The LLDP port for the local device. - neighbor_device str The system name of the LLDP neighbor device. - neighbor_port Interface The LLDP port on the neighboring device. - Source code in anta/input_models/connectivity.py class LLDPNeighbor(BaseModel):\n \"\"\"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n port: Interface\n \"\"\"The LLDP port for the local device.\"\"\"\n neighbor_device: str\n \"\"\"The system name of the LLDP neighbor device.\"\"\"\n neighbor_port: Interface\n \"\"\"The LLDP port on the neighboring device.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the LLDPNeighbor for reporting.\n\n Examples\n --------\n Port Ethernet1 (Neighbor: DC1-SPINE2, Neighbor Port: Ethernet2)\n\n \"\"\"\n return f\"Port {self.port} (Neighbor: {self.neighbor_device}, Neighbor Port: {self.neighbor_port})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor","title":"Neighbor","text":"Alias for the LLDPNeighbor model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the LLDPNeighbor model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/connectivity.py class Neighbor(LLDPNeighbor): # pragma: no cover\n \"\"\"Alias for the LLDPNeighbor model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the LLDPNeighbor model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor.__init__","title":"__init__","text":"__init__(**data: Any) -> None\n Source code in anta/input_models/connectivity.py def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"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 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\", revision=1)]\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 break\n else:\n self.result.is_failure(\"Aboot component not found\")\n return\n\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 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\", revision=1)]\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_failure(\"Error in running test - Component FixedSystemvrm1 not found in 'show version'\")\n"},{"location":"api/tests.flow_tracking/","title":"Flow Tracking","text":""},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus","title":"VerifyHardwareFlowTrackerStatus","text":"Verifies if hardware flow tracking is running and an input tracker is active. This test optionally verifies the tracker interval/timeout and exporter configuration. Expected Results Success: The test will pass if hardware flow tracking is running and an input tracker is active. Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active, or the tracker interval/timeout and exporter configuration does not match the expected values. Examples anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n Source code in anta/tests/flow_tracking.py class VerifyHardwareFlowTrackerStatus(AntaTest):\n \"\"\"Verifies if hardware flow tracking is running and an input tracker is active.\n\n This test optionally verifies the tracker interval/timeout and exporter configuration.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware flow tracking is running and an input tracker is active.\n * Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active,\n or the tracker interval/timeout and exporter configuration does not match the expected values.\n\n Examples\n --------\n ```yaml\n anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n ```\n \"\"\"\n\n description = (\n \"Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration.\"\n )\n categories: ClassVar[list[str]] = [\"flow tracking\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show flow tracking hardware tracker {name}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyHardwareFlowTrackerStatus test.\"\"\"\n\n trackers: list[FlowTracker]\n \"\"\"List of flow trackers to verify.\"\"\"\n\n class FlowTracker(BaseModel):\n \"\"\"Detail of a flow tracker.\"\"\"\n\n name: str\n \"\"\"Name of the flow tracker.\"\"\"\n\n record_export: RecordExport | None = None\n \"\"\"Record export configuration for the flow tracker.\"\"\"\n\n exporters: list[Exporter] | None = None\n \"\"\"List of exporters for the flow tracker.\"\"\"\n\n class RecordExport(BaseModel):\n \"\"\"Record export configuration.\"\"\"\n\n on_inactive_timeout: int\n \"\"\"Timeout in milliseconds for exporting records when inactive.\"\"\"\n\n on_interval: int\n \"\"\"Interval in milliseconds for exporting records.\"\"\"\n\n class Exporter(BaseModel):\n \"\"\"Detail of an exporter.\"\"\"\n\n name: str\n \"\"\"Name of the exporter.\"\"\"\n\n local_interface: str\n \"\"\"Local interface used by the exporter.\"\"\"\n\n template_interval: int\n \"\"\"Template interval in milliseconds for the exporter.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each hardware tracker.\"\"\"\n return [template.render(name=tracker.name) for tracker in self.inputs.trackers]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareFlowTrackerStatus.\"\"\"\n self.result.is_success()\n for command, tracker_input in zip(self.instance_commands, self.inputs.trackers):\n hardware_tracker_name = command.params.name\n record_export = tracker_input.record_export.model_dump() if tracker_input.record_export else None\n exporters = [exporter.model_dump() for exporter in tracker_input.exporters] if tracker_input.exporters else None\n command_output = command.json_output\n\n # Check if hardware flow tracking is configured\n if not command_output.get(\"running\"):\n self.result.is_failure(\"Hardware flow tracking is not running.\")\n return\n\n # Check if the input hardware tracker is configured\n tracker_info = command_output[\"trackers\"].get(hardware_tracker_name)\n if not tracker_info:\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not configured.\")\n continue\n\n # Check if the input hardware tracker is active\n if not tracker_info.get(\"active\"):\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not active.\")\n continue\n\n # Check the input hardware tracker timeouts\n failure_msg = \"\"\n if record_export:\n record_export_failure = validate_record_export(record_export, tracker_info)\n if record_export_failure:\n failure_msg += record_export_failure\n\n # Check the input hardware tracker exporters' configuration\n if exporters:\n exporters_failure = validate_exporters(exporters, tracker_info)\n if exporters_failure:\n failure_msg += exporters_failure\n\n if failure_msg:\n self.result.is_failure(f\"{hardware_tracker_name}: {failure_msg}\\n\")\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Inputs","text":"Name Type Description Default trackers list[FlowTracker] List of flow trackers to verify. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"FlowTracker","text":"Name Type Description Default name str Name of the flow tracker. - record_export RecordExport | None Record export configuration for the flow tracker. None exporters list[Exporter] | None List of exporters for the flow tracker. None"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"RecordExport","text":"Name Type Description Default on_inactive_timeout int Timeout in milliseconds for exporting records when inactive. - on_interval int Interval in milliseconds for exporting records. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Exporter","text":"Name Type Description Default name str Name of the exporter. - local_interface str Local interface used by the exporter. - template_interval int Template interval in milliseconds for the exporter. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_exporters","title":"validate_exporters","text":"validate_exporters(\n exporters: list[dict[str, str]],\n tracker_info: dict[str, str],\n) -> str\n Validate the exporter configurations against the tracker info. Parameters: Name Type Description Default exporters list[dict[str, str]] The list of expected exporter configurations. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str Failure message if any exporter configuration does not match. Source code in anta/tests/flow_tracking.py def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the exporter configurations against the tracker info.\n\n Parameters\n ----------\n exporters\n The list of expected exporter configurations.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n Failure message if any exporter configuration does not match.\n \"\"\"\n failed_log = \"\"\n for exporter in exporters:\n exporter_name = exporter[\"name\"]\n actual_exporter_info = tracker_info[\"exporters\"].get(exporter_name)\n if not actual_exporter_info:\n failed_log += f\"\\nExporter `{exporter_name}` is not configured.\"\n continue\n\n expected_exporter_data = {\"local interface\": exporter[\"local_interface\"], \"template interval\": exporter[\"template_interval\"]}\n actual_exporter_data = {\"local interface\": actual_exporter_info[\"localIntf\"], \"template interval\": actual_exporter_info[\"templateInterval\"]}\n\n if expected_exporter_data != actual_exporter_data:\n failed_msg = get_failed_logs(expected_exporter_data, actual_exporter_data)\n failed_log += f\"\\nExporter `{exporter_name}`: {failed_msg}\"\n return failed_log\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_record_export","title":"validate_record_export","text":"validate_record_export(\n record_export: dict[str, str],\n tracker_info: dict[str, str],\n) -> str\n Validate the record export configuration against the tracker info. Parameters: Name Type Description Default record_export dict[str, str] The expected record export configuration. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str A failure message if the record export configuration does not match, otherwise blank string. Source code in anta/tests/flow_tracking.py def validate_record_export(record_export: dict[str, str], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the record export configuration against the tracker info.\n\n Parameters\n ----------\n record_export\n The expected record export configuration.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n A failure message if the record export configuration does not match, otherwise blank string.\n \"\"\"\n failed_log = \"\"\n actual_export = {\"inactive timeout\": tracker_info.get(\"inactiveTimeout\"), \"interval\": tracker_info.get(\"activeInterval\")}\n expected_export = {\"inactive timeout\": record_export.get(\"on_inactive_timeout\"), \"interval\": record_export.get(\"on_interval\")}\n if actual_export != expected_export:\n failed_log = get_failed_logs(expected_export, actual_export)\n return failed_log\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 description = \"Verifies if a GreenT policy other than the default is created.\"\n categories: ClassVar[list[str]] = [\"greent\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show monitor telemetry postcard policy profile\", revision=1)]\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 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\", revision=1)]\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 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\", revision=1)]\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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment power\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment cooling\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show inventory\", revision=2)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature transceiver\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=2)]\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 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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show lacp counters all-ports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters discards\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces status\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters errors\", revision=1)]\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 description = \"Verifies the interface IPv4 addresses.\"\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip interface {interface}\", revision=2)]\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 [template.render(interface=interface.name) for interface in self.inputs.interfaces]\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 for interface in self.inputs.interfaces:\n if interface.name == intf:\n input_interface_detail = interface\n break\n else:\n self.result.is_failure(f\"Could not find `{intf}` in the input interfaces. {GITHUB_SUGGESTION}\")\n continue\n\n input_primary_ip = str(input_interface_detail.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 (param_secondary_ips := input_interface_detail.secondary_ips) is not None:\n input_secondary_ips = sorted([str(network) for network in param_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. This test has been implemented for full-duplex interfaces only. 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. Error: The test will error out if the device has at least one non full-duplex interface. 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 This test has been implemented for full-duplex interfaces only.\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 * Error: The test will error out if the device has at least one non full-duplex interface.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfaceUtilization:\n threshold: 70.0\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show interfaces counters rates\", revision=1),\n AntaCommand(command=\"show interfaces\", revision=1),\n ]\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 # The utilization logic has been implemented for full-duplex interfaces only\n if ((duplex := (interface := interfaces[\"interfaces\"][intf]).get(\"duplex\", None)) is not None and duplex != duplex_full) or (\n (members := interface.get(\"memberInterfaces\", None)) is not None and any(stats[\"duplex\"] != duplex_full for stats in members.values())\n ):\n self.result.is_failure(f\"Interface {intf} or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented.\")\n return\n\n if (bandwidth := interfaces[\"interfaces\"][intf][\"bandwidth\"]) == 0:\n self.logger.debug(\"Interface %s has been ignored due to null bandwidth value\", intf)\n continue\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.VerifyInterfacesSpeed","title":"VerifyInterfacesSpeed","text":"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces. If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input. If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input. Expected Results Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex. Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex. Examples anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n Source code in anta/tests/interfaces.py class VerifyInterfacesSpeed(AntaTest):\n \"\"\"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces.\n\n - If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input.\n - If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input.\n\n Expected Results\n ----------------\n * Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex.\n * Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n class Input(AntaTest.Input):\n \"\"\"Inputs for the VerifyInterfacesSpeed test.\"\"\"\n\n interfaces: list[InterfaceDetail]\n \"\"\"List of interfaces to be tested\"\"\"\n\n class InterfaceDetail(BaseModel):\n \"\"\"Detail of an interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"The name of the interface.\"\"\"\n auto: bool\n \"\"\"The auto-negotiation status of the interface.\"\"\"\n speed: float = Field(ge=1, le=1000)\n \"\"\"The speed of the interface in Gigabits per second. Valid range is 1 to 1000.\"\"\"\n lanes: None | int = Field(None, ge=1, le=8)\n \"\"\"The number of lanes in the interface. Valid range is 1 to 8. This field is optional.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesSpeed.\"\"\"\n self.result.is_success()\n command_output = self.instance_commands[0].json_output\n\n # Iterate over all the interfaces\n for interface in self.inputs.interfaces:\n intf = interface.name\n\n # Check if interface exists\n if not (interface_output := get_value(command_output, f\"interfaces.{intf}\")):\n self.result.is_failure(f\"Interface `{intf}` is not found.\")\n continue\n\n auto_negotiation = interface_output.get(\"autoNegotiate\")\n actual_lanes = interface_output.get(\"lanes\")\n\n # Collecting actual interface details\n actual_interface_output = {\n \"auto negotiation\": auto_negotiation if interface.auto is True else None,\n \"duplex mode\": interface_output.get(\"duplex\"),\n \"speed\": interface_output.get(\"bandwidth\"),\n \"lanes\": actual_lanes if interface.lanes is not None else None,\n }\n\n # Forming expected interface details\n expected_interface_output = {\n \"auto negotiation\": \"success\" if interface.auto is True else None,\n \"duplex mode\": \"duplexFull\",\n \"speed\": interface.speed * BPS_GBPS_CONVERSIONS,\n \"lanes\": interface.lanes,\n }\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n for output in [actual_interface_output, expected_interface_output]:\n # Convert speed to Gbps for readability\n if output[\"speed\"] is not None:\n output[\"speed\"] = f\"{custom_division(output['speed'], BPS_GBPS_CONVERSIONS)}Gbps\"\n failed_log = get_failed_logs(expected_interface_output, actual_interface_output)\n self.result.is_failure(f\"For interface {intf}:{failed_log}\\n\")\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceDetail] List of interfaces to be tested -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"InterfaceDetail","text":"Name Type Description Default name EthernetInterface The name of the interface. - auto bool The auto-negotiation status of the interface. - speed float The speed of the interface in Gigabits per second. Valid range is 1 to 1000. Field(ge=1, le=1000) lanes None | int The number of lanes in the interface. Valid range is 1 to 8. This field is optional. Field(None, ge=1, le=8)"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus","title":"VerifyInterfacesStatus","text":"Verifies the operational states of specified interfaces to ensure they match expected configurations. This test performs the following checks for each specified interface: If line_protocol_status is defined, both status and line_protocol_status are verified for the specified interface. If line_protocol_status is not provided but the status is \u201cup\u201d, it is assumed that both the status and line protocol should be \u201cup\u201d. If the interface status is not \u201cup\u201d, only the interface\u2019s status is validated, with no line protocol check performed. Expected Results Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces. Failure: If any of the following occur: The specified interface is not configured. The specified interface status and line protocol status does not match the expected operational state for any interface. 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 the operational states of specified interfaces to ensure they match expected configurations.\n\n This test performs the following checks for each specified interface:\n\n 1. If `line_protocol_status` is defined, both `status` and `line_protocol_status` are verified for the specified interface.\n 2. If `line_protocol_status` is not provided but the `status` is \"up\", it is assumed that both the status and line protocol should be \"up\".\n 3. If the interface `status` is not \"up\", only the interface's status is validated, with no line protocol check performed.\n\n Expected Results\n ----------------\n * Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces.\n * Failure: If any of the following occur:\n - The specified interface is not configured.\n - The specified interface status and line protocol status does not match the expected operational state for any interface.\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesStatus.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output\n for interface in self.inputs.interfaces:\n if (intf_status := get_value(command_output[\"interfaceDescriptions\"], interface.name, separator=\"..\")) is None:\n self.result.is_failure(f\"{interface.name} - Not configured\")\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 actual_state = f\"Expected: {interface.status}/{interface.line_protocol_status}, Actual: {status}/{proto}\"\n self.result.is_failure(f\"{interface.name} - {actual_state}\")\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\"):\n self.result.is_failure(f\"{interface.name} - Expected: up/up, Actual: {status}/{proto}\")\n elif interface.status != status:\n self.result.is_failure(f\"{interface.name} - Expected: {interface.status}, Actual: {status}\")\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.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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip virtual-router\", revision=2)]\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 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\", revision=1)]\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 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\", revision=1)]\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.VerifyLACPInterfacesStatus","title":"VerifyLACPInterfacesStatus","text":"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces. Verifies that the interface is a member of the LACP port channel. Ensures that the synchronization is established. Ensures the interfaces are in the correct state for collecting and distributing traffic. Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \u201cslow\u201d mode, is the default setting.) Expected Results Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct. Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct. Examples anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n Source code in anta/tests/interfaces.py class VerifyLACPInterfacesStatus(AntaTest):\n \"\"\"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces.\n\n - Verifies that the interface is a member of the LACP port channel.\n - Ensures that the synchronization is established.\n - Ensures the interfaces are in the correct state for collecting and distributing traffic.\n - Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \"slow\" mode, is the default setting.)\n\n Expected Results\n ----------------\n * Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct.\n * Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show lacp interface {interface}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLACPInterfacesStatus test.\"\"\"\n\n interfaces: list[LACPInterface]\n \"\"\"List of LACP member interface.\"\"\"\n\n class LACPInterface(BaseModel):\n \"\"\"Model for an LACP member interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"Ethernet interface to validate.\"\"\"\n portchannel: PortChannelInterface\n \"\"\"Port Channel in which the interface is bundled.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each interface in the input list.\"\"\"\n return [template.render(interface=interface.name) for interface in self.inputs.interfaces]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLACPInterfacesStatus.\"\"\"\n self.result.is_success()\n\n # Member port verification parameters.\n member_port_details = [\"activity\", \"aggregation\", \"synchronization\", \"collecting\", \"distributing\", \"timeout\"]\n\n # Iterating over command output for different interfaces\n for command, input_entry in zip(self.instance_commands, self.inputs.interfaces):\n interface = input_entry.name\n portchannel = input_entry.portchannel\n\n # Verify if a PortChannel is configured with the provided interface\n if not (interface_details := get_value(command.json_output, f\"portChannels.{portchannel}.interfaces.{interface}\")):\n self.result.is_failure(f\"Interface '{interface}' is not configured to be a member of LACP '{portchannel}'.\")\n continue\n\n # Verify the interface is bundled in port channel.\n actor_port_status = interface_details.get(\"actorPortStatus\")\n if actor_port_status != \"bundled\":\n message = f\"For Interface {interface}:\\nExpected `bundled` as the local port status, but found `{actor_port_status}` instead.\\n\"\n self.result.is_failure(message)\n continue\n\n # Collecting actor and partner port details\n actor_port_details = interface_details.get(\"actorPortState\", {})\n partner_port_details = interface_details.get(\"partnerPortState\", {})\n\n # Collecting actual interface details\n actual_interface_output = {\n \"actor_port_details\": {param: actor_port_details.get(param, \"NotFound\") for param in member_port_details},\n \"partner_port_details\": {param: partner_port_details.get(param, \"NotFound\") for param in member_port_details},\n }\n\n # Forming expected interface details\n expected_details = {param: param != \"timeout\" for param in member_port_details}\n expected_interface_output = {\"actor_port_details\": expected_details, \"partner_port_details\": expected_details}\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n message = f\"For Interface {interface}:\\n\"\n actor_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"actor_port_details\", {}), actual_interface_output.get(\"actor_port_details\", {})\n )\n partner_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"partner_port_details\", {}), actual_interface_output.get(\"partner_port_details\", {})\n )\n\n if actor_port_failed_log:\n message += f\"Actor port details:{actor_port_failed_log}\\n\"\n if partner_port_failed_log:\n message += f\"Partner port details:{partner_port_failed_log}\\n\"\n\n self.result.is_failure(message)\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[LACPInterface] List of LACP member interface. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"LACPInterface","text":"Name Type Description Default name EthernetInterface Ethernet interface to validate. - portchannel PortChannelInterface Port Channel in which the interface is bundled. -"},{"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 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\", revision=1)]\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: # pragma: no branch\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show port-channel\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip interface brief\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show storm-control\", revision=1)]\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.interfaces/#input-models","title":"Input models","text":""},{"location":"api/tests.interfaces/#anta.input_models.interfaces.InterfaceState","title":"InterfaceState","text":"Model for an interface state. 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 Source code in anta/input_models/interfaces.py 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"},{"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 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\", revision=1)]\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 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 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. This test performs the following checks: Retrieves the device\u2019s configured FQDN Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message includes the complete FQDN of the device Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the device\u2019s complete FQDN. Failure: If any of the following occur: The test message is not found in recent logs The log message does not include the device\u2019s FQDN The FQDN in the log message doesn\u2019t match the configured 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 This test performs the following checks:\n\n 1. Retrieves the device's configured FQDN\n 2. Sends a test log message at the **informational** level\n 3. Retrieves the most recent logs (last 30 seconds)\n 4. Verifies that the test message includes the complete FQDN of the device\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the device's complete FQDN.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The log message does not include the device's FQDN\n - The FQDN in the log message doesn't match the configured FQDN\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingHostname:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show hostname\", revision=1),\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingHostname validation\", ofmt=\"text\"),\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 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. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message was successfully logged Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are being generated and the test message is found in recent logs. Failure: If any of the following occur: The test message is not found in recent logs The logging system is not capturing new messages No logs are being 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 This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message was successfully logged\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are being generated and the test message is found in recent logs.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The logging system is not capturing new messages\n - No logs are being generated\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingLogsGeneration:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingLogsGeneration validation\", ofmt=\"text\"),\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 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 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 VerifyLoggingSourceIntf 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 VerifyLoggingSourceIntf.\"\"\"\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 appropriate timestamp. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message is present with a high-resolution RFC3339 timestamp format Example format: 2024-01-25T15:30:45.123456+00:00 Includes microsecond precision Contains timezone offset Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the correct high-resolution RFC3339 timestamp format. Failure: If any of the following occur: The test message is not found in recent logs The timestamp format does not match the expected RFC3339 format 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 appropriate timestamp.\n\n This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message is present with a high-resolution RFC3339 timestamp format\n - Example format: `2024-01-25T15:30:45.123456+00:00`\n - Includes microsecond precision\n - Contains timezone offset\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the correct high-resolution RFC3339 timestamp format.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The timestamp format does not match the expected RFC3339 format\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingTimestamp:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingTimestamp validation\", ofmt=\"text\"),\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(\n logger: logging.Logger, command_output: str\n) -> str\n Parse show logging output and gets operational logging states used in the tests in this module. Parameters: Name Type Description Default logger Logger The logger object. required command_output str The show logging output. required 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 Parameters\n ----------\n logger\n The logger object.\n command_output\n The `show logging` output.\n\n Returns\n -------\n str\n 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":"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 Adaptive Virtual Topology BFD Configuration Connectivity Field Notices Flow Tracking GreenT Hardware Interfaces LANZ Logging MLAG Multicast Profiles PTP Router Path Selection Routing Generic Routing BGP Routing ISIS Routing OSPF Security Services SNMP Software STP STUN 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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag config-sanity\", revision=1)]\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 command_output[\"mlagActive\"] 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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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.path_selection/","title":"Router Path Selection","text":""},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifyPathsHealth","title":"VerifyPathsHealth","text":"Verifies the path and telemetry state of all paths under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if all path states under router path-selection are either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and their telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if any path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifyPathsHealth:\n Source code in anta/tests/path_selection.py class VerifyPathsHealth(AntaTest):\n \"\"\"Verifies the path and telemetry state of all paths under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if all path states under router path-selection are either 'IPsec established' or 'Resolved'\n and their telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if any path state is not 'IPsec established' or 'Resolved',\n or the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifyPathsHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show path-selection paths\", revision=1)]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyPathsHealth.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"dpsPeers\"]\n\n # If no paths are configured for router path-selection, the test fails\n if not command_output:\n self.result.is_failure(\"No path configured for router path-selection.\")\n return\n\n # Check the state of each path\n for peer, peer_data in command_output.items():\n for group, group_data in peer_data[\"dpsGroups\"].items():\n for path_data in group_data[\"dpsPaths\"].values():\n path_state = path_data[\"state\"]\n session = path_data[\"dpsSessions\"][\"0\"][\"active\"]\n\n # If the path state of any path is not 'ipsecEstablished' or 'routeResolved', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for peer {peer} in path-group {group} is `{path_state}`.\")\n\n # If the telemetry state of any path is inactive, the test fails\n elif not session:\n self.result.is_failure(f\"Telemetry state for peer {peer} in path-group {group} is `inactive`.\")\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath","title":"VerifySpecificPath","text":"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if the path state under router path-selection is either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if the path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or if the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n Source code in anta/tests/path_selection.py class VerifySpecificPath(AntaTest):\n \"\"\"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if the path state under router path-selection is either 'IPsec established' or 'Resolved'\n and telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if the path state is not 'IPsec established' or 'Resolved',\n or if the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show path-selection paths peer {peer} path-group {group} source {source} destination {destination}\", revision=1)\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificPath test.\"\"\"\n\n paths: list[RouterPath]\n \"\"\"List of router paths to verify.\"\"\"\n\n class RouterPath(BaseModel):\n \"\"\"Detail of a router path.\"\"\"\n\n peer: IPv4Address\n \"\"\"Static peer IPv4 address.\"\"\"\n\n path_group: str\n \"\"\"Router path group name.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of path.\"\"\"\n\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of path.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each router path.\"\"\"\n return [\n template.render(peer=path.peer, group=path.path_group, source=path.source_address, destination=path.destination_address) for path in self.inputs.paths\n ]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificPath.\"\"\"\n self.result.is_success()\n\n # Check the state of each path\n for command in self.instance_commands:\n peer = command.params.peer\n path_group = command.params.group\n source = command.params.source\n destination = command.params.destination\n command_output = command.json_output.get(\"dpsPeers\", [])\n\n # If the peer is not configured for the path group, the test fails\n if not command_output:\n self.result.is_failure(f\"Path `peer: {peer} source: {source} destination: {destination}` is not configured for path-group `{path_group}`.\")\n continue\n\n # Extract the state of the path\n path_output = get_value(command_output, f\"{peer}..dpsGroups..{path_group}..dpsPaths\", separator=\"..\")\n path_state = next(iter(path_output.values())).get(\"state\")\n session = get_value(next(iter(path_output.values())), \"dpsSessions.0.active\")\n\n # If the state of the path is not 'ipsecEstablished' or 'routeResolved', or the telemetry state is 'inactive', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `{path_state}`.\")\n elif not session:\n self.result.is_failure(\n f\"Telemetry state for path `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `inactive`.\"\n )\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"Inputs","text":"Name Type Description Default paths list[RouterPath] List of router paths to verify. -"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"RouterPath","text":"Name Type Description Default peer IPv4Address Static peer IPv4 address. - path_group str Router path group name. - source_address IPv4Address Source IPv4 address of path. - destination_address IPv4Address Destination IPv4 address of path. -"},{"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 description = \"Verifies the device TCAM profile.\"\n categories: ClassVar[list[str]] = [\"profiles\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hardware tcam profile\", revision=1)]\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 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\", revision=1)]\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\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 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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpLockStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpModeStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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 on the device. 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 on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpOffset:\n ```\n \"\"\"\n\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\", revision=1)]\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 description = \"Verifies the PTP interfaces state.\"\n categories: ClassVar[list[str]] = [\"ptp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", revision=2)]\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/#tests","title":"Tests","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 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\", revision=3)]\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 the advertised and received routes of BGP peers. 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 the advertised and received routes of BGP peers.\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show bgp neighbors {peer} advertised-routes vrf {vrf}\", revision=3),\n AntaTemplate(template=\"show bgp neighbors {peer} routes vrf {vrf}\", revision=3),\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 [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\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 = command.params.peer\n vrf = command.params.vrf\n for input_entry in self.inputs.bgp_peers:\n if str(input_entry.peer_address) == peer and input_entry.vrf == vrf:\n advertised_routes = input_entry.advertised_routes\n received_routes = input_entry.received_routes\n break\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 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\", revision=3)]\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 given address families. This test performs the following checks for each specified address family: Confirms that the specified VRF is configured. Counts the number of peers that are: If check_peer_state is set to True, Counts the number of BGP peers that are in the Established state and have successfully negotiated the specified AFI/SAFI If check_peer_state is set to False, skips validation of the Established state and AFI/SAFI negotiation. Expected Results Success: If the count of BGP peers matches the expected count with check_peer_state enabled/disabled. Failure: If any of the following occur: The specified VRF is not configured. The BGP peer count does not match expected value with check_peer_state enabled/disabled.\u201d 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 given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Confirms that the specified VRF is configured.\n 2. Counts the number of peers that are:\n - If `check_peer_state` is set to True, Counts the number of BGP peers that are in the `Established` state and\n have successfully negotiated the specified AFI/SAFI\n - If `check_peer_state` is set to False, skips validation of the `Established` state and AFI/SAFI negotiation.\n\n Expected Results\n ----------------\n * Success: If the count of BGP peers matches the expected count with `check_peer_state` enabled/disabled.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - The BGP peer count does not match expected value with `check_peer_state` enabled/disabled.\"\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp summary vrf all\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerCount test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'num_peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.num_peers is None:\n msg = f\"{af} 'num_peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerCount.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n peers_data = vrf_output.get(\"peers\", {}).values()\n if not address_family.check_peer_state:\n # Count the number of peers without considering the state and negotiated AFI/SAFI check if the count matches the expected count\n peer_count = sum(1 for peer_data in peers_data if address_family.eos_key in peer_data)\n else:\n # Count the number of established peers with negotiated AFI/SAFI\n peer_count = sum(\n 1\n for peer_data in peers_data\n if peer_data.get(\"peerState\") == \"Established\" and get_value(peer_data, f\"{address_family.eos_key}.afiSafiState\") == \"negotiated\"\n )\n\n # Check if the count matches the expected count\n if address_family.num_peers != peer_count:\n self.result.is_failure(f\"{address_family} - Expected: {address_family.num_peers}, Actual: {peer_count}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats","title":"VerifyBGPPeerDropStats","text":"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s). By default, all drop statistics counters will be checked for any non-zero values. An optional list of specific drop statistics can be provided for granular testing. Expected Results Success: The test will pass if the BGP peer\u2019s drop statistic(s) are zero. Failure: The test will fail if the BGP peer\u2019s drop statistic(s) are non-zero/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerDropStats(AntaTest):\n \"\"\"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s).\n\n By default, all drop statistics counters will be checked for any non-zero values.\n An optional list of specific drop statistics can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's drop statistic(s) are zero.\n * Failure: The test will fail if the BGP peer's drop statistic(s) are non-zero/Not Found or peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerDropStats 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 drop_stats: list[BgpDropStats] | None = None\n \"\"\"Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerDropStats.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n drop_statistics = input_entry.drop_stats\n\n # Verify BGP peer\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify BGP peer's drop stats\n drop_stats_output = peer_detail.get(\"dropStats\", {})\n\n # In case drop stats not provided, It will check all drop statistics\n if not drop_statistics:\n drop_statistics = drop_stats_output\n\n # Verify BGP peer's drop stats\n drop_stats_not_ok = {\n drop_stat: drop_stats_output.get(drop_stat, \"Not Found\") for drop_stat in drop_statistics if drop_stats_output.get(drop_stat, \"Not Found\")\n }\n if any(drop_stats_not_ok):\n failures[peer] = {vrf: drop_stats_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero NLRI drop statistics counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats-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.VerifyBGPPeerDropStats-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' drop_stats list[BgpDropStats] | None Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics. None"},{"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 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\", revision=3)]\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. Supports strict: True to verify that only the specified capabilities are configured, requiring an exact match. 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 strict: False\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 Supports `strict: True` to verify that only the specified capabilities are configured, requiring an exact match.\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 strict: False\n capabilities:\n - ipv4Unicast\n ```\n \"\"\"\n\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\", revision=3)]\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 strict: bool = False\n \"\"\"If True, requires exact matching of provided capabilities. Defaults to False.\"\"\"\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 # Fetching the capabilities output.\n bgp_output = get_value(bgp_output, \"neighborCapabilities.multiprotocolCaps\")\n\n if bgp_peer.strict and sorted(capabilities) != sorted(bgp_output):\n failure[\"bgp_peers\"][peer][vrf] = {\n \"status\": f\"Expected only `{', '.join(capabilities)}` capabilities should be listed but found `{', '.join(bgp_output)}` instead.\"\n }\n failures = deep_update(failures, failure)\n continue\n\n # Check each capability\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' strict bool If True, requires exact matching of provided capabilities. Defaults to False. False capabilities list[MultiProtocolCaps] List of multiprotocol capabilities to be verified. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit","title":"VerifyBGPPeerRouteLimit","text":"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s). Expected Results Success: The test will pass if the BGP peer\u2019s maximum routes and, if provided, the maximum routes warning limit are equal to the given limits. Failure: The test will fail if the BGP peer\u2019s maximum routes do not match the given limit, or if the maximum routes warning limit is provided and does not match the given limit, or if the peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerRouteLimit(AntaTest):\n \"\"\"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's maximum routes and, if provided, the maximum routes warning limit are equal to the given limits.\n * Failure: The test will fail if the BGP peer's maximum routes do not match the given limit, or if the maximum routes warning limit is provided\n and does not match the given limit, or if the peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerRouteLimit 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 maximum_routes: int = Field(ge=0, le=4294967294)\n \"\"\"The maximum allowable number of BGP routes, `0` means unlimited.\"\"\"\n warning_limit: int = Field(default=0, ge=0, le=4294967294)\n \"\"\"Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerRouteLimit.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n maximum_routes = input_entry.maximum_routes\n warning_limit = input_entry.warning_limit\n failure: dict[Any, Any] = {}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify maximum routes configured.\n if (actual_routes := peer_detail.get(\"maxTotalRoutes\", \"Not Found\")) != maximum_routes:\n failure[\"Maximum total routes\"] = actual_routes\n\n # Verify warning limit if given.\n if warning_limit and (actual_warning_limit := peer_detail.get(\"totalRoutesWarnLimit\", \"Not Found\")) != warning_limit:\n failure[\"Warning limit\"] = actual_warning_limit\n\n # Updated failures if any.\n if failure:\n failures[peer] = {vrf: failure}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peer(s) are not configured or maximum routes and maximum routes warning limit is not correct:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit-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.VerifyBGPPeerRouteLimit-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' maximum_routes int The maximum allowable number of BGP routes, `0` means unlimited. Field(ge=0, le=4294967294) warning_limit int Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit. Field(default=0, ge=0, le=4294967294)"},{"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 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\", revision=3)]\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.VerifyBGPPeerUpdateErrors","title":"VerifyBGPPeerUpdateErrors","text":"Verifies BGP update error counters for the provided BGP IPv4 peer(s). By default, all update error counters will be checked for any non-zero values. An optional list of specific update error counters can be provided for granular testing. Note: For \u201cdisabledAfiSafi\u201d error counter field, checking that it\u2019s not \u201cNone\u201d versus 0. Expected Results Success: The test will pass if the BGP peer\u2019s update error counter(s) are zero/None. Failure: The test will fail if the BGP peer\u2019s update error counter(s) are non-zero/not None/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerUpdateErrors(AntaTest):\n \"\"\"Verifies BGP update error counters for the provided BGP IPv4 peer(s).\n\n By default, all update error counters will be checked for any non-zero values.\n An optional list of specific update error counters can be provided for granular testing.\n\n Note: For \"disabledAfiSafi\" error counter field, checking that it's not \"None\" versus 0.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's update error counter(s) are zero/None.\n * Failure: The test will fail if the BGP peer's update error counter(s) are non-zero/not None/Not Found or\n peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerUpdateErrors 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 update_errors: list[BgpUpdateError] | None = None\n \"\"\"Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerUpdateErrors.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n update_error_counters = input_entry.update_errors\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Getting the BGP peer's error counters output.\n error_counters_output = peer_detail.get(\"peerInUpdateErrors\", {})\n\n # In case update error counters not provided, It will check all the update error counters.\n if not update_error_counters:\n update_error_counters = error_counters_output\n\n # verifying the error counters.\n error_counters_not_ok = {\n (\"disabledAfiSafi\" if error_counter == \"disabledAfiSafi\" else error_counter): value\n for error_counter in update_error_counters\n if (value := error_counters_output.get(error_counter, \"Not Found\")) != \"None\" and value != 0\n }\n if error_counters_not_ok:\n failures[peer] = {vrf: error_counters_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero update error counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerUpdateErrors-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.VerifyBGPPeerUpdateErrors-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' update_errors list[BgpUpdateError] | None Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters. None"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth","title":"VerifyBGPPeersHealth","text":"Verifies the health of BGP peers for given address families. This test performs the following checks for each specified address family: Validates that the VRF is configured. Checks if there are any peers for the given AFI/SAFI. For each relevant peer: Verifies that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Checks that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified address families and their peers. Failure: If any of the following occur: The specified VRF is not configured. No peers are found for a given AFI/SAFI. Any BGP session is not in the Established state. The AFI/SAFI state is not \u2018negotiated\u2019 for any peer. Any TCP message queue (input or output) is not empty when check_tcp_queues is True (default). 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 check_tcp_queues: false\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeersHealth(AntaTest):\n \"\"\"Verifies the health of BGP peers for given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Validates that the VRF is configured.\n 2. Checks if there are any peers for the given AFI/SAFI.\n 3. For each relevant peer:\n - Verifies that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Checks that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified address families and their peers.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - No peers are found for a given AFI/SAFI.\n - Any BGP session is not in the `Established` state.\n - The AFI/SAFI state is not 'negotiated' for any peer.\n - Any TCP message queue (input or output) is not empty when `check_tcp_queues` is `True` (default).\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 check_tcp_queues: false\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeersHealth test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeersHealth.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n # Check if any peers are found for this AFI/SAFI\n relevant_peers = [\n peer for peer in vrf_output.get(\"peerList\", []) if get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\") is not None\n ]\n\n if not relevant_peers:\n self.result.is_failure(f\"{address_family} - No peers found\")\n continue\n\n for peer in relevant_peers:\n # Check if the BGP session is established\n if peer[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session state is not established; State: {peer['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers","title":"VerifyBGPSpecificPeers","text":"Verifies the health of specific BGP peer(s) for given address families. This test performs the following checks for each specified address family and peer: Confirms that the specified VRF is configured. For each specified peer: Verifies that the peer is found in the BGP configuration. Checks that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Ensures that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified peers in all address families. Failure: If any of the following occur: The specified VRF is not configured. A specified peer is not found in the BGP configuration. The BGP session for a peer is not in the Established state. The AFI/SAFI state is not negotiated for a peer. Any TCP message queue (input or output) is not empty for a peer when check_tcp_queues is True (default). 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) for given address families.\n\n This test performs the following checks for each specified address family and peer:\n\n 1. Confirms that the specified VRF is configured.\n 2. For each specified peer:\n - Verifies that the peer is found in the BGP configuration.\n - Checks that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Ensures that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified peers in all address families.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - A specified peer is not found in the BGP configuration.\n - The BGP session for a peer is not in the `Established` state.\n - The AFI/SAFI state is not `negotiated` for a peer.\n - Any TCP message queue (input or output) is not empty for a peer when `check_tcp_queues` is `True` (default).\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPSpecificPeers test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.peers is None:\n msg = f\"{af} 'peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPSpecificPeers.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n for peer in address_family.peers:\n peer_ip = str(peer)\n\n # Check if the peer is found\n if (peer_data := get_item(vrf_output[\"peerList\"], \"peerAddress\", peer_ip)) is None:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Not configured\")\n continue\n\n # Check if the BGP session is established\n if peer_data[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session state is not established; State: {peer_data['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer_data, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not capability_status:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated\")\n\n if capability_status and not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer_data[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer_data[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"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 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\", revision=3)]\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.VerifyBgpRouteMaps","title":"VerifyBgpRouteMaps","text":"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s). Expected Results Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF. Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction. Examples anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n Source code in anta/tests/routing/bgp.py class VerifyBgpRouteMaps(AntaTest):\n \"\"\"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF.\n * Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBgpRouteMaps 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 inbound_route_map: str | None = None\n \"\"\"Inbound route map applied, defaults to None.\"\"\"\n outbound_route_map: str | None = None\n \"\"\"Outbound route map applied, defaults to None.\"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpPeer class.\n\n At least one of 'inbound' or 'outbound' route-map must be provided.\n \"\"\"\n if not (self.inbound_route_map or self.outbound_route_map):\n msg = \"At least one of 'inbound_route_map' or 'outbound_route_map' must be provided.\"\n raise ValueError(msg)\n return self\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBgpRouteMaps.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n inbound_route_map = input_entry.inbound_route_map\n outbound_route_map = input_entry.outbound_route_map\n failure: dict[Any, Any] = {vrf: {}}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify Inbound route-map\n if inbound_route_map and (inbound_map := peer_detail.get(\"routeMapInbound\", \"Not Configured\")) != inbound_route_map:\n failure[vrf].update({\"Inbound route-map\": inbound_map})\n\n # Verify Outbound route-map\n if outbound_route_map and (outbound_map := peer_detail.get(\"routeMapOutbound\", \"Not Configured\")) != outbound_route_map:\n failure[vrf].update({\"Outbound route-map\": outbound_map})\n\n if failure[vrf]:\n failures[peer] = failure\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(\n f\"The following BGP peers are not configured or has an incorrect or missing route map in either the inbound or outbound direction:\\n{failures}\"\n )\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBgpRouteMaps-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.VerifyBgpRouteMaps-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' inbound_route_map str | None Inbound route map applied, defaults to None. None outbound_route_map str | None Outbound route map applied, defaults to None. None"},{"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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp evpn route-type mac-ip {address} vni {vni}\", revision=2)]\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=str(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 = 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/#input-models","title":"Input models","text":""},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily","title":"BgpAddressFamily","text":"Model for a BGP address family. Name Type Description Default afi Afi BGP Address Family Identifier (AFI). - safi Safi | None BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`. None vrf str Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`. If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`. These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6. 'default' num_peers PositiveInt | None Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test. None peers list[IPv4Address | IPv6Address] | None List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test. None check_tcp_queues bool Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`. Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests. True check_peer_state bool Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`. Can be enabled in the `VerifyBGPPeerCount` tests. False Source code in anta/input_models/routing/bgp.py class BgpAddressFamily(BaseModel):\n \"\"\"Model for a BGP address family.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n afi: Afi\n \"\"\"BGP Address Family Identifier (AFI).\"\"\"\n safi: Safi | None = None\n \"\"\"BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`.\"\"\"\n vrf: str = \"default\"\n \"\"\"Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`.\n\n If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`.\n\n These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6.\n \"\"\"\n num_peers: PositiveInt | None = None\n \"\"\"Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test.\"\"\"\n peers: list[IPv4Address | IPv6Address] | None = None\n \"\"\"List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test.\"\"\"\n check_tcp_queues: bool = True\n \"\"\"Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`.\n\n Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests.\n \"\"\"\n check_peer_state: bool = False\n \"\"\"Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`.\n\n Can be enabled in the `VerifyBGPPeerCount` tests.\n \"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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 @property\n def eos_key(self) -> str:\n \"\"\"AFI/SAFI EOS key representation.\"\"\"\n # Pydantic handles the validation of the AFI/SAFI combination, so we can ignore error handling here.\n return AFI_SAFI_EOS_KEY[(self.afi, self.safi)]\n\n def __str__(self) -> str:\n \"\"\"Return a string representation of the BgpAddressFamily model. Used in failure messages.\n\n Examples\n --------\n - AFI:ipv4 SAFI:unicast VRF:default\n - AFI:evpn\n \"\"\"\n base_string = f\"AFI: {self.afi}\"\n if self.safi is not None:\n base_string += f\" SAFI: {self.safi}\"\n if self.afi in [\"ipv4\", \"ipv6\"]:\n base_string += f\" VRF: {self.vrf}\"\n return base_string\n"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily.validate_inputs","title":"validate_inputs","text":"validate_inputs() -> Self\n Validate the inputs provided to the BgpAddressFamily class. If afi is either ipv4 or ipv6, safi must be provided. If afi is not ipv4 or ipv6, safi must NOT be provided and vrf must be default. Source code in anta/input_models/routing/bgp.py @model_validator(mode=\"after\")\ndef validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAfi","title":"BgpAfi","text":"Alias for the BgpAddressFamily model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the BgpAddressFamily model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/routing/bgp.py class BgpAfi(BgpAddressFamily): # pragma: no cover\n \"\"\"Alias for the BgpAddressFamily model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the BgpAddressFamily model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the BgpAfi class, emitting a deprecation warning.\"\"\"\n warn(\n message=\"BgpAfi model is deprecated and will be removed in ANTA v2.0.0. Use the BgpAddressFamily model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\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. 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.\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 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 categories: ClassVar[list[str]] = [\"routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show ip route vrf {vrf} {route}\", revision=4),\n AntaTemplate(template=\"show ip route vrf {vrf}\", revision=4),\n ]\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 collect: Literal[\"one\", \"all\"] = \"one\"\n \"\"\"Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one`\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for the input vrf.\"\"\"\n if template == VerifyRoutingTableEntry.commands[0] and self.inputs.collect == \"one\":\n return [template.render(vrf=self.inputs.vrf, route=route) for route in self.inputs.routes]\n\n if template == VerifyRoutingTableEntry.commands[1] and self.inputs.collect == \"all\":\n return [template.render(vrf=self.inputs.vrf)]\n\n return []\n\n @staticmethod\n @cache\n def ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRoutingTableEntry.\"\"\"\n commands_output_route_ips = set()\n\n for command in self.instance_commands:\n command_output_vrf = command.json_output[\"vrfs\"][self.inputs.vrf]\n commands_output_route_ips |= {self.ip_interface_ip(route) for route in command_output_vrf[\"routes\"]}\n\n missing_routes = [str(route) for route in self.inputs.routes if route not in commands_output_route_ips]\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. - collect Literal['one', 'all'] Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one` 'one'"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableEntry.ip_interface_ip","title":"ip_interface_ip cached staticmethod","text":"ip_interface_ip(route: str) -> IPv4Address\n Return the IP address of the provided ip route with mask. Source code in anta/tests/routing/generic.py @staticmethod\n@cache\ndef ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n"},{"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 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: PositiveInteger\n \"\"\"Expected minimum routing table size.\"\"\"\n maximum: PositiveInteger\n \"\"\"Expected maximum routing table size.\"\"\"\n\n @model_validator(mode=\"after\")\n def check_min_max(self) -> Self:\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 PositiveInteger Expected minimum routing table size. - maximum PositiveInteger Expected maximum routing table size. -"},{"location":"api/tests.routing.isis/","title":"ISIS","text":""},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode","title":"VerifyISISInterfaceMode","text":"Verifies ISIS Interfaces are running in correct mode. Expected Results Success: The test will pass if all listed interfaces are running in correct mode. Failure: The test will fail if any of the listed interfaces is not running in correct mode. Skipped: The test will be skipped if no ISIS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISInterfaceMode(AntaTest):\n \"\"\"Verifies ISIS Interfaces are running in correct mode.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces are running in correct mode.\n * Failure: The test will fail if any of the listed interfaces is not running in correct mode.\n * Skipped: The test will be skipped if no ISIS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n ```\n \"\"\"\n\n description = \"Verifies interface mode for IS-IS\"\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceState]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceState(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n mode: Literal[\"point-to-point\", \"broadcast\", \"passive\"]\n \"\"\"Number of IS-IS neighbors.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF where the interface should be configured\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISInterfaceMode.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # Check for p2p interfaces\n for interface in self.inputs.interfaces:\n interface_data = _get_interface_data(\n interface=interface.name,\n vrf=interface.vrf,\n command_output=command_output,\n )\n # Check for correct VRF\n if interface_data is not None:\n interface_type = get_value(dictionary=interface_data, key=\"interfaceType\", default=\"unset\")\n # Check for interfaceType\n if interface.mode == \"point-to-point\" and interface.mode != interface_type:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in {interface.mode} reporting {interface_type}\")\n # Check for passive\n elif interface.mode == \"passive\":\n json_path = f\"intfLevels.{interface.level}.passive\"\n if interface_data is None or get_value(dictionary=interface_data, key=json_path, default=False) is False:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in passive mode\")\n else:\n self.result.is_failure(f\"Interface {interface.name} not found in VRF {interface.vrf}\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceState] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"InterfaceState","text":"Name Type Description Default name Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 mode Literal['point-to-point', 'broadcast', 'passive'] Number of IS-IS neighbors. - vrf str VRF where the interface should be configured 'default'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount","title":"VerifyISISNeighborCount","text":"Verifies number of IS-IS neighbors per level and per interface. Expected Results Success: The test will pass if the number of neighbors is correct. Failure: The test will fail if the number of neighbors is incorrect. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborCount(AntaTest):\n \"\"\"Verifies number of IS-IS neighbors per level and per interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if the number of neighbors is correct.\n * Failure: The test will fail if the number of neighbors is incorrect.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceCount]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceCount(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: int = 2\n \"\"\"IS-IS level to check.\"\"\"\n count: int\n \"\"\"Number of IS-IS neighbors.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborCount.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n isis_neighbor_count = _get_isis_neighbors_count(command_output)\n if len(isis_neighbor_count) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n for interface in self.inputs.interfaces:\n eos_data = [ifl_data for ifl_data in isis_neighbor_count if ifl_data[\"interface\"] == interface.name and ifl_data[\"level\"] == interface.level]\n if not eos_data:\n self.result.is_failure(f\"No neighbor detected for interface {interface.name}\")\n continue\n if eos_data[0][\"count\"] != interface.count:\n self.result.is_failure(\n f\"Interface {interface.name}: \"\n f\"expected Level {interface.level}: count {interface.count}, \"\n f\"got Level {eos_data[0]['level']}: count {eos_data[0]['count']}\"\n )\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceCount] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"InterfaceCount","text":"Name Type Description Default name Interface Interface name to check. - level int IS-IS level to check. 2 count int Number of IS-IS neighbors. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborState","title":"VerifyISISNeighborState","text":"Verifies all IS-IS neighbors are in UP state. Expected Results Success: The test will pass if all IS-IS neighbors are in UP state. Failure: The test will fail if some IS-IS neighbors are not in UP state. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborState(AntaTest):\n \"\"\"Verifies all IS-IS neighbors are in UP state.\n\n Expected Results\n ----------------\n * Success: The test will pass if all IS-IS neighbors are in UP state.\n * Failure: The test will fail if some IS-IS neighbors are not in UP state.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis neighbors\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborState.\"\"\"\n command_output = self.instance_commands[0].json_output\n if _count_isis_neighbor(command_output) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n self.result.is_success()\n not_full_neighbors = _get_not_full_isis_neighbors(command_output)\n if not_full_neighbors:\n self.result.is_failure(f\"Some neighbors are not in the correct state (UP): {not_full_neighbors}.\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments","title":"VerifyISISSegmentRoutingAdjacencySegments","text":"Verify that all expected Adjacency segments are correctly visible for each interface. Expected Results Success: The test will pass if all listed interfaces have correct adjacencies. Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies. Skipped: The test will be skipped if no ISIS SR Adjacency is found. Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):\n \"\"\"Verify that all expected Adjacency segments are correctly visible for each interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces have correct adjacencies.\n * Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies.\n * Skipped: The test will be skipped if no ISIS SR Adjacency is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing adjacency-segments\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingAdjacencySegments test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n segments: list[Segment]\n \"\"\"List of Adjacency segments configured in this instance.\"\"\"\n\n class Segment(BaseModel):\n \"\"\"Segment model definition.\"\"\"\n\n interface: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n sid_origin: Literal[\"dynamic\"] = \"dynamic\"\n \"\"\"Adjacency type\"\"\"\n address: IPv4Address\n \"\"\"IP address of remote end of segment.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingAdjacencySegments.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routging.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n for input_segment in instance.segments:\n eos_segment = _get_adjacency_segment_data_by_neighbor(\n neighbor=str(input_segment.address),\n instance=instance.name,\n vrf=instance.vrf,\n command_output=command_output,\n )\n if eos_segment is None:\n failure_message.append(f\"Your segment has not been found: {input_segment}.\")\n\n elif (\n eos_segment[\"localIntf\"] != input_segment.interface\n or eos_segment[\"level\"] != input_segment.level\n or eos_segment[\"sidOrigin\"] != input_segment.sid_origin\n ):\n failure_message.append(f\"Your segment is not correct: Expected: {input_segment} - Found: {eos_segment}.\")\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' segments list[Segment] List of Adjacency segments configured in this instance. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Segment","text":"Name Type Description Default interface Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 sid_origin Literal['dynamic'] Adjacency type 'dynamic' address IPv4Address IP address of remote end of segment. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane","title":"VerifyISISSegmentRoutingDataplane","text":"Verify dataplane of a list of ISIS-SR instances. Expected Results Success: The test will pass if all instances have correct dataplane configured Failure: The test will fail if one of the instances has incorrect dataplane configured Skipped: The test will be skipped if ISIS is not running Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingDataplane(AntaTest):\n \"\"\"Verify dataplane of a list of ISIS-SR instances.\n\n Expected Results\n ----------------\n * Success: The test will pass if all instances have correct dataplane configured\n * Failure: The test will fail if one of the instances has incorrect dataplane configured\n * Skipped: The test will be skipped if ISIS is not running\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingDataplane test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n dataplane: Literal[\"MPLS\", \"mpls\", \"unset\"] = \"MPLS\"\n \"\"\"Configured dataplane for the instance.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingDataplane.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routing.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n eos_dataplane = get_value(dictionary=command_output, key=f\"vrfs.{instance.vrf}.isisInstances.{instance.name}.dataPlane\", default=None)\n if instance.dataplane.upper() != eos_dataplane:\n failure_message.append(f\"ISIS instance {instance.name} is not running dataplane {instance.dataplane} ({eos_dataplane})\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' dataplane Literal['MPLS', 'mpls', 'unset'] Configured dataplane for the instance. 'MPLS'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels","title":"VerifyISISSegmentRoutingTunnels","text":"Verify ISIS-SR tunnels computed by device. Expected Results Success: The test will pass if all listed tunnels are computed on device. Failure: The test will fail if one of the listed tunnels is missing. Skipped: The test will be skipped if ISIS-SR is not configured. Examples anta.tests.routing:\nisis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingTunnels(AntaTest):\n \"\"\"Verify ISIS-SR tunnels computed by device.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed tunnels are computed on device.\n * Failure: The test will fail if one of the listed tunnels is missing.\n * Skipped: The test will be skipped if ISIS-SR is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing tunnel\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingTunnels test.\"\"\"\n\n entries: list[Entry]\n \"\"\"List of tunnels to check on device.\"\"\"\n\n class Entry(BaseModel):\n \"\"\"Definition of a tunnel entry.\"\"\"\n\n endpoint: IPv4Network\n \"\"\"Endpoint IP of the tunnel.\"\"\"\n vias: list[Vias] | None = None\n \"\"\"Optional list of path to reach endpoint.\"\"\"\n\n class Vias(BaseModel):\n \"\"\"Definition of a tunnel path.\"\"\"\n\n nexthop: IPv4Address | None = None\n \"\"\"Nexthop of the tunnel. If None, then it is not tested. Default: None\"\"\"\n type: Literal[\"ip\", \"tunnel\"] | None = None\n \"\"\"Type of the tunnel. If None, then it is not tested. Default: None\"\"\"\n interface: Interface | None = None\n \"\"\"Interface of the tunnel. If None, then it is not tested. Default: None\"\"\"\n tunnel_id: Literal[\"TI-LFA\", \"ti-lfa\", \"unset\"] | None = None\n \"\"\"Computation method of the tunnel. If None, then it is not tested. Default: None\"\"\"\n\n def _eos_entry_lookup(self, search_value: IPv4Network, entries: dict[str, Any], search_key: str = \"endpoint\") -> dict[str, Any] | None:\n return next(\n (entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == str(search_value)),\n None,\n )\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingTunnels.\n\n This method performs the main test logic for verifying ISIS Segment Routing tunnels.\n It checks the command output, initiates defaults, and performs various checks on the tunnels.\n \"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n # initiate defaults\n failure_message = []\n\n if len(command_output[\"entries\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n for input_entry in self.inputs.entries:\n eos_entry = self._eos_entry_lookup(search_value=input_entry.endpoint, entries=command_output[\"entries\"])\n if eos_entry is None:\n failure_message.append(f\"Tunnel to {input_entry} is not found.\")\n elif input_entry.vias is not None:\n failure_src = []\n for via_input in input_entry.vias:\n if not self._check_tunnel_type(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel type\")\n if not self._check_tunnel_nexthop(via_input, eos_entry):\n failure_src.append(\"incorrect nexthop\")\n if not self._check_tunnel_interface(via_input, eos_entry):\n failure_src.append(\"incorrect interface\")\n if not self._check_tunnel_id(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel ID\")\n\n if failure_src:\n failure_message.append(f\"Tunnel to {input_entry.endpoint!s} is incorrect: {', '.join(failure_src)}\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n\n def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input tunnel type to check.\n eos_entry : dict[str, Any]\n The EOS entry containing the tunnel types.\n\n Returns\n -------\n bool\n True if the tunnel type matches any of the tunnel types in `eos_entry`, False otherwise.\n \"\"\"\n if via_input.type is not None:\n return any(\n via_input.type\n == get_value(\n dictionary=eos_via,\n key=\"type\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel nexthop matches the given input.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel nexthop matches, False otherwise.\n \"\"\"\n if via_input.nexthop is not None:\n return any(\n str(via_input.nexthop)\n == get_value(\n dictionary=eos_via,\n key=\"nexthop\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel interface exists in the given EOS entry.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel interface exists, False otherwise.\n \"\"\"\n if via_input.interface is not None:\n return any(\n via_input.interface\n == get_value(\n dictionary=eos_via,\n key=\"interface\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input vias to check.\n eos_entry : dict[str, Any])\n The EOS entry to compare against.\n\n Returns\n -------\n bool\n True if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias, False otherwise.\n \"\"\"\n if via_input.tunnel_id is not None:\n return any(\n via_input.tunnel_id.upper()\n == get_value(\n dictionary=eos_via,\n key=\"tunnelId.type\",\n default=\"undefined\",\n ).upper()\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Inputs","text":"Name Type Description Default entries list[Entry] List of tunnels to check on device. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Entry","text":"Name Type Description Default endpoint IPv4Network Endpoint IP of the tunnel. - vias list[Vias] | None Optional list of path to reach endpoint. None"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Vias","text":"Name Type Description Default nexthop IPv4Address | None Nexthop of the tunnel. If None, then it is not tested. Default: None None type Literal['ip', 'tunnel'] | None Type of the tunnel. If None, then it is not tested. Default: None None interface Interface | None Interface of the tunnel. If None, then it is not tested. Default: None None tunnel_id Literal['TI-LFA', 'ti-lfa', 'unset'] | None Computation method of the tunnel. If None, then it is not tested. Default: None None"},{"location":"api/tests.routing.ospf/","title":"OSPF","text":""},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFMaxLSA","title":"VerifyOSPFMaxLSA","text":"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold. Expected Results Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold. Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold. Skipped: The test will be skipped if no OSPF instance is found. Examples anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n Source code in anta/tests/routing/ospf.py class VerifyOSPFMaxLSA(AntaTest):\n \"\"\"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold.\n * Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold.\n * Skipped: The test will be skipped if no OSPF instance is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n ```\n \"\"\"\n\n description = \"Verifies all OSPF instances did not cross the maximum LSA threshold.\"\n categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyOSPFMaxLSA.\"\"\"\n command_output = self.instance_commands[0].json_output\n ospf_instance_info = _get_ospf_max_lsa_info(command_output)\n if not ospf_instance_info:\n self.result.is_skipped(\"No OSPF instance found.\")\n return\n all_instances_within_threshold = all(instance[\"numLsa\"] <= instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100) for instance in ospf_instance_info)\n if all_instances_within_threshold:\n self.result.is_success()\n else:\n exceeded_instances = [\n instance[\"instance\"] for instance in ospf_instance_info if instance[\"numLsa\"] > instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100)\n ]\n self.result.is_failure(f\"OSPF Instances {exceeded_instances} crossed the maximum LSA threshold.\")\n"},{"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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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.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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ip access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ipv6 access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show management security ssl certificate\", revision=1),\n AntaCommand(command=\"show clock\", revision=1),\n ]\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) -> Self:\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 get_args(RsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for RSA encryption. Allowed sizes are {get_args(RsaKeySize)}.\"\n raise ValueError(msg)\n\n if self.encryption_algorithm == \"ECDSA\" and self.key_size not in get_args(EcdsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for ECDSA encryption. Allowed sizes are {get_args(EcdsaKeySize)}.\"\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner login\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner motd\", revision=1)]\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.VerifyHardwareEntropy","title":"VerifyHardwareEntropy","text":"Verifies hardware entropy generation is enabled on device. Expected Results Success: The test will pass if hardware entropy generation is enabled. Failure: The test will fail if hardware entropy generation is not enabled. Examples anta.tests.security:\n - VerifyHardwareEntropy:\n Source code in anta/tests/security.py class VerifyHardwareEntropy(AntaTest):\n \"\"\"Verifies hardware entropy generation is enabled on device.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware entropy generation is enabled.\n * Failure: The test will fail if hardware entropy generation is not enabled.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyHardwareEntropy:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management security\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareEntropy.\"\"\"\n command_output = self.instance_commands[0].json_output\n\n # Check if hardware entropy generation is enabled.\n if not command_output.get(\"hardwareEntropyEnabled\"):\n self.result.is_failure(\"Hardware entropy generation is disabled.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.security/#anta.tests.security.VerifyIPSecConnHealth","title":"VerifyIPSecConnHealth","text":"Verifies all IPv4 security connections. Expected Results Success: The test will pass if all the IPv4 security connections are established in all vrf. Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf. Examples anta.tests.security:\n - VerifyIPSecConnHealth:\n Source code in anta/tests/security.py class VerifyIPSecConnHealth(AntaTest):\n \"\"\"Verifies all IPv4 security connections.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the IPv4 security connections are established in all vrf.\n * Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyIPSecConnHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip security connection vrf all\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyIPSecConnHealth.\"\"\"\n self.result.is_success()\n failure_conn = []\n command_output = self.instance_commands[0].json_output[\"connections\"]\n\n # Check if IP security connection is configured\n if not command_output:\n self.result.is_failure(\"No IPv4 security connection configured.\")\n return\n\n # Iterate over all ipsec connections\n for conn_data in command_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n failure_conn.append(f\"source:{source} destination:{destination} vrf:{vrf}\")\n if failure_conn:\n failure_msg = \"\\n\".join(failure_conn)\n self.result.is_failure(f\"The following IPv4 security connections are not established:\\n{failure_msg}.\")\n"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip access-lists {acl}\", revision=1)]\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) 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, acl in zip(self.instance_commands, self.inputs.ipv4_access_lists):\n # Collecting input ACL details\n acl_name = command_output.params.acl\n # Retrieve the expected entries from the inputs\n acl_entries = acl.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 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\", revision=1)]\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 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\", revision=1)]\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 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 try:\n line = next(line for line in command_output.split(\"\\n\") if line.startswith(\"SSHD status\"))\n except StopIteration:\n self.result.is_failure(\"Could not find SSH status in returned output.\")\n return\n status = line.split()[-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.VerifySpecificIPSecConn","title":"VerifySpecificIPSecConn","text":"Verifies the state of IPv4 security connections for a specified peer. It optionally allows for the verification of a specific path for a peer by providing source and destination addresses. If these addresses are not provided, it will verify all paths for the specified peer. Expected Results Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF. Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF. Examples anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n Source code in anta/tests/security.py class VerifySpecificIPSecConn(AntaTest):\n \"\"\"Verifies the state of IPv4 security connections for a specified peer.\n\n It optionally allows for the verification of a specific path for a peer by providing source and destination addresses.\n If these addresses are not provided, it will verify all paths for the specified peer.\n\n Expected Results\n ----------------\n * Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF.\n * Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n ```\n \"\"\"\n\n description = \"Verifies IPv4 security connections for a peer.\"\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip security connection vrf {vrf} path peer {peer}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificIPSecConn test.\"\"\"\n\n ip_security_connections: list[IPSecPeers]\n \"\"\"List of IP4v security peers.\"\"\"\n\n class IPSecPeers(BaseModel):\n \"\"\"Details of IPv4 security peers.\"\"\"\n\n peer: IPv4Address\n \"\"\"IPv4 address of the peer.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"Optional VRF for the IP security peer.\"\"\"\n\n connections: list[IPSecConn] | None = None\n \"\"\"Optional list of IPv4 security connections of a peer.\"\"\"\n\n class IPSecConn(BaseModel):\n \"\"\"Details of IPv4 security connections for a peer.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of the connection.\"\"\"\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of the connection.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input IP Sec connection.\"\"\"\n return [template.render(peer=conn.peer, vrf=conn.vrf) for conn in self.inputs.ip_security_connections]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificIPSecConn.\"\"\"\n self.result.is_success()\n for command_output, input_peer in zip(self.instance_commands, self.inputs.ip_security_connections):\n conn_output = command_output.json_output[\"connections\"]\n peer = command_output.params.peer\n vrf = command_output.params.vrf\n conn_input = input_peer.connections\n\n # Check if IPv4 security connection is configured\n if not conn_output:\n self.result.is_failure(f\"No IPv4 security connection configured for peer `{peer}`.\")\n continue\n\n # If connection details are not provided then check all connections of a peer\n if conn_input is None:\n for conn_data in conn_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source} destination:{destination} vrf:{vrf}` for peer `{peer}` is `Established` \"\n f\"but found `{state}` instead.\"\n )\n continue\n\n # Create a dictionary of existing connections for faster lookup\n existing_connections = {\n (conn_data.get(\"saddr\"), conn_data.get(\"daddr\"), conn_data.get(\"tunnelNs\")): next(iter(conn_data[\"pathDict\"].values()))\n for conn_data in conn_output.values()\n }\n for connection in conn_input:\n source_input = str(connection.source_address)\n destination_input = str(connection.destination_address)\n\n if (source_input, destination_input, vrf) in existing_connections:\n existing_state = existing_connections[(source_input, destination_input, vrf)]\n if existing_state != \"Established\":\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` \"\n f\"for peer `{peer}` is `Established` but found `{existing_state}` instead.\"\n )\n else:\n self.result.is_failure(\n f\"IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` for peer `{peer}` is not found.\"\n )\n"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"Inputs","text":"Name Type Description Default ip_security_connections list[IPSecPeers] List of IP4v security peers. -"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecPeers","text":"Name Type Description Default peer IPv4Address IPv4 address of the peer. - vrf str Optional VRF for the IP security peer. 'default' connections list[IPSecConn] | None Optional list of IPv4 security connections of a peer. None"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecConn","text":"Name Type Description Default source_address IPv4Address Source IPv4 address of the connection. - destination_address IPv4Address Destination IPv4 address of the connection. -"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management telnet\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=1)]\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. This test performs the following checks for each specified DNS Server: Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF. Ensuring an appropriate priority level. 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 any of the following conditions are met: The provided DNS server is not configured. The provided DNS server with designated VRF and priority does not match the expected information. 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 This test performs the following checks for each specified DNS Server:\n\n 1. Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF.\n 2. Ensuring an appropriate priority level.\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 any of the following conditions are met:\n - The provided DNS server is not configured.\n - The provided DNS server with designated VRF and priority does not match the expected information.\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 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\", revision=1)]\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 DnsServer: ClassVar[type[DnsServer]] = DnsServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyDNSServers.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"nameServerConfigs\"]\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 # Check if the DNS server is configured with specified VRF.\n if (output := get_dict_superset(command_output, input_dict)) is None:\n self.result.is_failure(f\"{server} - Not configured\")\n continue\n\n # Check if the DNS server priority matches with expected.\n if output[\"priority\"] != priority:\n self.result.is_failure(f\"{server} - Incorrect priority; Priority: {output['priority']}\")\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.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 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 categories: ClassVar[list[str]] = [\"services\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hostname\", revision=1)]\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.services/#input-models","title":"Input models","text":""},{"location":"api/tests.services/#anta.input_models.services.DnsServer","title":"DnsServer","text":"Model for a DNS server configuration. Name Type Description Default server_address IPv4Address | IPv6Address The IPv4 or IPv6 address of the DNS server. - vrf str The VRF instance in which the DNS server resides. Defaults to 'default'. 'default' priority int The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest. Field(ge=0, le=4) Source code in anta/input_models/services.py class DnsServer(BaseModel):\n \"\"\"Model for a DNS server configuration.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: IPv4Address | IPv6Address\n \"\"\"The IPv4 or IPv6 address of the DNS server.\"\"\"\n vrf: str = \"default\"\n \"\"\"The VRF instance in which the DNS server resides. Defaults to 'default'.\"\"\"\n priority: int = Field(ge=0, le=4)\n \"\"\"The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the DnsServer for reporting.\n\n Examples\n --------\n Server 10.0.0.1 (VRF: default, Priority: 1)\n \"\"\"\n return f\"Server {self.server_address} (VRF: {self.vrf}, Priority: {self.priority})\"\n"},{"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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP contact is configured.\n if not (contact := get_value(self.instance_commands[0].json_output, \"contact.contact\")):\n self.result.is_failure(\"SNMP contact is not configured.\")\n return\n\n # Verifies the expected SNMP contact.\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.VerifySnmpErrorCounters","title":"VerifySnmpErrorCounters","text":"Verifies the SNMP error counters. By default, all error counters will be checked for any non-zero values. An optional list of specific error counters can be provided for granular testing. Expected Results Success: The test will pass if the SNMP error counter(s) are zero/None. Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured. Examples ```yaml anta.tests.snmp: - VerifySnmpErrorCounters: error_counters: - inVersionErrs - inBadCommunityNames Source code in anta/tests/snmp.py class VerifySnmpErrorCounters(AntaTest):\n \"\"\"Verifies the SNMP error counters.\n\n By default, all error counters will be checked for any non-zero values.\n An optional list of specific error counters can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP error counter(s) are zero/None.\n * Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpErrorCounters:\n error_counters:\n - inVersionErrs\n - inBadCommunityNames\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpErrorCounters test.\"\"\"\n\n error_counters: list[SnmpErrorCounter] | None = None\n \"\"\"Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpErrorCounters.\"\"\"\n error_counters = self.inputs.error_counters\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (snmp_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP error counters not provided, It will check all the error counters.\n if not error_counters:\n error_counters = list(get_args(SnmpErrorCounter))\n\n error_counters_not_ok = {counter: value for counter in error_counters if (value := snmp_counters.get(counter))}\n\n # Check if any failures\n if not error_counters_not_ok:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP error counters are not found or have non-zero error counters:\\n{error_counters_not_ok}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpErrorCounters-attributes","title":"Inputs","text":"Name Type Description Default error_counters list[SnmpErrorCounter] | None Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters. None"},{"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 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\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP location is configured.\n if not (location := get_value(self.instance_commands[0].json_output, \"location.location\")):\n self.result.is_failure(\"SNMP location is not configured.\")\n return\n\n # Verifies the expected SNMP location.\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.VerifySnmpPDUCounters","title":"VerifySnmpPDUCounters","text":"Verifies the SNMP PDU counters. By default, all SNMP PDU counters will be checked for any non-zero values. An optional list of specific SNMP PDU(s) can be provided for granular testing. Expected Results Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero. Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found. Examples anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n Source code in anta/tests/snmp.py class VerifySnmpPDUCounters(AntaTest):\n \"\"\"Verifies the SNMP PDU counters.\n\n By default, all SNMP PDU counters will be checked for any non-zero values.\n An optional list of specific SNMP PDU(s) can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero.\n * Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpPDUCounters test.\"\"\"\n\n pdus: list[SnmpPdu] | None = None\n \"\"\"Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpPDUCounters.\"\"\"\n snmp_pdus = self.inputs.pdus\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (pdu_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP PDUs not provided, It will check all the update error counters.\n if not snmp_pdus:\n snmp_pdus = list(get_args(SnmpPdu))\n\n failures = {pdu: value for pdu in snmp_pdus if (value := pdu_counters.get(pdu, \"Not Found\")) == \"Not Found\" or value == 0}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP PDU counters are not found or have zero PDU counters:\\n{failures}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpPDUCounters-attributes","title":"Inputs","text":"Name Type Description Default pdus list[SnmpPdu] | None Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters. None"},{"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 description = \"Verifies if the SNMP agent is enabled.\"\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show extensions\", revision=2),\n AntaCommand(command=\"show boot-extensions\", revision=1),\n ]\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 description = \"Verifies the EOS version of the device.\"\n categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree blockedports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree counters\", revision=1)]\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 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\", revision=1)]\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 vlan_id = command.params.vlan\n if not (topologies := get_value(command.json_output, \"topologies\")):\n not_configured.append(vlan_id)\n else:\n interfaces_not_forwarding = []\n for value in topologies.values():\n if vlan_id and 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 forwarding 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show spanning-tree vlan {vlan}\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree root detail\", revision=1)]\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.stp/#anta.tests.stp.VerifyStpTopologyChanges","title":"VerifyStpTopologyChanges","text":"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold. Expected Results Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold. Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold, indicating potential instability in the topology. Examples anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n Source code in anta/tests/stp.py class VerifyStpTopologyChanges(AntaTest):\n \"\"\"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold.\n * Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold,\n indicating potential instability in the topology.\n\n Examples\n --------\n ```yaml\n anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree topology status detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStpTopologyChanges test.\"\"\"\n\n threshold: int\n \"\"\"The threshold number of changes in the STP topology.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStpTopologyChanges.\"\"\"\n failures: dict[str, Any] = {\"topologies\": {}}\n\n command_output = self.instance_commands[0].json_output\n stp_topologies = command_output.get(\"topologies\", {})\n\n # verifies all available topologies except the \"NoStp\" topology.\n stp_topologies.pop(\"NoStp\", None)\n\n # Verify the STP topology(s).\n if not stp_topologies:\n self.result.is_failure(\"STP is not configured.\")\n return\n\n # Verifies the number of changes across all interfaces\n for topology, topology_details in stp_topologies.items():\n interfaces = {\n interface: {\"Number of changes\": num_of_changes}\n for interface, details in topology_details.get(\"interfaces\", {}).items()\n if (num_of_changes := details.get(\"numChanges\")) > self.inputs.threshold\n }\n if interfaces:\n failures[\"topologies\"][topology] = interfaces\n\n if failures[\"topologies\"]:\n self.result.is_failure(f\"The following STP topologies are not configured or number of changes not within the threshold:\\n{failures}\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.stp/#anta.tests.stp.VerifyStpTopologyChanges-attributes","title":"Inputs","text":"Name Type Description Default threshold int The threshold number of changes in the STP topology. -"},{"location":"api/tests.stun/","title":"STUN","text":""},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient","title":"VerifyStunClient","text":"Verifies STUN client settings, including local IP/port and optionally public IP/port. Expected Results Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port. Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect. Examples anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n Source code in anta/tests/stun.py class VerifyStunClient(AntaTest):\n \"\"\"Verifies STUN client settings, including local IP/port and optionally public IP/port.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port.\n * Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show stun client translations {source_address} {source_port}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStunClient test.\"\"\"\n\n stun_clients: list[ClientAddress]\n\n class ClientAddress(BaseModel):\n \"\"\"Source and public address/port details of STUN client.\"\"\"\n\n source_address: IPv4Address\n \"\"\"IPv4 source address of STUN client.\"\"\"\n source_port: Port = 4500\n \"\"\"Source port number for STUN client.\"\"\"\n public_address: IPv4Address | None = None\n \"\"\"Optional IPv4 public address of STUN client.\"\"\"\n public_port: Port | None = None\n \"\"\"Optional public port number for STUN client.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each STUN translation.\"\"\"\n return [template.render(source_address=client.source_address, source_port=client.source_port) for client in self.inputs.stun_clients]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunClient.\"\"\"\n self.result.is_success()\n\n # Iterate over each command output and corresponding client input\n for command, client_input in zip(self.instance_commands, self.inputs.stun_clients):\n bindings = command.json_output[\"bindings\"]\n source_address = str(command.params.source_address)\n source_port = command.params.source_port\n\n # If no bindings are found for the STUN client, mark the test as a failure and continue with the next client\n if not bindings:\n self.result.is_failure(f\"STUN client transaction for source `{source_address}:{source_port}` is not found.\")\n continue\n\n # Extract the public address and port from the client input\n public_address = client_input.public_address\n public_port = client_input.public_port\n\n # Extract the transaction ID from the bindings\n transaction_id = next(iter(bindings.keys()))\n\n # Prepare the actual and expected STUN data for comparison\n actual_stun_data = {\n \"source ip\": get_value(bindings, f\"{transaction_id}.sourceAddress.ip\"),\n \"source port\": get_value(bindings, f\"{transaction_id}.sourceAddress.port\"),\n }\n expected_stun_data = {\"source ip\": source_address, \"source port\": source_port}\n\n # If public address is provided, add it to the actual and expected STUN data\n if public_address is not None:\n actual_stun_data[\"public ip\"] = get_value(bindings, f\"{transaction_id}.publicAddress.ip\")\n expected_stun_data[\"public ip\"] = str(public_address)\n\n # If public port is provided, add it to the actual and expected STUN data\n if public_port is not None:\n actual_stun_data[\"public port\"] = get_value(bindings, f\"{transaction_id}.publicAddress.port\")\n expected_stun_data[\"public port\"] = public_port\n\n # If the actual STUN data does not match the expected STUN data, mark the test as failure\n if actual_stun_data != expected_stun_data:\n failed_log = get_failed_logs(expected_stun_data, actual_stun_data)\n self.result.is_failure(f\"For STUN source `{source_address}:{source_port}`:{failed_log}\")\n"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"ClientAddress","text":"Name Type Description Default source_address IPv4Address IPv4 source address of STUN client. - source_port Port Source port number for STUN client. 4500 public_address IPv4Address | None Optional IPv4 public address of STUN client. None public_port Port | None Optional public port number for STUN client. None"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunServer","title":"VerifyStunServer","text":"Verifies the STUN server status is enabled and running. Expected Results Success: The test will pass if the STUN server status is enabled and running. Failure: The test will fail if the STUN server is disabled or not running. Examples anta.tests.stun:\n - VerifyStunServer:\n Source code in anta/tests/stun.py class VerifyStunServer(AntaTest):\n \"\"\"Verifies the STUN server status is enabled and running.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN server status is enabled and running.\n * Failure: The test will fail if the STUN server is disabled or not running.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunServer:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show stun server status\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunServer.\"\"\"\n command_output = self.instance_commands[0].json_output\n status_disabled = not command_output.get(\"enabled\")\n not_running = command_output.get(\"pid\") == 0\n\n if status_disabled and not_running:\n self.result.is_failure(\"STUN server status is disabled and not running.\")\n elif status_disabled:\n self.result.is_failure(\"STUN server status is disabled.\")\n elif not_running:\n self.result.is_failure(\"STUN server is not running.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.system/","title":"System","text":""},{"location":"api/tests.system/#tests","title":"Tests","text":""},{"location":"api/tests.system/#anta.tests.system.VerifyAgentLogs","title":"VerifyAgentLogs","text":"Verifies there are no agent crash reports. 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 there are no agent crash reports.\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show processes top once\", revision=1)]\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 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\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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.VerifyNTPAssociations","title":"VerifyNTPAssociations","text":"Verifies the Network Time Protocol (NTP) associations. Expected Results Success: The test will pass if the Primary NTP server (marked as preferred) has the condition \u2018sys.peer\u2019 and all other NTP servers have the condition \u2018candidate\u2019. Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition \u2018sys.peer\u2019 or if any other NTP server does not have the condition \u2018candidate\u2019. Examples anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n Source code in anta/tests/system.py class VerifyNTPAssociations(AntaTest):\n \"\"\"Verifies the Network Time Protocol (NTP) associations.\n\n Expected Results\n ----------------\n * Success: The test will pass if the Primary NTP server (marked as preferred) has the condition 'sys.peer' and\n all other NTP servers have the condition 'candidate'.\n * Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition 'sys.peer' or\n if any other NTP server does not have the condition 'candidate'.\n\n Examples\n --------\n ```yaml\n anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ntp associations\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyNTPAssociations test.\"\"\"\n\n ntp_servers: list[NTPServer]\n \"\"\"List of NTP servers.\"\"\"\n NTPServer: ClassVar[type[NTPServer]] = NTPServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyNTPAssociations.\"\"\"\n self.result.is_success()\n\n if not (peers := get_value(self.instance_commands[0].json_output, \"peers\")):\n self.result.is_failure(\"No NTP peers configured\")\n return\n\n # Iterate over each NTP server.\n for ntp_server in self.inputs.ntp_servers:\n server_address = str(ntp_server.server_address)\n\n # We check `peerIpAddr` in the peer details - covering IPv4Address input, or the peer key - covering Hostname input.\n matching_peer = next((peer for peer, peer_details in peers.items() if (server_address in {peer_details[\"peerIpAddr\"], peer})), None)\n\n if not matching_peer:\n self.result.is_failure(f\"{ntp_server} - Not configured\")\n continue\n\n # Collecting the expected/actual NTP peer details.\n exp_condition = \"sys.peer\" if ntp_server.preferred else \"candidate\"\n exp_stratum = ntp_server.stratum\n act_condition = get_value(peers[matching_peer], \"condition\")\n act_stratum = get_value(peers[matching_peer], \"stratumLevel\")\n\n if act_condition != exp_condition or act_stratum != exp_stratum:\n self.result.is_failure(f\"{ntp_server} - Bad association; Condition: {act_condition}, Stratum: {act_stratum}\")\n"},{"location":"api/tests.system/#anta.tests.system.VerifyNTPAssociations-attributes","title":"Inputs","text":"Name Type Description Default ntp_servers list[NTPServer] List of NTP servers. -"},{"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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show reload cause\", revision=1)]\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 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 description = \"Verifies the device uptime.\"\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show uptime\", revision=1)]\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.system/#input-models","title":"Input models","text":""},{"location":"api/tests.system/#anta.input_models.system.NTPServer","title":"NTPServer","text":"Model for a NTP server. Name Type Description Default server_address Hostname | IPv4Address The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name. For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output. - preferred bool Optional preferred for NTP server. If not provided, it defaults to `False`. False stratum int NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized. Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state. Field(ge=0, le=16) Source code in anta/input_models/system.py class NTPServer(BaseModel):\n \"\"\"Model for a NTP server.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: Hostname | IPv4Address\n \"\"\"The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration\n of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name.\n For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output.\"\"\"\n preferred: bool = False\n \"\"\"Optional preferred for NTP server. If not provided, it defaults to `False`.\"\"\"\n stratum: int = Field(ge=0, le=16)\n \"\"\"NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized.\n Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Representation of the NTPServer model.\"\"\"\n return f\"{self.server_address} (Preferred: {self.preferred}, Stratum: {self.stratum})\"\n"},{"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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\", revision=1)]\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 description = \"Verifies the Vxlan1 interface status.\"\n categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 there are no VXLAN config-sanity inconsistencies. 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 there are no VXLAN config-sanity inconsistencies.\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan config-sanity\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vni\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vtep\", revision=1)]\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[\n str, AfterValidator(aaa_group_prefix)\n]\n"},{"location":"api/types/#anta.custom_types.Afi","title":"Afi module-attribute","text":"Afi = Literal[\n \"ipv4\",\n \"ipv6\",\n \"vpn-ipv4\",\n \"vpn-ipv6\",\n \"evpn\",\n \"rt-membership\",\n \"path-selection\",\n \"link-state\",\n]\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.BfdProtocol","title":"BfdProtocol module-attribute","text":"BfdProtocol = Literal[\n \"bgp\",\n \"isis\",\n \"lag\",\n \"ospf\",\n \"ospfv3\",\n \"pim\",\n \"route-input\",\n \"static-bfd\",\n \"static-route\",\n \"vrrp\",\n \"vxlan\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpDropStats","title":"BgpDropStats module-attribute","text":"BgpDropStats = Literal[\n \"inDropAsloop\",\n \"inDropClusterIdLoop\",\n \"inDropMalformedMpbgp\",\n \"inDropOrigId\",\n \"inDropNhLocal\",\n \"inDropNhAfV6\",\n \"prefixDroppedMartianV4\",\n \"prefixDroppedMaxRouteLimitViolatedV4\",\n \"prefixDroppedMartianV6\",\n \"prefixDroppedMaxRouteLimitViolatedV6\",\n \"prefixLuDroppedV4\",\n \"prefixLuDroppedMartianV4\",\n \"prefixLuDroppedMaxRouteLimitViolatedV4\",\n \"prefixLuDroppedV6\",\n \"prefixLuDroppedMartianV6\",\n \"prefixLuDroppedMaxRouteLimitViolatedV6\",\n \"prefixEvpnDroppedUnsupportedRouteType\",\n \"prefixBgpLsDroppedReceptionUnsupported\",\n \"outDropV4LocalAddr\",\n \"outDropV6LocalAddr\",\n \"prefixVpnIpv4DroppedImportMatchFailure\",\n \"prefixVpnIpv4DroppedMaxRouteLimitViolated\",\n \"prefixVpnIpv6DroppedImportMatchFailure\",\n \"prefixVpnIpv6DroppedMaxRouteLimitViolated\",\n \"prefixEvpnDroppedImportMatchFailure\",\n \"prefixEvpnDroppedMaxRouteLimitViolated\",\n \"prefixRtMembershipDroppedLocalAsReject\",\n \"prefixRtMembershipDroppedMaxRouteLimitViolated\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpUpdateError","title":"BgpUpdateError module-attribute","text":"BgpUpdateError = Literal[\n \"inUpdErrWithdraw\",\n \"inUpdErrIgnore\",\n \"inUpdErrDisableAfiSafi\",\n \"disabledAfiSafi\",\n \"lastUpdErrTime\",\n]\n"},{"location":"api/types/#anta.custom_types.EcdsaKeySize","title":"EcdsaKeySize module-attribute","text":"EcdsaKeySize = Literal[256, 384, 512]\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[\n \"acl\",\n \"arp-inspection\",\n \"bpduguard\",\n \"dot1x-session-replace\",\n \"hitless-reload-down\",\n \"lacp-rate-limit\",\n \"link-flap\",\n \"no-internal-vlan\",\n \"portchannelguard\",\n \"portsec\",\n \"tapagg\",\n \"uplink-failure-detection\",\n]\n"},{"location":"api/types/#anta.custom_types.EthernetInterface","title":"EthernetInterface module-attribute","text":"EthernetInterface = Annotated[\n str,\n Field(pattern=\"^Ethernet[0-9]+(\\\\/[0-9]+)*$\"),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\n"},{"location":"api/types/#anta.custom_types.Hostname","title":"Hostname module-attribute","text":"Hostname = Annotated[\n str, Field(pattern=REGEXP_TYPE_HOSTNAME)\n]\n"},{"location":"api/types/#anta.custom_types.Interface","title":"Interface module-attribute","text":"Interface = Annotated[\n str,\n Field(pattern=REGEXP_TYPE_EOS_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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[\n str,\n BeforeValidator(\n bgp_multiprotocol_capabilities_abbreviations\n ),\n]\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.PortChannelInterface","title":"PortChannelInterface module-attribute","text":"PortChannelInterface = Annotated[\n str,\n Field(pattern=REGEX_TYPE_PORTCHANNEL),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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.REGEXP_BGP_IPV4_MPLS_LABELS","title":"REGEXP_BGP_IPV4_MPLS_LABELS module-attribute","text":"REGEXP_BGP_IPV4_MPLS_LABELS = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?label(s)?)\\\\b\"\n)\n Match IPv4 MPLS Labels."},{"location":"api/types/#anta.custom_types.REGEXP_BGP_L2VPN_AFI","title":"REGEXP_BGP_L2VPN_AFI module-attribute","text":"REGEXP_BGP_L2VPN_AFI = \"\\\\b(l2[\\\\s\\\\-]?vpn[\\\\s\\\\-]?evpn)\\\\b\"\n Match L2VPN EVPN AFI."},{"location":"api/types/#anta.custom_types.REGEXP_EOS_BLACKLIST_CMDS","title":"REGEXP_EOS_BLACKLIST_CMDS module-attribute","text":"REGEXP_EOS_BLACKLIST_CMDS = [\n \"^reload.*\",\n \"^conf\\\\w*\\\\s*(terminal|session)*\",\n \"^wr\\\\w*\\\\s*\\\\w+\",\n]\n List of regular expressions to blacklist from eos commands."},{"location":"api/types/#anta.custom_types.REGEXP_INTERFACE_ID","title":"REGEXP_INTERFACE_ID module-attribute","text":"REGEXP_INTERFACE_ID = '\\\\d+(\\\\/\\\\d+)*(\\\\.\\\\d+)?'\n Match Interface ID lilke 1/1.1."},{"location":"api/types/#anta.custom_types.REGEXP_PATH_MARKERS","title":"REGEXP_PATH_MARKERS module-attribute","text":"REGEXP_PATH_MARKERS = '[\\\\\\\\\\\\/\\\\s]'\n Match directory path from string."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_EOS_INTERFACE","title":"REGEXP_TYPE_EOS_INTERFACE module-attribute","text":"REGEXP_TYPE_EOS_INTERFACE = \"^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\\\\/[0-9]+)*(\\\\.[0-9]+)?$\"\n Match EOS interface types like Ethernet1/1, Vlan1, Loopback1, etc."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_HOSTNAME","title":"REGEXP_TYPE_HOSTNAME module-attribute","text":"REGEXP_TYPE_HOSTNAME = \"^(([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 Match hostname like my-hostname, my-hostname-1, my-hostname-1-2."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_VXLAN_SRC_INTERFACE","title":"REGEXP_TYPE_VXLAN_SRC_INTERFACE module-attribute","text":"REGEXP_TYPE_VXLAN_SRC_INTERFACE = \"^(Loopback)([0-9]|[1-9][0-9]{1,2}|[1-7][0-9]{3}|8[01][0-9]{2}|819[01])$\"\n Match Vxlan source interface like Loopback10."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_MPLS_VPN","title":"REGEX_BGP_IPV4_MPLS_VPN module-attribute","text":"REGEX_BGP_IPV4_MPLS_VPN = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?vpn)\\\\b\"\n)\n Match IPv4 MPLS VPN."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_UNICAST","title":"REGEX_BGP_IPV4_UNICAST module-attribute","text":"REGEX_BGP_IPV4_UNICAST = (\n \"\\\\b(ipv4[\\\\s\\\\-]?uni[\\\\s\\\\-]?cast)\\\\b\"\n)\n Match IPv4 Unicast."},{"location":"api/types/#anta.custom_types.REGEX_TYPE_PORTCHANNEL","title":"REGEX_TYPE_PORTCHANNEL module-attribute","text":"REGEX_TYPE_PORTCHANNEL = '^Port-Channel[0-9]{1,6}$'\n Match Port Channel interface like Port-Channel5."},{"location":"api/types/#anta.custom_types.RegexString","title":"RegexString module-attribute","text":"RegexString = Annotated[str, AfterValidator(validate_regex)]\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[\n \"unicast\", \"multicast\", \"labeled-unicast\", \"sr-te\"\n]\n"},{"location":"api/types/#anta.custom_types.SnmpErrorCounter","title":"SnmpErrorCounter module-attribute","text":"SnmpErrorCounter = Literal[\n \"inVersionErrs\",\n \"inBadCommunityNames\",\n \"inBadCommunityUses\",\n \"inParseErrs\",\n \"outTooBigErrs\",\n \"outNoSuchNameErrs\",\n \"outBadValueErrs\",\n \"outGeneralErrs\",\n]\n"},{"location":"api/types/#anta.custom_types.SnmpPdu","title":"SnmpPdu module-attribute","text":"SnmpPdu = Literal[\n \"inGetPdus\",\n \"inGetNextPdus\",\n \"inSetPdus\",\n \"outGetResponsePdus\",\n \"outTrapPdus\",\n]\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[\n str,\n Field(pattern=REGEXP_TYPE_VXLAN_SRC_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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(\n value: str,\n) -> str\n Abbreviations for different BGP multiprotocol capabilities. Examples IPv4 Unicast L2vpnEVPN ipv4 MPLS Labels ipv4Mplsvpn 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 REGEXP_BGP_L2VPN_AFI: \"l2VpnEvpn\",\n REGEXP_BGP_IPV4_MPLS_LABELS: \"ipv4MplsLabels\",\n REGEX_BGP_IPV4_MPLS_VPN: \"ipv4MplsVpn\",\n REGEX_BGP_IPV4_UNICAST: \"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(REGEXP_INTERFACE_ID)\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 return next((f\"{full_name}{intf_id}\" for alias, full_name in alias_map.items() if v.lower().startswith(alias)), 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 vlan -> Vlan loopback -> Loopback 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 v != \"\" and not v[0].isupper():\n return f\"{v[0].upper()}{v[1:]}\"\n return v\n"},{"location":"api/types/#anta.custom_types.validate_regex","title":"validate_regex","text":"validate_regex(value: str) -> str\n Validate that the input value is a valid regex format. Source code in anta/custom_types.py def validate_regex(value: str) -> str:\n \"\"\"Validate that the input value is a valid regex format.\"\"\"\n try:\n re.compile(value)\n except re.error as e:\n msg = f\"Invalid regex: {e}\"\n raise ValueError(msg) from e\n return value\n"},{"location":"cli/check/","title":"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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n --help Show this message and exit.\n"},{"location":"cli/debug/","title":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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":"Usage: 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.\n\n 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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"},{"location":"cli/debug/#example-of-multiple-arguments","title":"Example of multiple arguments","text":"Warning If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters. 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":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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_2024-04-09_15_56_19]\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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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 [DEPRECATED] Ensure devices have 'aaa authorization\n exec default local' configured (required for SCP on\n EOS). THIS WILL CHANGE THE CONFIGURATION OF YOUR\n 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. Warning ANTA can automatically configure aaa authorization exec default local using the anta exec collect-tech-support --configure option but this option is deprecated and will be removed in ANTA 2.0.0. 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":"The ANTA CLI offers multiple commands to access data from your local inventory."},{"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 (visit this page to learn more about tags). The --connected option allows to display only the devices where a connection has been established."},{"location":"cli/get-inventory-information/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 By default, anta get inventory only provides information that doesn\u2019t rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, use the --connected option."},{"location":"cli/get-inventory-information/#example","title":"Example","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 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={'DC1-SPINE1', 'DC1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC1-SPINE2': AsyncEOSDevice(\n name='DC1-SPINE2',\n tags={'DC1', 'SPINE', 'DC1-SPINE2'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE1': AsyncEOSDevice(\n name='DC2-SPINE1',\n tags={'DC2', 'DC2-SPINE1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE2': AsyncEOSDevice(\n name='DC2-SPINE2',\n tags={'DC2', 'DC2-SPINE2', 'SPINE'},\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=False,\n insecure=False\n )\n}\n"},{"location":"cli/inv-from-ansible/","title":"Inventory from Ansible","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\n NOTE: This command does not support inline vaulted variables. Make sure to\n comment them out.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var:\n ANTA_INVENTORY; required]\n --overwrite Do not prompt when overriding current inventory\n [env var: ANTA_GET_FROM_ANSIBLE_OVERWRITE]\n -g, --ansible-group TEXT Ansible group to filter\n --ansible-inventory FILE Path to your ansible inventory file to read\n [required]\n --help Show this message and exit.\n Warnings anta get from-ansible does not support inline vaulted variables, comment them out to generate your inventory. If the vaulted variable is necessary to build the inventory (e.g. ansible_host), it needs to be unvaulted for from-ansible command to work.\u201d The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritance 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: ---\nall:\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 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"},{"location":"cli/inv-from-cvp/","title":"Inventory from CVP","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. Info The current implementation only works with on-premises CloudVision instances, not with CloudVision as a Service (CVaaS)."},{"location":"cli/inv-from-cvp/#command-overview","title":"Command overview","text":"Usage: anta get from-cvp [OPTIONS]\n\n Build ANTA inventory from CloudVision.\n\n NOTE: Only username/password authentication is supported for on-premises CloudVision instances.\n Token authentication for both on-premises and CloudVision as a Service (CVaaS) is not supported.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var: ANTA_INVENTORY;\n required]\n --overwrite Do not prompt when overriding current inventory [env\n var: ANTA_GET_FROM_CVP_OVERWRITE]\n -host, --host TEXT CloudVision instance FQDN or IP [required]\n -u, --username TEXT CloudVision username [required]\n -p, --password TEXT CloudVision password [required]\n -c, --container TEXT CloudVision container where devices are configured\n --ignore-cert By default connection to CV will use HTTPS\n certificate, set this flag to disable it [env var:\n ANTA_GET_FROM_CVP_IGNORE_CERT]\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":"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 report Table report JSON report Custom template report CSV report Markdown report "},{"location":"cli/nrfu/#nrfu-command-overview","title":"NRFU Command overview","text":"Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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. Refer to the dedicated page for more information."},{"location":"cli/nrfu/#device-and-test-filtering","title":"Device and test filtering","text":"Options --device and --test can be used to target one or multiple devices and/or tests to run in your environment. The options can be repeated. Example: anta nrfu --device leaf1a --device leaf1b --test VerifyUptime --test VerifyReloadCause."},{"location":"cli/nrfu/#hide-results","title":"Hide results","text":"Option --hide can be used to hide test results in the output or report file based on their status. The option can be repeated. Example: anta nrfu --hide error --hide skipped."},{"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":"Usage: anta nrfu text [OPTIONS]\n\n ANTA command to check network states with text result.\n\nOptions:\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example","title":"Example","text":"anta nrfu --device DC1-LEAF1A text\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":"Usage: anta nrfu table [OPTIONS]\n\n ANTA command to check network states with table result.\n\nOptions:\n --group-by [device|test] Group result by test or device.\n --help Show this message and exit.\n 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 --device spine1 table\n anta nrfu --test VerifyZeroTouch table\n "},{"location":"cli/nrfu/#performing-nrfu-with-json-rendering","title":"Performing NRFU with JSON rendering","text":"The JSON rendering command in NRFU testing will generate an output of all test results in JSON format."},{"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 JSON 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. If specified, no output will be displayed in the terminal. This is useful for further processing or integration with other tools."},{"location":"cli/nrfu/#example_1","title":"Example","text":"anta nrfu --tags LEAF json\n"},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-csv-file","title":"Performing NRFU and saving results in a CSV file","text":"The csv command in NRFU testing is useful for generating a CSV file with all tests result. This file can be easily analyzed and filtered by operator for reporting purposes."},{"location":"cli/nrfu/#command-overview_3","title":"Command overview","text":"anta nrfu csv --help\nUsage: anta nrfu csv [OPTIONS]\n\n ANTA command to check network states with CSV result.\n\nOptions:\n --csv-output FILE Path to save report as a CSV file [env var:\n ANTA_NRFU_CSV_CSV_OUTPUT]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_2","title":"Example","text":""},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-markdown-file","title":"Performing NRFU and saving results in a Markdown file","text":"The md-report command in NRFU testing generates a comprehensive Markdown report containing various sections, including detailed statistics for devices and test categories."},{"location":"cli/nrfu/#command-overview_4","title":"Command overview","text":"anta nrfu md-report --help\n\nUsage: anta nrfu md-report [OPTIONS]\n\n ANTA command to check network state with Markdown report.\n\nOptions:\n --md-output FILE Path to save the report as a Markdown file [env var:\n ANTA_NRFU_MD_REPORT_MD_OUTPUT; required]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_3","title":"Example","text":""},{"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_5","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_4","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/nrfu/#dry-run-mode","title":"Dry-run mode","text":"It is possible to run anta nrfu --dry-run to execute ANTA up to the point where it should communicate with the network to execute the tests. When using --dry-run, all inventory devices are assumed to be online. This can be useful to check how many tests would be run using the catalog and inventory. "},{"location":"cli/overview/","title":"Overview","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."},{"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 selected inventory devices.\n"},{"location":"cli/overview/#anta-environment-variables","title":"ANTA environment 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_CATALOG=tests.yml\n Then, run the CLI without options: anta nrfu\n Note All environment variables may not be needed for every commands. Refer to <command> --help for the comprehensive environment variables names. Below are the environment 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":"ANTA uses tags to define test-to-device mappings (tests run on devices with matching tags) and the --tags CLI option acts as a filter to execute specific test/device combinations."},{"location":"cli/tag-management/#defining-tags","title":"Defining tags","text":""},{"location":"cli/tag-management/#device-tags","title":"Device tags","text":"Device tags can be defined in the inventory: anta_inventory:\n hosts:\n - name: leaf1\n host: leaf1.anta.arista.com\n tags: [\"leaf\"]\n - name: leaf2\n host: leaf2.anta.arista.com\n tags: [\"leaf\"]\n - name: spine1\n host: spine1.anta.arista.com\n tags: [\"spine\"]\n Each device also has its own name automatically added as a tag: $ anta get inventory\nCurrent inventory content is:\n{\n 'leaf1': AsyncEOSDevice(\n name='leaf1',\n tags={'leaf', 'leaf1'}, <--\n [...]\n host='leaf1.anta.arista.com',\n [...]\n ),\n 'leaf2': AsyncEOSDevice(\n name='leaf2',\n tags={'leaf', 'leaf2'}, <--\n [...]\n host='leaf2.anta.arista.com',\n [...]\n ),\n 'spine1': AsyncEOSDevice(\n name='spine1',\n tags={'spine1', 'spine'}, <--\n [...]\n host='spine1.anta.arista.com',\n [...]\n )\n}\n"},{"location":"cli/tag-management/#test-tags","title":"Test tags","text":"Tags can be defined in the test catalog to restrict tests to tagged devices: anta.tests.system:\n - VerifyUptime:\n minimum: 10\n filters:\n tags: ['spine']\n - VerifyUptime:\n minimum: 9\n filters:\n tags: ['leaf']\n - VerifyReloadCause:\n filters:\n tags: ['spine', 'leaf']\n - VerifyCoredump:\n - VerifyAgentLogs:\n - VerifyCPUUtilization:\n - VerifyMemoryUtilization:\n - VerifyFileSystemUtilization:\n - VerifyNTP:\n\nanta.tests.mlag:\n - VerifyMlagStatus:\n filters:\n tags: ['leaf']\n\nanta.tests.interfaces:\n - VerifyL3MTU:\n mtu: 1500\n filters:\n tags: ['spine']\n A tag used to filter a test can also be a device name Use different input values for a specific test Leverage tags to define different input values for a specific test. See the VerifyUptime example above."},{"location":"cli/tag-management/#using-tags","title":"Using tags","text":"Command Description No --tags option Run all tests on all devices according to the tag definitions in your inventory and test catalog. Tests without tags are executed on all devices. --tags leaf Run all tests marked with the leaf tag on all devices configured with the leaf tag. All other tests are ignored. --tags leaf,spine Run all tests marked with the leaf tag on all devices configured with the leaf tag.Run all tests marked with the spine tag on all devices configured with the spine tag. All other tests are ignored."},{"location":"cli/tag-management/#examples","title":"Examples","text":"The following examples use the inventory and test catalog defined above."},{"location":"cli/tag-management/#no-tags-option","title":"No --tags option","text":"Tests without tags are run on all devices. Tests with tags will only run on devices with matching tags. $ anta nrfu table --group-by device\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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 27\n---------------------------------\n Summary per device\n\u250f\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\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 leaf1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 leaf2 \u2502 7 \u2502 1 \u2502 1 \u2502 0 \u2502 VerifyAgentLogs \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 spine1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u2514\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/#single-tag","title":"Single tag","text":"With a tag specified, only tests matching this tag will be run on matching devices. $ anta nrfu --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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (2 established)\nTotal number of selected tests: 6\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\n In this case, only leaf devices defined in the inventory are used to run tests marked with the leaf in the test catalog."},{"location":"cli/tag-management/#multiple-tags","title":"Multiple tags","text":"It is possible to use multiple tags using the --tags tag1,tag2 syntax. $ anta nrfu --tags leaf,spine 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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 15\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyL3MTU :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyL3MTU :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyReloadCause :: SUCCESS\nspine1 :: VerifyMlagStatus :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyL3MTU :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\n"},{"location":"cli/tag-management/#obtaining-all-configured-tags","title":"Obtaining all configured tags","text":"As most ANTA commands accommodate tag filtering, this command is useful for enumerating all tags configured in the inventory. Running the anta get tags command will return a list of all tags configured in the inventory."},{"location":"cli/tag-management/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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/tag-management/#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 \"leaf\",\n \"leaf1\",\n \"leaf2\",\n \"spine\",\n \"spine1\"\n]\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: As a Python library in your own application The ANTA CLI "},{"location":"#install-anta-library","title":"Install ANTA library","text":"The library will NOT install the necessary dependencies for the CLI. # Install ANTA as a library\npip install anta\n"},{"location":"#install-anta-cli","title":"Install ANTA CLI","text":"If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx is a tool to install and run python applications in isolated environments. Refer to pipx instructions to install on your system. pipx installs ANTA in an isolated python environment and makes it available globally. This is not recommended if you plan to contribute to ANTA # Install ANTA CLI with pipx\n$ pipx install anta[cli]\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 You can also still choose to install it with directly with pip: pip install anta[cli]\n"},{"location":"#documentation","title":"Documentation","text":"The documentation is published on ANTA package website."},{"location":"#contribution-guide","title":"Contribution guide","text":"Contributions are welcome. Please refer to the contribution guide"},{"location":"#credits","title":"Credits","text":"Thank you to Jeremy Schulman for aio-eapi. 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":"Contribution model is based on a fork-model. Don\u2019t push to aristanetworks/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-environment","title":"Create a development environment","text":"Run the following commands to create an ANTA development environment: # Clone repository\n$ git clone https://github.com/aristanetworks/anta.git\n$ cd anta\n\n# Install ANTA in editable mode and its development tools\n$ pip install -e .[dev]\n# To also install the CLI\n$ pip install -e .[dev,cli]\n\n# Verify installation\n$ pip list -e\nPackage Version Editable project location\n------- ------- -------------------------\nanta 1.1.0 /mnt/lab/projects/anta\n Then, tox is configured 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\npy39 -> Run pytest with py39\npy310 -> Run pytest with py310\npy311 -> Run pytest with py311\npy312 -> Run pytest with py312\nreport -> Generate coverage report\n"},{"location":"contribution/#code-linting","title":"Code linting","text":"tox -e lint\n[...]\nlint: commands[0]> ruff check .\nAll checks passed!\nlint: commands[1]> ruff format . --check\n158 files already formatted\nlint: commands[2]> pylint anta\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\nlint: commands[3]> pylint tests\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\n lint: OK (22.69=setup[2.19]+cmd[0.02,0.02,9.71,10.75] seconds)\n congratulations :) (22.72 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 68 source files\ntype: commands[1]> mypy --config-file=pyproject.toml tests\nSuccess: no issues found in 82 source files\n type: OK (31.15=setup[14.62]+cmd[6.05,10.48] seconds)\n congratulations :) (31.18 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.units.anta_tests 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.units.anta_tests import test\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 pre-commit\ntrim trailing whitespace.................................................Passed\nfix end of files.........................................................Passed\ncheck for added large files..............................................Passed\ncheck for merge conflicts................................................Passed\nCheck and insert license on Python files.................................Passed\nCheck and insert license on Markdown files...............................Passed\nRun Ruff linter..........................................................Passed\nRun Ruff formatter.......................................................Passed\nCheck code style with pylint.............................................Passed\nChecks for common misspellings in text files.............................Passed\nCheck typing with mypy...................................................Passed\nCheck Markdown files style...............................................Passed\n"},{"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. The results can be viewed here."},{"location":"faq/","title":"FAQ","text":""},{"location":"faq/#a-local-os-error-occurred-while-connecting-to-a-device","title":"A local OS error occurred while connecting to a device","text":"A local OS error occurred while connecting to a device When running ANTA, you can receive A local OS error occurred while connecting to <device> errors. The underlying OSError exception can have various reasons: [Errno 24] Too many open files or [Errno 16] Device or resource busy. This usually means that the operating system refused to open a new file descriptor (or socket) for the ANTA process. This might be due to the hard limit for open file descriptors currently set for the ANTA process. At startup, ANTA sets the soft limit of its process to the hard limit up to 16384. This is because the soft limit is usually 1024 and the hard limit is usually higher (depends on the system). If the hard limit of the ANTA process is still lower than the number of selected tests in ANTA, the ANTA process may request to the operating system too many file descriptors and get an error, a WARNING is displayed at startup if this is the case."},{"location":"faq/#solution","title":"Solution","text":"One solution could be to raise the hard limit for the user starting the ANTA process. You can get the current hard limit for a user using the command ulimit -n -H while logged in. Create the file /etc/security/limits.d/10-anta.conf with the following content: <user> hard nofile <value>\n The user is the one with which the ANTA process is started. The value is the new hard limit. The maximum value depends on the system. A hard limit of 16384 should be sufficient for ANTA to run in most high scale scenarios. After creating this file, log out the current session and log in again."},{"location":"faq/#timeout-error-in-the-logs","title":"Timeout error in the logs","text":"Timeout error in the logs When running ANTA, you can receive <Foo>Timeout errors in the logs (could be ReadTimeout, WriteTimeout, ConnectTimeout or PoolTimeout). More details on the timeouts of the underlying library are available here: https://www.python-httpx.org/advanced/timeouts. This might be due to the time the host on which ANTA is run takes to reach the target devices (for instance if going through firewalls, NATs, \u2026) or when a lot of tests are being run at the same time on a device (eAPI has a queue mechanism to avoid exhausting EOS resources because of a high number of simultaneous eAPI requests)."},{"location":"faq/#solution_1","title":"Solution","text":"Use the timeout option. As an example for the nrfu command: anta nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml --timeout 50 text\n The previous command set a couple of options for ANTA NRFU, one them being the timeout command, by default, when running ANTA from CLI, it is set to 30s. The timeout is increased to 50s to allow ANTA to wait for API calls a little longer."},{"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_2","title":"Solution","text":" 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 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_3","title":"Solution","text":" 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_4","title":"Solution","text":" 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 follow the troubleshooting instructions and report the issue in our GitHub repository."},{"location":"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 install ANTA package is to run Python (>=3.9) and its pip package to install: pip install anta[cli]\n For more details about how to install package, please see the requirements and installation 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: s1-spine1\n tags: ['fabric', 'spine']\n - host: 192.168.0.11\n name: s1-spine2\n tags: ['fabric', 'spine']\n - host: 192.168.0.12\n name: s1-leaf1\n tags: ['fabric', 'leaf']\n - host: 192.168.0.13\n name: s1-leaf2\n tags: ['fabric', 'leaf']\n - host: 192.168.0.14\n name: s1-leaf3\n tags: ['fabric', 'leaf']\n - host: 192.168.0.15\n name: s1-leaf3\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: ---\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\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":""},{"location":"getting-started/#cli","title":"CLI","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. Usage: 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 selected inventory devices.\n Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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 Note The following examples shows how to pass all the CLI options. See how to use environment variables instead in the CLI overview"},{"location":"getting-started/#default-report-using-table","title":"Default report using table","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n `# table is default if not provided` \\\n table\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:01] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.058. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.069. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:02] INFO Running ANTA tests completed in: 0:00:00.969. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 s1-spine1 \u2502 VerifyMlagConfigSanity \u2502 skipped \u2502 MLAG is disabled \u2502 Verifies there are no MLAG config-sanity \u2502 MLAG \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502 inconsistencies. \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-spine1 \u2502 VerifyEOSVersion \u2502 failure \u2502 device is running version \u2502 Verifies the EOS version of the device. \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 \"4.32.2F-38195967.4322F (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)\" not in expected versions: \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['4.25.4M', '4.26.1F', \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 '4.28.3M-28837868.4283M (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n[...]\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyTerminAttrVersion \u2502 failure \u2502 device is running TerminAttr version \u2502 Verifies the TerminAttr version of the \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 v1.34.0 and is not in the allowed list: \u2502 device. \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['v1.22.1'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyZeroTouch \u2502 success \u2502 \u2502 Verifies ZeroTouch is disabled \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\u2500\u2500\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\u2500\u2500\u2500\u2500\u2500\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 arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n 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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:52:39] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.057. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.068. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:52:40] INFO Running ANTA tests completed in: 0:00:00.863. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\n\ns1-spine1 :: VerifyEOSVersion :: FAILURE(device is running version \"4.32.2F-38195967.4322F (engineering build)\" not in expected versions: ['4.25.4M', '4.26.1F',\n'4.28.3M-28837868.4283M (engineering build)'])\ns1-spine1 :: VerifyTerminAttrVersion :: FAILURE(device is running TerminAttr version v1.34.0 and is not in the allowed list: ['v1.22.1'])\ns1-spine1 :: VerifyZeroTouch :: SUCCESS()\ns1-spine1 :: VerifyMlagConfigSanity :: SKIPPED(MLAG is disabled)\n"},{"location":"getting-started/#report-in-json-format","title":"Report in JSON format","text":"anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable `\\\n `# --enable-password <password> `\\\n --catalog ./catalog.yml \\\n json\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:11] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.053. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.065. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:12] INFO Running ANTA tests completed in: 0:00:00.857. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \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\": \"s1-spine1\",\n \"test\": \"VerifyNTP\",\n \"categories\": [\n \"system\"\n ],\n \"description\": \"Verifies if NTP is synchronised.\",\n \"result\": \"success\",\n \"messages\": [],\n \"custom_field\": null\n },\n {\n \"name\": \"s1-spine1\",\n \"test\": \"VerifyMlagConfigSanity\",\n \"categories\": [\n \"mlag\"\n ],\n \"description\": \"Verifies there are no MLAG config-sanity inconsistencies.\",\n \"result\": \"skipped\",\n \"messages\": [\n \"MLAG is disabled\"\n ],\n \"custom_field\": null\n },\n [...]\n"},{"location":"getting-started/#basic-usage-in-a-python-script","title":"Basic usage in a Python script","text":"# 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\"\"\"Example script for ANTA.\n\nusage:\n\npython anta_runner.py\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport sys\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.cli.nrfu.utils import anta_progress_bar\nfrom anta.inventory import AntaInventory\nfrom anta.logger import Log, setup_logging\nfrom anta.models import AntaTest\nfrom anta.result_manager import ResultManager\nfrom anta.runner import main as anta_runner\n\n# setup logging\nsetup_logging(Log.INFO, Path(\"/tmp/anta.log\"))\nLOGGER = logging.getLogger()\nSCRIPT_LOG_PREFIX = \"[bold magenta][ANTA RUNNER SCRIPT][/] \" # For convenience purpose - there are nicer way to do this.\n\n\n# NOTE: The inventory and catalog files are not delivered with this script\nUSERNAME = \"admin\"\nPASSWORD = \"admin\"\nCATALOG_PATH = Path(\"/tmp/anta_catalog.yml\")\nINVENTORY_PATH = Path(\"/tmp/anta_inventory.yml\")\n\n# Load catalog file\ntry:\n catalog = AntaCatalog.parse(CATALOG_PATH)\nexcept Exception:\n LOGGER.exception(\"%s Catalog failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Catalog loaded!\", SCRIPT_LOG_PREFIX)\n\n# Load inventory\ntry:\n inventory = AntaInventory.parse(INVENTORY_PATH, username=USERNAME, password=PASSWORD)\nexcept Exception:\n LOGGER.exception(\"%s Inventory failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Inventory loaded!\", SCRIPT_LOG_PREFIX)\n\n# Create result manager object\nmanager = ResultManager()\n\n# Launch ANTA\nLOGGER.info(\"%s Starting ANTA runner...\", SCRIPT_LOG_PREFIX)\nwith anta_progress_bar() as AntaTest.progress:\n # Set dry_run to True to avoid connecting to the devices\n asyncio.run(anta_runner(manager, inventory, catalog, dry_run=False))\n\nLOGGER.info(\"%s ANTA run completed!\", SCRIPT_LOG_PREFIX)\n\n# Manipulate the test result object\nfor test_result in manager.results:\n LOGGER.info(\"%s %s:%s:%s\", SCRIPT_LOG_PREFIX, test_result.name, test_result.test, test_result.result)\n"},{"location":"requirements-and-installation/","title":"Installation","text":""},{"location":"requirements-and-installation/#python-version","title":"Python version","text":"Python 3 (>=3.9) is required: python --version\nPython 3.11.8\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-library-from-pypi-server","title":"Install library from Pypi server","text":"pip install anta\n Warning This command alone will not install the ANTA CLI requirements. "},{"location":"requirements-and-installation/#install-anta-cli-as-an-application-with-pipx","title":"Install ANTA CLI as an application with pipx","text":"pipx is a tool to install and run python applications in isolated environments. If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx installs ANTA in an isolated python environment and makes it available globally. pipx install anta[cli]\n Info Please take the time to read through the installation instructions of pipx before getting started."},{"location":"requirements-and-installation/#install-cli-from-pypi-server","title":"Install CLI from Pypi server","text":"Alternatively, pip install with cli extra is enough to install the ANTA CLI. pip install anta[cli]\n"},{"location":"requirements-and-installation/#install-anta-from-github","title":"Install ANTA from github","text":"pip install git+https://github.com/aristanetworks/anta.git\npip install git+https://github.com/aristanetworks/anta.git#egg=anta[cli]\n\n# You can even specify the branch, tag or commit:\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>#egg=anta[cli]\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 v1.1.0\n"},{"location":"requirements-and-installation/#eos-requirements","title":"EOS Requirements","text":"To get ANTA working, the targeted Arista EOS devices must have eAPI enabled. They need to use 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":"troubleshooting/","title":"Troubleshooting ANTA","text":"A couple of things to check when hitting an issue with ANTA: flowchart LR\n A>Hitting an issue with ANTA] --> B{Is my issue <br >listed in the FAQ?}\n B -- Yes --> C{Does the FAQ solution<br />works for me?}\n C -- Yes --> V(((Victory)))\n B -->|No| E{Is my problem<br />mentioned in one<br />of the open issues?}\n C -->|No| E\n E -- Yes --> F{Has the issue been<br />fixed in a newer<br />release or in main?}\n F -- Yes --> U[Upgrade]\n E -- No ---> H((Follow the steps below<br />and open a Github issue))\n U --> I{Did it fix<br /> your problem}\n I -- Yes --> V\n I -- No --> H\n F -- No ----> G((Add a comment on the <br />issue indicating you<br >are hitting this and<br />describing your setup<br /> and adding your logs.))\n\n click B \"../faq\" \"FAQ\"\n click E \"https://github.com/aristanetworks/anta/issues\"\n click H \"https://github.com/aristanetworks/anta/issues\"\n style A stroke:#f00,stroke-width:2px"},{"location":"troubleshooting/#capturing-logs","title":"Capturing logs","text":"To help document the issue in Github, it is important to capture some logs so the developers can understand what is affecting your system. No logs mean that the first question asked on the issue will probably be \u201cCan you share some logs please?\u201d. ANTA provides very verbose logs when using the DEBUG level. When using DEBUG log level with a log file, the DEBUG logging level is not sent to stdout, but only to the file. Danger On real deployments, do not use DEBUG logging level without setting a log file at the same time. To save the logs to a file called anta.log, use the following flags: # Where ANTA_COMMAND is one of nrfu, debug, get, exec, check\nanta -l DEBUG \u2013log-file anta.log <ANTA_COMMAND>\n See anta --help for more information. These have to precede the nrfu cmd. Tip Remember that in ANTA, each level of command has its own options and they can only be set at this level. so the -l and --log-file MUST be between anta and the ANTA_COMMAND. similarly, all the nrfu options MUST be set between the nrfu and the ANTA_NRFU_SUBCOMMAND (json, text, table or tpl-report). As an example, for the nrfu command, it would look like: anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#anta_debug-environment-variable","title":"ANTA_DEBUG environment variable","text":"Warning Do not use this if you do not know why. This produces a lot of logs and can create confusion if you do not know what to look for. The environment variable ANTA_DEBUG=true enable ANTA Debug Mode. This flag is used by various functions in ANTA: when set to true, the function will display or log more information. In particular, when an Exception occurs in the code and this variable is set, the logging function used by ANTA is different to also produce the Python traceback for debugging. This typically needs to be done when opening a GitHub issue and an Exception is seen at runtime. Example: ANTA_DEBUG=true anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n"},{"location":"troubleshooting/#troubleshooting-on-eos","title":"Troubleshooting on EOS","text":"ANTA is using a specific ID in eAPI requests towards EOS. This allows for easier eAPI requests debugging on the device using EOS configuration trace CapiApp setting UwsgiRequestContext/4,CapiUwsgiServer/4 to set up CapiApp agent logs. Then, you can view agent logs using: bash tail -f /var/log/agents/CapiApp-*\n\n2024-05-15 15:32:54.056166 1429 UwsgiRequestContext 4 request content b'{\"jsonrpc\": \"2.0\", \"method\": \"runCmds\", \"params\": {\"version\": \"latest\", \"cmds\": [{\"cmd\": \"show ip route vrf default 10.255.0.3\", \"revision\": 4}], \"format\": \"json\", \"autoComplete\": false, \"expandAliases\": false}, \"id\": \"ANTA-VerifyRoutingTableEntry-132366530677328\"}'\n"},{"location":"usage-inventory-catalog/","title":"Inventory and Test catalog","text":"The ANTA framework needs 2 important inputs from the user to run: A device inventory 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 accessible via eAPI ranges: scan a range for devices accessible 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 in either YAML or JSON: ---\n<Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\n {\n \"<Python module>\": [\n {\n \"<AntaTest subclass>\": <AntaTest.Input compliant dictionary>\n }\n ]\n}\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 or equivalent in JSON: {\n \"anta.tests.connectivity\": [\n {\n \"VerifyReachability\": {\n \"result_overwrite\": {\n \"description\": \"Test with overwritten description\",\n \"categories\": [\n \"Overwritten category 1\"\n ],\n \"custom_field\": \"Test run by John Doe\"\n },\n \"filters\": {\n \"tags\": [\n \"leaf\"\n ]\n },\n \"hosts\": [\n {\n \"destination\": \"1.1.1.1\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n },\n {\n \"destination\": \"8.8.8.8\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n }\n ]\n }\n }\n ]\n}\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 or JSON file: anta.tests.software:\n - VerifyEOSVersion:\n # List of allowed EOS versions.\n versions:\n - 4.25.4M\n - 4.26.1F\n {\n \"anta.tests.software\": [\n {\n \"VerifyEOSVersion\": {\n \"versions\": [\n \"4.25.4M\",\n \"4.31.1F\"\n ]\n }\n }\n ]\n}\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 custom.tests.system Python module, the test catalog will be: custom.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":"usage-inventory-catalog/#example-script-to-merge-catalogs","title":"Example script to merge catalogs","text":"The following script reads all the files in intended/test_catalogs/ with names <device_name>-catalog.yml and merge them together inside one big catalog anta-catalog.yml using the new AntaCatalog.merge_catalogs() class method. # Copyright (c) 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\"\"\"Script that merge a collection of catalogs into one AntaCatalog.\"\"\"\n\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.models import AntaTest\n\nCATALOG_SUFFIX = \"-catalog.yml\"\nCATALOG_DIR = \"intended/test_catalogs/\"\n\nif __name__ == \"__main__\":\n catalogs = []\n for file in Path(CATALOG_DIR).glob(\"*\" + CATALOG_SUFFIX):\n device = str(file).removesuffix(CATALOG_SUFFIX).removeprefix(CATALOG_DIR)\n print(f\"Loading test catalog for device {device}\")\n catalog = AntaCatalog.parse(file)\n # Add the device name as a tag to all tests in the catalog\n for test in catalog.tests:\n test.inputs.filters = AntaTest.Input.Filters(tags={device})\n catalogs.append(catalog)\n\n # Merge all catalogs\n merged_catalog = AntaCatalog.merge_catalogs(catalogs)\n\n # Save the merged catalog to a file\n with Path(\"anta-catalog.yml\").open(\"w\") as f:\n f.write(merged_catalog.dump().yaml())\n Warning The AntaCatalog.merge() method is deprecated and will be removed in ANTA v2.0. Please use the AntaCatalog.merge_catalogs() class method instead."},{"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. "},{"location":"advanced_usages/as-python-lib/#examples","title":"Examples","text":""},{"location":"advanced_usages/as-python-lib/#parse-an-anta-inventory-file","title":"Parse an ANTA inventory file","text":"# Copyright (c) 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\"\"\"Script that parses an ANTA inventory file, connects to devices and print their status.\"\"\"\n\nimport asyncio\n\nfrom anta.inventory import AntaInventory\n\n\nasync def main(inv: AntaInventory) -> None:\n \"\"\"Read an AntaInventory and try to connect to every device in the inventory.\n\n 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\n\nif __name__ == \"__main__\":\n # Create the AntaInventory instance\n inventory = AntaInventory.parse(\n filename=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\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."},{"location":"advanced_usages/as-python-lib/#run-eos-commands","title":"Run EOS commands","text":"# Copyright (c) 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\"\"\"Script that runs a list of EOS commands on reachable devices.\"\"\"\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 \"\"\"Run a list of commands against each valid device in the inventory.\n\n Take an AntaInventory and a list of commands as string\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 -------\n dict[str, list[AntaCommand]]\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=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\n )\n\n # Create a list of commands with json output\n command_list = [\"show version\", \"show ip bgp summary\"]\n\n # Run the main asyncio entry point\n res = asyncio.run(main(inventory, command_list))\n\n pprint(res)\n"},{"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: 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 Caching can be disabled per device, network or range by setting the disable_cache key to True when defining 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. For tests developers, 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: set = None) -> None:\n super().__init__(name, tags, disable_cache=True)\n"},{"location":"advanced_usages/custom-tests/","title":"Developing ANTA tests","text":"Info 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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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":"Full AntaTest API documentation is available in the API documentation section"},{"location":"advanced_usages/custom-tests/#class-attributes","title":"Class Attributes","text":" name (str, optional): Name of the test. Used during reporting. By default set to the Class name. description (str, optional): A human readable description of your test. By default set to the first line of the docstring. 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":"Attributes: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. 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":"Full Input model documentation is available in API documentation section Attributes: Name Type Description result_overwrite ResultOverwrite | None Define fields to overwrite in the TestResult object."},{"location":"advanced_usages/custom-tests/#resultoverwrite-model","title":"ResultOverwrite model","text":"Full ResultOverwrite model documentation is available in API documentation section Attributes: Name Type Description description str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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 occurrence 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: 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. 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. 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. 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. 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, the first line is used as description of the test by default>\n \"\"\"\n\n # name = <override> # uncomment to override default behavior of name=Class Name\n # description = <override> # uncomment to override default behavior of description=first line of docstring\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 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 values 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, integrations with existing tools are not broken. This is done by using by default version=1 in eAPI calls. By default, ANTA uses version=\"latest\" in AntaCommand, but when developing tests, the revision MUST be provided when the outformat of the command is json. As explained earlier, this is to ensure that the eAPI always returns the same output model and that the test remains always valid from the day it was created. For some commands, you may also want to run them with a different revision or version. For instance, the VerifyBFDPeersHealth 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/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: Parse the command outputs using the self.instance_commands instance attribute. If needed, access the test inputs using the self.inputs instance attribute and write your conditional logic. 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_custom and the test is defined in anta_custom.dc_project Python module, the test catalog would look like: anta_custom.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(\n tests: list[AntaTestDefinition] | None = None,\n filename: str | Path | None = None,\n)\n Class representing an ANTA Catalog. It can be instantiated using its constructor or one of the static methods: parse(), from_list() or from_dict() Parameters: Name Type Description Default tests list[AntaTestDefinition] | None A list of AntaTestDefinition instances. None filename str | Path | None The path from which the catalog is loaded. None"},{"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.build_indexes","title":"build_indexes","text":"build_indexes(\n filtered_tests: set[str] | None = None,\n) -> None\n Indexes tests by their tags for quick access during filtering operations. If a filtered_tests set is provided, only the tests in this set will be indexed. This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests. Once the indexes are built, the indexes_built attribute is set to True. Source code in anta/catalog.py def build_indexes(self, filtered_tests: set[str] | None = None) -> None:\n \"\"\"Indexes tests by their tags for quick access during filtering operations.\n\n If a `filtered_tests` set is provided, only the tests in this set will be indexed.\n\n This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests.\n\n Once the indexes are built, the `indexes_built` attribute is set to True.\n \"\"\"\n for test in self.tests:\n # Skip tests that are not in the specified filtered_tests set\n if filtered_tests and test.test.name not in filtered_tests:\n continue\n\n # Indexing by tag\n if test.inputs.filters and (test_tags := test.inputs.filters.tags):\n for tag in test_tags:\n self.tag_to_tests[tag].add(test)\n else:\n self.tag_to_tests[None].add(test)\n\n self.indexes_built = True\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.clear_indexes","title":"clear_indexes","text":"clear_indexes() -> None\n Clear this AntaCatalog instance indexes. Source code in anta/catalog.py def clear_indexes(self) -> None:\n \"\"\"Clear this AntaCatalog instance indexes.\"\"\"\n self._init_indexes()\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.dump","title":"dump","text":"dump() -> AntaCatalogFile\n Return an AntaCatalogFile instance from this AntaCatalog instance. Returns: Type Description AntaCatalogFile An AntaCatalogFile instance containing tests of this AntaCatalog instance. Source code in anta/catalog.py def dump(self) -> AntaCatalogFile:\n \"\"\"Return an AntaCatalogFile instance from this AntaCatalog instance.\n\n Returns\n -------\n AntaCatalogFile\n An AntaCatalogFile instance containing tests of this AntaCatalog instance.\n \"\"\"\n root: dict[ImportString[Any], list[AntaTestDefinition]] = {}\n for test in self.tests:\n # Cannot use AntaTest.module property as the class is not instantiated\n root.setdefault(test.test.__module__, []).append(test)\n return AntaCatalogFile(root=root)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.from_dict","title":"from_dict staticmethod","text":"from_dict(\n data: RawCatalogInput,\n filename: str | Path | None = None,\n) -> 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. Parameters: Name Type Description Default data RawCatalogInput Python dictionary used to instantiate the AntaCatalog instance. required filename str | Path | None value to be set as AntaCatalog instance attribute None Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 dictionary content. 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 Parameters\n ----------\n data\n Python dictionary used to instantiate the AntaCatalog instance.\n filename\n value to be set as AntaCatalog instance attribute\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' dictionary content.\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. Parameters: Name Type Description Default data ListAntaTestTuples Python list used to instantiate the AntaCatalog instance. required Returns: Type Description AntaCatalog An AntaCatalog populated with the \u2018data\u2019 list content. 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 Parameters\n ----------\n data\n Python list used to instantiate the AntaCatalog instance.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' list content.\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(\n tags: set[str], *, strict: bool = False\n) -> set[AntaTestDefinition]\n Return all tests that match a given set of tags, according to the specified strictness. Parameters: Name Type Description Default tags set[str] The tags to filter tests by. If empty, return all tests without tags. required strict bool If True, returns only tests that contain all specified tags (intersection). If False, returns tests that contain any of the specified tags (union). False Returns: Type Description set[AntaTestDefinition] A set of tests that match the given tags. Raises: Type Description ValueError If the indexes have not been built prior to method call. Source code in anta/catalog.py def get_tests_by_tags(self, tags: set[str], *, strict: bool = False) -> set[AntaTestDefinition]:\n \"\"\"Return all tests that match a given set of tags, according to the specified strictness.\n\n Parameters\n ----------\n tags\n The tags to filter tests by. If empty, return all tests without tags.\n strict\n If True, returns only tests that contain all specified tags (intersection).\n If False, returns tests that contain any of the specified tags (union).\n\n Returns\n -------\n set[AntaTestDefinition]\n A set of tests that match the given tags.\n\n Raises\n ------\n ValueError\n If the indexes have not been built prior to method call.\n \"\"\"\n if not self.indexes_built:\n msg = \"Indexes have not been built yet. Call build_indexes() first.\"\n raise ValueError(msg)\n if not tags:\n return self.tag_to_tests[None]\n\n filtered_sets = [self.tag_to_tests[tag] for tag in tags if tag in self.tag_to_tests]\n if not filtered_sets:\n return set()\n\n if strict:\n return set.intersection(*filtered_sets)\n return set.union(*filtered_sets)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge","title":"merge","text":"merge(catalog: AntaCatalog) -> AntaCatalog\n Merge two AntaCatalog instances. Warning This method is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead. Parameters: Name Type Description Default catalog AntaCatalog AntaCatalog instance to merge to this instance. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of the two instances. Source code in anta/catalog.py def merge(self, catalog: AntaCatalog) -> AntaCatalog:\n \"\"\"Merge two AntaCatalog instances.\n\n Warning\n -------\n This method is deprecated and will be removed in ANTA v2.0. Use `AntaCatalog.merge_catalogs()` instead.\n\n Parameters\n ----------\n catalog\n AntaCatalog instance to merge to this instance.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of the two instances.\n \"\"\"\n # TODO: Use a decorator to deprecate this method instead. See https://github.com/aristanetworks/anta/issues/754\n warn(\n message=\"AntaCatalog.merge() is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n return self.merge_catalogs([self, catalog])\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.merge_catalogs","title":"merge_catalogs classmethod","text":"merge_catalogs(catalogs: list[AntaCatalog]) -> AntaCatalog\n Merge multiple AntaCatalog instances. Parameters: Name Type Description Default catalogs list[AntaCatalog] A list of AntaCatalog instances to merge. required Returns: Type Description AntaCatalog A new AntaCatalog instance containing the tests of all the input catalogs. Source code in anta/catalog.py @classmethod\ndef merge_catalogs(cls, catalogs: list[AntaCatalog]) -> AntaCatalog:\n \"\"\"Merge multiple AntaCatalog instances.\n\n Parameters\n ----------\n catalogs\n A list of AntaCatalog instances to merge.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of all the input catalogs.\n \"\"\"\n combined_tests = list(chain(*(catalog.tests for catalog in catalogs)))\n return cls(tests=combined_tests)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalog.parse","title":"parse staticmethod","text":"parse(\n filename: str | Path,\n file_format: Literal[\"yaml\", \"json\"] = \"yaml\",\n) -> AntaCatalog\n Create an AntaCatalog instance from a test catalog file. Parameters: Name Type Description Default filename str | Path Path to test catalog YAML or JSON file. required file_format Literal['yaml', 'json'] Format of the file, either \u2018yaml\u2019 or \u2018json\u2019. 'yaml' Returns: Type Description AntaCatalog An AntaCatalog populated with the file content. Source code in anta/catalog.py @staticmethod\ndef parse(filename: str | Path, file_format: Literal[\"yaml\", \"json\"] = \"yaml\") -> AntaCatalog:\n \"\"\"Create an AntaCatalog instance from a test catalog file.\n\n Parameters\n ----------\n filename\n Path to test catalog YAML or JSON file.\n file_format\n Format of the file, either 'yaml' or 'json'.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the file content.\n \"\"\"\n if file_format not in [\"yaml\", \"json\"]:\n message = f\"'{file_format}' is not a valid format for an AntaCatalog file. Only 'yaml' and 'json' are supported.\"\n raise ValueError(message)\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) if file_format == \"yaml\" else json_load(f)\n except (TypeError, YAMLError, OSError, ValueError) 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(\n **data: (\n type[AntaTest]\n | AntaTest.Input\n | dict[str, Any]\n | None\n )\n)\n Bases: BaseModel Define a test with its associated inputs. Attributes: Name Type Description test type[AntaTest] An AntaTest concrete subclass. inputs Input The associated AntaTest.Input subclass instance. https://docs.pydantic.dev/2.0/usage/validators/#using-validation-context-with-basemodel-initialization."},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.check_inputs","title":"check_inputs","text":"check_inputs() -> Self\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) -> Self:\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(\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. 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\"Could not instantiate inputs as type {type(data).__name__} is not valid\"\n raise ValueError(msg)\n"},{"location":"api/catalog/#anta.catalog.AntaTestDefinition.serialize_model","title":"serialize_model","text":"serialize_model() -> dict[str, AntaTest.Input]\n Serialize the AntaTestDefinition model. The dictionary representing the model will be look like: <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n Returns: Type Description dict A dictionary representing the model. Source code in anta/catalog.py @model_serializer()\ndef serialize_model(self) -> dict[str, AntaTest.Input]:\n \"\"\"Serialize the AntaTestDefinition model.\n\n The dictionary representing the model will be look like:\n ```\n <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n ```\n\n Returns\n -------\n dict\n A dictionary representing the model.\n \"\"\"\n return {self.test.__name__: self.inputs}\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: <Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\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 if not data:\n return data\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 isinstance(test_definition, AntaTestDefinition):\n test_definitions.append(test_definition)\n continue\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 return data\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.flatten_modules","title":"flatten_modules staticmethod","text":"flatten_modules(\n data: dict[str, Any], package: str | None = None\n) -> dict[ModuleType, list[Any]]\n Allow the user to provide a data structure with nested Python modules. Example anta.tests.routing:\n generic:\n - <AntaTestDefinition>\n bgp:\n - <AntaTestDefinition>\n anta.tests.routing.generic and anta.tests.routing.bgp are importable Python modules. 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:\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 elif isinstance(tests, list):\n # This is a list of AntaTestDefinition\n modules[module] = tests\n else:\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 return modules\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.to_json","title":"to_json","text":"to_json() -> str\n Return a JSON representation string of this model. Returns: Type Description str The JSON representation string of this model. Source code in anta/catalog.py def to_json(self) -> str:\n \"\"\"Return a JSON representation string of this model.\n\n Returns\n -------\n str\n The JSON representation string of this model.\n \"\"\"\n return self.model_dump_json(serialize_as_any=True, exclude_unset=True, indent=2)\n"},{"location":"api/catalog/#anta.catalog.AntaCatalogFile.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/catalog.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return safe_dump(safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/csv_reporter/","title":"CSV reporter","text":"CSV Report management for ANTA."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv","title":"ReportCsv","text":"Build a CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_name: str = \"Test Name\",\n test_status: str = \"Test Status\",\n messages: str = \"Message(s)\",\n description: str = \"Test description\",\n categories: str = \"Test category\",\n)\n Headers for the CSV report."},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.convert_to_list","title":"convert_to_list classmethod","text":"convert_to_list(result: TestResult) -> list[str]\n Convert a TestResult into a list of string for creating file content. Parameters: Name Type Description Default result TestResult A TestResult to convert into list. required Returns: Type Description list[str] TestResult converted into a list. Source code in anta/reporter/csv_reporter.py @classmethod\ndef convert_to_list(cls, result: TestResult) -> list[str]:\n \"\"\"Convert a TestResult into a list of string for creating file content.\n\n Parameters\n ----------\n result\n A TestResult to convert into list.\n\n Returns\n -------\n list[str]\n TestResult converted into a list.\n \"\"\"\n message = cls.split_list_to_txt_list(result.messages) if len(result.messages) > 0 else \"\"\n categories = cls.split_list_to_txt_list(convert_categories(result.categories)) if len(result.categories) > 0 else \"None\"\n return [\n str(result.name),\n result.test,\n result.result,\n message,\n result.description,\n categories,\n ]\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.generate","title":"generate classmethod","text":"generate(\n results: ResultManager, csv_filename: pathlib.Path\n) -> None\n Build CSV flle with tests results. Parameters: Name Type Description Default results ResultManager A ResultManager instance. required csv_filename Path File path where to save CSV data. required Raises: Type Description OSError if any is raised while writing the CSV file. Source code in anta/reporter/csv_reporter.py @classmethod\ndef generate(cls, results: ResultManager, csv_filename: pathlib.Path) -> None:\n \"\"\"Build CSV flle with tests results.\n\n Parameters\n ----------\n results\n A ResultManager instance.\n csv_filename\n File path where to save CSV data.\n\n Raises\n ------\n OSError\n if any is raised while writing the CSV file.\n \"\"\"\n headers = [\n cls.Headers.device,\n cls.Headers.test_name,\n cls.Headers.test_status,\n cls.Headers.messages,\n cls.Headers.description,\n cls.Headers.categories,\n ]\n\n try:\n with csv_filename.open(mode=\"w\", encoding=\"utf-8\") as csvfile:\n csvwriter = csv.writer(\n csvfile,\n delimiter=\",\",\n )\n csvwriter.writerow(headers)\n for entry in results.results:\n csvwriter.writerow(cls.convert_to_list(entry))\n except OSError as exc:\n message = f\"OSError caught while writing the CSV file '{csv_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/csv_reporter/#anta.reporter.csv_reporter.ReportCsv.split_list_to_txt_list","title":"split_list_to_txt_list classmethod","text":"split_list_to_txt_list(\n usr_list: list[str], delimiter: str = \" - \"\n) -> str\n Split list to multi-lines string. Parameters: Name Type Description Default usr_list list[str] List of string to concatenate. required delimiter str A delimiter to use to start string. Defaults to None. ' - ' Returns: Type Description str Multi-lines string. Source code in anta/reporter/csv_reporter.py @classmethod\ndef split_list_to_txt_list(cls, usr_list: list[str], delimiter: str = \" - \") -> str:\n \"\"\"Split list to multi-lines string.\n\n Parameters\n ----------\n usr_list\n List of string to concatenate.\n delimiter\n A delimiter to use to start string. Defaults to None.\n\n Returns\n -------\n str\n Multi-lines string.\n\n \"\"\"\n return f\"{delimiter}\".join(f\"{line}\" for line in usr_list)\n"},{"location":"api/device/","title":"Device","text":""},{"location":"api/device/#antadevice-base-class","title":"AntaDevice base class","text":""},{"location":"api/device/#anta.device.AntaDevice","title":"AntaDevice","text":"AntaDevice(\n name: str,\n tags: set[str] | None = None,\n *,\n disable_cache: bool = False\n)\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 str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. cache Cache | None In-memory cache from aiocache library for this device (None if cache is disabled). cache_locks dict Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled. Parameters: Name Type Description Default name str Device name. required tags set[str] | None Tags for this device. None disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AntaDevice.cache_statistics","title":"cache_statistics property","text":"cache_statistics: dict[str, Any] | None\n Return 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.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AntaDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AntaDevice.\"\"\"\n return (\n f\"AntaDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AntaDevice._collect","title":"_collect abstractmethod async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output. This abstract coroutine can be used to implement any command collection method for a device in ANTA. The _collect() implementation needs to populate the output attribute of the AntaCommand object passed as argument. If a failure occurs, the _collect() implementation is expected to catch the exception and implement proper logging, the output attribute of the AntaCommand object passed as argument would be None in this case. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py @abstractmethod\nasync def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None:\n \"\"\"Collect device command output.\n\n This abstract coroutine can be used to implement any command collection method\n for a device in ANTA.\n\n The `_collect()` implementation needs to populate the `output` attribute\n of the `AntaCommand` object passed as argument.\n\n If a failure occurs, the `_collect()` implementation is expected to catch the\n exception and implement proper logging, the `output` attribute of the\n `AntaCommand` object passed as argument would be `None` in this case.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n"},{"location":"api/device/#anta.device.AntaDevice.collect","title":"collect async","text":"collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> 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. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect(self, command: AntaCommand, *, collection_id: str | None = None) -> 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 Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\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, collection_id=collection_id)\n await self.cache.set(command.uid, command.output) # pylint: disable=no-member\n else:\n await self._collect(command=command, collection_id=collection_id)\n"},{"location":"api/device/#anta.device.AntaDevice.collect_commands","title":"collect_commands async","text":"collect_commands(\n commands: list[AntaCommand],\n *,\n collection_id: str | None = None\n) -> None\n Collect multiple commands. Parameters: Name Type Description Default commands list[AntaCommand] The commands to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def collect_commands(self, commands: list[AntaCommand], *, collection_id: str | None = None) -> None:\n \"\"\"Collect multiple commands.\n\n Parameters\n ----------\n commands\n The commands to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n await asyncio.gather(*(self.collect(command=command, collection_id=collection_id) for command in commands))\n"},{"location":"api/device/#anta.device.AntaDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device, usually through SCP. It is not mandatory to implement this for a valid AntaDevice subclass. Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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\n - `is_online`: When the device IP is reachable and a port can be open.\n\n - `established`: When a command execution succeeds.\n\n - `hw_model`: The hardware model of the device.\n \"\"\"\n"},{"location":"api/device/#async-eos-device-class","title":"Async EOS device class","text":""},{"location":"api/device/#anta.device.AsyncEOSDevice","title":"AsyncEOSDevice","text":"AsyncEOSDevice(\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: set[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)\n Bases: AntaDevice Implementation of AntaDevice for EOS using aio-eapi. Attributes: Name Type Description name str Device name. is_online bool True if the device IP is reachable and a port can be open. established bool True if remote command execution succeeds. hw_model str Hardware model of the device. tags set[str] Tags for this device. Parameters: Name Type Description Default host str Device FQDN or IP. required username str Username to connect to eAPI and SSH. required password str Password to connect to eAPI and SSH. required name str | None Device name. None enable bool Collect commands using privileged mode. False enable_password str | None Password used to gain privileged access on EOS. None port int | None eAPI port. Defaults to 80 is proto is \u2018http\u2019 or 443 if proto is \u2018https\u2019. None ssh_port int | None SSH port. 22 tags set[str] | None Tags for this device. None timeout float | None Timeout value in seconds for outgoing API calls. None insecure bool Disable SSH Host Key validation. False proto Literal['http', 'https'] eAPI protocol. Value can be \u2018http\u2019 or \u2018https\u2019. 'https' disable_cache bool Disable caching for all commands for this device. False"},{"location":"api/device/#anta.device.AsyncEOSDevice.__repr__","title":"__repr__","text":"__repr__() -> str\n Return a printable representation of an AsyncEOSDevice. Source code in anta/device.py def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AsyncEOSDevice.\"\"\"\n return (\n f\"AsyncEOSDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r}, \"\n f\"host={self._session.host!r}, \"\n f\"eapi_port={self._session.port!r}, \"\n f\"username={self._ssh_opts.username!r}, \"\n f\"enable={self.enable!r}, \"\n f\"insecure={self._ssh_opts.known_hosts is None!r})\"\n )\n"},{"location":"api/device/#anta.device.AsyncEOSDevice._collect","title":"_collect async","text":"_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n Collect device command output from EOS using aio-eapi. Supports outformat json and text as output structure. Gain privileged access using the enable_password attribute of the AntaDevice instance if populated. Parameters: Name Type Description Default command AntaCommand The command to collect. required collection_id str | None An identifier used to build the eAPI request ID. None Source code in anta/device.py async def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None: # noqa: C901 function is too complex - because of many required except blocks\n \"\"\"Collect device command output from EOS using aio-eapi.\n\n Supports outformat `json` and `text` as output structure.\n Gain privileged access using the `enable_password` attribute\n of the `AntaDevice` instance if populated.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n commands: list[dict[str, str | int]] = []\n if self.enable and self._enable_password is not None:\n commands.append(\n {\n \"cmd\": \"enable\",\n \"input\": str(self._enable_password),\n },\n )\n elif self.enable:\n # No password\n commands.append({\"cmd\": \"enable\"})\n commands += [{\"cmd\": command.command, \"revision\": command.revision}] if command.revision else [{\"cmd\": command.command}]\n try:\n response: list[dict[str, Any] | str] = await self._session.cli(\n commands=commands,\n ofmt=command.ofmt,\n version=command.version,\n req_id=f\"ANTA-{collection_id}-{id(command)}\" if collection_id else f\"ANTA-{id(command)}\",\n ) # type: ignore[assignment] # multiple commands returns a list\n # Do not keep response of 'enable' command\n command.output = response[-1]\n except asynceapi.EapiCommandError as e:\n # This block catches exceptions related to EOS issuing an error.\n command.errors = e.errors\n if command.requires_privileges:\n logger.error(\n \"Command '%s' requires privileged mode on %s. Verify user permissions and if the `enable` option is required.\", command.command, self.name\n )\n if command.supported:\n logger.error(\"Command '%s' failed on %s: %s\", command.command, self.name, e.errors[0] if len(e.errors) == 1 else e.errors)\n else:\n logger.debug(\"Command '%s' is not supported on '%s' (%s)\", command.command, self.name, self.hw_model)\n except TimeoutException as e:\n # This block catches Timeout exceptions.\n command.errors = [exc_to_str(e)]\n timeouts = self._session.timeout.as_dict()\n logger.error(\n \"%s occurred while sending a command to %s. Consider increasing the timeout.\\nCurrent timeouts: Connect: %s | Read: %s | Write: %s | Pool: %s\",\n exc_to_str(e),\n self.name,\n timeouts[\"connect\"],\n timeouts[\"read\"],\n timeouts[\"write\"],\n timeouts[\"pool\"],\n )\n except (ConnectError, OSError) as e:\n # This block catches OSError and socket issues related exceptions.\n command.errors = [exc_to_str(e)]\n if (isinstance(exc := e.__cause__, httpcore.ConnectError) and isinstance(os_error := exc.__context__, OSError)) or isinstance(os_error := e, OSError): # pylint: disable=no-member\n if isinstance(os_error.__cause__, OSError):\n os_error = os_error.__cause__\n logger.error(\"A local OS error occurred while connecting to %s: %s.\", self.name, os_error)\n else:\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n except HTTPError as e:\n # This block catches most of the httpx Exceptions and logs a general message.\n command.errors = [exc_to_str(e)]\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n logger.debug(\"%s: %s\", self.name, command)\n"},{"location":"api/device/#anta.device.AsyncEOSDevice.copy","title":"copy async","text":"copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n Copy files to and from the device using asyncssh.scp(). Parameters: Name Type Description Default sources list[Path] List of files to copy to or from the device. required destination Path Local or remote destination when copying the files. Can be a folder. required direction Literal['to', 'from'] Defines if this coroutine copies files to or from the device. 'from' 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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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() function 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 = AntaCommand(command=\"show version\")\n await self._collect(show_version)\n if not show_version.collected:\n logger.warning(\"Cannot get hardware information from device %s\", self.name)\n else:\n self.hw_model = show_version.json_output.get(\"modelName\", None)\n if self.hw_model is None:\n logger.critical(\"Cannot parse 'show version' returned by device %s\", self.name)\n # in some cases it is possible that 'modelName' comes back empty\n # and it is nice to get a meaninfule error message\n elif self.hw_model == \"\":\n logger.critical(\"Got an empty 'modelName' in the 'show version' returned by device %s\", self.name)\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.devices","title":"devices property","text":"devices: list[AntaDevice]\n List of AntaDevice in this inventory."},{"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. Parameters: Name Type Description Default device AntaDevice Device object to be added. required Source code in anta/inventory/__init__.py def add_device(self, device: AntaDevice) -> None:\n \"\"\"Add a device to final inventory.\n\n Parameters\n ----------\n device\n 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(\n *,\n established_only: bool = False,\n tags: set[str] | None = None,\n devices: set[str] | None = None\n) -> AntaInventory\n Return a filtered inventory. Parameters: Name Type Description Default established_only bool Whether or not to include only established devices. False tags set[str] | None Tags to filter devices. None devices set[str] | None Names to filter devices. None 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: set[str] | None = None, devices: set[str] | None = None) -> AntaInventory:\n \"\"\"Return a filtered inventory.\n\n Parameters\n ----------\n established_only\n Whether or not to include only established devices.\n tags\n Tags to filter devices.\n devices\n Names to filter devices.\n\n Returns\n -------\n AntaInventory\n An inventory with filtered AntaDevice objects.\n \"\"\"\n\n def _filter_devices(device: AntaDevice) -> bool:\n \"\"\"Select the devices based on the inputs `tags`, `devices` and `established_only`.\"\"\"\n if tags is not None and all(tag not in tags for tag in device.tags):\n return False\n if devices is None or device.name in devices:\n return bool(not established_only or device.established)\n return False\n\n filtered_devices: list[AntaDevice] = list(filter(_filter_devices, self.values()))\n result = AntaInventory()\n for device in filtered_devices:\n result.add_device(device)\n return result\n"},{"location":"api/inventory/#anta.inventory.AntaInventory.parse","title":"parse staticmethod","text":"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. The inventory devices are AsyncEOSDevice instances. Parameters: Name Type Description Default filename str | Path Path to device inventory YAML file. required username str Username to use to connect to devices. required password str Password to use to connect to devices. required enable_password str | None Enable password to use if required. None timeout float | None Timeout value in seconds for outgoing API calls. None enable bool Whether or not the commands need to be run in enable mode towards the devices. False insecure bool Disable SSH Host Key validation. False disable_cache bool Disable cache globally. False 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 Parameters\n ----------\n filename\n Path to device inventory YAML file.\n username\n Username to use to connect to devices.\n password\n Password to use to connect to devices.\n enable_password\n Enable password to use if required.\n timeout\n Timeout value in seconds for outgoing API calls.\n enable\n Whether or not the commands need to be run in enable mode towards the devices.\n insecure\n Disable SSH Host Key validation.\n disable_cache\n Disable cache globally.\n\n Raises\n ------\n InventoryRootKeyError\n Root key of inventory is missing.\n InventoryIncorrectSchemaError\n 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 Device inventory input model."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryInput.yaml","title":"yaml","text":"yaml() -> str\n Return a YAML representation string of this model. Returns: Type Description str The YAML representation string of this model. Source code in anta/inventory/models.py def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return yaml.safe_dump(yaml.safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n"},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryHost","title":"AntaInventoryHost","text":" Bases: BaseModel Host entry of AntaInventoryInput. Attributes: Name Type Description host Hostname | IPvAnyAddress IP Address or FQDN of the device. port Port | None Custom eAPI port to use. name str | None Custom name of the device. tags set[str] Tags of the device. disable_cache bool Disable cache for this device."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryNetwork","title":"AntaInventoryNetwork","text":" Bases: BaseModel Network entry of AntaInventoryInput. Attributes: Name Type Description network IPvAnyNetwork Subnet to use for scanning. tags set[str] Tags of the devices in this network. disable_cache bool Disable cache for all devices in this network."},{"location":"api/inventory.models.input/#anta.inventory.models.AntaInventoryRange","title":"AntaInventoryRange","text":" Bases: BaseModel IP Range entry of AntaInventoryInput. Attributes: Name Type Description start IPvAnyAddress IPv4 or IPv6 address for the beginning of the range. stop IPvAnyAddress IPv4 or IPv6 address for the end of the range. tags set[str] Tags of the devices in this IP range. disable_cache bool Disable cache for all devices in this IP range."},{"location":"api/md_reporter/","title":"Markdown reporter","text":"Markdown report generator for ANTA test results."},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport","title":"ANTAReport","text":"ANTAReport(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generate the # ANTA Report section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.ANTAReport.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the # ANTA Report section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `# ANTA Report` section of the markdown report.\"\"\"\n self.write_heading(heading_level=1)\n toc = MD_REPORT_TOC\n self.mdfile.write(toc + \"\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase","title":"MDReportBase","text":"MDReportBase(mdfile: TextIOWrapper, results: ResultManager)\n Bases: ABC Base class for all sections subclasses. Every subclasses must implement the generate_section method that uses the ResultManager object to generate and write content to the provided markdown file. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_heading_name","title":"generate_heading_name","text":"generate_heading_name() -> str\n Generate a formatted heading name based on the class name. Returns: Type Description str Formatted header name. Example ANTAReport will become ANTA Report. TestResultsSummary will become Test Results Summary. Source code in anta/reporter/md_reporter.py def generate_heading_name(self) -> str:\n \"\"\"Generate a formatted heading name based on the class name.\n\n Returns\n -------\n str\n Formatted header name.\n\n Example\n -------\n - `ANTAReport` will become `ANTA Report`.\n - `TestResultsSummary` will become `Test Results Summary`.\n \"\"\"\n class_name = self.__class__.__name__\n\n # Split the class name into words, keeping acronyms together\n words = re.findall(r\"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\\d|\\W|$)|\\d+\", class_name)\n\n # Capitalize each word, but keep acronyms in all caps\n formatted_words = [word if word.isupper() else word.capitalize() for word in words]\n\n return \" \".join(formatted_words)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of a markdown table for a specific report section. Subclasses can implement this method to generate the content of the table rows. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of a markdown table for a specific report section.\n\n Subclasses can implement this method to generate the content of the table rows.\n \"\"\"\n msg = \"Subclasses should implement this method\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.generate_section","title":"generate_section abstractmethod","text":"generate_section() -> None\n Abstract method to generate a specific section of the markdown report. Must be implemented by subclasses. Source code in anta/reporter/md_reporter.py @abstractmethod\ndef generate_section(self) -> None:\n \"\"\"Abstract method to generate a specific section of the markdown report.\n\n Must be implemented by subclasses.\n \"\"\"\n msg = \"Must be implemented by subclasses\"\n raise NotImplementedError(msg)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.safe_markdown","title":"safe_markdown","text":"safe_markdown(text: str | None) -> str\n Escape markdown characters in the text to prevent markdown rendering issues. Parameters: Name Type Description Default text str | None The text to escape markdown characters from. required Returns: Type Description str The text with escaped markdown characters. Source code in anta/reporter/md_reporter.py def safe_markdown(self, text: str | None) -> str:\n \"\"\"Escape markdown characters in the text to prevent markdown rendering issues.\n\n Parameters\n ----------\n text\n The text to escape markdown characters from.\n\n Returns\n -------\n str\n The text with escaped markdown characters.\n \"\"\"\n # Custom field from a TestResult object can be None\n if text is None:\n return \"\"\n\n # Replace newlines with spaces to keep content on one line\n text = text.replace(\"\\n\", \" \")\n\n # Replace backticks with single quotes\n return text.replace(\"`\", \"'\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_heading","title":"write_heading","text":"write_heading(heading_level: int) -> None\n Write a markdown heading to the markdown file. The heading name used is the class name. Parameters: Name Type Description Default heading_level int The level of the heading (1-6). required Example ## Test Results Summary Source code in anta/reporter/md_reporter.py def write_heading(self, heading_level: int) -> None:\n \"\"\"Write a markdown heading to the markdown file.\n\n The heading name used is the class name.\n\n Parameters\n ----------\n heading_level\n The level of the heading (1-6).\n\n Example\n -------\n `## Test Results Summary`\n \"\"\"\n # Ensure the heading level is within the valid range of 1 to 6\n heading_level = max(1, min(heading_level, 6))\n heading_name = self.generate_heading_name()\n heading = \"#\" * heading_level + \" \" + heading_name\n self.mdfile.write(f\"{heading}\\n\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportBase.write_table","title":"write_table","text":"write_table(\n table_heading: list[str], *, last_table: bool = False\n) -> None\n Write a markdown table with a table heading and multiple rows to the markdown file. Parameters: Name Type Description Default table_heading list[str] List of strings to join for the table heading. required last_table bool Flag to determine if it\u2019s the last table of the markdown file to avoid unnecessary new line. Defaults to False. False Source code in anta/reporter/md_reporter.py def write_table(self, table_heading: list[str], *, last_table: bool = False) -> None:\n \"\"\"Write a markdown table with a table heading and multiple rows to the markdown file.\n\n Parameters\n ----------\n table_heading\n List of strings to join for the table heading.\n last_table\n Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.\n \"\"\"\n self.mdfile.write(\"\\n\".join(table_heading) + \"\\n\")\n for row in self.generate_rows():\n self.mdfile.write(row)\n if not last_table:\n self.mdfile.write(\"\\n\")\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator","title":"MDReportGenerator","text":"Class responsible for generating a Markdown report based on the provided ResultManager object. It aggregates different report sections, each represented by a subclass of MDReportBase, and sequentially generates their content into a markdown file. The generate class method will loop over all the section subclasses and call their generate_section method. The final report will be generated in the same order as the sections list of the method."},{"location":"api/md_reporter/#anta.reporter.md_reporter.MDReportGenerator.generate","title":"generate classmethod","text":"generate(results: ResultManager, md_filename: Path) -> None\n Generate and write the various sections of the markdown report. Parameters: Name Type Description Default results ResultManager The ResultsManager instance containing all test results. required md_filename Path The path to the markdown file to write the report into. required Source code in anta/reporter/md_reporter.py @classmethod\ndef generate(cls, results: ResultManager, md_filename: Path) -> None:\n \"\"\"Generate and write the various sections of the markdown report.\n\n Parameters\n ----------\n results\n The ResultsManager instance containing all test results.\n md_filename\n The path to the markdown file to write the report into.\n \"\"\"\n try:\n with md_filename.open(\"w\", encoding=\"utf-8\") as mdfile:\n sections: list[MDReportBase] = [\n ANTAReport(mdfile, results),\n TestResultsSummary(mdfile, results),\n SummaryTotals(mdfile, results),\n SummaryTotalsDeviceUnderTest(mdfile, results),\n SummaryTotalsPerCategory(mdfile, results),\n TestResults(mdfile, results),\n ]\n for section in sections:\n section.generate_section()\n except OSError as exc:\n message = f\"OSError caught while writing the Markdown file '{md_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals","title":"SummaryTotals","text":"SummaryTotals(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals table.\"\"\"\n yield (\n f\"| {self.results.get_total_results()} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SUCCESS})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SKIPPED})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.FAILURE})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.ERROR})} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotals.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest","title":"SummaryTotalsDeviceUnderTest","text":"SummaryTotalsDeviceUnderTest(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Devices Under Tests section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals device under test table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals device under test table.\"\"\"\n for device, stat in self.results.device_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n categories_skipped = \", \".join(sorted(convert_categories(list(stat.categories_skipped))))\n categories_failed = \", \".join(sorted(convert_categories(list(stat.categories_failed))))\n yield (\n f\"| {device} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} | {stat.tests_error_count} \"\n f\"| {categories_skipped or '-'} | {categories_failed or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsDeviceUnderTest.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Devices Under Tests section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Devices Under Tests` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory","title":"SummaryTotalsPerCategory","text":"SummaryTotalsPerCategory(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ### Summary Totals Per Category section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the summary totals per category table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals per category table.\"\"\"\n for category, stat in self.results.sorted_category_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n yield (\n f\"| {category} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} \"\n f\"| {stat.tests_error_count} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.SummaryTotalsPerCategory.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ### Summary Totals Per Category section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Per Category` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults","title":"TestResults","text":"TestResults(mdfile: TextIOWrapper, results: ResultManager)\n Bases: MDReportBase Generates the ## Test Results section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_rows","title":"generate_rows","text":"generate_rows() -> Generator[str, None, None]\n Generate the rows of the all test results table. Source code in anta/reporter/md_reporter.py def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the all test results table.\"\"\"\n for result in self.results.get_results(sort_by=[\"name\", \"test\"]):\n messages = self.safe_markdown(\", \".join(result.messages))\n categories = \", \".join(convert_categories(result.categories))\n yield (\n f\"| {result.name or '-'} | {categories or '-'} | {result.test or '-'} \"\n f\"| {result.description or '-'} | {self.safe_markdown(result.custom_field) or '-'} | {result.result or '-'} | {messages or '-'} |\\n\"\n )\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResults.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n self.write_table(table_heading=self.TABLE_HEADING, last_table=True)\n"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary","title":"TestResultsSummary","text":"TestResultsSummary(\n mdfile: TextIOWrapper, results: ResultManager\n)\n Bases: MDReportBase Generate the ## Test Results Summary section of the markdown report. Parameters: Name Type Description Default mdfile TextIOWrapper An open file object to write the markdown data into. required results ResultManager The ResultsManager instance containing all test results. required"},{"location":"api/md_reporter/#anta.reporter.md_reporter.TestResultsSummary.generate_section","title":"generate_section","text":"generate_section() -> None\n Generate the ## Test Results Summary section of the markdown report. Source code in anta/reporter/md_reporter.py def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results Summary` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n"},{"location":"api/models/","title":"Test models","text":""},{"location":"api/models/#test-definition","title":"Test definition","text":""},{"location":"api/models/#anta.models.AntaTest","title":"AntaTest","text":"AntaTest(\n device: AntaDevice,\n inputs: dict[str, Any] | AntaTest.Input | None = None,\n eos_data: list[dict[Any, Any] | str] | None = None,\n)\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 '''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 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: Name Type Description device AntaDevice AntaDevice instance on which this test is run. inputs Input AntaTest.Input instance carrying the test inputs. instance_commands list[AntaCommand] List of AntaCommand instances of this test. result TestResult TestResult instance representing the result of this test. logger Logger Python logger for this test instance. Parameters: Name Type Description Default device AntaDevice AntaDevice instance on which the test will be run. required inputs dict[str, Any] | Input | None Dictionary of attributes used to instantiate the AntaTest.Input instance. None eos_data list[dict[Any, Any] | str] | None Populate outputs of the test commands instead of collecting from devices. This list must have the same length and order than the instance_commands instance attribute. None"},{"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 Return 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 Return a list of all the commands that have failed."},{"location":"api/models/#anta.models.AntaTest.module","title":"module property","text":"module: str\n Return the Python module in which this AntaTest class is defined."},{"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: Name Type Description result_overwrite ResultOverwrite | None 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 set[str] | None Tag of devices on which to run 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 str | None Overwrite TestResult.description. categories list[str] | None Overwrite TestResult.categories. custom_field str | None 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(\n function: F,\n) -> Callable[..., Coroutine[Any, Any, TestResult]]\n Decorate the test() method in child classes. This decorator implements (in this order): Instantiate the command outputs if eos_data is provided to the test() method Collect the commands from the device Run the test() method 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 Parameters\n ----------\n self\n The test instance.\n eos_data\n 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\n Any keyword argument to pass to the test.\n\n Returns\n -------\n TestResult\n The TestResult instance attribute populated with error status if any.\n\n \"\"\"\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 AntaTest.update_progress()\n return self.result\n\n if cmds := self.failed_commands:\n unsupported_commands = [f\"'{c.command}' is not supported on {self.device.hw_model}\" for c in cmds if not c.supported]\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 else:\n self.result.is_error(message=\"\\n\".join([f\"{c.command} has failed: {', '.join(c.errors)}\" for c in cmds]))\n AntaTest.update_progress()\n return self.result\n\n try:\n function(self, **kwargs)\n except Exception as e: # noqa: BLE001\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 # TODO: find a correct way to time test execution\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, collection_id=self.name)\n except Exception as e: # noqa: BLE001\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]:\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 _ = template\n msg = f\"AntaTemplate are provided but render() method has not been implemented for {self.module}.{self.__class__.__name__}\"\n raise NotImplementedError(msg)\n"},{"location":"api/models/#anta.models.AntaTest.save_commands_data","title":"save_commands_data","text":"save_commands_data(\n eos_data: list[dict[str, Any] | str]\n) -> 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 reason\")\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 reason\")\n ```\n\n \"\"\"\n"},{"location":"api/models/#command-definition","title":"Command definition","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 str Device command. version Literal[1, 'latest'] eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision | None eAPI revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt Literal['json', 'text'] eAPI output - json or text. output dict[str, Any] | str | None Output of the command. Only defined if there was no errors. template AntaTemplate | None AntaTemplate object used to render this command. errors list[str] If the command execution fails, eAPI returns a list of strings detailing the error(s). params AntaParamsBaseModel Pydantic Model containing the variables values used to render the template. use_cache bool Enable or disable caching for this AntaCommand if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaCommand.collected","title":"collected property","text":"collected: bool\n Return True if the command has been collected, False otherwise. A command that has not been collected could have returned an error. See error property."},{"location":"api/models/#anta.models.AntaCommand.error","title":"error property","text":"error: bool\n Return True if the command returned an error, False otherwise."},{"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.requires_privileges","title":"requires_privileges property","text":"requires_privileges: bool\n Return True if the command requires privileged mode, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"location":"api/models/#anta.models.AntaCommand.supported","title":"supported property","text":"supported: bool\n Return True if the command is supported on the device hardware platform, False otherwise. Raises: Type Description RuntimeError If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property."},{"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/#anta.models.AntaTemplate","title":"AntaTemplate","text":"AntaTemplate(\n template: str,\n version: Literal[1, \"latest\"] = \"latest\",\n revision: Revision | None = None,\n ofmt: Literal[\"json\", \"text\"] = \"json\",\n *,\n use_cache: bool = True\n)\n 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: \u2018show vlan {vlan_id}\u2019. version eAPI version - valid values are 1 or \u201clatest\u201d. revision Revision of the command. Valid values are 1 to 99. Revision has precedence over version. ofmt eAPI output - json or text. use_cache Enable or disable caching for this AntaTemplate if the AntaDevice supports it."},{"location":"api/models/#anta.models.AntaTemplate.__repr__","title":"__repr__","text":"__repr__() -> str\n Return the representation of the class. Copying pydantic model style, excluding params_schema Source code in anta/models.py def __repr__(self) -> str:\n \"\"\"Return the representation of the class.\n\n Copying pydantic model style, excluding `params_schema`\n \"\"\"\n return \" \".join(f\"{a}={v!r}\" for a, v in vars(self).items() if a != \"params_schema\")\n"},{"location":"api/models/#anta.models.AntaTemplate.render","title":"render","text":"render(**params: str | int | bool) -> AntaCommand\n Render an AntaCommand from an AntaTemplate instance. Keep the parameters used in the AntaTemplate instance. Parameters: Name Type Description Default params str | int | bool Dictionary of variables with string values to render the Python f-string. {} Returns: Type Description AntaCommand The rendered AntaCommand. This AntaCommand instance have a template attribute that references this AntaTemplate instance. Raises: Type Description AntaTemplateRenderError If a parameter is missing to render the AntaTemplate instance. Source code in anta/models.py def render(self, **params: str | int | bool) -> AntaCommand:\n \"\"\"Render an AntaCommand from an AntaTemplate instance.\n\n Keep the parameters used in the AntaTemplate instance.\n\n Parameters\n ----------\n params\n Dictionary of variables with string values to render the Python f-string.\n\n Returns\n -------\n AntaCommand\n The rendered AntaCommand.\n This AntaCommand instance have a template attribute that references this\n AntaTemplate instance.\n\n Raises\n ------\n AntaTemplateRenderError\n If a parameter is missing to render the AntaTemplate instance.\n \"\"\"\n try:\n command = self.template.format(**params)\n except (KeyError, SyntaxError) as e:\n raise AntaTemplateRenderError(self, e.args[0]) from e\n return AntaCommand(\n command=command,\n ofmt=self.ofmt,\n version=self.version,\n revision=self.revision,\n template=self,\n params=self.params_schema(**params),\n use_cache=self.use_cache,\n )\n"},{"location":"api/reporters/","title":"Other reporters","text":"Report management for ANTA."},{"location":"api/reporters/#anta.reporter.ReportJinja","title":"ReportJinja","text":"ReportJinja(template_path: pathlib.Path)\n Report builder based on a Jinja2 template."},{"location":"api/reporters/#anta.reporter.ReportJinja.render","title":"render","text":"render(\n data: list[dict[str, Any]],\n *,\n trim_blocks: bool = True,\n lstrip_blocks: bool = True\n) -> str\n Build a report based on a Jinja2 template. Report is built based on a J2 template provided by user. Data structure sent to template is: Example >>> print(ResultManager.json)\n[\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n]\n Parameters: Name Type Description Default data list[dict[str, Any]] List of results from ResultManager.results. required trim_blocks bool enable trim_blocks for J2 rendering. True lstrip_blocks bool enable lstrip_blocks for J2 rendering. True Returns: Type Description str Rendered template Source code in anta/reporter/__init__.py def render(self, data: list[dict[str, Any]], *, trim_blocks: bool = True, lstrip_blocks: bool = True) -> str:\n \"\"\"Build a report based on a Jinja2 template.\n\n Report is built based on a J2 template provided by user.\n Data structure sent to template is:\n\n Example\n -------\n ```\n >>> print(ResultManager.json)\n [\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n ]\n ```\n\n Parameters\n ----------\n data\n List of results from `ResultManager.results`.\n trim_blocks\n enable trim_blocks for J2 rendering.\n lstrip_blocks\n enable lstrip_blocks for J2 rendering.\n\n Returns\n -------\n str\n Rendered template\n\n \"\"\"\n with self.template_path.open(encoding=\"utf-8\") as file_:\n template = Template(file_.read(), trim_blocks=trim_blocks, lstrip_blocks=lstrip_blocks)\n\n return template.render({\"data\": data})\n"},{"location":"api/reporters/#anta.reporter.ReportTable","title":"ReportTable","text":"TableReport Generate a Table based on TestResult."},{"location":"api/reporters/#anta.reporter.ReportTable.Headers","title":"Headers dataclass","text":"Headers(\n device: str = \"Device\",\n test_case: str = \"Test Name\",\n number_of_success: str = \"# of success\",\n number_of_failure: str = \"# of failure\",\n number_of_skipped: str = \"# of skipped\",\n number_of_errors: str = \"# of errors\",\n list_of_error_nodes: str = \"List of failed or error nodes\",\n list_of_error_tests: str = \"List of failed or error test cases\",\n)\n Headers for the table report."},{"location":"api/reporters/#anta.reporter.ReportTable.report_all","title":"report_all","text":"report_all(\n manager: ResultManager, title: str = \"All tests results\"\n) -> Table\n Create a table report with all tests for one or all devices. Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required title str Title for the report. Defaults to \u2018All tests results\u2019. 'All tests results' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_all(self, manager: ResultManager, title: str = \"All tests results\") -> Table:\n \"\"\"Create a table report with all tests for one or all devices.\n\n Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n title\n Title for the report. Defaults to 'All tests results'.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\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 def add_line(result: TestResult) -> None:\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(convert_categories(result.categories))\n table.add_row(str(result.name), result.test, state, message, result.description, categories)\n\n for result in manager.results:\n add_line(result)\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_devices","title":"report_summary_devices","text":"report_summary_devices(\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table\n Create a table report with result aggregated per device. Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required devices list[str] | None List of device names to include. None to select all devices. None title str Title of the report. 'Summary per device' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_devices(\n self,\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per device.\n\n Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n devices\n List of device names to include. None to select all devices.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.device,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_tests,\n ]\n table = self._build_headers(headers=headers, table=table)\n for device, stats in sorted(manager.device_stats.items()):\n if devices is None or device in devices:\n table.add_row(\n device,\n str(stats.tests_success_count),\n str(stats.tests_skipped_count),\n str(stats.tests_failure_count),\n str(stats.tests_error_count),\n \", \".join(stats.tests_failure),\n )\n return table\n"},{"location":"api/reporters/#anta.reporter.ReportTable.report_summary_tests","title":"report_summary_tests","text":"report_summary_tests(\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table\n Create a table report with result aggregated per test. Create table with full output: Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes Parameters: Name Type Description Default manager ResultManager A ResultManager instance. required tests list[str] | None List of test names to include. None to select all tests. None title str Title of the report. 'Summary per test' Returns: Type Description Table A fully populated rich Table. Source code in anta/reporter/__init__.py def report_summary_tests(\n self,\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per test.\n\n Create table with full output:\n Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n tests\n List of test names to include. None to select all tests.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.test_case,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_nodes,\n ]\n table = self._build_headers(headers=headers, table=table)\n for test, stats in sorted(manager.test_stats.items()):\n if tests is None or test in tests:\n table.add_row(\n test,\n str(stats.devices_success_count),\n str(stats.devices_skipped_count),\n str(stats.devices_failure_count),\n str(stats.devices_error_count),\n \", \".join(stats.devices_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":" options:\n filters: [\"!^_[^_]\", \"!^__len__\"]\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager","title":"ResultManager","text":"ResultManager()\n Helper to manage Test Results and generate reports. Examples Create Inventory: inventory_anta = AntaInventory.parse(\n filename='examples/inventory.yml',\n username='ansible',\n password='ansible',\n)\n Create Result Manager: manager = ResultManager()\n Run tests for all connected devices: for device in inventory_anta.get_inventory().devices:\n manager.add(\n VerifyNTP(device=device).test()\n )\n manager.add(\n VerifyEOSVersion(device=device).test(version='4.28.3M')\n )\n Print result in native format: manager.results\n[\n TestResult(\n name=\"pf1\",\n test=\"VerifyZeroTouch\",\n categories=[\"configuration\"],\n description=\"Verifies ZeroTouch is disabled\",\n result=\"success\",\n messages=[],\n custom_field=None,\n ),\n TestResult(\n name=\"pf1\",\n test='VerifyNTP',\n categories=[\"software\"],\n categories=['system'],\n description='Verifies if NTP is synchronised.',\n result='failure',\n messages=[\"The device is not synchronized with the configured NTP server(s): 'NTP is disabled.'\"],\n custom_field=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."},{"location":"api/result_manager/#anta.result_manager.ResultManager.json","title":"json property","text":"json: str\n Get a JSON representation of the results."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results","title":"results property writable","text":"results: list[TestResult]\n Get the list of TestResult."},{"location":"api/result_manager/#anta.result_manager.ResultManager.results_by_status","title":"results_by_status cached property","text":"results_by_status: dict[AntaTestStatus, list[TestResult]]\n A cached property that returns the results grouped by status."},{"location":"api/result_manager/#anta.result_manager.ResultManager.sorted_category_stats","title":"sorted_category_stats property","text":"sorted_category_stats: dict[str, CategoryStats]\n A property that returns the category_stats dictionary sorted by key name."},{"location":"api/result_manager/#anta.result_manager.ResultManager.__len__","title":"__len__","text":"__len__() -> int\n Implement len method to count number of results. Source code in anta/result_manager/__init__.py def __len__(self) -> int:\n \"\"\"Implement __len__ method to count number of results.\"\"\"\n return len(self._result_entries)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.add","title":"add","text":"add(result: TestResult) -> None\n Add a result to the ResultManager instance. The result is added to the internal list of results and the overall status of the ResultManager instance is updated based on the added test status. Parameters: Name Type Description Default result TestResult TestResult to add to the ResultManager instance. required Source code in anta/result_manager/__init__.py def add(self, result: TestResult) -> None:\n \"\"\"Add a result to the ResultManager instance.\n\n The result is added to the internal list of results and the overall status\n of the ResultManager instance is updated based on the added test status.\n\n Parameters\n ----------\n result\n TestResult to add to the ResultManager instance.\n \"\"\"\n self._result_entries.append(result)\n self._update_status(result.result)\n self._update_stats(result)\n\n # Every time a new result is added, we need to clear the cached property\n self.__dict__.pop(\"results_by_status\", None)\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter","title":"filter","text":"filter(hide: set[AntaTestStatus]) -> ResultManager\n Get a filtered ResultManager based on test status. Parameters: Name Type Description Default hide set[AntaTestStatus] Set of AntaTestStatus enum members to select tests to hide based on their status. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter(self, hide: set[AntaTestStatus]) -> ResultManager:\n \"\"\"Get a filtered ResultManager based on test status.\n\n Parameters\n ----------\n hide\n Set of AntaTestStatus enum members to select tests to hide based on their status.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n possible_statuses = set(AntaTestStatus)\n manager = ResultManager()\n manager.results = self.get_results(possible_statuses - hide)\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_devices","title":"filter_by_devices","text":"filter_by_devices(devices: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific devices. Parameters: Name Type Description Default devices set[str] Set of device names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_devices(self, devices: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific devices.\n\n Parameters\n ----------\n devices\n Set of device names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.name in devices]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.filter_by_tests","title":"filter_by_tests","text":"filter_by_tests(tests: set[str]) -> ResultManager\n Get a filtered ResultManager that only contains specific tests. Parameters: Name Type Description Default tests set[str] Set of test names to filter the results. required Returns: Type Description ResultManager A filtered ResultManager. Source code in anta/result_manager/__init__.py def filter_by_tests(self, tests: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific tests.\n\n Parameters\n ----------\n tests\n Set of test names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.test in tests]\n return manager\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_devices","title":"get_devices","text":"get_devices() -> set[str]\n Get the set of all the device names. Returns: Type Description set[str] Set of device names. Source code in anta/result_manager/__init__.py def get_devices(self) -> set[str]:\n \"\"\"Get the set of all the device names.\n\n Returns\n -------\n set[str]\n Set of device names.\n \"\"\"\n return {str(result.name) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_results","title":"get_results","text":"get_results(\n status: set[AntaTestStatus] | None = None,\n sort_by: list[str] | None = None,\n) -> list[TestResult]\n Get the results, optionally filtered by status and sorted by TestResult fields. If no status is provided, all results are returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None sort_by list[str] | None Optional list of TestResult fields to sort the results. None Returns: Type Description list[TestResult] List of results. Source code in anta/result_manager/__init__.py def get_results(self, status: set[AntaTestStatus] | None = None, sort_by: list[str] | None = None) -> list[TestResult]:\n \"\"\"Get the results, optionally filtered by status and sorted by TestResult fields.\n\n If no status is provided, all results are returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n sort_by\n Optional list of TestResult fields to sort the results.\n\n Returns\n -------\n list[TestResult]\n List of results.\n \"\"\"\n # Return all results if no status is provided, otherwise return results for multiple statuses\n results = self._result_entries if status is None else list(chain.from_iterable(self.results_by_status.get(status, []) for status in status))\n\n if sort_by:\n accepted_fields = TestResult.model_fields.keys()\n if not set(sort_by).issubset(set(accepted_fields)):\n msg = f\"Invalid sort_by fields: {sort_by}. Accepted fields are: {list(accepted_fields)}\"\n raise ValueError(msg)\n results = sorted(results, key=lambda result: [getattr(result, field) for field in sort_by])\n\n return results\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_tests","title":"get_tests","text":"get_tests() -> set[str]\n Get the set of all the test names. Returns: Type Description set[str] Set of test names. Source code in anta/result_manager/__init__.py def get_tests(self) -> set[str]:\n \"\"\"Get the set of all the test names.\n\n Returns\n -------\n set[str]\n Set of test names.\n \"\"\"\n return {str(result.test) for result in self._result_entries}\n"},{"location":"api/result_manager/#anta.result_manager.ResultManager.get_total_results","title":"get_total_results","text":"get_total_results(\n status: set[AntaTestStatus] | None = None,\n) -> int\n Get the total number of results, optionally filtered by status. If no status is provided, the total number of results is returned. Parameters: Name Type Description Default status set[AntaTestStatus] | None Optional set of AntaTestStatus enum members to filter the results. None Returns: Type Description int Total number of results. Source code in anta/result_manager/__init__.py def get_total_results(self, status: set[AntaTestStatus] | None = None) -> int:\n \"\"\"Get the total number of results, optionally filtered by status.\n\n If no status is provided, the total number of results is returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n\n Returns\n -------\n int\n Total number of results.\n \"\"\"\n if status is None:\n # Return the total number of results\n return sum(len(results) for results in self.results_by_status.values())\n\n # Return the total number of results for multiple statuses\n return sum(len(self.results_by_status.get(status, [])) for status in status)\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/#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 str Name of the device where the test was run. test str Name of the test run on the device. categories list[str] List of categories the TestResult belongs to. Defaults to the AntaTest categories. description str Description of the TestResult. Defaults to the AntaTest description. result AntaTestStatus Result of the test. Must be one of the AntaTestStatus Enum values: unset, success, failure, error or skipped. messages list[str] Messages to report after the test, if any. custom_field str | None 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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_error(self, message: str | None = None) -> None:\n \"\"\"Set status to error.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_failure(self, message: str | None = None) -> None:\n \"\"\"Set status to failure.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_skipped(self, message: str | None = None) -> None:\n \"\"\"Set status to skipped.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.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. Parameters: Name Type Description Default message str | None Optional message related to the test. None Source code in anta/result_manager/models.py def is_success(self, message: str | None = None) -> None:\n \"\"\"Set status to success.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.SUCCESS, message)\n"},{"location":"api/runner/","title":"Runner","text":""},{"location":"api/runner/#anta.runner","title":"runner","text":"ANTA runner function."},{"location":"api/runner/#anta.runner.adjust_rlimit_nofile","title":"adjust_rlimit_nofile","text":"adjust_rlimit_nofile() -> tuple[int, int]\n Adjust the maximum number of open file descriptors for the ANTA process. The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable. If the ANTA_NOFILE environment variable is not set or is invalid, DEFAULT_NOFILE is used. Returns: Type Description tuple[int, int] The new soft and hard limits for open file descriptors. Source code in anta/runner.py def adjust_rlimit_nofile() -> tuple[int, int]:\n \"\"\"Adjust the maximum number of open file descriptors for the ANTA process.\n\n The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable.\n\n If the `ANTA_NOFILE` environment variable is not set or is invalid, `DEFAULT_NOFILE` is used.\n\n Returns\n -------\n tuple[int, int]\n The new soft and hard limits for open file descriptors.\n \"\"\"\n try:\n nofile = int(os.environ.get(\"ANTA_NOFILE\", DEFAULT_NOFILE))\n except ValueError as exception:\n logger.warning(\"The ANTA_NOFILE environment variable value is invalid: %s\\nDefault to %s.\", exc_to_str(exception), DEFAULT_NOFILE)\n nofile = DEFAULT_NOFILE\n\n limits = resource.getrlimit(resource.RLIMIT_NOFILE)\n logger.debug(\"Initial limit numbers for open file descriptors for the current ANTA process: Soft Limit: %s | Hard Limit: %s\", limits[0], limits[1])\n nofile = min(limits[1], nofile)\n logger.debug(\"Setting soft limit for open file descriptors for the current ANTA process to %s\", nofile)\n resource.setrlimit(resource.RLIMIT_NOFILE, (nofile, limits[1]))\n return resource.getrlimit(resource.RLIMIT_NOFILE)\n"},{"location":"api/runner/#anta.runner.get_coroutines","title":"get_coroutines","text":"get_coroutines(\n selected_tests: defaultdict[\n AntaDevice, set[AntaTestDefinition]\n ],\n manager: ResultManager,\n) -> list[Coroutine[Any, Any, TestResult]]\n Get the coroutines for the ANTA run. Parameters: Name Type Description Default selected_tests defaultdict[AntaDevice, set[AntaTestDefinition]] A mapping of devices to the tests to run. The selected tests are generated by the prepare_tests function. required manager ResultManager A ResultManager required Returns: Type Description list[Coroutine[Any, Any, TestResult]] The list of coroutines to run. Source code in anta/runner.py def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]], manager: ResultManager) -> list[Coroutine[Any, Any, TestResult]]:\n \"\"\"Get the coroutines for the ANTA run.\n\n Parameters\n ----------\n selected_tests\n A mapping of devices to the tests to run. The selected tests are generated by the `prepare_tests` function.\n manager\n A ResultManager\n\n Returns\n -------\n list[Coroutine[Any, Any, TestResult]]\n The list of coroutines to run.\n \"\"\"\n coros = []\n for device, test_definitions in selected_tests.items():\n for test in test_definitions:\n try:\n test_instance = test.test(device=device, inputs=test.inputs)\n manager.add(test_instance.result)\n coros.append(test_instance.test())\n except Exception as e: # noqa: PERF203, BLE001\n # An AntaTest instance is potentially user-defined code.\n # We need to catch everything and exit gracefully with an error message.\n message = \"\\n\".join(\n [\n f\"There is an error when creating test {test.test.__module__}.{test.test.__name__}.\",\n f\"If this is not a custom test implementation: {GITHUB_SUGGESTION}\",\n ],\n )\n anta_log_exception(e, message, logger)\n return coros\n"},{"location":"api/runner/#anta.runner.log_cache_statistics","title":"log_cache_statistics","text":"log_cache_statistics(devices: list[AntaDevice]) -> None\n Log cache statistics for each device in the inventory. Parameters: Name Type Description Default devices list[AntaDevice] List of devices in the inventory. required Source code in anta/runner.py def log_cache_statistics(devices: list[AntaDevice]) -> None:\n \"\"\"Log cache statistics for each device in the inventory.\n\n Parameters\n ----------\n devices\n List of devices in the inventory.\n \"\"\"\n for device in devices:\n if device.cache_statistics is not None:\n msg = (\n f\"Cache statistics for '{device.name}': \"\n f\"{device.cache_statistics['cache_hits']} hits / {device.cache_statistics['total_commands_sent']} \"\n f\"command(s) ({device.cache_statistics['cache_hit_ratio']})\"\n )\n logger.info(msg)\n else:\n logger.info(\"Caching is not enabled on %s\", device.name)\n"},{"location":"api/runner/#anta.runner.main","title":"main async","text":"main(\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False\n) -> None\n Run ANTA. Use this as an entrypoint to the test framework in your script. ResultManager object gets updated with the test results. Parameters: Name Type Description Default manager ResultManager ResultManager object to populate with the test results. required inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required devices set[str] | None Devices on which to run tests. None means all devices. These may come from the --device / -d CLI option in NRFU. None tests set[str] | None Tests to run against devices. None means all tests. These may come from the --test / -t CLI option in NRFU. None tags set[str] | None Tags to filter devices from the inventory. These may come from the --tags CLI option in NRFU. None established_only bool Include only established device(s). True dry_run bool Build the list of coroutine to run and stop before test execution. False Source code in anta/runner.py @cprofile()\nasync def main( # noqa: PLR0913\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False,\n) -> None:\n \"\"\"Run ANTA.\n\n Use this as an entrypoint to the test framework in your script.\n ResultManager object gets updated with the test results.\n\n Parameters\n ----------\n manager\n ResultManager object to populate with the test results.\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n devices\n Devices on which to run tests. None means all devices. These may come from the `--device / -d` CLI option in NRFU.\n tests\n Tests to run against devices. None means all tests. These may come from the `--test / -t` CLI option in NRFU.\n tags\n Tags to filter devices from the inventory. These may come from the `--tags` CLI option in NRFU.\n established_only\n Include only established device(s).\n dry_run\n Build the list of coroutine to run and stop before test execution.\n \"\"\"\n # Adjust the maximum number of open file descriptors for the ANTA process\n limits = adjust_rlimit_nofile()\n\n if not catalog.tests:\n logger.info(\"The list of tests is empty, exiting\")\n return\n\n with Catchtime(logger=logger, message=\"Preparing ANTA NRFU Run\"):\n # Setup the inventory\n selected_inventory = inventory if dry_run else await setup_inventory(inventory, tags, devices, established_only=established_only)\n if selected_inventory is None:\n return\n\n with Catchtime(logger=logger, message=\"Preparing the tests\"):\n selected_tests = prepare_tests(selected_inventory, catalog, tests, tags)\n if selected_tests is None:\n return\n final_tests_count = sum(len(tests) for tests in selected_tests.values())\n\n run_info = (\n \"--- ANTA NRFU Run Information ---\\n\"\n f\"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\\n\"\n f\"Total number of selected tests: {final_tests_count}\\n\"\n f\"Maximum number of open file descriptors for the current ANTA process: {limits[0]}\\n\"\n \"---------------------------------\"\n )\n\n logger.info(run_info)\n\n if final_tests_count > limits[0]:\n logger.warning(\n \"The number of concurrent tests is higher than the open file descriptors limit for this ANTA process.\\n\"\n \"Errors may occur while running the tests.\\n\"\n \"Please consult the ANTA FAQ.\"\n )\n\n coroutines = get_coroutines(selected_tests, manager)\n\n if dry_run:\n logger.info(\"Dry-run mode, exiting before running the tests.\")\n for coro in coroutines:\n coro.close()\n return\n\n if AntaTest.progress is not None:\n AntaTest.nrfu_task = AntaTest.progress.add_task(\"Running NRFU Tests...\", total=len(coroutines))\n\n with Catchtime(logger=logger, message=\"Running ANTA tests\"):\n await asyncio.gather(*coroutines)\n\n log_cache_statistics(selected_inventory.devices)\n"},{"location":"api/runner/#anta.runner.prepare_tests","title":"prepare_tests","text":"prepare_tests(\n inventory: AntaInventory,\n catalog: AntaCatalog,\n tests: set[str] | None,\n tags: set[str] | None,\n) -> (\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n)\n Prepare the tests to run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required catalog AntaCatalog AntaCatalog object that includes the list of tests. required tests set[str] | None Tests to run against devices. None means all tests. required tags set[str] | None Tags to filter devices from the inventory. required Returns: Type Description defaultdict[AntaDevice, set[AntaTestDefinition]] | None A mapping of devices to the tests to run or None if there are no tests to run. Source code in anta/runner.py def prepare_tests(\n inventory: AntaInventory, catalog: AntaCatalog, tests: set[str] | None, tags: set[str] | None\n) -> defaultdict[AntaDevice, set[AntaTestDefinition]] | None:\n \"\"\"Prepare the tests to run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n tests\n Tests to run against devices. None means all tests.\n tags\n Tags to filter devices from the inventory.\n\n Returns\n -------\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n A mapping of devices to the tests to run or None if there are no tests to run.\n \"\"\"\n # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests\n catalog.build_indexes(filtered_tests=tests)\n\n # Using a set to avoid inserting duplicate tests\n device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)\n\n total_test_count = 0\n\n # Create the device to tests mapping from the tags\n for device in inventory.devices:\n if tags:\n # If there are CLI tags, execute tests with matching tags for this device\n if not (matching_tags := tags.intersection(device.tags)):\n # The device does not have any selected tag, skipping\n continue\n device_to_tests[device].update(catalog.get_tests_by_tags(matching_tags))\n else:\n # If there is no CLI tags, execute all tests that do not have any tags\n device_to_tests[device].update(catalog.tag_to_tests[None])\n\n # Then add the tests with matching tags from device tags\n device_to_tests[device].update(catalog.get_tests_by_tags(device.tags))\n\n total_test_count += len(device_to_tests[device])\n\n if total_test_count == 0:\n msg = (\n f\"There are no tests{f' matching the tags {tags} ' if tags else ' '}to run in the current test catalog and device inventory, please verify your inputs.\"\n )\n logger.warning(msg)\n return None\n\n return device_to_tests\n"},{"location":"api/runner/#anta.runner.setup_inventory","title":"setup_inventory async","text":"setup_inventory(\n inventory: AntaInventory,\n tags: set[str] | None,\n devices: set[str] | None,\n *,\n established_only: bool\n) -> AntaInventory | None\n Set up the inventory for the ANTA run. Parameters: Name Type Description Default inventory AntaInventory AntaInventory object that includes the device(s). required tags set[str] | None Tags to filter devices from the inventory. required devices set[str] | None Devices on which to run tests. None means all devices. required established_only bool If True use return only devices where a connection is established. required Returns: Type Description AntaInventory | None The filtered inventory or None if there are no devices to run tests on. Source code in anta/runner.py async def setup_inventory(inventory: AntaInventory, tags: set[str] | None, devices: set[str] | None, *, established_only: bool) -> AntaInventory | None:\n \"\"\"Set up the inventory for the ANTA run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n tags\n Tags to filter devices from the inventory.\n devices\n Devices on which to run tests. None means all devices.\n established_only\n If True use return only devices where a connection is established.\n\n Returns\n -------\n AntaInventory | None\n The filtered inventory or None if there are no devices to run tests on.\n \"\"\"\n if len(inventory) == 0:\n logger.info(\"The inventory is empty, exiting\")\n return None\n\n # Filter the inventory based on the CLI provided tags and devices if any\n selected_inventory = inventory.get_inventory(tags=tags, devices=devices) if tags or devices else inventory\n\n with Catchtime(logger=logger, message=\"Connecting to devices\"):\n # Connect to the devices\n await selected_inventory.connect_inventory()\n\n # Remove devices that are unreachable\n selected_inventory = selected_inventory.get_inventory(established_only=established_only)\n\n # If there are no devices in the inventory after filtering, exit\n if not selected_inventory.devices:\n msg = f'No reachable device {f\"matching the tags {tags} \" if tags else \"\"}was found.{f\" Selected devices: {devices} \" if devices is not None else \"\"}'\n logger.warning(msg)\n return None\n\n return selected_inventory\n"},{"location":"api/test.cvx/","title":"Test.cvx","text":""},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX","title":"VerifyManagementCVX","text":"Verifies the management CVX global status. Expected Results Success: The test will pass if the management CVX global status matches the expected status. Failure: The test will fail if the management CVX global status does not match the expected status. Examples anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n Source code in anta/tests/cvx.py class VerifyManagementCVX(AntaTest):\n \"\"\"Verifies the management CVX global status.\n\n Expected Results\n ----------------\n * Success: The test will pass if the management CVX global status matches the expected status.\n * Failure: The test will fail if the management CVX global status does not match the expected status.\n\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyManagementCVX test.\"\"\"\n\n enabled: bool\n \"\"\"Whether management CVX must be enabled (True) or disabled (False).\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyManagementCVX.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n cluster_status = command_output[\"clusterStatus\"]\n if (cluster_state := cluster_status.get(\"enabled\")) != self.inputs.enabled:\n self.result.is_failure(f\"Management CVX status is not valid: {cluster_state}\")\n"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyManagementCVX-attributes","title":"Inputs","text":"Name Type Description Default enabled bool Whether management CVX must be enabled (True) or disabled (False). -"},{"location":"api/test.cvx/#anta.tests.cvx.VerifyMcsClientMounts","title":"VerifyMcsClientMounts","text":"Verify if all MCS client mounts are in mountStateMountComplete. Expected Results Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete. Failure: The test will fail even if one switch\u2019s MCS client mount status is not mountStateMountComplete. Examples anta.tests.cvx:\n- VerifyMcsClientMounts:\n Source code in anta/tests/cvx.py class VerifyMcsClientMounts(AntaTest):\n \"\"\"Verify if all MCS client mounts are in mountStateMountComplete.\n\n Expected Results\n ----------------\n * Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete.\n * Failure: The test will fail even if one switch's MCS client mount status is not mountStateMountComplete.\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyMcsClientMounts:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx mounts\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyMcsClientMounts.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n mount_states = command_output[\"mountStates\"]\n mcs_mount_state_detected = False\n for mount_state in mount_states:\n if not mount_state[\"type\"].startswith(\"Mcs\"):\n continue\n mcs_mount_state_detected = True\n if (state := mount_state[\"state\"]) != \"mountStateMountComplete\":\n self.result.is_failure(f\"MCS Client mount states are not valid: {state}\")\n\n if not mcs_mount_state_detected:\n self.result.is_failure(\"MCS Client mount states are not present\")\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authentication\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authorization\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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.avt/","title":"Adaptive Virtual Topology","text":""},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTPathHealth","title":"VerifyAVTPathHealth","text":"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs. Expected Results Success: The test will pass if all AVT paths for all VRFs are active and valid. Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid. Examples anta.tests.avt:\n - VerifyAVTPathHealth:\n Source code in anta/tests/avt.py class VerifyAVTPathHealth(AntaTest):\n \"\"\"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for all VRFs are active and valid.\n * Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTPathHealth:\n ```\n \"\"\"\n\n description = \"Verifies the status of all AVT paths for all VRFs.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTPathHealth.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output.get(\"vrfs\", {})\n\n # Check if AVT is configured\n if not command_output:\n self.result.is_failure(\"Adaptive virtual topology paths are not configured.\")\n return\n\n # Iterate over each VRF\n for vrf, vrf_data in command_output.items():\n # Iterate over each AVT path\n for profile, avt_path in vrf_data.get(\"avts\", {}).items():\n for path, flags in avt_path.get(\"avtPaths\", {}).items():\n # Get the status of the AVT path\n valid = flags[\"flags\"][\"valid\"]\n active = flags[\"flags\"][\"active\"]\n\n # Check the status of the AVT path\n if not valid and not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid and not active.\")\n elif not valid:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid.\")\n elif not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is not active.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole","title":"VerifyAVTRole","text":"Verifies the Adaptive Virtual Topology (AVT) role of a device. Expected Results Success: The test will pass if the AVT role of the device matches the expected role. Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role. Examples anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n Source code in anta/tests/avt.py class VerifyAVTRole(AntaTest):\n \"\"\"Verifies the Adaptive Virtual Topology (AVT) role of a device.\n\n Expected Results\n ----------------\n * Success: The test will pass if the AVT role of the device matches the expected role.\n * Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n ```\n \"\"\"\n\n description = \"Verifies the AVT role of a device.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTRole test.\"\"\"\n\n role: str\n \"\"\"Expected AVT role of the device.\"\"\"\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTRole.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output\n\n # Check if the AVT role matches the expected role\n if self.inputs.role != command_output.get(\"role\"):\n self.result.is_failure(f\"Expected AVT role as `{self.inputs.role}`, but found `{command_output.get('role')}` instead.\")\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTRole-attributes","title":"Inputs","text":"Name Type Description Default role str Expected AVT role of the device. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath","title":"VerifyAVTSpecificPath","text":"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF. Expected Results Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided. If multiple paths are configured, the test will pass only if all the paths are valid and active. Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid, or does not match the specified type. Examples anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n Source code in anta/tests/avt.py class VerifyAVTSpecificPath(AntaTest):\n \"\"\"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided.\n If multiple paths are configured, the test will pass only if all the paths are valid and active.\n * Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid,\n or does not match the specified type.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n ```\n \"\"\"\n\n description = \"Verifies the status and type of an AVT path for a specified VRF.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show adaptive-virtual-topology path vrf {vrf} avt {avt_name} destination {destination}\")\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTSpecificPath test.\"\"\"\n\n avt_paths: list[AVTPaths]\n \"\"\"List of AVT paths to verify.\"\"\"\n\n class AVTPaths(BaseModel):\n \"\"\"Model for the details of AVT paths.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"The VRF for the AVT path. Defaults to 'default' if not provided.\"\"\"\n avt_name: str\n \"\"\"Name of the adaptive virtual topology.\"\"\"\n destination: IPv4Address\n \"\"\"The IPv4 address of the AVT peer.\"\"\"\n next_hop: IPv4Address\n \"\"\"The IPv4 address of the next hop for the AVT peer.\"\"\"\n path_type: str | None = None\n \"\"\"The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input AVT path/peer.\"\"\"\n return [template.render(vrf=path.vrf, avt_name=path.avt_name, destination=path.destination) for path in self.inputs.avt_paths]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTSpecificPath.\"\"\"\n # Assume the test is successful until a failure is detected\n self.result.is_success()\n\n # Process each command in the instance\n for command, input_avt in zip(self.instance_commands, self.inputs.avt_paths):\n # Extract the command output and parameters\n vrf = command.params.vrf\n avt_name = command.params.avt_name\n peer = str(command.params.destination)\n\n command_output = command.json_output.get(\"vrfs\", {})\n\n # If no AVT is configured, mark the test as failed and skip to the next command\n if not command_output:\n self.result.is_failure(f\"AVT configuration for peer '{peer}' under topology '{avt_name}' in VRF '{vrf}' is not found.\")\n continue\n\n # Extract the AVT paths\n avt_paths = get_value(command_output, f\"{vrf}.avts.{avt_name}.avtPaths\")\n next_hop, input_path_type = str(input_avt.next_hop), input_avt.path_type\n\n nexthop_path_found = path_type_found = False\n\n # Check each AVT path\n for path, path_data in avt_paths.items():\n # If the path does not match the expected next hop, skip to the next path\n if path_data.get(\"nexthopAddr\") != next_hop:\n continue\n\n nexthop_path_found = True\n path_type = \"direct\" if get_value(path_data, \"flags.directPath\") else \"multihop\"\n\n # If the path type does not match the expected path type, skip to the next path\n if input_path_type and path_type != input_path_type:\n continue\n\n path_type_found = True\n valid = get_value(path_data, \"flags.valid\")\n active = get_value(path_data, \"flags.active\")\n\n # Check the path status and type against the expected values\n if not all([valid, active]):\n failure_reasons = []\n if not get_value(path_data, \"flags.active\"):\n failure_reasons.append(\"inactive\")\n if not get_value(path_data, \"flags.valid\"):\n failure_reasons.append(\"invalid\")\n # Construct the failure message prefix\n failed_log = f\"AVT path '{path}' for topology '{avt_name}' in VRF '{vrf}'\"\n self.result.is_failure(f\"{failed_log} is {', '.join(failure_reasons)}.\")\n\n # If no matching next hop or path type was found, mark the test as failed\n if not nexthop_path_found or not path_type_found:\n self.result.is_failure(\n f\"No '{input_path_type}' path found with next-hop address '{next_hop}' for AVT peer '{peer}' under topology '{avt_name}' in VRF '{vrf}'.\"\n )\n"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"Inputs","text":"Name Type Description Default avt_paths list[AVTPaths] List of AVT paths to verify. -"},{"location":"api/tests.avt/#anta.tests.avt.VerifyAVTSpecificPath-attributes","title":"AVTPaths","text":"Name Type Description Default vrf str The VRF for the AVT path. Defaults to 'default' if not provided. 'default' avt_name str Name of the adaptive virtual topology. - destination IPv4Address The IPv4 address of the AVT peer. - next_hop IPv4Address The IPv4 address of the next hop for the AVT peer. - path_type str | None The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered. None"},{"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 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\", revision=1),\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 categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\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 tx_interval = bfd_peers.tx_interval\n rx_interval = bfd_peers.rx_interval\n multiplier = bfd_peers.multiplier\n\n # Check if BFD peer configured\n bfd_output = get_value(\n self.instance_commands[0].json_output,\n f\"vrfs..{vrf}..ipv4Neighbors..{peer}..peerStats..\",\n separator=\"..\",\n )\n if not bfd_output:\n failures[peer] = {vrf: \"Not Configured\"}\n continue\n\n # Convert interval timer(s) into milliseconds to be consistent with the inputs.\n bfd_details = bfd_output.get(\"peerStatsDetail\", {})\n op_tx_interval = bfd_details.get(\"operTxInterval\") // 1000\n op_rx_interval = bfd_details.get(\"operRxInterval\") // 1000\n detect_multiplier = bfd_details.get(\"detectMult\")\n intervals_ok = op_tx_interval == tx_interval and op_rx_interval == rx_interval and detect_multiplier == multiplier\n\n # Check timers of BFD peer\n if not intervals_ok:\n failures[peer] = {\n vrf: {\n \"tx_interval\": op_tx_interval,\n \"rx_interval\": op_rx_interval,\n \"multiplier\": detect_multiplier,\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.VerifyBFDPeersRegProtocols","title":"VerifyBFDPeersRegProtocols","text":"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered. Expected Results Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s). Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s). Examples anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n Source code in anta/tests/bfd.py class VerifyBFDPeersRegProtocols(AntaTest):\n \"\"\"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered.\n\n Expected Results\n ----------------\n * Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s).\n * Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s).\n\n Examples\n --------\n ```yaml\n anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBFDPeersRegProtocols 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 protocols: list[BfdProtocol]\n \"\"\"List of protocols to be verified.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBFDPeersRegProtocols.\"\"\"\n # Initialize failure messages\n failures: dict[Any, Any] = {}\n\n # Iterating over BFD peers, extract the parameters and command output\n for bfd_peer in self.inputs.bfd_peers:\n peer = str(bfd_peer.peer_address)\n vrf = bfd_peer.vrf\n protocols = bfd_peer.protocols\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 registered protocols\n difference = set(protocols) - set(get_value(bfd_output, \"peerStatsDetail.apps\"))\n\n if difference:\n failures[peer] = {vrf: sorted(difference)}\n\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BFD peers are not configured or have non-registered protocol(s):\\n{failures}\")\n"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-attributes","title":"Inputs","text":"Name Type Description Default bfd_peers list[BFDPeer] List of IPv4 BFD peers. -"},{"location":"api/tests.bfd/#anta.tests.bfd.VerifyBFDPeersRegProtocols-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' protocols list[BfdProtocol] List of protocols to be verified. -"},{"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 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\", revision=1)]\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 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.VerifyRunningConfigLines","title":"VerifyRunningConfigLines","text":"Verifies the given regular expression patterns are present in the running-config. Warning Since this uses regular expression searches on the whole running-config, it can drastically impact performance and should only be used if no other test is available. If possible, try using another ANTA test that is more specific. Expected Results Success: The test will pass if all the patterns are found in the running-config. Failure: The test will fail if any of the patterns are NOT found in the running-config. Examples anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n Source code in anta/tests/configuration.py class VerifyRunningConfigLines(AntaTest):\n \"\"\"Verifies the given regular expression patterns are present in the running-config.\n\n !!! warning\n Since this uses regular expression searches on the whole running-config, it can\n drastically impact performance and should only be used if no other test is available.\n\n If possible, try using another ANTA test that is more specific.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the patterns are found in the running-config.\n * Failure: The test will fail if any of the patterns are NOT found in the running-config.\n\n Examples\n --------\n ```yaml\n anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n ```\n \"\"\"\n\n description = \"Search the Running-Config for the given RegEx patterns.\"\n categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show running-config\", ofmt=\"text\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyRunningConfigLines test.\"\"\"\n\n regex_patterns: list[RegexString]\n \"\"\"List of regular expressions.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRunningConfigLines.\"\"\"\n failure_msgs = []\n command_output = self.instance_commands[0].text_output\n\n for pattern in self.inputs.regex_patterns:\n re_search = re.compile(pattern, flags=re.MULTILINE)\n\n if not re_search.search(command_output):\n failure_msgs.append(f\"'{pattern}'\")\n\n if not failure_msgs:\n self.result.is_success()\n else:\n self.result.is_failure(\"Following patterns were not found: \" + \",\".join(failure_msgs))\n"},{"location":"api/tests.configuration/#anta.tests.configuration.VerifyRunningConfigLines-attributes","title":"Inputs","text":"Name Type Description Default regex_patterns list[RegexString] List of regular expressions. -"},{"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 categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show zerotouch\", revision=1)]\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/#tests","title":"Tests","text":""},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors","title":"VerifyLLDPNeighbors","text":"Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors. This test performs the following checks for each specified LLDP neighbor: Confirming matching ports on both local and neighboring devices. Ensuring compatibility of device names and interface identifiers. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored. Expected Results Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table. The system name or port of the LLDP neighbor does not match the expected 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 the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.\n\n This test performs the following checks for each specified LLDP neighbor:\n\n 1. Confirming matching ports on both local and neighboring devices.\n 2. Ensuring compatibility of device names and interface identifiers.\n 3. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table.\n - The system name or port of the LLDP neighbor does not match the expected 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 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\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLLDPNeighbors test.\"\"\"\n\n neighbors: list[LLDPNeighbor]\n \"\"\"List of LLDP neighbors.\"\"\"\n Neighbor: ClassVar[type[Neighbor]] = Neighbor\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLLDPNeighbors.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output[\"lldpNeighbors\"]\n for neighbor in self.inputs.neighbors:\n if neighbor.port not in output:\n self.result.is_failure(f\"{neighbor} - Port not found\")\n continue\n\n if len(lldp_neighbor_info := output[neighbor.port][\"lldpNeighborInfo\"]) == 0:\n self.result.is_failure(f\"{neighbor} - No LLDP neighbors\")\n continue\n\n # Check if the system name and neighbor port matches\n match_found = any(\n info[\"systemName\"] == neighbor.neighbor_device and info[\"neighborInterfaceInfo\"][\"interfaceId_v2\"] == neighbor.neighbor_port\n for info in lldp_neighbor_info\n )\n if not match_found:\n failure_msg = [f\"{info['systemName']}/{info['neighborInterfaceInfo']['interfaceId_v2']}\" for info in lldp_neighbor_info]\n self.result.is_failure(f\"{neighbor} - Wrong LLDP neighbors: {', '.join(failure_msg)}\")\n"},{"location":"api/tests.connectivity/#anta.tests.connectivity.VerifyLLDPNeighbors-attributes","title":"Inputs","text":"Name Type Description Default neighbors list[LLDPNeighbor] List of LLDP neighbors. -"},{"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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"connectivity\"]\n # Template uses '{size}{df_bit}' without space since df_bit includes leading space when enabled\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"ping vrf {vrf} {destination} source {source} size {size}{df_bit} repeat {repeat}\", revision=1)\n ]\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 Host: ClassVar[type[Host]] = Host\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each host in the input list.\"\"\"\n commands = []\n for host in self.inputs.hosts:\n # df_bit includes leading space when enabled, empty string when disabled\n df_bit = \" df-bit\" if host.df_bit else \"\"\n command = template.render(destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat, size=host.size, df_bit=df_bit)\n commands.append(command)\n return commands\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyReachability.\"\"\"\n self.result.is_success()\n\n for command, host in zip(self.instance_commands, self.inputs.hosts):\n if f\"{host.repeat} received\" not in command.json_output[\"messages\"][0]:\n self.result.is_failure(f\"{host} - Unreachable\")\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/#input-models","title":"Input models","text":""},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Host","title":"Host","text":"Model for a remote host to ping. 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 size int Specify datagram size. Defaults to 100. 100 df_bit bool Enable do not fragment bit in IP header. Defaults to False. False Source code in anta/input_models/connectivity.py class Host(BaseModel):\n \"\"\"Model for a remote host to ping.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\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 size: int = 100\n \"\"\"Specify datagram size. Defaults to 100.\"\"\"\n df_bit: bool = False\n \"\"\"Enable do not fragment bit in IP header. Defaults to False.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the Host for reporting.\n\n Examples\n --------\n Host 10.1.1.1 (src: 10.2.2.2, vrf: mgmt, size: 100B, repeat: 2)\n\n \"\"\"\n df_status = \", df-bit: enabled\" if self.df_bit else \"\"\n return f\"Host {self.destination} (src: {self.source}, vrf: {self.vrf}, size: {self.size}B, repeat: {self.repeat}{df_status})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.LLDPNeighbor","title":"LLDPNeighbor","text":"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information. Name Type Description Default port Interface The LLDP port for the local device. - neighbor_device str The system name of the LLDP neighbor device. - neighbor_port Interface The LLDP port on the neighboring device. - Source code in anta/input_models/connectivity.py class LLDPNeighbor(BaseModel):\n \"\"\"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n port: Interface\n \"\"\"The LLDP port for the local device.\"\"\"\n neighbor_device: str\n \"\"\"The system name of the LLDP neighbor device.\"\"\"\n neighbor_port: Interface\n \"\"\"The LLDP port on the neighboring device.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the LLDPNeighbor for reporting.\n\n Examples\n --------\n Port Ethernet1 (Neighbor: DC1-SPINE2, Neighbor Port: Ethernet2)\n\n \"\"\"\n return f\"Port {self.port} (Neighbor: {self.neighbor_device}, Neighbor Port: {self.neighbor_port})\"\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor","title":"Neighbor","text":"Alias for the LLDPNeighbor model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the LLDPNeighbor model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/connectivity.py class Neighbor(LLDPNeighbor): # pragma: no cover\n \"\"\"Alias for the LLDPNeighbor model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the LLDPNeighbor model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"location":"api/tests.connectivity/#anta.input_models.connectivity.Neighbor.__init__","title":"__init__","text":"__init__(**data: Any) -> None\n Source code in anta/input_models/connectivity.py def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n"},{"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 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\", revision=1)]\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 break\n else:\n self.result.is_failure(\"Aboot component not found\")\n return\n\n self.result.is_success()\n incorrect_aboot_version = (\n (aboot_version.startswith(\"4.0.\") and int(aboot_version.split(\".\")[2]) < 7)\n or (aboot_version.startswith(\"4.1.\") and int(aboot_version.split(\".\")[2]) < 1)\n or (\n (aboot_version.startswith(\"6.0.\") and int(aboot_version.split(\".\")[2]) < 9)\n or (aboot_version.startswith(\"6.1.\") 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 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\", revision=1)]\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_failure(\"Error in running test - Component FixedSystemvrm1 not found in 'show version'\")\n"},{"location":"api/tests.flow_tracking/","title":"Flow Tracking","text":""},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus","title":"VerifyHardwareFlowTrackerStatus","text":"Verifies if hardware flow tracking is running and an input tracker is active. This test optionally verifies the tracker interval/timeout and exporter configuration. Expected Results Success: The test will pass if hardware flow tracking is running and an input tracker is active. Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active, or the tracker interval/timeout and exporter configuration does not match the expected values. Examples anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n Source code in anta/tests/flow_tracking.py class VerifyHardwareFlowTrackerStatus(AntaTest):\n \"\"\"Verifies if hardware flow tracking is running and an input tracker is active.\n\n This test optionally verifies the tracker interval/timeout and exporter configuration.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware flow tracking is running and an input tracker is active.\n * Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active,\n or the tracker interval/timeout and exporter configuration does not match the expected values.\n\n Examples\n --------\n ```yaml\n anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n ```\n \"\"\"\n\n description = (\n \"Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration.\"\n )\n categories: ClassVar[list[str]] = [\"flow tracking\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show flow tracking hardware tracker {name}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyHardwareFlowTrackerStatus test.\"\"\"\n\n trackers: list[FlowTracker]\n \"\"\"List of flow trackers to verify.\"\"\"\n\n class FlowTracker(BaseModel):\n \"\"\"Detail of a flow tracker.\"\"\"\n\n name: str\n \"\"\"Name of the flow tracker.\"\"\"\n\n record_export: RecordExport | None = None\n \"\"\"Record export configuration for the flow tracker.\"\"\"\n\n exporters: list[Exporter] | None = None\n \"\"\"List of exporters for the flow tracker.\"\"\"\n\n class RecordExport(BaseModel):\n \"\"\"Record export configuration.\"\"\"\n\n on_inactive_timeout: int\n \"\"\"Timeout in milliseconds for exporting records when inactive.\"\"\"\n\n on_interval: int\n \"\"\"Interval in milliseconds for exporting records.\"\"\"\n\n class Exporter(BaseModel):\n \"\"\"Detail of an exporter.\"\"\"\n\n name: str\n \"\"\"Name of the exporter.\"\"\"\n\n local_interface: str\n \"\"\"Local interface used by the exporter.\"\"\"\n\n template_interval: int\n \"\"\"Template interval in milliseconds for the exporter.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each hardware tracker.\"\"\"\n return [template.render(name=tracker.name) for tracker in self.inputs.trackers]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareFlowTrackerStatus.\"\"\"\n self.result.is_success()\n for command, tracker_input in zip(self.instance_commands, self.inputs.trackers):\n hardware_tracker_name = command.params.name\n record_export = tracker_input.record_export.model_dump() if tracker_input.record_export else None\n exporters = [exporter.model_dump() for exporter in tracker_input.exporters] if tracker_input.exporters else None\n command_output = command.json_output\n\n # Check if hardware flow tracking is configured\n if not command_output.get(\"running\"):\n self.result.is_failure(\"Hardware flow tracking is not running.\")\n return\n\n # Check if the input hardware tracker is configured\n tracker_info = command_output[\"trackers\"].get(hardware_tracker_name)\n if not tracker_info:\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not configured.\")\n continue\n\n # Check if the input hardware tracker is active\n if not tracker_info.get(\"active\"):\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not active.\")\n continue\n\n # Check the input hardware tracker timeouts\n failure_msg = \"\"\n if record_export:\n record_export_failure = validate_record_export(record_export, tracker_info)\n if record_export_failure:\n failure_msg += record_export_failure\n\n # Check the input hardware tracker exporters' configuration\n if exporters:\n exporters_failure = validate_exporters(exporters, tracker_info)\n if exporters_failure:\n failure_msg += exporters_failure\n\n if failure_msg:\n self.result.is_failure(f\"{hardware_tracker_name}: {failure_msg}\\n\")\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Inputs","text":"Name Type Description Default trackers list[FlowTracker] List of flow trackers to verify. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"FlowTracker","text":"Name Type Description Default name str Name of the flow tracker. - record_export RecordExport | None Record export configuration for the flow tracker. None exporters list[Exporter] | None List of exporters for the flow tracker. None"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"RecordExport","text":"Name Type Description Default on_inactive_timeout int Timeout in milliseconds for exporting records when inactive. - on_interval int Interval in milliseconds for exporting records. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.VerifyHardwareFlowTrackerStatus-attributes","title":"Exporter","text":"Name Type Description Default name str Name of the exporter. - local_interface str Local interface used by the exporter. - template_interval int Template interval in milliseconds for the exporter. -"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_exporters","title":"validate_exporters","text":"validate_exporters(\n exporters: list[dict[str, str]],\n tracker_info: dict[str, str],\n) -> str\n Validate the exporter configurations against the tracker info. Parameters: Name Type Description Default exporters list[dict[str, str]] The list of expected exporter configurations. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str Failure message if any exporter configuration does not match. Source code in anta/tests/flow_tracking.py def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the exporter configurations against the tracker info.\n\n Parameters\n ----------\n exporters\n The list of expected exporter configurations.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n Failure message if any exporter configuration does not match.\n \"\"\"\n failed_log = \"\"\n for exporter in exporters:\n exporter_name = exporter[\"name\"]\n actual_exporter_info = tracker_info[\"exporters\"].get(exporter_name)\n if not actual_exporter_info:\n failed_log += f\"\\nExporter `{exporter_name}` is not configured.\"\n continue\n\n expected_exporter_data = {\"local interface\": exporter[\"local_interface\"], \"template interval\": exporter[\"template_interval\"]}\n actual_exporter_data = {\"local interface\": actual_exporter_info[\"localIntf\"], \"template interval\": actual_exporter_info[\"templateInterval\"]}\n\n if expected_exporter_data != actual_exporter_data:\n failed_msg = get_failed_logs(expected_exporter_data, actual_exporter_data)\n failed_log += f\"\\nExporter `{exporter_name}`: {failed_msg}\"\n return failed_log\n"},{"location":"api/tests.flow_tracking/#anta.tests.flow_tracking.validate_record_export","title":"validate_record_export","text":"validate_record_export(\n record_export: dict[str, str],\n tracker_info: dict[str, str],\n) -> str\n Validate the record export configuration against the tracker info. Parameters: Name Type Description Default record_export dict[str, str] The expected record export configuration. required tracker_info dict[str, str] The actual tracker info from the command output. required Returns: Type Description str A failure message if the record export configuration does not match, otherwise blank string. Source code in anta/tests/flow_tracking.py def validate_record_export(record_export: dict[str, str], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the record export configuration against the tracker info.\n\n Parameters\n ----------\n record_export\n The expected record export configuration.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n A failure message if the record export configuration does not match, otherwise blank string.\n \"\"\"\n failed_log = \"\"\n actual_export = {\"inactive timeout\": tracker_info.get(\"inactiveTimeout\"), \"interval\": tracker_info.get(\"activeInterval\")}\n expected_export = {\"inactive timeout\": record_export.get(\"on_inactive_timeout\"), \"interval\": record_export.get(\"on_interval\")}\n if actual_export != expected_export:\n failed_log = get_failed_logs(expected_export, actual_export)\n return failed_log\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 description = \"Verifies if a GreenT policy other than the default is created.\"\n categories: ClassVar[list[str]] = [\"greent\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show monitor telemetry postcard policy profile\", revision=1)]\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 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\", revision=1)]\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 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\", revision=1)]\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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment power\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment cooling\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show inventory\", revision=2)]\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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature transceiver\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=2)]\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 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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show lacp counters all-ports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters discards\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces status\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters errors\", revision=1)]\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 description = \"Verifies the interface IPv4 addresses.\"\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip interface {interface}\", revision=2)]\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 [template.render(interface=interface.name) for interface in self.inputs.interfaces]\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 for interface in self.inputs.interfaces:\n if interface.name == intf:\n input_interface_detail = interface\n break\n else:\n self.result.is_failure(f\"Could not find `{intf}` in the input interfaces. {GITHUB_SUGGESTION}\")\n continue\n\n input_primary_ip = str(input_interface_detail.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 (param_secondary_ips := input_interface_detail.secondary_ips) is not None:\n input_secondary_ips = sorted([str(network) for network in param_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. This test has been implemented for full-duplex interfaces only. 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. Error: The test will error out if the device has at least one non full-duplex interface. 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 This test has been implemented for full-duplex interfaces only.\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 * Error: The test will error out if the device has at least one non full-duplex interface.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfaceUtilization:\n threshold: 70.0\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show interfaces counters rates\", revision=1),\n AntaCommand(command=\"show interfaces\", revision=1),\n ]\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 # The utilization logic has been implemented for full-duplex interfaces only\n if ((duplex := (interface := interfaces[\"interfaces\"][intf]).get(\"duplex\", None)) is not None and duplex != duplex_full) or (\n (members := interface.get(\"memberInterfaces\", None)) is not None and any(stats[\"duplex\"] != duplex_full for stats in members.values())\n ):\n self.result.is_failure(f\"Interface {intf} or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented.\")\n return\n\n if (bandwidth := interfaces[\"interfaces\"][intf][\"bandwidth\"]) == 0:\n self.logger.debug(\"Interface %s has been ignored due to null bandwidth value\", intf)\n continue\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.VerifyInterfacesSpeed","title":"VerifyInterfacesSpeed","text":"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces. If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input. If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input. Expected Results Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex. Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex. Examples anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n Source code in anta/tests/interfaces.py class VerifyInterfacesSpeed(AntaTest):\n \"\"\"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces.\n\n - If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input.\n - If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input.\n\n Expected Results\n ----------------\n * Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex.\n * Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n class Input(AntaTest.Input):\n \"\"\"Inputs for the VerifyInterfacesSpeed test.\"\"\"\n\n interfaces: list[InterfaceDetail]\n \"\"\"List of interfaces to be tested\"\"\"\n\n class InterfaceDetail(BaseModel):\n \"\"\"Detail of an interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"The name of the interface.\"\"\"\n auto: bool\n \"\"\"The auto-negotiation status of the interface.\"\"\"\n speed: float = Field(ge=1, le=1000)\n \"\"\"The speed of the interface in Gigabits per second. Valid range is 1 to 1000.\"\"\"\n lanes: None | int = Field(None, ge=1, le=8)\n \"\"\"The number of lanes in the interface. Valid range is 1 to 8. This field is optional.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesSpeed.\"\"\"\n self.result.is_success()\n command_output = self.instance_commands[0].json_output\n\n # Iterate over all the interfaces\n for interface in self.inputs.interfaces:\n intf = interface.name\n\n # Check if interface exists\n if not (interface_output := get_value(command_output, f\"interfaces.{intf}\")):\n self.result.is_failure(f\"Interface `{intf}` is not found.\")\n continue\n\n auto_negotiation = interface_output.get(\"autoNegotiate\")\n actual_lanes = interface_output.get(\"lanes\")\n\n # Collecting actual interface details\n actual_interface_output = {\n \"auto negotiation\": auto_negotiation if interface.auto is True else None,\n \"duplex mode\": interface_output.get(\"duplex\"),\n \"speed\": interface_output.get(\"bandwidth\"),\n \"lanes\": actual_lanes if interface.lanes is not None else None,\n }\n\n # Forming expected interface details\n expected_interface_output = {\n \"auto negotiation\": \"success\" if interface.auto is True else None,\n \"duplex mode\": \"duplexFull\",\n \"speed\": interface.speed * BPS_GBPS_CONVERSIONS,\n \"lanes\": interface.lanes,\n }\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n for output in [actual_interface_output, expected_interface_output]:\n # Convert speed to Gbps for readability\n if output[\"speed\"] is not None:\n output[\"speed\"] = f\"{custom_division(output['speed'], BPS_GBPS_CONVERSIONS)}Gbps\"\n failed_log = get_failed_logs(expected_interface_output, actual_interface_output)\n self.result.is_failure(f\"For interface {intf}:{failed_log}\\n\")\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceDetail] List of interfaces to be tested -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesSpeed-attributes","title":"InterfaceDetail","text":"Name Type Description Default name EthernetInterface The name of the interface. - auto bool The auto-negotiation status of the interface. - speed float The speed of the interface in Gigabits per second. Valid range is 1 to 1000. Field(ge=1, le=1000) lanes None | int The number of lanes in the interface. Valid range is 1 to 8. This field is optional. Field(None, ge=1, le=8)"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyInterfacesStatus","title":"VerifyInterfacesStatus","text":"Verifies the operational states of specified interfaces to ensure they match expected configurations. This test performs the following checks for each specified interface: If line_protocol_status is defined, both status and line_protocol_status are verified for the specified interface. If line_protocol_status is not provided but the status is \u201cup\u201d, it is assumed that both the status and line protocol should be \u201cup\u201d. If the interface status is not \u201cup\u201d, only the interface\u2019s status is validated, with no line protocol check performed. Expected Results Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces. Failure: If any of the following occur: The specified interface is not configured. The specified interface status and line protocol status does not match the expected operational state for any interface. 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 the operational states of specified interfaces to ensure they match expected configurations.\n\n This test performs the following checks for each specified interface:\n\n 1. If `line_protocol_status` is defined, both `status` and `line_protocol_status` are verified for the specified interface.\n 2. If `line_protocol_status` is not provided but the `status` is \"up\", it is assumed that both the status and line protocol should be \"up\".\n 3. If the interface `status` is not \"up\", only the interface's status is validated, with no line protocol check performed.\n\n Expected Results\n ----------------\n * Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces.\n * Failure: If any of the following occur:\n - The specified interface is not configured.\n - The specified interface status and line protocol status does not match the expected operational state for any interface.\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesStatus.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output\n for interface in self.inputs.interfaces:\n if (intf_status := get_value(command_output[\"interfaceDescriptions\"], interface.name, separator=\"..\")) is None:\n self.result.is_failure(f\"{interface.name} - Not configured\")\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 actual_state = f\"Expected: {interface.status}/{interface.line_protocol_status}, Actual: {status}/{proto}\"\n self.result.is_failure(f\"{interface.name} - {actual_state}\")\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\"):\n self.result.is_failure(f\"{interface.name} - Expected: up/up, Actual: {status}/{proto}\")\n elif interface.status != status:\n self.result.is_failure(f\"{interface.name} - Expected: {interface.status}, Actual: {status}\")\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.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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip virtual-router\", revision=2)]\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 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\", revision=1)]\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 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\", revision=1)]\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.VerifyLACPInterfacesStatus","title":"VerifyLACPInterfacesStatus","text":"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces. Verifies that the interface is a member of the LACP port channel. Ensures that the synchronization is established. Ensures the interfaces are in the correct state for collecting and distributing traffic. Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \u201cslow\u201d mode, is the default setting.) Expected Results Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct. Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct. Examples anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n Source code in anta/tests/interfaces.py class VerifyLACPInterfacesStatus(AntaTest):\n \"\"\"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces.\n\n - Verifies that the interface is a member of the LACP port channel.\n - Ensures that the synchronization is established.\n - Ensures the interfaces are in the correct state for collecting and distributing traffic.\n - Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \"slow\" mode, is the default setting.)\n\n Expected Results\n ----------------\n * Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct.\n * Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show lacp interface {interface}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLACPInterfacesStatus test.\"\"\"\n\n interfaces: list[LACPInterface]\n \"\"\"List of LACP member interface.\"\"\"\n\n class LACPInterface(BaseModel):\n \"\"\"Model for an LACP member interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"Ethernet interface to validate.\"\"\"\n portchannel: PortChannelInterface\n \"\"\"Port Channel in which the interface is bundled.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each interface in the input list.\"\"\"\n return [template.render(interface=interface.name) for interface in self.inputs.interfaces]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLACPInterfacesStatus.\"\"\"\n self.result.is_success()\n\n # Member port verification parameters.\n member_port_details = [\"activity\", \"aggregation\", \"synchronization\", \"collecting\", \"distributing\", \"timeout\"]\n\n # Iterating over command output for different interfaces\n for command, input_entry in zip(self.instance_commands, self.inputs.interfaces):\n interface = input_entry.name\n portchannel = input_entry.portchannel\n\n # Verify if a PortChannel is configured with the provided interface\n if not (interface_details := get_value(command.json_output, f\"portChannels.{portchannel}.interfaces.{interface}\")):\n self.result.is_failure(f\"Interface '{interface}' is not configured to be a member of LACP '{portchannel}'.\")\n continue\n\n # Verify the interface is bundled in port channel.\n actor_port_status = interface_details.get(\"actorPortStatus\")\n if actor_port_status != \"bundled\":\n message = f\"For Interface {interface}:\\nExpected `bundled` as the local port status, but found `{actor_port_status}` instead.\\n\"\n self.result.is_failure(message)\n continue\n\n # Collecting actor and partner port details\n actor_port_details = interface_details.get(\"actorPortState\", {})\n partner_port_details = interface_details.get(\"partnerPortState\", {})\n\n # Collecting actual interface details\n actual_interface_output = {\n \"actor_port_details\": {param: actor_port_details.get(param, \"NotFound\") for param in member_port_details},\n \"partner_port_details\": {param: partner_port_details.get(param, \"NotFound\") for param in member_port_details},\n }\n\n # Forming expected interface details\n expected_details = {param: param != \"timeout\" for param in member_port_details}\n expected_interface_output = {\"actor_port_details\": expected_details, \"partner_port_details\": expected_details}\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n message = f\"For Interface {interface}:\\n\"\n actor_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"actor_port_details\", {}), actual_interface_output.get(\"actor_port_details\", {})\n )\n partner_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"partner_port_details\", {}), actual_interface_output.get(\"partner_port_details\", {})\n )\n\n if actor_port_failed_log:\n message += f\"Actor port details:{actor_port_failed_log}\\n\"\n if partner_port_failed_log:\n message += f\"Partner port details:{partner_port_failed_log}\\n\"\n\n self.result.is_failure(message)\n"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[LACPInterface] List of LACP member interface. -"},{"location":"api/tests.interfaces/#anta.tests.interfaces.VerifyLACPInterfacesStatus-attributes","title":"LACPInterface","text":"Name Type Description Default name EthernetInterface Ethernet interface to validate. - portchannel PortChannelInterface Port Channel in which the interface is bundled. -"},{"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 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\", revision=1)]\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: # pragma: no branch\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show port-channel\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip interface brief\", revision=1)]\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show storm-control\", revision=1)]\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.interfaces/#input-models","title":"Input models","text":""},{"location":"api/tests.interfaces/#anta.input_models.interfaces.InterfaceState","title":"InterfaceState","text":"Model for an interface state. 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 Source code in anta/input_models/interfaces.py 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"},{"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 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\", revision=1)]\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 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 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. This test performs the following checks: Retrieves the device\u2019s configured FQDN Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message includes the complete FQDN of the device Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the device\u2019s complete FQDN. Failure: If any of the following occur: The test message is not found in recent logs The log message does not include the device\u2019s FQDN The FQDN in the log message doesn\u2019t match the configured 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 This test performs the following checks:\n\n 1. Retrieves the device's configured FQDN\n 2. Sends a test log message at the **informational** level\n 3. Retrieves the most recent logs (last 30 seconds)\n 4. Verifies that the test message includes the complete FQDN of the device\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the device's complete FQDN.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The log message does not include the device's FQDN\n - The FQDN in the log message doesn't match the configured FQDN\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingHostname:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show hostname\", revision=1),\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingHostname validation\", ofmt=\"text\"),\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 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. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message was successfully logged Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are being generated and the test message is found in recent logs. Failure: If any of the following occur: The test message is not found in recent logs The logging system is not capturing new messages No logs are being 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 This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message was successfully logged\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are being generated and the test message is found in recent logs.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The logging system is not capturing new messages\n - No logs are being generated\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingLogsGeneration:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingLogsGeneration validation\", ofmt=\"text\"),\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 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 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 VerifyLoggingSourceIntf 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 VerifyLoggingSourceIntf.\"\"\"\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 appropriate timestamp. This test performs the following checks: Sends a test log message at the informational level Retrieves the most recent logs (last 30 seconds) Verifies that the test message is present with a high-resolution RFC3339 timestamp format Example format: 2024-01-25T15:30:45.123456+00:00 Includes microsecond precision Contains timezone offset Warning EOS logging buffer should be set to severity level informational or higher for this test to work. Expected Results Success: If logs are generated with the correct high-resolution RFC3339 timestamp format. Failure: If any of the following occur: The test message is not found in recent logs The timestamp format does not match the expected RFC3339 format 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 appropriate timestamp.\n\n This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message is present with a high-resolution RFC3339 timestamp format\n - Example format: `2024-01-25T15:30:45.123456+00:00`\n - Includes microsecond precision\n - Contains timezone offset\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the correct high-resolution RFC3339 timestamp format.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The timestamp format does not match the expected RFC3339 format\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingTimestamp:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingTimestamp validation\", ofmt=\"text\"),\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(\n logger: logging.Logger, command_output: str\n) -> str\n Parse show logging output and gets operational logging states used in the tests in this module. Parameters: Name Type Description Default logger Logger The logger object. required command_output str The show logging output. required 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 Parameters\n ----------\n logger\n The logger object.\n command_output\n The `show logging` output.\n\n Returns\n -------\n str\n 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":"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 Adaptive Virtual Topology BFD Configuration Connectivity Field Notices Flow Tracking GreenT Hardware Interfaces LANZ Logging MLAG Multicast Profiles PTP Router Path Selection Routing Generic Routing BGP Routing ISIS Routing OSPF Security Services SNMP Software STP STUN 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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag config-sanity\", revision=1)]\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 command_output[\"mlagActive\"] 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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 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\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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.path_selection/","title":"Router Path Selection","text":""},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifyPathsHealth","title":"VerifyPathsHealth","text":"Verifies the path and telemetry state of all paths under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if all path states under router path-selection are either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and their telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if any path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifyPathsHealth:\n Source code in anta/tests/path_selection.py class VerifyPathsHealth(AntaTest):\n \"\"\"Verifies the path and telemetry state of all paths under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if all path states under router path-selection are either 'IPsec established' or 'Resolved'\n and their telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if any path state is not 'IPsec established' or 'Resolved',\n or the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifyPathsHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show path-selection paths\", revision=1)]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyPathsHealth.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"dpsPeers\"]\n\n # If no paths are configured for router path-selection, the test fails\n if not command_output:\n self.result.is_failure(\"No path configured for router path-selection.\")\n return\n\n # Check the state of each path\n for peer, peer_data in command_output.items():\n for group, group_data in peer_data[\"dpsGroups\"].items():\n for path_data in group_data[\"dpsPaths\"].values():\n path_state = path_data[\"state\"]\n session = path_data[\"dpsSessions\"][\"0\"][\"active\"]\n\n # If the path state of any path is not 'ipsecEstablished' or 'routeResolved', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for peer {peer} in path-group {group} is `{path_state}`.\")\n\n # If the telemetry state of any path is inactive, the test fails\n elif not session:\n self.result.is_failure(f\"Telemetry state for peer {peer} in path-group {group} is `inactive`.\")\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath","title":"VerifySpecificPath","text":"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection. The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry. Expected Results Success: The test will pass if the path state under router path-selection is either \u2018IPsec established\u2019 or \u2018Resolved\u2019 and telemetry state as \u2018active\u2019. Failure: The test will fail if router path-selection is not configured or if the path state is not \u2018IPsec established\u2019 or \u2018Resolved\u2019, or if the telemetry state is \u2018inactive\u2019. Examples anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n Source code in anta/tests/path_selection.py class VerifySpecificPath(AntaTest):\n \"\"\"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if the path state under router path-selection is either 'IPsec established' or 'Resolved'\n and telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if the path state is not 'IPsec established' or 'Resolved',\n or if the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show path-selection paths peer {peer} path-group {group} source {source} destination {destination}\", revision=1)\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificPath test.\"\"\"\n\n paths: list[RouterPath]\n \"\"\"List of router paths to verify.\"\"\"\n\n class RouterPath(BaseModel):\n \"\"\"Detail of a router path.\"\"\"\n\n peer: IPv4Address\n \"\"\"Static peer IPv4 address.\"\"\"\n\n path_group: str\n \"\"\"Router path group name.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of path.\"\"\"\n\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of path.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each router path.\"\"\"\n return [\n template.render(peer=path.peer, group=path.path_group, source=path.source_address, destination=path.destination_address) for path in self.inputs.paths\n ]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificPath.\"\"\"\n self.result.is_success()\n\n # Check the state of each path\n for command in self.instance_commands:\n peer = command.params.peer\n path_group = command.params.group\n source = command.params.source\n destination = command.params.destination\n command_output = command.json_output.get(\"dpsPeers\", [])\n\n # If the peer is not configured for the path group, the test fails\n if not command_output:\n self.result.is_failure(f\"Path `peer: {peer} source: {source} destination: {destination}` is not configured for path-group `{path_group}`.\")\n continue\n\n # Extract the state of the path\n path_output = get_value(command_output, f\"{peer}..dpsGroups..{path_group}..dpsPaths\", separator=\"..\")\n path_state = next(iter(path_output.values())).get(\"state\")\n session = get_value(next(iter(path_output.values())), \"dpsSessions.0.active\")\n\n # If the state of the path is not 'ipsecEstablished' or 'routeResolved', or the telemetry state is 'inactive', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `{path_state}`.\")\n elif not session:\n self.result.is_failure(\n f\"Telemetry state for path `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `inactive`.\"\n )\n"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"Inputs","text":"Name Type Description Default paths list[RouterPath] List of router paths to verify. -"},{"location":"api/tests.path_selection/#anta.tests.path_selection.VerifySpecificPath-attributes","title":"RouterPath","text":"Name Type Description Default peer IPv4Address Static peer IPv4 address. - path_group str Router path group name. - source_address IPv4Address Source IPv4 address of path. - destination_address IPv4Address Destination IPv4 address of path. -"},{"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 description = \"Verifies the device TCAM profile.\"\n categories: ClassVar[list[str]] = [\"profiles\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hardware tcam profile\", revision=1)]\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 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\", revision=1)]\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\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 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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpLockStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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. Skipped: The test will be skipped if PTP is not configured on the device. 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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpModeStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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 on the device. 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 on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpOffset:\n ```\n \"\"\"\n\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\", revision=1)]\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 description = \"Verifies the PTP interfaces state.\"\n categories: ClassVar[list[str]] = [\"ptp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", revision=2)]\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/#tests","title":"Tests","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 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\", revision=3)]\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 the advertised and received routes of BGP peers. 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 the advertised and received routes of BGP peers.\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show bgp neighbors {peer} advertised-routes vrf {vrf}\", revision=3),\n AntaTemplate(template=\"show bgp neighbors {peer} routes vrf {vrf}\", revision=3),\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 [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\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 = command.params.peer\n vrf = command.params.vrf\n for input_entry in self.inputs.bgp_peers:\n if str(input_entry.peer_address) == peer and input_entry.vrf == vrf:\n advertised_routes = input_entry.advertised_routes\n received_routes = input_entry.received_routes\n break\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 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\", revision=3)]\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 given address families. This test performs the following checks for each specified address family: Confirms that the specified VRF is configured. Counts the number of peers that are: If check_peer_state is set to True, Counts the number of BGP peers that are in the Established state and have successfully negotiated the specified AFI/SAFI If check_peer_state is set to False, skips validation of the Established state and AFI/SAFI negotiation. Expected Results Success: If the count of BGP peers matches the expected count with check_peer_state enabled/disabled. Failure: If any of the following occur: The specified VRF is not configured. The BGP peer count does not match expected value with check_peer_state enabled/disabled.\u201d 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 given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Confirms that the specified VRF is configured.\n 2. Counts the number of peers that are:\n - If `check_peer_state` is set to True, Counts the number of BGP peers that are in the `Established` state and\n have successfully negotiated the specified AFI/SAFI\n - If `check_peer_state` is set to False, skips validation of the `Established` state and AFI/SAFI negotiation.\n\n Expected Results\n ----------------\n * Success: If the count of BGP peers matches the expected count with `check_peer_state` enabled/disabled.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - The BGP peer count does not match expected value with `check_peer_state` enabled/disabled.\"\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp summary vrf all\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerCount test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'num_peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.num_peers is None:\n msg = f\"{af} 'num_peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerCount.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n peers_data = vrf_output.get(\"peers\", {}).values()\n if not address_family.check_peer_state:\n # Count the number of peers without considering the state and negotiated AFI/SAFI check if the count matches the expected count\n peer_count = sum(1 for peer_data in peers_data if address_family.eos_key in peer_data)\n else:\n # Count the number of established peers with negotiated AFI/SAFI\n peer_count = sum(\n 1\n for peer_data in peers_data\n if peer_data.get(\"peerState\") == \"Established\" and get_value(peer_data, f\"{address_family.eos_key}.afiSafiState\") == \"negotiated\"\n )\n\n # Check if the count matches the expected count\n if address_family.num_peers != peer_count:\n self.result.is_failure(f\"{address_family} - Expected: {address_family.num_peers}, Actual: {peer_count}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerCount-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats","title":"VerifyBGPPeerDropStats","text":"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s). By default, all drop statistics counters will be checked for any non-zero values. An optional list of specific drop statistics can be provided for granular testing. Expected Results Success: The test will pass if the BGP peer\u2019s drop statistic(s) are zero. Failure: The test will fail if the BGP peer\u2019s drop statistic(s) are non-zero/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerDropStats(AntaTest):\n \"\"\"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s).\n\n By default, all drop statistics counters will be checked for any non-zero values.\n An optional list of specific drop statistics can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's drop statistic(s) are zero.\n * Failure: The test will fail if the BGP peer's drop statistic(s) are non-zero/Not Found or peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerDropStats 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 drop_stats: list[BgpDropStats] | None = None\n \"\"\"Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerDropStats.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n drop_statistics = input_entry.drop_stats\n\n # Verify BGP peer\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify BGP peer's drop stats\n drop_stats_output = peer_detail.get(\"dropStats\", {})\n\n # In case drop stats not provided, It will check all drop statistics\n if not drop_statistics:\n drop_statistics = drop_stats_output\n\n # Verify BGP peer's drop stats\n drop_stats_not_ok = {\n drop_stat: drop_stats_output.get(drop_stat, \"Not Found\") for drop_stat in drop_statistics if drop_stats_output.get(drop_stat, \"Not Found\")\n }\n if any(drop_stats_not_ok):\n failures[peer] = {vrf: drop_stats_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero NLRI drop statistics counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerDropStats-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.VerifyBGPPeerDropStats-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' drop_stats list[BgpDropStats] | None Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics. None"},{"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 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\", revision=3)]\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. Supports strict: True to verify that only the specified capabilities are configured, requiring an exact match. 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 strict: False\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 Supports `strict: True` to verify that only the specified capabilities are configured, requiring an exact match.\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 strict: False\n capabilities:\n - ipv4Unicast\n ```\n \"\"\"\n\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\", revision=3)]\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 strict: bool = False\n \"\"\"If True, requires exact matching of provided capabilities. Defaults to False.\"\"\"\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 # Fetching the capabilities output.\n bgp_output = get_value(bgp_output, \"neighborCapabilities.multiprotocolCaps\")\n\n if bgp_peer.strict and sorted(capabilities) != sorted(bgp_output):\n failure[\"bgp_peers\"][peer][vrf] = {\n \"status\": f\"Expected only `{', '.join(capabilities)}` capabilities should be listed but found `{', '.join(bgp_output)}` instead.\"\n }\n failures = deep_update(failures, failure)\n continue\n\n # Check each capability\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' strict bool If True, requires exact matching of provided capabilities. Defaults to False. False capabilities list[MultiProtocolCaps] List of multiprotocol capabilities to be verified. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit","title":"VerifyBGPPeerRouteLimit","text":"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s). Expected Results Success: The test will pass if the BGP peer\u2019s maximum routes and, if provided, the maximum routes warning limit are equal to the given limits. Failure: The test will fail if the BGP peer\u2019s maximum routes do not match the given limit, or if the maximum routes warning limit is provided and does not match the given limit, or if the peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerRouteLimit(AntaTest):\n \"\"\"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's maximum routes and, if provided, the maximum routes warning limit are equal to the given limits.\n * Failure: The test will fail if the BGP peer's maximum routes do not match the given limit, or if the maximum routes warning limit is provided\n and does not match the given limit, or if the peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerRouteLimit 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 maximum_routes: int = Field(ge=0, le=4294967294)\n \"\"\"The maximum allowable number of BGP routes, `0` means unlimited.\"\"\"\n warning_limit: int = Field(default=0, ge=0, le=4294967294)\n \"\"\"Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerRouteLimit.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n maximum_routes = input_entry.maximum_routes\n warning_limit = input_entry.warning_limit\n failure: dict[Any, Any] = {}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify maximum routes configured.\n if (actual_routes := peer_detail.get(\"maxTotalRoutes\", \"Not Found\")) != maximum_routes:\n failure[\"Maximum total routes\"] = actual_routes\n\n # Verify warning limit if given.\n if warning_limit and (actual_warning_limit := peer_detail.get(\"totalRoutesWarnLimit\", \"Not Found\")) != warning_limit:\n failure[\"Warning limit\"] = actual_warning_limit\n\n # Updated failures if any.\n if failure:\n failures[peer] = {vrf: failure}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peer(s) are not configured or maximum routes and maximum routes warning limit is not correct:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerRouteLimit-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.VerifyBGPPeerRouteLimit-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' maximum_routes int The maximum allowable number of BGP routes, `0` means unlimited. Field(ge=0, le=4294967294) warning_limit int Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit. Field(default=0, ge=0, le=4294967294)"},{"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 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\", revision=3)]\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.VerifyBGPPeerUpdateErrors","title":"VerifyBGPPeerUpdateErrors","text":"Verifies BGP update error counters for the provided BGP IPv4 peer(s). By default, all update error counters will be checked for any non-zero values. An optional list of specific update error counters can be provided for granular testing. Note: For \u201cdisabledAfiSafi\u201d error counter field, checking that it\u2019s not \u201cNone\u201d versus 0. Expected Results Success: The test will pass if the BGP peer\u2019s update error counter(s) are zero/None. Failure: The test will fail if the BGP peer\u2019s update error counter(s) are non-zero/not None/Not Found or peer is not configured. Examples anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeerUpdateErrors(AntaTest):\n \"\"\"Verifies BGP update error counters for the provided BGP IPv4 peer(s).\n\n By default, all update error counters will be checked for any non-zero values.\n An optional list of specific update error counters can be provided for granular testing.\n\n Note: For \"disabledAfiSafi\" error counter field, checking that it's not \"None\" versus 0.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's update error counter(s) are zero/None.\n * Failure: The test will fail if the BGP peer's update error counter(s) are non-zero/not None/Not Found or\n peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerUpdateErrors 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 update_errors: list[BgpUpdateError] | None = None\n \"\"\"Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerUpdateErrors.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n update_error_counters = input_entry.update_errors\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Getting the BGP peer's error counters output.\n error_counters_output = peer_detail.get(\"peerInUpdateErrors\", {})\n\n # In case update error counters not provided, It will check all the update error counters.\n if not update_error_counters:\n update_error_counters = error_counters_output\n\n # verifying the error counters.\n error_counters_not_ok = {\n (\"disabledAfiSafi\" if error_counter == \"disabledAfiSafi\" else error_counter): value\n for error_counter in update_error_counters\n if (value := error_counters_output.get(error_counter, \"Not Found\")) != \"None\" and value != 0\n }\n if error_counters_not_ok:\n failures[peer] = {vrf: error_counters_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero update error counters:\\n{failures}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeerUpdateErrors-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.VerifyBGPPeerUpdateErrors-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' update_errors list[BgpUpdateError] | None Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters. None"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth","title":"VerifyBGPPeersHealth","text":"Verifies the health of BGP peers for given address families. This test performs the following checks for each specified address family: Validates that the VRF is configured. Checks if there are any peers for the given AFI/SAFI. For each relevant peer: Verifies that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Checks that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified address families and their peers. Failure: If any of the following occur: The specified VRF is not configured. No peers are found for a given AFI/SAFI. Any BGP session is not in the Established state. The AFI/SAFI state is not \u2018negotiated\u2019 for any peer. Any TCP message queue (input or output) is not empty when check_tcp_queues is True (default). 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 check_tcp_queues: false\n Source code in anta/tests/routing/bgp.py class VerifyBGPPeersHealth(AntaTest):\n \"\"\"Verifies the health of BGP peers for given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Validates that the VRF is configured.\n 2. Checks if there are any peers for the given AFI/SAFI.\n 3. For each relevant peer:\n - Verifies that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Checks that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified address families and their peers.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - No peers are found for a given AFI/SAFI.\n - Any BGP session is not in the `Established` state.\n - The AFI/SAFI state is not 'negotiated' for any peer.\n - Any TCP message queue (input or output) is not empty when `check_tcp_queues` is `True` (default).\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 check_tcp_queues: false\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeersHealth test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeersHealth.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n # Check if any peers are found for this AFI/SAFI\n relevant_peers = [\n peer for peer in vrf_output.get(\"peerList\", []) if get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\") is not None\n ]\n\n if not relevant_peers:\n self.result.is_failure(f\"{address_family} - No peers found\")\n continue\n\n for peer in relevant_peers:\n # Check if the BGP session is established\n if peer[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session state is not established; State: {peer['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPPeersHealth-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers","title":"VerifyBGPSpecificPeers","text":"Verifies the health of specific BGP peer(s) for given address families. This test performs the following checks for each specified address family and peer: Confirms that the specified VRF is configured. For each specified peer: Verifies that the peer is found in the BGP configuration. Checks that the BGP session is in the Established state. Confirms that the AFI/SAFI state is negotiated. Ensures that both input and output TCP message queues are empty. Can be disabled by setting check_tcp_queues to False. Expected Results Success: If all checks pass for all specified peers in all address families. Failure: If any of the following occur: The specified VRF is not configured. A specified peer is not found in the BGP configuration. The BGP session for a peer is not in the Established state. The AFI/SAFI state is not negotiated for a peer. Any TCP message queue (input or output) is not empty for a peer when check_tcp_queues is True (default). 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) for given address families.\n\n This test performs the following checks for each specified address family and peer:\n\n 1. Confirms that the specified VRF is configured.\n 2. For each specified peer:\n - Verifies that the peer is found in the BGP configuration.\n - Checks that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Ensures that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified peers in all address families.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - A specified peer is not found in the BGP configuration.\n - The BGP session for a peer is not in the `Established` state.\n - The AFI/SAFI state is not `negotiated` for a peer.\n - Any TCP message queue (input or output) is not empty for a peer when `check_tcp_queues` is `True` (default).\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPSpecificPeers test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.peers is None:\n msg = f\"{af} 'peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPSpecificPeers.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n for peer in address_family.peers:\n peer_ip = str(peer)\n\n # Check if the peer is found\n if (peer_data := get_item(vrf_output[\"peerList\"], \"peerAddress\", peer_ip)) is None:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Not configured\")\n continue\n\n # Check if the BGP session is established\n if peer_data[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session state is not established; State: {peer_data['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer_data, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not capability_status:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated\")\n\n if capability_status and not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer_data[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer_data[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBGPSpecificPeers-attributes","title":"Inputs","text":"Name Type Description Default address_families list[BgpAddressFamily] List of BGP address families. -"},{"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 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\", revision=3)]\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.VerifyBgpRouteMaps","title":"VerifyBgpRouteMaps","text":"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s). Expected Results Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF. Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction. Examples anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n Source code in anta/tests/routing/bgp.py class VerifyBgpRouteMaps(AntaTest):\n \"\"\"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF.\n * Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBgpRouteMaps 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 inbound_route_map: str | None = None\n \"\"\"Inbound route map applied, defaults to None.\"\"\"\n outbound_route_map: str | None = None\n \"\"\"Outbound route map applied, defaults to None.\"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpPeer class.\n\n At least one of 'inbound' or 'outbound' route-map must be provided.\n \"\"\"\n if not (self.inbound_route_map or self.outbound_route_map):\n msg = \"At least one of 'inbound_route_map' or 'outbound_route_map' must be provided.\"\n raise ValueError(msg)\n return self\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBgpRouteMaps.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n inbound_route_map = input_entry.inbound_route_map\n outbound_route_map = input_entry.outbound_route_map\n failure: dict[Any, Any] = {vrf: {}}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify Inbound route-map\n if inbound_route_map and (inbound_map := peer_detail.get(\"routeMapInbound\", \"Not Configured\")) != inbound_route_map:\n failure[vrf].update({\"Inbound route-map\": inbound_map})\n\n # Verify Outbound route-map\n if outbound_route_map and (outbound_map := peer_detail.get(\"routeMapOutbound\", \"Not Configured\")) != outbound_route_map:\n failure[vrf].update({\"Outbound route-map\": outbound_map})\n\n if failure[vrf]:\n failures[peer] = failure\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(\n f\"The following BGP peers are not configured or has an incorrect or missing route map in either the inbound or outbound direction:\\n{failures}\"\n )\n"},{"location":"api/tests.routing.bgp/#anta.tests.routing.bgp.VerifyBgpRouteMaps-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.VerifyBgpRouteMaps-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' inbound_route_map str | None Inbound route map applied, defaults to None. None outbound_route_map str | None Outbound route map applied, defaults to None. None"},{"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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp evpn route-type mac-ip {address} vni {vni}\", revision=2)]\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=str(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 = 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/#input-models","title":"Input models","text":""},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily","title":"BgpAddressFamily","text":"Model for a BGP address family. Name Type Description Default afi Afi BGP Address Family Identifier (AFI). - safi Safi | None BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`. None vrf str Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`. If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`. These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6. 'default' num_peers PositiveInt | None Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test. None peers list[IPv4Address | IPv6Address] | None List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test. None check_tcp_queues bool Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`. Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests. True check_peer_state bool Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`. Can be enabled in the `VerifyBGPPeerCount` tests. False Source code in anta/input_models/routing/bgp.py class BgpAddressFamily(BaseModel):\n \"\"\"Model for a BGP address family.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n afi: Afi\n \"\"\"BGP Address Family Identifier (AFI).\"\"\"\n safi: Safi | None = None\n \"\"\"BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`.\"\"\"\n vrf: str = \"default\"\n \"\"\"Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`.\n\n If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`.\n\n These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6.\n \"\"\"\n num_peers: PositiveInt | None = None\n \"\"\"Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test.\"\"\"\n peers: list[IPv4Address | IPv6Address] | None = None\n \"\"\"List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test.\"\"\"\n check_tcp_queues: bool = True\n \"\"\"Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`.\n\n Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests.\n \"\"\"\n check_peer_state: bool = False\n \"\"\"Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`.\n\n Can be enabled in the `VerifyBGPPeerCount` tests.\n \"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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 @property\n def eos_key(self) -> str:\n \"\"\"AFI/SAFI EOS key representation.\"\"\"\n # Pydantic handles the validation of the AFI/SAFI combination, so we can ignore error handling here.\n return AFI_SAFI_EOS_KEY[(self.afi, self.safi)]\n\n def __str__(self) -> str:\n \"\"\"Return a string representation of the BgpAddressFamily model. Used in failure messages.\n\n Examples\n --------\n - AFI:ipv4 SAFI:unicast VRF:default\n - AFI:evpn\n \"\"\"\n base_string = f\"AFI: {self.afi}\"\n if self.safi is not None:\n base_string += f\" SAFI: {self.safi}\"\n if self.afi in [\"ipv4\", \"ipv6\"]:\n base_string += f\" VRF: {self.vrf}\"\n return base_string\n"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAddressFamily.validate_inputs","title":"validate_inputs","text":"validate_inputs() -> Self\n Validate the inputs provided to the BgpAddressFamily class. If afi is either ipv4 or ipv6, safi must be provided. If afi is not ipv4 or ipv6, safi must NOT be provided and vrf must be default. Source code in anta/input_models/routing/bgp.py @model_validator(mode=\"after\")\ndef validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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"},{"location":"api/tests.routing.bgp/#anta.input_models.routing.bgp.BgpAfi","title":"BgpAfi","text":"Alias for the BgpAddressFamily model to maintain backward compatibility. When initialized, it will emit a deprecation warning and call the BgpAddressFamily model. TODO: Remove this class in ANTA v2.0.0. Source code in anta/input_models/routing/bgp.py class BgpAfi(BgpAddressFamily): # pragma: no cover\n \"\"\"Alias for the BgpAddressFamily model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the BgpAddressFamily model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the BgpAfi class, emitting a deprecation warning.\"\"\"\n warn(\n message=\"BgpAfi model is deprecated and will be removed in ANTA v2.0.0. Use the BgpAddressFamily model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\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. 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.\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 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 categories: ClassVar[list[str]] = [\"routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show ip route vrf {vrf} {route}\", revision=4),\n AntaTemplate(template=\"show ip route vrf {vrf}\", revision=4),\n ]\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 collect: Literal[\"one\", \"all\"] = \"one\"\n \"\"\"Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one`\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for the input vrf.\"\"\"\n if template == VerifyRoutingTableEntry.commands[0] and self.inputs.collect == \"one\":\n return [template.render(vrf=self.inputs.vrf, route=route) for route in self.inputs.routes]\n\n if template == VerifyRoutingTableEntry.commands[1] and self.inputs.collect == \"all\":\n return [template.render(vrf=self.inputs.vrf)]\n\n return []\n\n @staticmethod\n @cache\n def ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRoutingTableEntry.\"\"\"\n commands_output_route_ips = set()\n\n for command in self.instance_commands:\n command_output_vrf = command.json_output[\"vrfs\"][self.inputs.vrf]\n commands_output_route_ips |= {self.ip_interface_ip(route) for route in command_output_vrf[\"routes\"]}\n\n missing_routes = [str(route) for route in self.inputs.routes if route not in commands_output_route_ips]\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. - collect Literal['one', 'all'] Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one` 'one'"},{"location":"api/tests.routing.generic/#anta.tests.routing.generic.VerifyRoutingTableEntry.ip_interface_ip","title":"ip_interface_ip cached staticmethod","text":"ip_interface_ip(route: str) -> IPv4Address\n Return the IP address of the provided ip route with mask. Source code in anta/tests/routing/generic.py @staticmethod\n@cache\ndef ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n"},{"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 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: PositiveInteger\n \"\"\"Expected minimum routing table size.\"\"\"\n maximum: PositiveInteger\n \"\"\"Expected maximum routing table size.\"\"\"\n\n @model_validator(mode=\"after\")\n def check_min_max(self) -> Self:\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 PositiveInteger Expected minimum routing table size. - maximum PositiveInteger Expected maximum routing table size. -"},{"location":"api/tests.routing.isis/","title":"ISIS","text":""},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode","title":"VerifyISISInterfaceMode","text":"Verifies ISIS Interfaces are running in correct mode. Expected Results Success: The test will pass if all listed interfaces are running in correct mode. Failure: The test will fail if any of the listed interfaces is not running in correct mode. Skipped: The test will be skipped if no ISIS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISInterfaceMode(AntaTest):\n \"\"\"Verifies ISIS Interfaces are running in correct mode.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces are running in correct mode.\n * Failure: The test will fail if any of the listed interfaces is not running in correct mode.\n * Skipped: The test will be skipped if no ISIS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n ```\n \"\"\"\n\n description = \"Verifies interface mode for IS-IS\"\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceState]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceState(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n mode: Literal[\"point-to-point\", \"broadcast\", \"passive\"]\n \"\"\"Number of IS-IS neighbors.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF where the interface should be configured\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISInterfaceMode.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # Check for p2p interfaces\n for interface in self.inputs.interfaces:\n interface_data = _get_interface_data(\n interface=interface.name,\n vrf=interface.vrf,\n command_output=command_output,\n )\n # Check for correct VRF\n if interface_data is not None:\n interface_type = get_value(dictionary=interface_data, key=\"interfaceType\", default=\"unset\")\n # Check for interfaceType\n if interface.mode == \"point-to-point\" and interface.mode != interface_type:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in {interface.mode} reporting {interface_type}\")\n # Check for passive\n elif interface.mode == \"passive\":\n json_path = f\"intfLevels.{interface.level}.passive\"\n if interface_data is None or get_value(dictionary=interface_data, key=json_path, default=False) is False:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in passive mode\")\n else:\n self.result.is_failure(f\"Interface {interface.name} not found in VRF {interface.vrf}\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceState] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISInterfaceMode-attributes","title":"InterfaceState","text":"Name Type Description Default name Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 mode Literal['point-to-point', 'broadcast', 'passive'] Number of IS-IS neighbors. - vrf str VRF where the interface should be configured 'default'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount","title":"VerifyISISNeighborCount","text":"Verifies number of IS-IS neighbors per level and per interface. Expected Results Success: The test will pass if the number of neighbors is correct. Failure: The test will fail if the number of neighbors is incorrect. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborCount(AntaTest):\n \"\"\"Verifies number of IS-IS neighbors per level and per interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if the number of neighbors is correct.\n * Failure: The test will fail if the number of neighbors is incorrect.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceCount]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceCount(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: int = 2\n \"\"\"IS-IS level to check.\"\"\"\n count: int\n \"\"\"Number of IS-IS neighbors.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborCount.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n isis_neighbor_count = _get_isis_neighbors_count(command_output)\n if len(isis_neighbor_count) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n for interface in self.inputs.interfaces:\n eos_data = [ifl_data for ifl_data in isis_neighbor_count if ifl_data[\"interface\"] == interface.name and ifl_data[\"level\"] == interface.level]\n if not eos_data:\n self.result.is_failure(f\"No neighbor detected for interface {interface.name}\")\n continue\n if eos_data[0][\"count\"] != interface.count:\n self.result.is_failure(\n f\"Interface {interface.name}: \"\n f\"expected Level {interface.level}: count {interface.count}, \"\n f\"got Level {eos_data[0]['level']}: count {eos_data[0]['count']}\"\n )\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"Inputs","text":"Name Type Description Default interfaces list[InterfaceCount] list of interfaces with their information. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborCount-attributes","title":"InterfaceCount","text":"Name Type Description Default name Interface Interface name to check. - level int IS-IS level to check. 2 count int Number of IS-IS neighbors. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISNeighborState","title":"VerifyISISNeighborState","text":"Verifies all IS-IS neighbors are in UP state. Expected Results Success: The test will pass if all IS-IS neighbors are in UP state. Failure: The test will fail if some IS-IS neighbors are not in UP state. Skipped: The test will be skipped if no IS-IS neighbor is found. Examples anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n Source code in anta/tests/routing/isis.py class VerifyISISNeighborState(AntaTest):\n \"\"\"Verifies all IS-IS neighbors are in UP state.\n\n Expected Results\n ----------------\n * Success: The test will pass if all IS-IS neighbors are in UP state.\n * Failure: The test will fail if some IS-IS neighbors are not in UP state.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis neighbors\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborState.\"\"\"\n command_output = self.instance_commands[0].json_output\n if _count_isis_neighbor(command_output) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n self.result.is_success()\n not_full_neighbors = _get_not_full_isis_neighbors(command_output)\n if not_full_neighbors:\n self.result.is_failure(f\"Some neighbors are not in the correct state (UP): {not_full_neighbors}.\")\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments","title":"VerifyISISSegmentRoutingAdjacencySegments","text":"Verify that all expected Adjacency segments are correctly visible for each interface. Expected Results Success: The test will pass if all listed interfaces have correct adjacencies. Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies. Skipped: The test will be skipped if no ISIS SR Adjacency is found. Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):\n \"\"\"Verify that all expected Adjacency segments are correctly visible for each interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces have correct adjacencies.\n * Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies.\n * Skipped: The test will be skipped if no ISIS SR Adjacency is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing adjacency-segments\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingAdjacencySegments test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n segments: list[Segment]\n \"\"\"List of Adjacency segments configured in this instance.\"\"\"\n\n class Segment(BaseModel):\n \"\"\"Segment model definition.\"\"\"\n\n interface: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n sid_origin: Literal[\"dynamic\"] = \"dynamic\"\n \"\"\"Adjacency type\"\"\"\n address: IPv4Address\n \"\"\"IP address of remote end of segment.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingAdjacencySegments.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routging.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n for input_segment in instance.segments:\n eos_segment = _get_adjacency_segment_data_by_neighbor(\n neighbor=str(input_segment.address),\n instance=instance.name,\n vrf=instance.vrf,\n command_output=command_output,\n )\n if eos_segment is None:\n failure_message.append(f\"Your segment has not been found: {input_segment}.\")\n\n elif (\n eos_segment[\"localIntf\"] != input_segment.interface\n or eos_segment[\"level\"] != input_segment.level\n or eos_segment[\"sidOrigin\"] != input_segment.sid_origin\n ):\n failure_message.append(f\"Your segment is not correct: Expected: {input_segment} - Found: {eos_segment}.\")\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' segments list[Segment] List of Adjacency segments configured in this instance. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingAdjacencySegments-attributes","title":"Segment","text":"Name Type Description Default interface Interface Interface name to check. - level Literal[1, 2] ISIS level configured for interface. Default is 2. 2 sid_origin Literal['dynamic'] Adjacency type 'dynamic' address IPv4Address IP address of remote end of segment. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane","title":"VerifyISISSegmentRoutingDataplane","text":"Verify dataplane of a list of ISIS-SR instances. Expected Results Success: The test will pass if all instances have correct dataplane configured Failure: The test will fail if one of the instances has incorrect dataplane configured Skipped: The test will be skipped if ISIS is not running Examples anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingDataplane(AntaTest):\n \"\"\"Verify dataplane of a list of ISIS-SR instances.\n\n Expected Results\n ----------------\n * Success: The test will pass if all instances have correct dataplane configured\n * Failure: The test will fail if one of the instances has incorrect dataplane configured\n * Skipped: The test will be skipped if ISIS is not running\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingDataplane test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n dataplane: Literal[\"MPLS\", \"mpls\", \"unset\"] = \"MPLS\"\n \"\"\"Configured dataplane for the instance.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingDataplane.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routing.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n eos_dataplane = get_value(dictionary=command_output, key=f\"vrfs.{instance.vrf}.isisInstances.{instance.name}.dataPlane\", default=None)\n if instance.dataplane.upper() != eos_dataplane:\n failure_message.append(f\"ISIS instance {instance.name} is not running dataplane {instance.dataplane} ({eos_dataplane})\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingDataplane-attributes","title":"IsisInstance","text":"Name Type Description Default name str ISIS instance name. - vrf str VRF name where ISIS instance is configured. 'default' dataplane Literal['MPLS', 'mpls', 'unset'] Configured dataplane for the instance. 'MPLS'"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels","title":"VerifyISISSegmentRoutingTunnels","text":"Verify ISIS-SR tunnels computed by device. Expected Results Success: The test will pass if all listed tunnels are computed on device. Failure: The test will fail if one of the listed tunnels is missing. Skipped: The test will be skipped if ISIS-SR is not configured. Examples anta.tests.routing:\nisis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n Source code in anta/tests/routing/isis.py class VerifyISISSegmentRoutingTunnels(AntaTest):\n \"\"\"Verify ISIS-SR tunnels computed by device.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed tunnels are computed on device.\n * Failure: The test will fail if one of the listed tunnels is missing.\n * Skipped: The test will be skipped if ISIS-SR is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing tunnel\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingTunnels test.\"\"\"\n\n entries: list[Entry]\n \"\"\"List of tunnels to check on device.\"\"\"\n\n class Entry(BaseModel):\n \"\"\"Definition of a tunnel entry.\"\"\"\n\n endpoint: IPv4Network\n \"\"\"Endpoint IP of the tunnel.\"\"\"\n vias: list[Vias] | None = None\n \"\"\"Optional list of path to reach endpoint.\"\"\"\n\n class Vias(BaseModel):\n \"\"\"Definition of a tunnel path.\"\"\"\n\n nexthop: IPv4Address | None = None\n \"\"\"Nexthop of the tunnel. If None, then it is not tested. Default: None\"\"\"\n type: Literal[\"ip\", \"tunnel\"] | None = None\n \"\"\"Type of the tunnel. If None, then it is not tested. Default: None\"\"\"\n interface: Interface | None = None\n \"\"\"Interface of the tunnel. If None, then it is not tested. Default: None\"\"\"\n tunnel_id: Literal[\"TI-LFA\", \"ti-lfa\", \"unset\"] | None = None\n \"\"\"Computation method of the tunnel. If None, then it is not tested. Default: None\"\"\"\n\n def _eos_entry_lookup(self, search_value: IPv4Network, entries: dict[str, Any], search_key: str = \"endpoint\") -> dict[str, Any] | None:\n return next(\n (entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == str(search_value)),\n None,\n )\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingTunnels.\n\n This method performs the main test logic for verifying ISIS Segment Routing tunnels.\n It checks the command output, initiates defaults, and performs various checks on the tunnels.\n \"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n # initiate defaults\n failure_message = []\n\n if len(command_output[\"entries\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n for input_entry in self.inputs.entries:\n eos_entry = self._eos_entry_lookup(search_value=input_entry.endpoint, entries=command_output[\"entries\"])\n if eos_entry is None:\n failure_message.append(f\"Tunnel to {input_entry} is not found.\")\n elif input_entry.vias is not None:\n failure_src = []\n for via_input in input_entry.vias:\n if not self._check_tunnel_type(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel type\")\n if not self._check_tunnel_nexthop(via_input, eos_entry):\n failure_src.append(\"incorrect nexthop\")\n if not self._check_tunnel_interface(via_input, eos_entry):\n failure_src.append(\"incorrect interface\")\n if not self._check_tunnel_id(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel ID\")\n\n if failure_src:\n failure_message.append(f\"Tunnel to {input_entry.endpoint!s} is incorrect: {', '.join(failure_src)}\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n\n def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input tunnel type to check.\n eos_entry : dict[str, Any]\n The EOS entry containing the tunnel types.\n\n Returns\n -------\n bool\n True if the tunnel type matches any of the tunnel types in `eos_entry`, False otherwise.\n \"\"\"\n if via_input.type is not None:\n return any(\n via_input.type\n == get_value(\n dictionary=eos_via,\n key=\"type\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel nexthop matches the given input.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel nexthop matches, False otherwise.\n \"\"\"\n if via_input.nexthop is not None:\n return any(\n str(via_input.nexthop)\n == get_value(\n dictionary=eos_via,\n key=\"nexthop\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel interface exists in the given EOS entry.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel interface exists, False otherwise.\n \"\"\"\n if via_input.interface is not None:\n return any(\n via_input.interface\n == get_value(\n dictionary=eos_via,\n key=\"interface\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input vias to check.\n eos_entry : dict[str, Any])\n The EOS entry to compare against.\n\n Returns\n -------\n bool\n True if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias, False otherwise.\n \"\"\"\n if via_input.tunnel_id is not None:\n return any(\n via_input.tunnel_id.upper()\n == get_value(\n dictionary=eos_via,\n key=\"tunnelId.type\",\n default=\"undefined\",\n ).upper()\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Inputs","text":"Name Type Description Default entries list[Entry] List of tunnels to check on device. -"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Entry","text":"Name Type Description Default endpoint IPv4Network Endpoint IP of the tunnel. - vias list[Vias] | None Optional list of path to reach endpoint. None"},{"location":"api/tests.routing.isis/#anta.tests.routing.isis.VerifyISISSegmentRoutingTunnels-attributes","title":"Vias","text":"Name Type Description Default nexthop IPv4Address | None Nexthop of the tunnel. If None, then it is not tested. Default: None None type Literal['ip', 'tunnel'] | None Type of the tunnel. If None, then it is not tested. Default: None None interface Interface | None Interface of the tunnel. If None, then it is not tested. Default: None None tunnel_id Literal['TI-LFA', 'ti-lfa', 'unset'] | None Computation method of the tunnel. If None, then it is not tested. Default: None None"},{"location":"api/tests.routing.ospf/","title":"OSPF","text":""},{"location":"api/tests.routing.ospf/#anta.tests.routing.ospf.VerifyOSPFMaxLSA","title":"VerifyOSPFMaxLSA","text":"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold. Expected Results Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold. Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold. Skipped: The test will be skipped if no OSPF instance is found. Examples anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n Source code in anta/tests/routing/ospf.py class VerifyOSPFMaxLSA(AntaTest):\n \"\"\"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold.\n * Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold.\n * Skipped: The test will be skipped if no OSPF instance is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n ```\n \"\"\"\n\n description = \"Verifies all OSPF instances did not cross the maximum LSA threshold.\"\n categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyOSPFMaxLSA.\"\"\"\n command_output = self.instance_commands[0].json_output\n ospf_instance_info = _get_ospf_max_lsa_info(command_output)\n if not ospf_instance_info:\n self.result.is_skipped(\"No OSPF instance found.\")\n return\n all_instances_within_threshold = all(instance[\"numLsa\"] <= instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100) for instance in ospf_instance_info)\n if all_instances_within_threshold:\n self.result.is_success()\n else:\n exceeded_instances = [\n instance[\"instance\"] for instance in ospf_instance_info if instance[\"numLsa\"] > instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100)\n ]\n self.result.is_failure(f\"OSPF Instances {exceeded_instances} crossed the maximum LSA threshold.\")\n"},{"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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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.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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ip access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ipv6 access-list summary\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show management security ssl certificate\", revision=1),\n AntaCommand(command=\"show clock\", revision=1),\n ]\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) -> Self:\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 get_args(RsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for RSA encryption. Allowed sizes are {get_args(RsaKeySize)}.\"\n raise ValueError(msg)\n\n if self.encryption_algorithm == \"ECDSA\" and self.key_size not in get_args(EcdsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for ECDSA encryption. Allowed sizes are {get_args(EcdsaKeySize)}.\"\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner login\", revision=1)]\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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner motd\", revision=1)]\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.VerifyHardwareEntropy","title":"VerifyHardwareEntropy","text":"Verifies hardware entropy generation is enabled on device. Expected Results Success: The test will pass if hardware entropy generation is enabled. Failure: The test will fail if hardware entropy generation is not enabled. Examples anta.tests.security:\n - VerifyHardwareEntropy:\n Source code in anta/tests/security.py class VerifyHardwareEntropy(AntaTest):\n \"\"\"Verifies hardware entropy generation is enabled on device.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware entropy generation is enabled.\n * Failure: The test will fail if hardware entropy generation is not enabled.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyHardwareEntropy:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management security\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareEntropy.\"\"\"\n command_output = self.instance_commands[0].json_output\n\n # Check if hardware entropy generation is enabled.\n if not command_output.get(\"hardwareEntropyEnabled\"):\n self.result.is_failure(\"Hardware entropy generation is disabled.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.security/#anta.tests.security.VerifyIPSecConnHealth","title":"VerifyIPSecConnHealth","text":"Verifies all IPv4 security connections. Expected Results Success: The test will pass if all the IPv4 security connections are established in all vrf. Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf. Examples anta.tests.security:\n - VerifyIPSecConnHealth:\n Source code in anta/tests/security.py class VerifyIPSecConnHealth(AntaTest):\n \"\"\"Verifies all IPv4 security connections.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the IPv4 security connections are established in all vrf.\n * Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyIPSecConnHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip security connection vrf all\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyIPSecConnHealth.\"\"\"\n self.result.is_success()\n failure_conn = []\n command_output = self.instance_commands[0].json_output[\"connections\"]\n\n # Check if IP security connection is configured\n if not command_output:\n self.result.is_failure(\"No IPv4 security connection configured.\")\n return\n\n # Iterate over all ipsec connections\n for conn_data in command_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n failure_conn.append(f\"source:{source} destination:{destination} vrf:{vrf}\")\n if failure_conn:\n failure_msg = \"\\n\".join(failure_conn)\n self.result.is_failure(f\"The following IPv4 security connections are not established:\\n{failure_msg}.\")\n"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip access-lists {acl}\", revision=1)]\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) 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, acl in zip(self.instance_commands, self.inputs.ipv4_access_lists):\n # Collecting input ACL details\n acl_name = command_output.params.acl\n # Retrieve the expected entries from the inputs\n acl_entries = acl.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 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\", revision=1)]\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 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\", revision=1)]\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 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 try:\n line = next(line for line in command_output.split(\"\\n\") if line.startswith(\"SSHD status\"))\n except StopIteration:\n self.result.is_failure(\"Could not find SSH status in returned output.\")\n return\n status = line.split()[-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.VerifySpecificIPSecConn","title":"VerifySpecificIPSecConn","text":"Verifies the state of IPv4 security connections for a specified peer. It optionally allows for the verification of a specific path for a peer by providing source and destination addresses. If these addresses are not provided, it will verify all paths for the specified peer. Expected Results Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF. Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF. Examples anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n Source code in anta/tests/security.py class VerifySpecificIPSecConn(AntaTest):\n \"\"\"Verifies the state of IPv4 security connections for a specified peer.\n\n It optionally allows for the verification of a specific path for a peer by providing source and destination addresses.\n If these addresses are not provided, it will verify all paths for the specified peer.\n\n Expected Results\n ----------------\n * Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF.\n * Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n ```\n \"\"\"\n\n description = \"Verifies IPv4 security connections for a peer.\"\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip security connection vrf {vrf} path peer {peer}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificIPSecConn test.\"\"\"\n\n ip_security_connections: list[IPSecPeers]\n \"\"\"List of IP4v security peers.\"\"\"\n\n class IPSecPeers(BaseModel):\n \"\"\"Details of IPv4 security peers.\"\"\"\n\n peer: IPv4Address\n \"\"\"IPv4 address of the peer.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"Optional VRF for the IP security peer.\"\"\"\n\n connections: list[IPSecConn] | None = None\n \"\"\"Optional list of IPv4 security connections of a peer.\"\"\"\n\n class IPSecConn(BaseModel):\n \"\"\"Details of IPv4 security connections for a peer.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of the connection.\"\"\"\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of the connection.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input IP Sec connection.\"\"\"\n return [template.render(peer=conn.peer, vrf=conn.vrf) for conn in self.inputs.ip_security_connections]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificIPSecConn.\"\"\"\n self.result.is_success()\n for command_output, input_peer in zip(self.instance_commands, self.inputs.ip_security_connections):\n conn_output = command_output.json_output[\"connections\"]\n peer = command_output.params.peer\n vrf = command_output.params.vrf\n conn_input = input_peer.connections\n\n # Check if IPv4 security connection is configured\n if not conn_output:\n self.result.is_failure(f\"No IPv4 security connection configured for peer `{peer}`.\")\n continue\n\n # If connection details are not provided then check all connections of a peer\n if conn_input is None:\n for conn_data in conn_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source} destination:{destination} vrf:{vrf}` for peer `{peer}` is `Established` \"\n f\"but found `{state}` instead.\"\n )\n continue\n\n # Create a dictionary of existing connections for faster lookup\n existing_connections = {\n (conn_data.get(\"saddr\"), conn_data.get(\"daddr\"), conn_data.get(\"tunnelNs\")): next(iter(conn_data[\"pathDict\"].values()))\n for conn_data in conn_output.values()\n }\n for connection in conn_input:\n source_input = str(connection.source_address)\n destination_input = str(connection.destination_address)\n\n if (source_input, destination_input, vrf) in existing_connections:\n existing_state = existing_connections[(source_input, destination_input, vrf)]\n if existing_state != \"Established\":\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` \"\n f\"for peer `{peer}` is `Established` but found `{existing_state}` instead.\"\n )\n else:\n self.result.is_failure(\n f\"IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` for peer `{peer}` is not found.\"\n )\n"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"Inputs","text":"Name Type Description Default ip_security_connections list[IPSecPeers] List of IP4v security peers. -"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecPeers","text":"Name Type Description Default peer IPv4Address IPv4 address of the peer. - vrf str Optional VRF for the IP security peer. 'default' connections list[IPSecConn] | None Optional list of IPv4 security connections of a peer. None"},{"location":"api/tests.security/#anta.tests.security.VerifySpecificIPSecConn-attributes","title":"IPSecConn","text":"Name Type Description Default source_address IPv4Address Source IPv4 address of the connection. - destination_address IPv4Address Destination IPv4 address of the connection. -"},{"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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management telnet\", revision=1)]\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/#tests","title":"Tests","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 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}\", revision=1)]\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. This test performs the following checks for each specified DNS Server: Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF. Ensuring an appropriate priority level. 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 any of the following conditions are met: The provided DNS server is not configured. The provided DNS server with designated VRF and priority does not match the expected information. 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 This test performs the following checks for each specified DNS Server:\n\n 1. Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF.\n 2. Ensuring an appropriate priority level.\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 any of the following conditions are met:\n - The provided DNS server is not configured.\n - The provided DNS server with designated VRF and priority does not match the expected information.\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 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\", revision=1)]\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 DnsServer: ClassVar[type[DnsServer]] = DnsServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyDNSServers.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"nameServerConfigs\"]\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 # Check if the DNS server is configured with specified VRF.\n if (output := get_dict_superset(command_output, input_dict)) is None:\n self.result.is_failure(f\"{server} - Not configured\")\n continue\n\n # Check if the DNS server priority matches with expected.\n if output[\"priority\"] != priority:\n self.result.is_failure(f\"{server} - Incorrect priority; Priority: {output['priority']}\")\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.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 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 categories: ClassVar[list[str]] = [\"services\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hostname\", revision=1)]\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.services/#input-models","title":"Input models","text":""},{"location":"api/tests.services/#anta.input_models.services.DnsServer","title":"DnsServer","text":"Model for a DNS server configuration. Name Type Description Default server_address IPv4Address | IPv6Address The IPv4 or IPv6 address of the DNS server. - vrf str The VRF instance in which the DNS server resides. Defaults to 'default'. 'default' priority int The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest. Field(ge=0, le=4) Source code in anta/input_models/services.py class DnsServer(BaseModel):\n \"\"\"Model for a DNS server configuration.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: IPv4Address | IPv6Address\n \"\"\"The IPv4 or IPv6 address of the DNS server.\"\"\"\n vrf: str = \"default\"\n \"\"\"The VRF instance in which the DNS server resides. Defaults to 'default'.\"\"\"\n priority: int = Field(ge=0, le=4)\n \"\"\"The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the DnsServer for reporting.\n\n Examples\n --------\n Server 10.0.0.1 (VRF: default, Priority: 1)\n \"\"\"\n return f\"Server {self.server_address} (VRF: {self.vrf}, Priority: {self.priority})\"\n"},{"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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP contact is configured.\n if not (contact := get_value(self.instance_commands[0].json_output, \"contact.contact\")):\n self.result.is_failure(\"SNMP contact is not configured.\")\n return\n\n # Verifies the expected SNMP contact.\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.VerifySnmpErrorCounters","title":"VerifySnmpErrorCounters","text":"Verifies the SNMP error counters. By default, all error counters will be checked for any non-zero values. An optional list of specific error counters can be provided for granular testing. Expected Results Success: The test will pass if the SNMP error counter(s) are zero/None. Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured. Examples ```yaml anta.tests.snmp: - VerifySnmpErrorCounters: error_counters: - inVersionErrs - inBadCommunityNames Source code in anta/tests/snmp.py class VerifySnmpErrorCounters(AntaTest):\n \"\"\"Verifies the SNMP error counters.\n\n By default, all error counters will be checked for any non-zero values.\n An optional list of specific error counters can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP error counter(s) are zero/None.\n * Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpErrorCounters:\n error_counters:\n - inVersionErrs\n - inBadCommunityNames\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpErrorCounters test.\"\"\"\n\n error_counters: list[SnmpErrorCounter] | None = None\n \"\"\"Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpErrorCounters.\"\"\"\n error_counters = self.inputs.error_counters\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (snmp_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP error counters not provided, It will check all the error counters.\n if not error_counters:\n error_counters = list(get_args(SnmpErrorCounter))\n\n error_counters_not_ok = {counter: value for counter in error_counters if (value := snmp_counters.get(counter))}\n\n # Check if any failures\n if not error_counters_not_ok:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP error counters are not found or have non-zero error counters:\\n{error_counters_not_ok}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpErrorCounters-attributes","title":"Inputs","text":"Name Type Description Default error_counters list[SnmpErrorCounter] | None Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters. None"},{"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 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\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP location is configured.\n if not (location := get_value(self.instance_commands[0].json_output, \"location.location\")):\n self.result.is_failure(\"SNMP location is not configured.\")\n return\n\n # Verifies the expected SNMP location.\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.VerifySnmpPDUCounters","title":"VerifySnmpPDUCounters","text":"Verifies the SNMP PDU counters. By default, all SNMP PDU counters will be checked for any non-zero values. An optional list of specific SNMP PDU(s) can be provided for granular testing. Expected Results Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero. Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found. Examples anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n Source code in anta/tests/snmp.py class VerifySnmpPDUCounters(AntaTest):\n \"\"\"Verifies the SNMP PDU counters.\n\n By default, all SNMP PDU counters will be checked for any non-zero values.\n An optional list of specific SNMP PDU(s) can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero.\n * Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpPDUCounters test.\"\"\"\n\n pdus: list[SnmpPdu] | None = None\n \"\"\"Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpPDUCounters.\"\"\"\n snmp_pdus = self.inputs.pdus\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (pdu_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP PDUs not provided, It will check all the update error counters.\n if not snmp_pdus:\n snmp_pdus = list(get_args(SnmpPdu))\n\n failures = {pdu: value for pdu in snmp_pdus if (value := pdu_counters.get(pdu, \"Not Found\")) == \"Not Found\" or value == 0}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP PDU counters are not found or have zero PDU counters:\\n{failures}\")\n"},{"location":"api/tests.snmp/#anta.tests.snmp.VerifySnmpPDUCounters-attributes","title":"Inputs","text":"Name Type Description Default pdus list[SnmpPdu] | None Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters. None"},{"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 description = \"Verifies if the SNMP agent is enabled.\"\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show extensions\", revision=2),\n AntaCommand(command=\"show boot-extensions\", revision=1),\n ]\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 description = \"Verifies the EOS version of the device.\"\n categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree blockedports\", revision=1)]\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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree counters\", revision=1)]\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 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\", revision=1)]\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 vlan_id = command.params.vlan\n if not (topologies := get_value(command.json_output, \"topologies\")):\n not_configured.append(vlan_id)\n else:\n interfaces_not_forwarding = []\n for value in topologies.values():\n if vlan_id and 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 forwarding 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show spanning-tree vlan {vlan}\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree root detail\", revision=1)]\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.stp/#anta.tests.stp.VerifyStpTopologyChanges","title":"VerifyStpTopologyChanges","text":"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold. Expected Results Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold. Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold, indicating potential instability in the topology. Examples anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n Source code in anta/tests/stp.py class VerifyStpTopologyChanges(AntaTest):\n \"\"\"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold.\n * Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold,\n indicating potential instability in the topology.\n\n Examples\n --------\n ```yaml\n anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree topology status detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStpTopologyChanges test.\"\"\"\n\n threshold: int\n \"\"\"The threshold number of changes in the STP topology.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStpTopologyChanges.\"\"\"\n failures: dict[str, Any] = {\"topologies\": {}}\n\n command_output = self.instance_commands[0].json_output\n stp_topologies = command_output.get(\"topologies\", {})\n\n # verifies all available topologies except the \"NoStp\" topology.\n stp_topologies.pop(\"NoStp\", None)\n\n # Verify the STP topology(s).\n if not stp_topologies:\n self.result.is_failure(\"STP is not configured.\")\n return\n\n # Verifies the number of changes across all interfaces\n for topology, topology_details in stp_topologies.items():\n interfaces = {\n interface: {\"Number of changes\": num_of_changes}\n for interface, details in topology_details.get(\"interfaces\", {}).items()\n if (num_of_changes := details.get(\"numChanges\")) > self.inputs.threshold\n }\n if interfaces:\n failures[\"topologies\"][topology] = interfaces\n\n if failures[\"topologies\"]:\n self.result.is_failure(f\"The following STP topologies are not configured or number of changes not within the threshold:\\n{failures}\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.stp/#anta.tests.stp.VerifyStpTopologyChanges-attributes","title":"Inputs","text":"Name Type Description Default threshold int The threshold number of changes in the STP topology. -"},{"location":"api/tests.stun/","title":"STUN","text":""},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient","title":"VerifyStunClient","text":"Verifies STUN client settings, including local IP/port and optionally public IP/port. Expected Results Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port. Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect. Examples anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n Source code in anta/tests/stun.py class VerifyStunClient(AntaTest):\n \"\"\"Verifies STUN client settings, including local IP/port and optionally public IP/port.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port.\n * Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show stun client translations {source_address} {source_port}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStunClient test.\"\"\"\n\n stun_clients: list[ClientAddress]\n\n class ClientAddress(BaseModel):\n \"\"\"Source and public address/port details of STUN client.\"\"\"\n\n source_address: IPv4Address\n \"\"\"IPv4 source address of STUN client.\"\"\"\n source_port: Port = 4500\n \"\"\"Source port number for STUN client.\"\"\"\n public_address: IPv4Address | None = None\n \"\"\"Optional IPv4 public address of STUN client.\"\"\"\n public_port: Port | None = None\n \"\"\"Optional public port number for STUN client.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each STUN translation.\"\"\"\n return [template.render(source_address=client.source_address, source_port=client.source_port) for client in self.inputs.stun_clients]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunClient.\"\"\"\n self.result.is_success()\n\n # Iterate over each command output and corresponding client input\n for command, client_input in zip(self.instance_commands, self.inputs.stun_clients):\n bindings = command.json_output[\"bindings\"]\n source_address = str(command.params.source_address)\n source_port = command.params.source_port\n\n # If no bindings are found for the STUN client, mark the test as a failure and continue with the next client\n if not bindings:\n self.result.is_failure(f\"STUN client transaction for source `{source_address}:{source_port}` is not found.\")\n continue\n\n # Extract the public address and port from the client input\n public_address = client_input.public_address\n public_port = client_input.public_port\n\n # Extract the transaction ID from the bindings\n transaction_id = next(iter(bindings.keys()))\n\n # Prepare the actual and expected STUN data for comparison\n actual_stun_data = {\n \"source ip\": get_value(bindings, f\"{transaction_id}.sourceAddress.ip\"),\n \"source port\": get_value(bindings, f\"{transaction_id}.sourceAddress.port\"),\n }\n expected_stun_data = {\"source ip\": source_address, \"source port\": source_port}\n\n # If public address is provided, add it to the actual and expected STUN data\n if public_address is not None:\n actual_stun_data[\"public ip\"] = get_value(bindings, f\"{transaction_id}.publicAddress.ip\")\n expected_stun_data[\"public ip\"] = str(public_address)\n\n # If public port is provided, add it to the actual and expected STUN data\n if public_port is not None:\n actual_stun_data[\"public port\"] = get_value(bindings, f\"{transaction_id}.publicAddress.port\")\n expected_stun_data[\"public port\"] = public_port\n\n # If the actual STUN data does not match the expected STUN data, mark the test as failure\n if actual_stun_data != expected_stun_data:\n failed_log = get_failed_logs(expected_stun_data, actual_stun_data)\n self.result.is_failure(f\"For STUN source `{source_address}:{source_port}`:{failed_log}\")\n"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"Inputs","text":"Name Type Description Default"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunClient-attributes","title":"ClientAddress","text":"Name Type Description Default source_address IPv4Address IPv4 source address of STUN client. - source_port Port Source port number for STUN client. 4500 public_address IPv4Address | None Optional IPv4 public address of STUN client. None public_port Port | None Optional public port number for STUN client. None"},{"location":"api/tests.stun/#anta.tests.stun.VerifyStunServer","title":"VerifyStunServer","text":"Verifies the STUN server status is enabled and running. Expected Results Success: The test will pass if the STUN server status is enabled and running. Failure: The test will fail if the STUN server is disabled or not running. Examples anta.tests.stun:\n - VerifyStunServer:\n Source code in anta/tests/stun.py class VerifyStunServer(AntaTest):\n \"\"\"Verifies the STUN server status is enabled and running.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN server status is enabled and running.\n * Failure: The test will fail if the STUN server is disabled or not running.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunServer:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show stun server status\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunServer.\"\"\"\n command_output = self.instance_commands[0].json_output\n status_disabled = not command_output.get(\"enabled\")\n not_running = command_output.get(\"pid\") == 0\n\n if status_disabled and not_running:\n self.result.is_failure(\"STUN server status is disabled and not running.\")\n elif status_disabled:\n self.result.is_failure(\"STUN server status is disabled.\")\n elif not_running:\n self.result.is_failure(\"STUN server is not running.\")\n else:\n self.result.is_success()\n"},{"location":"api/tests.system/","title":"System","text":""},{"location":"api/tests.system/#tests","title":"Tests","text":""},{"location":"api/tests.system/#anta.tests.system.VerifyAgentLogs","title":"VerifyAgentLogs","text":"Verifies there are no agent crash reports. 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 there are no agent crash reports.\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show processes top once\", revision=1)]\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 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\", revision=1)]\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 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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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 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.VerifyNTPAssociations","title":"VerifyNTPAssociations","text":"Verifies the Network Time Protocol (NTP) associations. Expected Results Success: The test will pass if the Primary NTP server (marked as preferred) has the condition \u2018sys.peer\u2019 and all other NTP servers have the condition \u2018candidate\u2019. Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition \u2018sys.peer\u2019 or if any other NTP server does not have the condition \u2018candidate\u2019. Examples anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n Source code in anta/tests/system.py class VerifyNTPAssociations(AntaTest):\n \"\"\"Verifies the Network Time Protocol (NTP) associations.\n\n Expected Results\n ----------------\n * Success: The test will pass if the Primary NTP server (marked as preferred) has the condition 'sys.peer' and\n all other NTP servers have the condition 'candidate'.\n * Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition 'sys.peer' or\n if any other NTP server does not have the condition 'candidate'.\n\n Examples\n --------\n ```yaml\n anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ntp associations\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyNTPAssociations test.\"\"\"\n\n ntp_servers: list[NTPServer]\n \"\"\"List of NTP servers.\"\"\"\n NTPServer: ClassVar[type[NTPServer]] = NTPServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyNTPAssociations.\"\"\"\n self.result.is_success()\n\n if not (peers := get_value(self.instance_commands[0].json_output, \"peers\")):\n self.result.is_failure(\"No NTP peers configured\")\n return\n\n # Iterate over each NTP server.\n for ntp_server in self.inputs.ntp_servers:\n server_address = str(ntp_server.server_address)\n\n # We check `peerIpAddr` in the peer details - covering IPv4Address input, or the peer key - covering Hostname input.\n matching_peer = next((peer for peer, peer_details in peers.items() if (server_address in {peer_details[\"peerIpAddr\"], peer})), None)\n\n if not matching_peer:\n self.result.is_failure(f\"{ntp_server} - Not configured\")\n continue\n\n # Collecting the expected/actual NTP peer details.\n exp_condition = \"sys.peer\" if ntp_server.preferred else \"candidate\"\n exp_stratum = ntp_server.stratum\n act_condition = get_value(peers[matching_peer], \"condition\")\n act_stratum = get_value(peers[matching_peer], \"stratumLevel\")\n\n if act_condition != exp_condition or act_stratum != exp_stratum:\n self.result.is_failure(f\"{ntp_server} - Bad association; Condition: {act_condition}, Stratum: {act_stratum}\")\n"},{"location":"api/tests.system/#anta.tests.system.VerifyNTPAssociations-attributes","title":"Inputs","text":"Name Type Description Default ntp_servers list[NTPServer] List of NTP servers. -"},{"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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show reload cause\", revision=1)]\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 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 description = \"Verifies the device uptime.\"\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show uptime\", revision=1)]\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.system/#input-models","title":"Input models","text":""},{"location":"api/tests.system/#anta.input_models.system.NTPServer","title":"NTPServer","text":"Model for a NTP server. Name Type Description Default server_address Hostname | IPv4Address The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name. For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output. - preferred bool Optional preferred for NTP server. If not provided, it defaults to `False`. False stratum int NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized. Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state. Field(ge=0, le=16) Source code in anta/input_models/system.py class NTPServer(BaseModel):\n \"\"\"Model for a NTP server.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: Hostname | IPv4Address\n \"\"\"The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration\n of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name.\n For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output.\"\"\"\n preferred: bool = False\n \"\"\"Optional preferred for NTP server. If not provided, it defaults to `False`.\"\"\"\n stratum: int = Field(ge=0, le=16)\n \"\"\"NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized.\n Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Representation of the NTPServer model.\"\"\"\n return f\"{self.server_address} (Preferred: {self.preferred}, Stratum: {self.stratum})\"\n"},{"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 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\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\", revision=1)]\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 description = \"Verifies the Vxlan1 interface status.\"\n categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 there are no VXLAN config-sanity inconsistencies. 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 there are no VXLAN config-sanity inconsistencies.\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan config-sanity\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vni\", revision=1)]\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vtep\", revision=1)]\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[\n str, AfterValidator(aaa_group_prefix)\n]\n"},{"location":"api/types/#anta.custom_types.Afi","title":"Afi module-attribute","text":"Afi = Literal[\n \"ipv4\",\n \"ipv6\",\n \"vpn-ipv4\",\n \"vpn-ipv6\",\n \"evpn\",\n \"rt-membership\",\n \"path-selection\",\n \"link-state\",\n]\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.BfdProtocol","title":"BfdProtocol module-attribute","text":"BfdProtocol = Literal[\n \"bgp\",\n \"isis\",\n \"lag\",\n \"ospf\",\n \"ospfv3\",\n \"pim\",\n \"route-input\",\n \"static-bfd\",\n \"static-route\",\n \"vrrp\",\n \"vxlan\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpDropStats","title":"BgpDropStats module-attribute","text":"BgpDropStats = Literal[\n \"inDropAsloop\",\n \"inDropClusterIdLoop\",\n \"inDropMalformedMpbgp\",\n \"inDropOrigId\",\n \"inDropNhLocal\",\n \"inDropNhAfV6\",\n \"prefixDroppedMartianV4\",\n \"prefixDroppedMaxRouteLimitViolatedV4\",\n \"prefixDroppedMartianV6\",\n \"prefixDroppedMaxRouteLimitViolatedV6\",\n \"prefixLuDroppedV4\",\n \"prefixLuDroppedMartianV4\",\n \"prefixLuDroppedMaxRouteLimitViolatedV4\",\n \"prefixLuDroppedV6\",\n \"prefixLuDroppedMartianV6\",\n \"prefixLuDroppedMaxRouteLimitViolatedV6\",\n \"prefixEvpnDroppedUnsupportedRouteType\",\n \"prefixBgpLsDroppedReceptionUnsupported\",\n \"outDropV4LocalAddr\",\n \"outDropV6LocalAddr\",\n \"prefixVpnIpv4DroppedImportMatchFailure\",\n \"prefixVpnIpv4DroppedMaxRouteLimitViolated\",\n \"prefixVpnIpv6DroppedImportMatchFailure\",\n \"prefixVpnIpv6DroppedMaxRouteLimitViolated\",\n \"prefixEvpnDroppedImportMatchFailure\",\n \"prefixEvpnDroppedMaxRouteLimitViolated\",\n \"prefixRtMembershipDroppedLocalAsReject\",\n \"prefixRtMembershipDroppedMaxRouteLimitViolated\",\n]\n"},{"location":"api/types/#anta.custom_types.BgpUpdateError","title":"BgpUpdateError module-attribute","text":"BgpUpdateError = Literal[\n \"inUpdErrWithdraw\",\n \"inUpdErrIgnore\",\n \"inUpdErrDisableAfiSafi\",\n \"disabledAfiSafi\",\n \"lastUpdErrTime\",\n]\n"},{"location":"api/types/#anta.custom_types.EcdsaKeySize","title":"EcdsaKeySize module-attribute","text":"EcdsaKeySize = Literal[256, 384, 512]\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[\n \"acl\",\n \"arp-inspection\",\n \"bpduguard\",\n \"dot1x-session-replace\",\n \"hitless-reload-down\",\n \"lacp-rate-limit\",\n \"link-flap\",\n \"no-internal-vlan\",\n \"portchannelguard\",\n \"portsec\",\n \"tapagg\",\n \"uplink-failure-detection\",\n]\n"},{"location":"api/types/#anta.custom_types.EthernetInterface","title":"EthernetInterface module-attribute","text":"EthernetInterface = Annotated[\n str,\n Field(pattern=\"^Ethernet[0-9]+(\\\\/[0-9]+)*$\"),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\n"},{"location":"api/types/#anta.custom_types.Hostname","title":"Hostname module-attribute","text":"Hostname = Annotated[\n str, Field(pattern=REGEXP_TYPE_HOSTNAME)\n]\n"},{"location":"api/types/#anta.custom_types.Interface","title":"Interface module-attribute","text":"Interface = Annotated[\n str,\n Field(pattern=REGEXP_TYPE_EOS_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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[\n str,\n BeforeValidator(\n bgp_multiprotocol_capabilities_abbreviations\n ),\n]\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.PortChannelInterface","title":"PortChannelInterface module-attribute","text":"PortChannelInterface = Annotated[\n str,\n Field(pattern=REGEX_TYPE_PORTCHANNEL),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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.REGEXP_BGP_IPV4_MPLS_LABELS","title":"REGEXP_BGP_IPV4_MPLS_LABELS module-attribute","text":"REGEXP_BGP_IPV4_MPLS_LABELS = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?label(s)?)\\\\b\"\n)\n Match IPv4 MPLS Labels."},{"location":"api/types/#anta.custom_types.REGEXP_BGP_L2VPN_AFI","title":"REGEXP_BGP_L2VPN_AFI module-attribute","text":"REGEXP_BGP_L2VPN_AFI = \"\\\\b(l2[\\\\s\\\\-]?vpn[\\\\s\\\\-]?evpn)\\\\b\"\n Match L2VPN EVPN AFI."},{"location":"api/types/#anta.custom_types.REGEXP_EOS_BLACKLIST_CMDS","title":"REGEXP_EOS_BLACKLIST_CMDS module-attribute","text":"REGEXP_EOS_BLACKLIST_CMDS = [\n \"^reload.*\",\n \"^conf\\\\w*\\\\s*(terminal|session)*\",\n \"^wr\\\\w*\\\\s*\\\\w+\",\n]\n List of regular expressions to blacklist from eos commands."},{"location":"api/types/#anta.custom_types.REGEXP_INTERFACE_ID","title":"REGEXP_INTERFACE_ID module-attribute","text":"REGEXP_INTERFACE_ID = '\\\\d+(\\\\/\\\\d+)*(\\\\.\\\\d+)?'\n Match Interface ID lilke 1/1.1."},{"location":"api/types/#anta.custom_types.REGEXP_PATH_MARKERS","title":"REGEXP_PATH_MARKERS module-attribute","text":"REGEXP_PATH_MARKERS = '[\\\\\\\\\\\\/\\\\s]'\n Match directory path from string."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_EOS_INTERFACE","title":"REGEXP_TYPE_EOS_INTERFACE module-attribute","text":"REGEXP_TYPE_EOS_INTERFACE = \"^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\\\\/[0-9]+)*(\\\\.[0-9]+)?$\"\n Match EOS interface types like Ethernet1/1, Vlan1, Loopback1, etc."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_HOSTNAME","title":"REGEXP_TYPE_HOSTNAME module-attribute","text":"REGEXP_TYPE_HOSTNAME = \"^(([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 Match hostname like my-hostname, my-hostname-1, my-hostname-1-2."},{"location":"api/types/#anta.custom_types.REGEXP_TYPE_VXLAN_SRC_INTERFACE","title":"REGEXP_TYPE_VXLAN_SRC_INTERFACE module-attribute","text":"REGEXP_TYPE_VXLAN_SRC_INTERFACE = \"^(Loopback)([0-9]|[1-9][0-9]{1,2}|[1-7][0-9]{3}|8[01][0-9]{2}|819[01])$\"\n Match Vxlan source interface like Loopback10."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_MPLS_VPN","title":"REGEX_BGP_IPV4_MPLS_VPN module-attribute","text":"REGEX_BGP_IPV4_MPLS_VPN = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?vpn)\\\\b\"\n)\n Match IPv4 MPLS VPN."},{"location":"api/types/#anta.custom_types.REGEX_BGP_IPV4_UNICAST","title":"REGEX_BGP_IPV4_UNICAST module-attribute","text":"REGEX_BGP_IPV4_UNICAST = (\n \"\\\\b(ipv4[\\\\s\\\\-]?uni[\\\\s\\\\-]?cast)\\\\b\"\n)\n Match IPv4 Unicast."},{"location":"api/types/#anta.custom_types.REGEX_TYPE_PORTCHANNEL","title":"REGEX_TYPE_PORTCHANNEL module-attribute","text":"REGEX_TYPE_PORTCHANNEL = '^Port-Channel[0-9]{1,6}$'\n Match Port Channel interface like Port-Channel5."},{"location":"api/types/#anta.custom_types.RegexString","title":"RegexString module-attribute","text":"RegexString = Annotated[str, AfterValidator(validate_regex)]\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[\n \"unicast\", \"multicast\", \"labeled-unicast\", \"sr-te\"\n]\n"},{"location":"api/types/#anta.custom_types.SnmpErrorCounter","title":"SnmpErrorCounter module-attribute","text":"SnmpErrorCounter = Literal[\n \"inVersionErrs\",\n \"inBadCommunityNames\",\n \"inBadCommunityUses\",\n \"inParseErrs\",\n \"outTooBigErrs\",\n \"outNoSuchNameErrs\",\n \"outBadValueErrs\",\n \"outGeneralErrs\",\n]\n"},{"location":"api/types/#anta.custom_types.SnmpPdu","title":"SnmpPdu module-attribute","text":"SnmpPdu = Literal[\n \"inGetPdus\",\n \"inGetNextPdus\",\n \"inSetPdus\",\n \"outGetResponsePdus\",\n \"outTrapPdus\",\n]\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[\n str,\n Field(pattern=REGEXP_TYPE_VXLAN_SRC_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\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(\n value: str,\n) -> str\n Abbreviations for different BGP multiprotocol capabilities. Examples IPv4 Unicast L2vpnEVPN ipv4 MPLS Labels ipv4Mplsvpn 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 REGEXP_BGP_L2VPN_AFI: \"l2VpnEvpn\",\n REGEXP_BGP_IPV4_MPLS_LABELS: \"ipv4MplsLabels\",\n REGEX_BGP_IPV4_MPLS_VPN: \"ipv4MplsVpn\",\n REGEX_BGP_IPV4_UNICAST: \"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(REGEXP_INTERFACE_ID)\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 return next((f\"{full_name}{intf_id}\" for alias, full_name in alias_map.items() if v.lower().startswith(alias)), 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 vlan -> Vlan loopback -> Loopback 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 v != \"\" and not v[0].isupper():\n return f\"{v[0].upper()}{v[1:]}\"\n return v\n"},{"location":"api/types/#anta.custom_types.validate_regex","title":"validate_regex","text":"validate_regex(value: str) -> str\n Validate that the input value is a valid regex format. Source code in anta/custom_types.py def validate_regex(value: str) -> str:\n \"\"\"Validate that the input value is a valid regex format.\"\"\"\n try:\n re.compile(value)\n except re.error as e:\n msg = f\"Invalid regex: {e}\"\n raise ValueError(msg) from e\n return value\n"},{"location":"cli/check/","title":"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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n --help Show this message and exit.\n"},{"location":"cli/debug/","title":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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":"Usage: 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.\n\n 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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"},{"location":"cli/debug/#example-of-multiple-arguments","title":"Example of multiple arguments","text":"Warning If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters. 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":"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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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":"Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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_2024-04-09_15_56_19]\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":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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 [DEPRECATED] Ensure devices have 'aaa authorization\n exec default local' configured (required for SCP on\n EOS). THIS WILL CHANGE THE CONFIGURATION OF YOUR\n 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. Warning ANTA can automatically configure aaa authorization exec default local using the anta exec collect-tech-support --configure option but this option is deprecated and will be removed in ANTA 2.0.0. 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":"The ANTA CLI offers multiple commands to access data from your local inventory."},{"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 (visit this page to learn more about tags). The --connected option allows to display only the devices where a connection has been established."},{"location":"cli/get-inventory-information/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 By default, anta get inventory only provides information that doesn\u2019t rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, use the --connected option."},{"location":"cli/get-inventory-information/#example","title":"Example","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 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={'DC1-SPINE1', 'DC1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC1-SPINE2': AsyncEOSDevice(\n name='DC1-SPINE2',\n tags={'DC1', 'SPINE', 'DC1-SPINE2'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE1': AsyncEOSDevice(\n name='DC2-SPINE1',\n tags={'DC2', 'DC2-SPINE1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE2': AsyncEOSDevice(\n name='DC2-SPINE2',\n tags={'DC2', 'DC2-SPINE2', 'SPINE'},\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=False,\n insecure=False\n )\n}\n"},{"location":"cli/inv-from-ansible/","title":"Inventory from Ansible","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\n NOTE: This command does not support inline vaulted variables. Make sure to\n comment them out.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var:\n ANTA_INVENTORY; required]\n --overwrite Do not prompt when overriding current inventory\n [env var: ANTA_GET_FROM_ANSIBLE_OVERWRITE]\n -g, --ansible-group TEXT Ansible group to filter\n --ansible-inventory FILE Path to your ansible inventory file to read\n [required]\n --help Show this message and exit.\n Warnings anta get from-ansible does not support inline vaulted variables, comment them out to generate your inventory. If the vaulted variable is necessary to build the inventory (e.g. ansible_host), it needs to be unvaulted for from-ansible command to work.\u201d The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritance 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: ---\nall:\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 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"},{"location":"cli/inv-from-cvp/","title":"Inventory from CVP","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. Info The current implementation only works with on-premises CloudVision instances, not with CloudVision as a Service (CVaaS)."},{"location":"cli/inv-from-cvp/#command-overview","title":"Command overview","text":"Usage: anta get from-cvp [OPTIONS]\n\n Build ANTA inventory from CloudVision.\n\n NOTE: Only username/password authentication is supported for on-premises CloudVision instances.\n Token authentication for both on-premises and CloudVision as a Service (CVaaS) is not supported.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var: ANTA_INVENTORY;\n required]\n --overwrite Do not prompt when overriding current inventory [env\n var: ANTA_GET_FROM_CVP_OVERWRITE]\n -host, --host TEXT CloudVision instance FQDN or IP [required]\n -u, --username TEXT CloudVision username [required]\n -p, --password TEXT CloudVision password [required]\n -c, --container TEXT CloudVision container where devices are configured\n --ignore-cert By default connection to CV will use HTTPS\n certificate, set this flag to disable it [env var:\n ANTA_GET_FROM_CVP_IGNORE_CERT]\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":"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 report Table report JSON report Custom template report CSV report Markdown report "},{"location":"cli/nrfu/#nrfu-command-overview","title":"NRFU Command overview","text":"Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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. Refer to the dedicated page for more information."},{"location":"cli/nrfu/#device-and-test-filtering","title":"Device and test filtering","text":"Options --device and --test can be used to target one or multiple devices and/or tests to run in your environment. The options can be repeated. Example: anta nrfu --device leaf1a --device leaf1b --test VerifyUptime --test VerifyReloadCause."},{"location":"cli/nrfu/#hide-results","title":"Hide results","text":"Option --hide can be used to hide test results in the output or report file based on their status. The option can be repeated. Example: anta nrfu --hide error --hide skipped."},{"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":"Usage: anta nrfu text [OPTIONS]\n\n ANTA command to check network states with text result.\n\nOptions:\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example","title":"Example","text":"anta nrfu --device DC1-LEAF1A text\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":"Usage: anta nrfu table [OPTIONS]\n\n ANTA command to check network states with table result.\n\nOptions:\n --group-by [device|test] Group result by test or device.\n --help Show this message and exit.\n 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 --device spine1 table\n anta nrfu --test VerifyZeroTouch table\n "},{"location":"cli/nrfu/#performing-nrfu-with-json-rendering","title":"Performing NRFU with JSON rendering","text":"The JSON rendering command in NRFU testing will generate an output of all test results in JSON format."},{"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 JSON 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. If specified, no output will be displayed in the terminal. This is useful for further processing or integration with other tools."},{"location":"cli/nrfu/#example_1","title":"Example","text":"anta nrfu --tags LEAF json\n"},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-csv-file","title":"Performing NRFU and saving results in a CSV file","text":"The csv command in NRFU testing is useful for generating a CSV file with all tests result. This file can be easily analyzed and filtered by operator for reporting purposes."},{"location":"cli/nrfu/#command-overview_3","title":"Command overview","text":"anta nrfu csv --help\nUsage: anta nrfu csv [OPTIONS]\n\n ANTA command to check network states with CSV result.\n\nOptions:\n --csv-output FILE Path to save report as a CSV file [env var:\n ANTA_NRFU_CSV_CSV_OUTPUT]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_2","title":"Example","text":""},{"location":"cli/nrfu/#performing-nrfu-and-saving-results-in-a-markdown-file","title":"Performing NRFU and saving results in a Markdown file","text":"The md-report command in NRFU testing generates a comprehensive Markdown report containing various sections, including detailed statistics for devices and test categories."},{"location":"cli/nrfu/#command-overview_4","title":"Command overview","text":"anta nrfu md-report --help\n\nUsage: anta nrfu md-report [OPTIONS]\n\n ANTA command to check network state with Markdown report.\n\nOptions:\n --md-output FILE Path to save the report as a Markdown file [env var:\n ANTA_NRFU_MD_REPORT_MD_OUTPUT; required]\n --help Show this message and exit.\n"},{"location":"cli/nrfu/#example_3","title":"Example","text":""},{"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_5","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_4","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/nrfu/#dry-run-mode","title":"Dry-run mode","text":"It is possible to run anta nrfu --dry-run to execute ANTA up to the point where it should communicate with the network to execute the tests. When using --dry-run, all inventory devices are assumed to be online. This can be useful to check how many tests would be run using the catalog and inventory. "},{"location":"cli/overview/","title":"Overview","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."},{"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 selected inventory devices.\n"},{"location":"cli/overview/#anta-environment-variables","title":"ANTA environment 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_CATALOG=tests.yml\n Then, run the CLI without options: anta nrfu\n Note All environment variables may not be needed for every commands. Refer to <command> --help for the comprehensive environment variables names. Below are the environment 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":"ANTA uses tags to define test-to-device mappings (tests run on devices with matching tags) and the --tags CLI option acts as a filter to execute specific test/device combinations."},{"location":"cli/tag-management/#defining-tags","title":"Defining tags","text":""},{"location":"cli/tag-management/#device-tags","title":"Device tags","text":"Device tags can be defined in the inventory: anta_inventory:\n hosts:\n - name: leaf1\n host: leaf1.anta.arista.com\n tags: [\"leaf\"]\n - name: leaf2\n host: leaf2.anta.arista.com\n tags: [\"leaf\"]\n - name: spine1\n host: spine1.anta.arista.com\n tags: [\"spine\"]\n Each device also has its own name automatically added as a tag: $ anta get inventory\nCurrent inventory content is:\n{\n 'leaf1': AsyncEOSDevice(\n name='leaf1',\n tags={'leaf', 'leaf1'}, <--\n [...]\n host='leaf1.anta.arista.com',\n [...]\n ),\n 'leaf2': AsyncEOSDevice(\n name='leaf2',\n tags={'leaf', 'leaf2'}, <--\n [...]\n host='leaf2.anta.arista.com',\n [...]\n ),\n 'spine1': AsyncEOSDevice(\n name='spine1',\n tags={'spine1', 'spine'}, <--\n [...]\n host='spine1.anta.arista.com',\n [...]\n )\n}\n"},{"location":"cli/tag-management/#test-tags","title":"Test tags","text":"Tags can be defined in the test catalog to restrict tests to tagged devices: anta.tests.system:\n - VerifyUptime:\n minimum: 10\n filters:\n tags: ['spine']\n - VerifyUptime:\n minimum: 9\n filters:\n tags: ['leaf']\n - VerifyReloadCause:\n filters:\n tags: ['spine', 'leaf']\n - VerifyCoredump:\n - VerifyAgentLogs:\n - VerifyCPUUtilization:\n - VerifyMemoryUtilization:\n - VerifyFileSystemUtilization:\n - VerifyNTP:\n\nanta.tests.mlag:\n - VerifyMlagStatus:\n filters:\n tags: ['leaf']\n\nanta.tests.interfaces:\n - VerifyL3MTU:\n mtu: 1500\n filters:\n tags: ['spine']\n A tag used to filter a test can also be a device name Use different input values for a specific test Leverage tags to define different input values for a specific test. See the VerifyUptime example above."},{"location":"cli/tag-management/#using-tags","title":"Using tags","text":"Command Description No --tags option Run all tests on all devices according to the tag definitions in your inventory and test catalog. Tests without tags are executed on all devices. --tags leaf Run all tests marked with the leaf tag on all devices configured with the leaf tag. All other tests are ignored. --tags leaf,spine Run all tests marked with the leaf tag on all devices configured with the leaf tag.Run all tests marked with the spine tag on all devices configured with the spine tag. All other tests are ignored."},{"location":"cli/tag-management/#examples","title":"Examples","text":"The following examples use the inventory and test catalog defined above."},{"location":"cli/tag-management/#no-tags-option","title":"No --tags option","text":"Tests without tags are run on all devices. Tests with tags will only run on devices with matching tags. $ anta nrfu table --group-by device\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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 27\n---------------------------------\n Summary per device\n\u250f\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\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 leaf1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 leaf2 \u2502 7 \u2502 1 \u2502 1 \u2502 0 \u2502 VerifyAgentLogs \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 spine1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u2514\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/#single-tag","title":"Single tag","text":"With a tag specified, only tests matching this tag will be run on matching devices. $ anta nrfu --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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (2 established)\nTotal number of selected tests: 6\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\n In this case, only leaf devices defined in the inventory are used to run tests marked with the leaf in the test catalog."},{"location":"cli/tag-management/#multiple-tags","title":"Multiple tags","text":"It is possible to use multiple tags using the --tags tag1,tag2 syntax. $ anta nrfu --tags leaf,spine 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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 15\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyL3MTU :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyL3MTU :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyReloadCause :: SUCCESS\nspine1 :: VerifyMlagStatus :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyL3MTU :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\n"},{"location":"cli/tag-management/#obtaining-all-configured-tags","title":"Obtaining all configured tags","text":"As most ANTA commands accommodate tag filtering, this command is useful for enumerating all tags configured in the inventory. Running the anta get tags command will return a list of all tags configured in the inventory."},{"location":"cli/tag-management/#command-overview","title":"Command overview","text":"Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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/tag-management/#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 \"leaf\",\n \"leaf1\",\n \"leaf2\",\n \"spine\",\n \"spine1\"\n]\n"}]} \ No newline at end of file diff --git a/main/sitemap.xml.gz b/main/sitemap.xml.gz index cf5ee0d0e..74b57bcd3 100644 Binary files a/main/sitemap.xml.gz and b/main/sitemap.xml.gz differ diff --git a/main/troubleshooting/index.html b/main/troubleshooting/index.html index d8d155e45..d8db962e8 100644 --- a/main/troubleshooting/index.html +++ b/main/troubleshooting/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2418,7 +2418,7 @@ Troubleshooting on EOS - + @@ -2429,7 +2429,7 @@ Troubleshooting on EOS - + @@ -2440,7 +2440,7 @@ Troubleshooting on EOS - + diff --git a/main/usage-inventory-catalog/index.html b/main/usage-inventory-catalog/index.html index e066d66c8..272bea518 100644 --- a/main/usage-inventory-catalog/index.html +++ b/main/usage-inventory-catalog/index.html @@ -20,7 +20,7 @@ - + @@ -28,7 +28,7 @@ - + @@ -133,7 +133,7 @@ - + @@ -230,7 +230,7 @@ - + ANTA on Github @@ -266,7 +266,7 @@ - + Arista Network Test Automation - ANTA @@ -276,7 +276,7 @@ - + ANTA on Github @@ -2879,7 +2879,7 @@ Example script to merge catalogs - + @@ -2890,7 +2890,7 @@ Example script to merge catalogs - + @@ -2901,7 +2901,7 @@ Example script to merge catalogs - +
123 +124 +125 +126 +127 128 129 130 @@ -2666,11 +2662,7 @@ 190 191 192 -193 -194 -195 -196 -197
class VerifyFieldNotice72Resolution(AntaTest): +193
class VerifyFieldNotice72Resolution(AntaTest): """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 @@ -2834,7 +2826,7 @@ - + @@ -2845,7 +2837,7 @@ - + @@ -2856,7 +2848,7 @@ - +
ANTA is Python framework that automates tests for Arista devices.
The library will NOT install the necessary dependencies for the CLI.
# Install ANTA as a library\npip install anta\n
If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx is a tool to install and run python applications in isolated environments. Refer to pipx instructions to install on your system. pipx installs ANTA in an isolated python environment and makes it available globally.
pipx
This is not recommended if you plan to contribute to ANTA
# Install ANTA CLI with pipx\n$ pipx install anta[cli]\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
You can also still choose to install it with directly with pip:
pip
pip install anta[cli]\n
The documentation is published on ANTA package website.
Contributions are welcome. Please refer to the contribution guide
Thank you to Jeremy Schulman for aio-eapi.
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.
Contribution model is based on a fork-model. Don\u2019t push to aristanetworks/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.
Run the following commands to create an ANTA development environment:
# Clone repository\n$ git clone https://github.com/aristanetworks/anta.git\n$ cd anta\n\n# Install ANTA in editable mode and its development tools\n$ pip install -e .[dev]\n# To also install the CLI\n$ pip install -e .[dev,cli]\n\n# Verify installation\n$ pip list -e\nPackage Version Editable project location\n------- ------- -------------------------\nanta 1.1.0 /mnt/lab/projects/anta\n
Then, tox is configured with few environments to run CI locally:
tox
$ tox list -d\ndefault environments:\nclean -> Erase previous coverage reports\nlint -> Check the code style\ntype -> Check typing\npy39 -> Run pytest with py39\npy310 -> Run pytest with py310\npy311 -> Run pytest with py311\npy312 -> Run pytest with py312\nreport -> Generate coverage report\n
tox -e lint\n[...]\nlint: commands[0]> ruff check .\nAll checks passed!\nlint: commands[1]> ruff format . --check\n158 files already formatted\nlint: commands[2]> pylint anta\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\nlint: commands[3]> pylint tests\n\n--------------------------------------------------------------------\nYour code has been rated at 10.00/10 (previous run: 10.00/10, +0.00)\n\n lint: OK (22.69=setup[2.19]+cmd[0.02,0.02,9.71,10.75] seconds)\n congratulations :) (22.72 seconds)\n
tox -e type\n\n[...]\ntype: commands[0]> mypy --config-file=pyproject.toml anta\nSuccess: no issues found in 68 source files\ntype: commands[1]> mypy --config-file=pyproject.toml tests\nSuccess: no issues found in 82 source files\n type: OK (31.15=setup[14.62]+cmd[6.05,10.48] seconds)\n congratulations :) (31.18 seconds)\n
NOTE: Typing is configured quite strictly, do not hesitate to reach out if you have any questions, struggles, nightmares.
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.
tests/units/anta_tests/<submodule-name>.py
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.units.anta_tests module.
tests/units/anta_tests
tests.units.anta_tests
The pytest_generate_tests function definition in conftest.py is called during test collection.
pytest_generate_tests
conftest.py
The pytest_generate_tests function will parametrize the generic test function based on the DATA data structure defined in tests.units.anta_tests modules.
DATA
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
test
eos_data
inputs
expected
result
Literal['success', 'failure', 'unset', 'skipped', 'error']
messages
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.
anta.tests.system.VerifyUptime
# Import the generic test function\nfrom tests.units.anta_tests import test\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
pip install pre-commit\npre-commit install\n
When running a commit or a pre-commit check:
\u276f pre-commit\ntrim trailing whitespace.................................................Passed\nfix end of files.........................................................Passed\ncheck for added large files..............................................Passed\ncheck for merge conflicts................................................Passed\nCheck and insert license on Python files.................................Passed\nCheck and insert license on Markdown files...............................Passed\nRun Ruff linter..........................................................Passed\nRun Ruff formatter.......................................................Passed\nCheck code style with pylint.............................................Passed\nChecks for common misspellings in text files.............................Passed\nCheck typing with mypy...................................................Passed\nCheck Markdown files style...............................................Passed\n
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:
MYPYPATH
# Option 1: use local folder\nexport MYPYPATH=.\n\n# Option 2: use absolute path\nexport MYPYPATH=/path/to/your/local/anta/repository\n
mkdocs is used to generate the documentation. A PR should always update the documentation to avoid documentation debt.
mkdocs
Run pip to install the documentation requirements from the root of the repo:
pip install -e .[doc]\n
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
To build class diagram to use in API documentation, you can use pyreverse part of pylint with graphviz installed for jpeg generation.
pyreverse
pylint
graphviz
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.
docs/imgs/uml/
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
muffet -c 2 --color=always http://127.0.0.1:8000 -e fonts.gstatic.com -b 8192\n
GitHub actions is used to test git pushes and pull requests. The workflows are defined in this directory. The results can be viewed here.
When running ANTA, you can receive A local OS error occurred while connecting to <device> errors. The underlying OSError exception can have various reasons: [Errno 24] Too many open files or [Errno 16] Device or resource busy.
A local OS error occurred while connecting to <device>
OSError
[Errno 24] Too many open files
[Errno 16] Device or resource busy
This usually means that the operating system refused to open a new file descriptor (or socket) for the ANTA process. This might be due to the hard limit for open file descriptors currently set for the ANTA process.
At startup, ANTA sets the soft limit of its process to the hard limit up to 16384. This is because the soft limit is usually 1024 and the hard limit is usually higher (depends on the system). If the hard limit of the ANTA process is still lower than the number of selected tests in ANTA, the ANTA process may request to the operating system too many file descriptors and get an error, a WARNING is displayed at startup if this is the case.
One solution could be to raise the hard limit for the user starting the ANTA process. You can get the current hard limit for a user using the command ulimit -n -H while logged in. Create the file /etc/security/limits.d/10-anta.conf with the following content:
ulimit -n -H
/etc/security/limits.d/10-anta.conf
<user> hard nofile <value>\n
user
value
Timeout
When running ANTA, you can receive <Foo>Timeout errors in the logs (could be ReadTimeout, WriteTimeout, ConnectTimeout or PoolTimeout). More details on the timeouts of the underlying library are available here: https://www.python-httpx.org/advanced/timeouts.
<Foo>Timeout
This might be due to the time the host on which ANTA is run takes to reach the target devices (for instance if going through firewalls, NATs, \u2026) or when a lot of tests are being run at the same time on a device (eAPI has a queue mechanism to avoid exhausting EOS resources because of a high number of simultaneous eAPI requests).
Use the timeout option. As an example for the nrfu command:
timeout
nrfu
anta nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml --timeout 50 text\n
The previous command set a couple of options for ANTA NRFU, one them being the timeout command, by default, when running ANTA from CLI, it is set to 30s. The timeout is increased to 50s to allow ANTA to wait for API calls a little longer.
ImportError
urllib3
When running the anta --help command, some users might encounter the following error:
anta --help
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.
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
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
AttributeError: module 'lib' has no attribute 'OpenSSL_add_all_algorithms'
When running the anta commands after installation, some users might encounter the following error:
anta
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.
cryptography
pyopenssl
asyncssh
Upgrade pyopenssl
pip install -U pyopenssl>22.0\n
__NSCFConstantString initialize
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
Set the following environment variable
export OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES\n
If you\u2019ve tried the above solutions and continue to experience problems, please follow the troubleshooting instructions and report the issue in our GitHub repository.
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.
The easiest way to install ANTA package is to run Python (>=3.9) and its pip package to install:
>=3.9
For more details about how to install package, please see the requirements and installation section.
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
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: s1-spine1\n tags: ['fabric', 'spine']\n - host: 192.168.0.11\n name: s1-spine2\n tags: ['fabric', 'spine']\n - host: 192.168.0.12\n name: s1-leaf1\n tags: ['fabric', 'leaf']\n - host: 192.168.0.13\n name: s1-leaf2\n tags: ['fabric', 'leaf']\n - host: 192.168.0.14\n name: s1-leaf3\n tags: ['fabric', 'leaf']\n - host: 192.168.0.15\n name: s1-leaf3\n tags: ['fabric', 'leaf']\n
You can read more details about how to build your inventory here
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:
---\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\nanta.tests.mlag:\n - VerifyMlagStatus:\n - VerifyMlagInterfaces:\n - VerifyMlagConfigSanity:\n\nanta.tests.configuration:\n - VerifyZeroTouch: # Verifies ZeroTouch is disabled.\n - VerifyRunningConfigDiffs:\n
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.
Usage: 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 selected inventory devices.\n
Usage: anta nrfu [OPTIONS] COMMAND [ARGS]...\n\n Run ANTA tests on selected inventory devices.\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 '--\n prompt' 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\n EXEC mode. This option tries to access this\n mode before sending a command to the device.\n [env var: ANTA_ENABLE]\n -P, --prompt Prompt for passwords if they are not\n provided. [env var: ANTA_PROMPT]\n --timeout FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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 file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n -d, --device TEXT Run tests on a specific device. Can be\n provided multiple times.\n -t, --test TEXT Run a specific test. Can be provided\n multiple times.\n --ignore-status Exit code will always be 0. [env var:\n ANTA_NRFU_IGNORE_STATUS]\n --ignore-error Exit code will be 0 if all tests succeeded\n or 1 if any test failed. [env var:\n ANTA_NRFU_IGNORE_ERROR]\n --hide [success|failure|error|skipped]\n Hide results by type: success / failure /\n error / skipped'.\n --dry-run Run anta nrfu command but stop before\n starting to execute the tests. Considers all\n devices as connected. [env var:\n ANTA_NRFU_DRY_RUN]\n --help Show this message and exit.\n\nCommands:\n csv ANTA command to check network state with CSV report.\n json ANTA command to check network state with JSON results.\n md-report ANTA command to check network state with Markdown report.\n table ANTA command to check network state with table results.\n text ANTA command to check network state with text results.\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
table
Note
The following examples shows how to pass all the CLI options.
See how to use environment variables instead in the CLI overview
anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n `# table is default if not provided` \\\n table\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:01] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.058. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.069. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:02] INFO Running ANTA tests completed in: 0:00:00.969. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 \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\u2501\u2501\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\u2501\u2501\u2501\u2501\u2501\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 s1-spine1 \u2502 VerifyMlagConfigSanity \u2502 skipped \u2502 MLAG is disabled \u2502 Verifies there are no MLAG config-sanity \u2502 MLAG \u2502\n\u2502 \u2502 \u2502 \u2502 \u2502 inconsistencies. \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-spine1 \u2502 VerifyEOSVersion \u2502 failure \u2502 device is running version \u2502 Verifies the EOS version of the device. \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 \"4.32.2F-38195967.4322F (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)\" not in expected versions: \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['4.25.4M', '4.26.1F', \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 '4.28.3M-28837868.4283M (engineering \u2502 \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 build)'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n[...]\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyTerminAttrVersion \u2502 failure \u2502 device is running TerminAttr version \u2502 Verifies the TerminAttr version of the \u2502 Software \u2502\n\u2502 \u2502 \u2502 \u2502 v1.34.0 and is not in the allowed list: \u2502 device. \u2502 \u2502\n\u2502 \u2502 \u2502 \u2502 ['v1.22.1'] \u2502 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 s1-leaf3 \u2502 VerifyZeroTouch \u2502 success \u2502 \u2502 Verifies ZeroTouch is disabled \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\u2500\u2500\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\u2500\u2500\u2500\u2500\u2500\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
anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable` \\\n `# --enable-password <password>` \\\n --catalog ./catalog.yml \\\n 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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:52:39] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.057. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.068. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:52:40] INFO Running ANTA tests completed in: 0:00:00.863. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\n\ns1-spine1 :: VerifyEOSVersion :: FAILURE(device is running version \"4.32.2F-38195967.4322F (engineering build)\" not in expected versions: ['4.25.4M', '4.26.1F',\n'4.28.3M-28837868.4283M (engineering build)'])\ns1-spine1 :: VerifyTerminAttrVersion :: FAILURE(device is running TerminAttr version v1.34.0 and is not in the allowed list: ['v1.22.1'])\ns1-spine1 :: VerifyZeroTouch :: SUCCESS()\ns1-spine1 :: VerifyMlagConfigSanity :: SKIPPED(MLAG is disabled)\n
anta nrfu \\\n --username arista \\\n --password arista \\\n --inventory ./inventory.yml \\\n `# uncomment the next two lines if you have an enable password `\\\n `# --enable `\\\n `# --enable-password <password> `\\\n --catalog ./catalog.yml \\\n json\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 - ANTA Inventory contains 5 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 9 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[10:53:11] INFO Preparing ANTA NRFU Run ... tools.py:294\n INFO Connecting to devices ... tools.py:294\n INFO Connecting to devices completed in: 0:00:00.053. tools.py:302\n INFO Preparing the tests ... tools.py:294\n INFO Preparing the tests completed in: 0:00:00.001. tools.py:302\n INFO --- ANTA NRFU Run Information --- runner.py:276\n Number of devices: 5 (5 established)\n Total number of selected tests: 45\n Maximum number of open file descriptors for the current ANTA process: 16384\n ---------------------------------\n INFO Preparing ANTA NRFU Run completed in: 0:00:00.065. tools.py:302\n INFO Running ANTA tests ... tools.py:294\n[10:53:12] INFO Running ANTA tests completed in: 0:00:00.857. tools.py:302\n INFO Cache statistics for 's1-spine1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-spine2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf1': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf2': 1 hits / 9 command(s) (11.11%) runner.py:75\n INFO Cache statistics for 's1-leaf3': 1 hits / 9 command(s) (11.11%) 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 45/45 \u2022 0:00:00 \u2022 0:00:00\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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\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 \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\": \"s1-spine1\",\n \"test\": \"VerifyNTP\",\n \"categories\": [\n \"system\"\n ],\n \"description\": \"Verifies if NTP is synchronised.\",\n \"result\": \"success\",\n \"messages\": [],\n \"custom_field\": null\n },\n {\n \"name\": \"s1-spine1\",\n \"test\": \"VerifyMlagConfigSanity\",\n \"categories\": [\n \"mlag\"\n ],\n \"description\": \"Verifies there are no MLAG config-sanity inconsistencies.\",\n \"result\": \"skipped\",\n \"messages\": [\n \"MLAG is disabled\"\n ],\n \"custom_field\": null\n },\n [...]\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\"\"\"Example script for ANTA.\n\nusage:\n\npython anta_runner.py\n\"\"\"\n\nfrom __future__ import annotations\n\nimport asyncio\nimport logging\nimport sys\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.cli.nrfu.utils import anta_progress_bar\nfrom anta.inventory import AntaInventory\nfrom anta.logger import Log, setup_logging\nfrom anta.models import AntaTest\nfrom anta.result_manager import ResultManager\nfrom anta.runner import main as anta_runner\n\n# setup logging\nsetup_logging(Log.INFO, Path(\"/tmp/anta.log\"))\nLOGGER = logging.getLogger()\nSCRIPT_LOG_PREFIX = \"[bold magenta][ANTA RUNNER SCRIPT][/] \" # For convenience purpose - there are nicer way to do this.\n\n\n# NOTE: The inventory and catalog files are not delivered with this script\nUSERNAME = \"admin\"\nPASSWORD = \"admin\"\nCATALOG_PATH = Path(\"/tmp/anta_catalog.yml\")\nINVENTORY_PATH = Path(\"/tmp/anta_inventory.yml\")\n\n# Load catalog file\ntry:\n catalog = AntaCatalog.parse(CATALOG_PATH)\nexcept Exception:\n LOGGER.exception(\"%s Catalog failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Catalog loaded!\", SCRIPT_LOG_PREFIX)\n\n# Load inventory\ntry:\n inventory = AntaInventory.parse(INVENTORY_PATH, username=USERNAME, password=PASSWORD)\nexcept Exception:\n LOGGER.exception(\"%s Inventory failed to load!\", SCRIPT_LOG_PREFIX)\n sys.exit(1)\nLOGGER.info(\"%s Inventory loaded!\", SCRIPT_LOG_PREFIX)\n\n# Create result manager object\nmanager = ResultManager()\n\n# Launch ANTA\nLOGGER.info(\"%s Starting ANTA runner...\", SCRIPT_LOG_PREFIX)\nwith anta_progress_bar() as AntaTest.progress:\n # Set dry_run to True to avoid connecting to the devices\n asyncio.run(anta_runner(manager, inventory, catalog, dry_run=False))\n\nLOGGER.info(\"%s ANTA run completed!\", SCRIPT_LOG_PREFIX)\n\n# Manipulate the test result object\nfor test_result in manager.results:\n LOGGER.info(\"%s %s:%s:%s\", SCRIPT_LOG_PREFIX, test_result.name, test_result.test, test_result.result)\n
Python 3 (>=3.9) is required:
python --version\nPython 3.11.8\n
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.
pip install anta\n
Warning
pipx is a tool to install and run python applications in isolated environments. If you plan to use ANTA only as a CLI tool you can use pipx to install it. pipx installs ANTA in an isolated python environment and makes it available globally.
pipx install anta[cli]\n
Info
Please take the time to read through the installation instructions of pipx before getting started.
Alternatively, pip install with cli extra is enough to install the ANTA CLI.
cli
pip install git+https://github.com/aristanetworks/anta.git\npip install git+https://github.com/aristanetworks/anta.git#egg=anta[cli]\n\n# You can even specify the branch, tag or commit:\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>\npip install git+https://github.com/aristanetworks/anta.git@<cool-feature-branch>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>\npip install git+https://github.com/aristanetworks/anta.git@<cool-tag>#egg=anta[cli]\n\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>\npip install git+https://github.com/aristanetworks/anta.git@<more-or-less-cool-hash>#egg=anta[cli]\n
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
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.
anta --version
# Check ANTA version\nanta --version\nanta, version v1.1.0\n
To get ANTA working, the targeted Arista EOS devices must have eAPI enabled. They need to use 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
A couple of things to check when hitting an issue with ANTA:
flowchart LR\n A>Hitting an issue with ANTA] --> B{Is my issue <br >listed in the FAQ?}\n B -- Yes --> C{Does the FAQ solution<br />works for me?}\n C -- Yes --> V(((Victory)))\n B -->|No| E{Is my problem<br />mentioned in one<br />of the open issues?}\n C -->|No| E\n E -- Yes --> F{Has the issue been<br />fixed in a newer<br />release or in main?}\n F -- Yes --> U[Upgrade]\n E -- No ---> H((Follow the steps below<br />and open a Github issue))\n U --> I{Did it fix<br /> your problem}\n I -- Yes --> V\n I -- No --> H\n F -- No ----> G((Add a comment on the <br />issue indicating you<br >are hitting this and<br />describing your setup<br /> and adding your logs.))\n\n click B \"../faq\" \"FAQ\"\n click E \"https://github.com/aristanetworks/anta/issues\"\n click H \"https://github.com/aristanetworks/anta/issues\"\n style A stroke:#f00,stroke-width:2px
To help document the issue in Github, it is important to capture some logs so the developers can understand what is affecting your system. No logs mean that the first question asked on the issue will probably be \u201cCan you share some logs please?\u201d.
ANTA provides very verbose logs when using the DEBUG level. When using DEBUG log level with a log file, the DEBUG logging level is not sent to stdout, but only to the file.
DEBUG
Danger
On real deployments, do not use DEBUG logging level without setting a log file at the same time.
To save the logs to a file called anta.log, use the following flags:
anta.log
# Where ANTA_COMMAND is one of nrfu, debug, get, exec, check\nanta -l DEBUG \u2013log-file anta.log <ANTA_COMMAND>\n
See anta --help for more information. These have to precede the nrfu cmd.
Tip
Remember that in ANTA, each level of command has its own options and they can only be set at this level. so the -l and --log-file MUST be between anta and the ANTA_COMMAND. similarly, all the nrfu options MUST be set between the nrfu and the ANTA_NRFU_SUBCOMMAND (json, text, table or tpl-report).
-l
--log-file
ANTA_COMMAND
ANTA_NRFU_SUBCOMMAND
json
text
tpl-report
As an example, for the nrfu command, it would look like:
anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n
ANTA_DEBUG
Do not use this if you do not know why. This produces a lot of logs and can create confusion if you do not know what to look for.
The environment variable ANTA_DEBUG=true enable ANTA Debug Mode.
ANTA_DEBUG=true
This flag is used by various functions in ANTA: when set to true, the function will display or log more information. In particular, when an Exception occurs in the code and this variable is set, the logging function used by ANTA is different to also produce the Python traceback for debugging. This typically needs to be done when opening a GitHub issue and an Exception is seen at runtime.
Example:
ANTA_DEBUG=true anta -l DEBUG --log-file anta.log nrfu --enable --username username --password arista --inventory inventory.yml -c nrfu.yml text\n
ANTA is using a specific ID in eAPI requests towards EOS. This allows for easier eAPI requests debugging on the device using EOS configuration trace CapiApp setting UwsgiRequestContext/4,CapiUwsgiServer/4 to set up CapiApp agent logs.
trace CapiApp setting UwsgiRequestContext/4,CapiUwsgiServer/4
Then, you can view agent logs using:
bash tail -f /var/log/agents/CapiApp-*\n\n2024-05-15 15:32:54.056166 1429 UwsgiRequestContext 4 request content b'{\"jsonrpc\": \"2.0\", \"method\": \"runCmds\", \"params\": {\"version\": \"latest\", \"cmds\": [{\"cmd\": \"show ip route vrf default 10.255.0.3\", \"revision\": 4}], \"format\": \"json\", \"autoComplete\": false, \"expandAliases\": false}, \"id\": \"ANTA-VerifyRoutingTableEntry-132366530677328\"}'\n
The ANTA framework needs 2 important inputs from the user to run:
Both inputs can be defined in a file or programmatically.
A device inventory is an instance of the AntaInventory class.
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:
anta_inventory
hosts
networks
ranges
A full description of the inventory model is available in API documentation
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.
disable_cache
True
---\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
A test catalog is an instance of the AntaCatalog class.
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 in either YAML or JSON:
---\n<Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\n
{\n \"<Python module>\": [\n {\n \"<AntaTest subclass>\": <AntaTest.Input compliant dictionary>\n }\n ]\n}\n
---\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
or equivalent in JSON:
{\n \"anta.tests.connectivity\": [\n {\n \"VerifyReachability\": {\n \"result_overwrite\": {\n \"description\": \"Test with overwritten description\",\n \"categories\": [\n \"Overwritten category 1\"\n ],\n \"custom_field\": \"Test run by John Doe\"\n },\n \"filters\": {\n \"tags\": [\n \"leaf\"\n ]\n },\n \"hosts\": [\n {\n \"destination\": \"1.1.1.1\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n },\n {\n \"destination\": \"8.8.8.8\",\n \"source\": \"Management0\",\n \"vrf\": \"MGMT\"\n }\n ]\n }\n }\n ]\n}\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.
anta.tests
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
When using the CLI, you can filter the NRFU execution using tags. Refer to this section of the CLI documentation.
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 or JSON file:
VerifyEOSVersion
anta.tests.software
anta.tests.software:\n - VerifyEOSVersion:\n # List of allowed EOS versions.\n versions:\n - 4.25.4M\n - 4.26.1F\n
{\n \"anta.tests.software\": [\n {\n \"VerifyEOSVersion\": {\n \"versions\": [\n \"4.25.4M\",\n \"4.31.1F\"\n ]\n }\n }\n ]\n}\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
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 custom.tests.system Python module, the test catalog will be:
custom.tests.system
custom.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
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.
categories
description
In your test catalog, use result_overwrite dictionary with categories and description to just overwrite this values in your report:
result_overwrite
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:
anta nrfu table
\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
The following script reads all the files in intended/test_catalogs/ with names <device_name>-catalog.yml and merge them together inside one big catalog anta-catalog.yml using the new AntaCatalog.merge_catalogs() class method.
intended/test_catalogs/
<device_name>-catalog.yml
anta-catalog.yml
AntaCatalog.merge_catalogs()
# Copyright (c) 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\"\"\"Script that merge a collection of catalogs into one AntaCatalog.\"\"\"\n\nfrom pathlib import Path\n\nfrom anta.catalog import AntaCatalog\nfrom anta.models import AntaTest\n\nCATALOG_SUFFIX = \"-catalog.yml\"\nCATALOG_DIR = \"intended/test_catalogs/\"\n\nif __name__ == \"__main__\":\n catalogs = []\n for file in Path(CATALOG_DIR).glob(\"*\" + CATALOG_SUFFIX):\n device = str(file).removesuffix(CATALOG_SUFFIX).removeprefix(CATALOG_DIR)\n print(f\"Loading test catalog for device {device}\")\n catalog = AntaCatalog.parse(file)\n # Add the device name as a tag to all tests in the catalog\n for test in catalog.tests:\n test.inputs.filters = AntaTest.Input.Filters(tags={device})\n catalogs.append(catalog)\n\n # Merge all catalogs\n merged_catalog = AntaCatalog.merge_catalogs(catalogs)\n\n # Save the merged catalog to a file\n with Path(\"anta-catalog.yml\").open(\"w\") as f:\n f.write(merged_catalog.dump().yaml())\n
The AntaCatalog.merge() method is deprecated and will be removed in ANTA v2.0. Please use the AntaCatalog.merge_catalogs() class method instead.
AntaCatalog.merge()
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.
If you are unfamiliar with asyncio, refer to the Python documentation relevant to your Python version - https://docs.python.org/3/library/asyncio.html
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 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.
The AsyncEOSDevice class is an implementation of AntaDevice for Arista EOS. It uses the aio-eapi eAPI client and the AsyncSSH library.
is_online
show version
established
hw_model
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:
# Copyright (c) 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\"\"\"Script that parses an ANTA inventory file, connects to devices and print their status.\"\"\"\n\nimport asyncio\n\nfrom anta.inventory import AntaInventory\n\n\nasync def main(inv: AntaInventory) -> None:\n \"\"\"Read an AntaInventory and try to connect to every device in the inventory.\n\n 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\n\nif __name__ == \"__main__\":\n # Create the AntaInventory instance\n inventory = AntaInventory.parse(\n filename=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\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.
# Copyright (c) 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\"\"\"Script that runs a list of EOS commands on reachable devices.\"\"\"\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 \"\"\"Run a list of commands against each valid device in the inventory.\n\n Take an AntaInventory and a list of commands as string\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 -------\n dict[str, list[AntaCommand]]\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=\"inventory.yaml\",\n username=\"arista\",\n password=\"@rista123\",\n )\n\n # Create a list of commands with json output\n command_list = [\"show version\", \"show ip bgp summary\"]\n\n # Run the main asyncio entry point\n res = asyncio.run(main(inventory, command_list))\n\n pprint(res)\n
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.
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.
SimpleMemoryCache
asyncio
The _init_cache() method of the AntaDevice abstract class initializes the cache. Child classes can override this method to tweak the cache configuration:
_init_cache()
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.
aiocache
HitMissRatioPlugin
The cache is initialized per AntaDevice and uses the following cache key design:
AntaDevice
<device_name>:<uid>
The uid is an attribute of AntaCommand, which is a unique identifier generated from the command, version, revision and output format.
uid
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.
self.cache_locks
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.
collect()
_collect()
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:
Caching can be disabled globally, for ALL commands on ALL devices, using the --disable-cache global flag when invoking anta at the CLI:
--disable-cache
anta --disable-cache --username arista --password arista nrfu table\n
Caching can be disabled per device, network or range by setting the disable_cache key to True when defining 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.
For tests developers, 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.
AntaCommand
AntaTemplate
use_cache
False
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: set = None) -> None:\n super().__init__(name, tags, disable_cache=True)\n
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.
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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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.
Full AntaTest API documentation is available in the API documentation section
str
optional
list[str]
commands
[list[AntaCommand | AntaTemplate]]
All these class attributes are mandatory. If any attribute is missing, a NotImplementedError exception will be raised during class instantiation.
NotImplementedError
Attributes:
device
AntaDevice instance on which this test is run.
Input
AntaTest.Input instance carrying the test inputs.
instance_commands
list[AntaCommand]
List of AntaCommand instances of this test.
TestResult
TestResult instance representing the result of this test.
logger
Logger
Python logger for this test instance.
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.
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:
Full Input model documentation is available in API documentation section
ResultOverwrite | None
Define fields to overwrite in the TestResult object.
Full ResultOverwrite model documentation is available in API documentation section
ResultOverwrite
str | None
Overwrite TestResult.description.
TestResult.description
list[str] | None
Overwrite TestResult.categories.
TestResult.categories
custom_field
A free string that will be included in the TestResult object.
The pydantic model is configured using the extra=forbid that will fail input validation if extra fields are provided.
extra=forbid
AntaTest.anta_test
test()
Below is a high level description of the test execution flow in ANTA:
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.
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.
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.
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.
The test() method is executed.
In this section, we will go into all the details of writing an AntaTest subclass.
Import anta.models.AntaTest and define your own class. Define the mandatory class attributes using anta.models.AntaCommand, anta.models.AntaTemplate or both.
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, the first line is used as description of the test by default>\n \"\"\"\n\n # name = <override> # uncomment to override default behavior of name=Class Name\n # description = <override> # uncomment to override default behavior of description=first line of docstring\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
Command revision and version
1
latest
version=1
By default, ANTA uses version=\"latest\" in AntaCommand, but when developing tests, the revision MUST be provided when the outformat of the command is json. As explained earlier, this is to ensure that the eAPI always returns the same output model and that the test remains always valid from the day it was created. For some commands, you may also want to run them with a different revision or version.
version=\"latest\"
For instance, the VerifyBFDPeersHealth test leverages the first revision of show bfd peers:
VerifyBFDPeersHealth
show bfd peers
# revision 1 as later revision introduce additional nesting for type\ncommands = [AntaCommand(command=\"show bfd peers\", revision=1)]\n
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.
All the pydantic features are supported. For instance you can define validators for complex input validation.
pydantic
Define the render() method if you have AntaTemplate instances in your commands class attribute:
render()
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.
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:
self.instance_commands
self.inputs
self.result.is_success()
self.result.is_failure(\"<FAILURE REASON>\")
skipped
self.result.is_skipped(\"<SKIPPED REASON>\")
error
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:
AntaTest.anta_tests
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:
true
$ ANTA_DEBUG=true anta nrfu --catalog test_custom.yml text\n
In addition to the required AntaTest.anta_tests decorator, ANTA offers a set of optional decorators for further test customization:
anta.decorators.deprecated_test
anta.decorators.skip_on_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
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).
sys.path
PYTHONPATH
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_custom and the test is defined in anta_custom.dc_project Python module, the test catalog would look like:
anta_custom
anta_custom.dc_project
anta_custom.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
AntaCatalog(\n tests: list[AntaTestDefinition] | None = None,\n filename: str | Path | None = None,\n)\n
Class representing an ANTA Catalog.
It can be instantiated using its constructor or one of the static methods: parse(), from_list() or from_dict()
parse()
from_list()
from_dict()
Parameters:
tests
list[AntaTestDefinition] | None
A list of AntaTestDefinition instances.
None
filename
str | Path | None
The path from which the catalog is loaded.
property
filename: Path | None\n
Path of the file used to create this AntaCatalog instance.
writable
tests: list[AntaTestDefinition]\n
List of AntaTestDefinition in this catalog.
build_indexes(\n filtered_tests: set[str] | None = None,\n) -> None\n
Indexes tests by their tags for quick access during filtering operations.
If a filtered_tests set is provided, only the tests in this set will be indexed.
filtered_tests
This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests.
Once the indexes are built, the indexes_built attribute is set to True.
indexes_built
anta/catalog.py
def build_indexes(self, filtered_tests: set[str] | None = None) -> None:\n \"\"\"Indexes tests by their tags for quick access during filtering operations.\n\n If a `filtered_tests` set is provided, only the tests in this set will be indexed.\n\n This method populates the tag_to_tests attribute, which is a dictionary mapping tags to sets of tests.\n\n Once the indexes are built, the `indexes_built` attribute is set to True.\n \"\"\"\n for test in self.tests:\n # Skip tests that are not in the specified filtered_tests set\n if filtered_tests and test.test.name not in filtered_tests:\n continue\n\n # Indexing by tag\n if test.inputs.filters and (test_tags := test.inputs.filters.tags):\n for tag in test_tags:\n self.tag_to_tests[tag].add(test)\n else:\n self.tag_to_tests[None].add(test)\n\n self.indexes_built = True\n
clear_indexes() -> None\n
Clear this AntaCatalog instance indexes.
def clear_indexes(self) -> None:\n \"\"\"Clear this AntaCatalog instance indexes.\"\"\"\n self._init_indexes()\n
dump() -> AntaCatalogFile\n
Return an AntaCatalogFile instance from this AntaCatalog instance.
Returns:
AntaCatalogFile
An AntaCatalogFile instance containing tests of this AntaCatalog instance.
def dump(self) -> AntaCatalogFile:\n \"\"\"Return an AntaCatalogFile instance from this AntaCatalog instance.\n\n Returns\n -------\n AntaCatalogFile\n An AntaCatalogFile instance containing tests of this AntaCatalog instance.\n \"\"\"\n root: dict[ImportString[Any], list[AntaTestDefinition]] = {}\n for test in self.tests:\n # Cannot use AntaTest.module property as the class is not instantiated\n root.setdefault(test.test.__module__, []).append(test)\n return AntaCatalogFile(root=root)\n
staticmethod
from_dict(\n data: RawCatalogInput,\n filename: str | Path | None = None,\n) -> 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.
yaml.load()
data
RawCatalogInput
Python dictionary used to instantiate the AntaCatalog instance.
value to be set as AntaCatalog instance attribute
AntaCatalog
An AntaCatalog populated with the \u2018data\u2019 dictionary content.
@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 Parameters\n ----------\n data\n Python dictionary used to instantiate the AntaCatalog instance.\n filename\n value to be set as AntaCatalog instance attribute\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' dictionary content.\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
from_list(data: ListAntaTestTuples) -> AntaCatalog\n
Create an AntaCatalog instance from a list data structure.
See ListAntaTestTuples type alias for details.
ListAntaTestTuples
Python list used to instantiate the AntaCatalog instance.
An AntaCatalog populated with the \u2018data\u2019 list content.
@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 Parameters\n ----------\n data\n Python list used to instantiate the AntaCatalog instance.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the 'data' list content.\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
get_tests_by_tags(\n tags: set[str], *, strict: bool = False\n) -> set[AntaTestDefinition]\n
Return all tests that match a given set of tags, according to the specified strictness.
tags
set[str]
The tags to filter tests by. If empty, return all tests without tags.
strict
bool
If True, returns only tests that contain all specified tags (intersection). If False, returns tests that contain any of the specified tags (union).
set[AntaTestDefinition]
A set of tests that match the given tags.
Raises:
ValueError
If the indexes have not been built prior to method call.
def get_tests_by_tags(self, tags: set[str], *, strict: bool = False) -> set[AntaTestDefinition]:\n \"\"\"Return all tests that match a given set of tags, according to the specified strictness.\n\n Parameters\n ----------\n tags\n The tags to filter tests by. If empty, return all tests without tags.\n strict\n If True, returns only tests that contain all specified tags (intersection).\n If False, returns tests that contain any of the specified tags (union).\n\n Returns\n -------\n set[AntaTestDefinition]\n A set of tests that match the given tags.\n\n Raises\n ------\n ValueError\n If the indexes have not been built prior to method call.\n \"\"\"\n if not self.indexes_built:\n msg = \"Indexes have not been built yet. Call build_indexes() first.\"\n raise ValueError(msg)\n if not tags:\n return self.tag_to_tests[None]\n\n filtered_sets = [self.tag_to_tests[tag] for tag in tags if tag in self.tag_to_tests]\n if not filtered_sets:\n return set()\n\n if strict:\n return set.intersection(*filtered_sets)\n return set.union(*filtered_sets)\n
merge(catalog: AntaCatalog) -> AntaCatalog\n
Merge two AntaCatalog instances.
This method is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead.
catalog
AntaCatalog instance to merge to this instance.
A new AntaCatalog instance containing the tests of the two instances.
def merge(self, catalog: AntaCatalog) -> AntaCatalog:\n \"\"\"Merge two AntaCatalog instances.\n\n Warning\n -------\n This method is deprecated and will be removed in ANTA v2.0. Use `AntaCatalog.merge_catalogs()` instead.\n\n Parameters\n ----------\n catalog\n AntaCatalog instance to merge to this instance.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of the two instances.\n \"\"\"\n # TODO: Use a decorator to deprecate this method instead. See https://github.com/aristanetworks/anta/issues/754\n warn(\n message=\"AntaCatalog.merge() is deprecated and will be removed in ANTA v2.0. Use AntaCatalog.merge_catalogs() instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n return self.merge_catalogs([self, catalog])\n
classmethod
merge_catalogs(catalogs: list[AntaCatalog]) -> AntaCatalog\n
Merge multiple AntaCatalog instances.
catalogs
list[AntaCatalog]
A list of AntaCatalog instances to merge.
A new AntaCatalog instance containing the tests of all the input catalogs.
@classmethod\ndef merge_catalogs(cls, catalogs: list[AntaCatalog]) -> AntaCatalog:\n \"\"\"Merge multiple AntaCatalog instances.\n\n Parameters\n ----------\n catalogs\n A list of AntaCatalog instances to merge.\n\n Returns\n -------\n AntaCatalog\n A new AntaCatalog instance containing the tests of all the input catalogs.\n \"\"\"\n combined_tests = list(chain(*(catalog.tests for catalog in catalogs)))\n return cls(tests=combined_tests)\n
parse(\n filename: str | Path,\n file_format: Literal[\"yaml\", \"json\"] = \"yaml\",\n) -> AntaCatalog\n
Create an AntaCatalog instance from a test catalog file.
str | Path
Path to test catalog YAML or JSON file.
file_format
Literal['yaml', 'json']
Format of the file, either \u2018yaml\u2019 or \u2018json\u2019.
'yaml'
An AntaCatalog populated with the file content.
@staticmethod\ndef parse(filename: str | Path, file_format: Literal[\"yaml\", \"json\"] = \"yaml\") -> AntaCatalog:\n \"\"\"Create an AntaCatalog instance from a test catalog file.\n\n Parameters\n ----------\n filename\n Path to test catalog YAML or JSON file.\n file_format\n Format of the file, either 'yaml' or 'json'.\n\n Returns\n -------\n AntaCatalog\n An AntaCatalog populated with the file content.\n \"\"\"\n if file_format not in [\"yaml\", \"json\"]:\n message = f\"'{file_format}' is not a valid format for an AntaCatalog file. Only 'yaml' and 'json' are supported.\"\n raise ValueError(message)\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) if file_format == \"yaml\" else json_load(f)\n except (TypeError, YAMLError, OSError, ValueError) 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
AntaTestDefinition(\n **data: (\n type[AntaTest]\n | AntaTest.Input\n | dict[str, Any]\n | None\n )\n)\n
Bases: BaseModel
BaseModel
Define a test with its associated inputs.
type[AntaTest]
An AntaTest concrete subclass.
The associated AntaTest.Input subclass instance.
https://docs.pydantic.dev/2.0/usage/validators/#using-validation-context-with-basemodel-initialization.
check_inputs() -> Self\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.
@model_validator(mode=\"after\")\ndef check_inputs(self) -> Self:\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
instantiate_inputs(\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.
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.
@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\"Could not instantiate inputs as type {type(data).__name__} is not valid\"\n raise ValueError(msg)\n
serialize_model() -> dict[str, AntaTest.Input]\n
Serialize the AntaTestDefinition model.
The dictionary representing the model will be look like:
<AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n
dict
A dictionary representing the model.
@model_serializer()\ndef serialize_model(self) -> dict[str, AntaTest.Input]:\n \"\"\"Serialize the AntaTestDefinition model.\n\n The dictionary representing the model will be look like:\n ```\n <AntaTest subclass name>:\n <AntaTest.Input compliant dictionary>\n ```\n\n Returns\n -------\n dict\n A dictionary representing the model.\n \"\"\"\n return {self.test.__name__: self.inputs}\n
Bases: RootModel[dict[ImportString[Any], list[AntaTestDefinition]]]
RootModel[dict[ImportString[Any], list[AntaTestDefinition]]]
Represents an ANTA Test Catalog File.
A valid test catalog file must have the following structure:
<Python module>:\n - <AntaTest subclass>:\n <AntaTest.Input compliant dictionary>\n
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.
@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 if not data:\n return data\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 isinstance(test_definition, AntaTestDefinition):\n test_definitions.append(test_definition)\n continue\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 return data\n
flatten_modules(\n data: dict[str, Any], package: str | None = None\n) -> dict[ModuleType, list[Any]]\n
Allow the user to provide a data structure with nested Python modules.
anta.tests.routing:\n generic:\n - <AntaTestDefinition>\n bgp:\n - <AntaTestDefinition>\n
anta.tests.routing.generic
anta.tests.routing.bgp
@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:\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 elif isinstance(tests, list):\n # This is a list of AntaTestDefinition\n modules[module] = tests\n else:\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 return modules\n
to_json() -> str\n
Return a JSON representation string of this model.
The JSON representation string of this model.
def to_json(self) -> str:\n \"\"\"Return a JSON representation string of this model.\n\n Returns\n -------\n str\n The JSON representation string of this model.\n \"\"\"\n return self.model_dump_json(serialize_as_any=True, exclude_unset=True, indent=2)\n
yaml() -> str\n
Return a YAML representation string of this model.
The YAML representation string of this model.
def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return safe_dump(safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n
CSV Report management for ANTA.
Build a CSV report.
dataclass
Headers(\n device: str = \"Device\",\n test_name: str = \"Test Name\",\n test_status: str = \"Test Status\",\n messages: str = \"Message(s)\",\n description: str = \"Test description\",\n categories: str = \"Test category\",\n)\n
Headers for the CSV report.
convert_to_list(result: TestResult) -> list[str]\n
Convert a TestResult into a list of string for creating file content.
A TestResult to convert into list.
TestResult converted into a list.
anta/reporter/csv_reporter.py
@classmethod\ndef convert_to_list(cls, result: TestResult) -> list[str]:\n \"\"\"Convert a TestResult into a list of string for creating file content.\n\n Parameters\n ----------\n result\n A TestResult to convert into list.\n\n Returns\n -------\n list[str]\n TestResult converted into a list.\n \"\"\"\n message = cls.split_list_to_txt_list(result.messages) if len(result.messages) > 0 else \"\"\n categories = cls.split_list_to_txt_list(convert_categories(result.categories)) if len(result.categories) > 0 else \"None\"\n return [\n str(result.name),\n result.test,\n result.result,\n message,\n result.description,\n categories,\n ]\n
generate(\n results: ResultManager, csv_filename: pathlib.Path\n) -> None\n
Build CSV flle with tests results.
results
ResultManager
A ResultManager instance.
csv_filename
Path
File path where to save CSV data.
if any is raised while writing the CSV file.
@classmethod\ndef generate(cls, results: ResultManager, csv_filename: pathlib.Path) -> None:\n \"\"\"Build CSV flle with tests results.\n\n Parameters\n ----------\n results\n A ResultManager instance.\n csv_filename\n File path where to save CSV data.\n\n Raises\n ------\n OSError\n if any is raised while writing the CSV file.\n \"\"\"\n headers = [\n cls.Headers.device,\n cls.Headers.test_name,\n cls.Headers.test_status,\n cls.Headers.messages,\n cls.Headers.description,\n cls.Headers.categories,\n ]\n\n try:\n with csv_filename.open(mode=\"w\", encoding=\"utf-8\") as csvfile:\n csvwriter = csv.writer(\n csvfile,\n delimiter=\",\",\n )\n csvwriter.writerow(headers)\n for entry in results.results:\n csvwriter.writerow(cls.convert_to_list(entry))\n except OSError as exc:\n message = f\"OSError caught while writing the CSV file '{csv_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n
split_list_to_txt_list(\n usr_list: list[str], delimiter: str = \" - \"\n) -> str\n
Split list to multi-lines string.
usr_list
List of string to concatenate.
delimiter
A delimiter to use to start string. Defaults to None.
' - '
Multi-lines string.
@classmethod\ndef split_list_to_txt_list(cls, usr_list: list[str], delimiter: str = \" - \") -> str:\n \"\"\"Split list to multi-lines string.\n\n Parameters\n ----------\n usr_list\n List of string to concatenate.\n delimiter\n A delimiter to use to start string. Defaults to None.\n\n Returns\n -------\n str\n Multi-lines string.\n\n \"\"\"\n return f\"{delimiter}\".join(f\"{line}\" for line in usr_list)\n
AntaDevice(\n name: str,\n tags: set[str] | None = None,\n *,\n disable_cache: bool = False\n)\n
Bases: ABC
ABC
Abstract class representing a device in ANTA.
An implementation of this class must override the abstract coroutines _collect() and refresh().
refresh()
Device name.
True if the device IP is reachable and a port can be open.
True if remote command execution succeeds.
Hardware model of the device.
Tags for this device.
cache
Cache | None
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.
set[str] | None
Disable caching for all commands for this device.
cache_statistics: dict[str, Any] | None\n
Return the device cache statistics for logging purposes.
__hash__() -> int\n
Implement hashing for AntaDevice objects.
anta/device.py
def __hash__(self) -> int:\n \"\"\"Implement hashing for AntaDevice objects.\"\"\"\n return hash(self._keys)\n
__repr__() -> str\n
Return a printable representation of an AntaDevice.
def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AntaDevice.\"\"\"\n return (\n f\"AntaDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r})\"\n )\n
abstractmethod
async
_collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> None\n
Collect device command output.
This abstract coroutine can be used to implement any command collection method for a device in ANTA.
The _collect() implementation needs to populate the output attribute of the AntaCommand object passed as argument.
output
If a failure occurs, the _collect() implementation is expected to catch the exception and implement proper logging, the output attribute of the AntaCommand object passed as argument would be None in this case.
command
The command to collect.
collection_id
An identifier used to build the eAPI request ID.
@abstractmethod\nasync def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None:\n \"\"\"Collect device command output.\n\n This abstract coroutine can be used to implement any command collection method\n for a device in ANTA.\n\n The `_collect()` implementation needs to populate the `output` attribute\n of the `AntaCommand` object passed as argument.\n\n If a failure occurs, the `_collect()` implementation is expected to catch the\n exception and implement proper logging, the `output` attribute of the\n `AntaCommand` object passed as argument would be `None` in this case.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n
collect(\n command: AntaCommand,\n *,\n collection_id: str | None = None\n) -> 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.
_collect
async def collect(self, command: AntaCommand, *, collection_id: str | None = None) -> 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 Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\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, collection_id=collection_id)\n await self.cache.set(command.uid, command.output) # pylint: disable=no-member\n else:\n await self._collect(command=command, collection_id=collection_id)\n
collect_commands(\n commands: list[AntaCommand],\n *,\n collection_id: str | None = None\n) -> None\n
Collect multiple commands.
The commands to collect.
async def collect_commands(self, commands: list[AntaCommand], *, collection_id: str | None = None) -> None:\n \"\"\"Collect multiple commands.\n\n Parameters\n ----------\n commands\n The commands to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n await asyncio.gather(*(self.collect(command=command, collection_id=collection_id) for command in commands))\n
copy(\n sources: list[Path],\n destination: Path,\n direction: Literal[\"to\", \"from\"] = \"from\",\n) -> None\n
Copy files to and from the device, usually through SCP.
It is not mandatory to implement this for a valid AntaDevice subclass.
sources
list[Path]
List of files to copy to or from the device.
destination
Local or remote destination when copying the files. Can be a folder.
direction
Literal['to', 'from']
Defines if this coroutine copies files to or from the device.
'from'
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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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
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.
@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\n - `is_online`: When the device IP is reachable and a port can be open.\n\n - `established`: When a command execution succeeds.\n\n - `hw_model`: The hardware model of the device.\n \"\"\"\n
AsyncEOSDevice(\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: set[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)\n
Bases: AntaDevice
Implementation of AntaDevice for EOS using aio-eapi.
host
Device FQDN or IP.
username
Username to connect to eAPI and SSH.
password
Password to connect to eAPI and SSH.
enable
Collect commands using privileged mode.
enable_password
Password used to gain privileged access on EOS.
port
int | None
eAPI port. Defaults to 80 is proto is \u2018http\u2019 or 443 if proto is \u2018https\u2019.
ssh_port
SSH port.
22
float | None
Timeout value in seconds for outgoing API calls.
insecure
Disable SSH Host Key validation.
proto
Literal['http', 'https']
eAPI protocol. Value can be \u2018http\u2019 or \u2018https\u2019.
'https'
Return a printable representation of an AsyncEOSDevice.
def __repr__(self) -> str:\n \"\"\"Return a printable representation of an AsyncEOSDevice.\"\"\"\n return (\n f\"AsyncEOSDevice({self.name!r}, \"\n f\"tags={self.tags!r}, \"\n f\"hw_model={self.hw_model!r}, \"\n f\"is_online={self.is_online!r}, \"\n f\"established={self.established!r}, \"\n f\"disable_cache={self.cache is None!r}, \"\n f\"host={self._session.host!r}, \"\n f\"eapi_port={self._session.port!r}, \"\n f\"username={self._ssh_opts.username!r}, \"\n f\"enable={self.enable!r}, \"\n f\"insecure={self._ssh_opts.known_hosts is None!r})\"\n )\n
Collect device command output from EOS using aio-eapi.
Supports outformat json and text as output structure. Gain privileged access using the enable_password attribute of the AntaDevice instance if populated.
async def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None: # noqa: C901 function is too complex - because of many required except blocks\n \"\"\"Collect device command output from EOS using aio-eapi.\n\n Supports outformat `json` and `text` as output structure.\n Gain privileged access using the `enable_password` attribute\n of the `AntaDevice` instance if populated.\n\n Parameters\n ----------\n command\n The command to collect.\n collection_id\n An identifier used to build the eAPI request ID.\n \"\"\"\n commands: list[dict[str, str | int]] = []\n if self.enable and self._enable_password is not None:\n commands.append(\n {\n \"cmd\": \"enable\",\n \"input\": str(self._enable_password),\n },\n )\n elif self.enable:\n # No password\n commands.append({\"cmd\": \"enable\"})\n commands += [{\"cmd\": command.command, \"revision\": command.revision}] if command.revision else [{\"cmd\": command.command}]\n try:\n response: list[dict[str, Any] | str] = await self._session.cli(\n commands=commands,\n ofmt=command.ofmt,\n version=command.version,\n req_id=f\"ANTA-{collection_id}-{id(command)}\" if collection_id else f\"ANTA-{id(command)}\",\n ) # type: ignore[assignment] # multiple commands returns a list\n # Do not keep response of 'enable' command\n command.output = response[-1]\n except asynceapi.EapiCommandError as e:\n # This block catches exceptions related to EOS issuing an error.\n command.errors = e.errors\n if command.requires_privileges:\n logger.error(\n \"Command '%s' requires privileged mode on %s. Verify user permissions and if the `enable` option is required.\", command.command, self.name\n )\n if command.supported:\n logger.error(\"Command '%s' failed on %s: %s\", command.command, self.name, e.errors[0] if len(e.errors) == 1 else e.errors)\n else:\n logger.debug(\"Command '%s' is not supported on '%s' (%s)\", command.command, self.name, self.hw_model)\n except TimeoutException as e:\n # This block catches Timeout exceptions.\n command.errors = [exc_to_str(e)]\n timeouts = self._session.timeout.as_dict()\n logger.error(\n \"%s occurred while sending a command to %s. Consider increasing the timeout.\\nCurrent timeouts: Connect: %s | Read: %s | Write: %s | Pool: %s\",\n exc_to_str(e),\n self.name,\n timeouts[\"connect\"],\n timeouts[\"read\"],\n timeouts[\"write\"],\n timeouts[\"pool\"],\n )\n except (ConnectError, OSError) as e:\n # This block catches OSError and socket issues related exceptions.\n command.errors = [exc_to_str(e)]\n if (isinstance(exc := e.__cause__, httpcore.ConnectError) and isinstance(os_error := exc.__context__, OSError)) or isinstance(os_error := e, OSError): # pylint: disable=no-member\n if isinstance(os_error.__cause__, OSError):\n os_error = os_error.__cause__\n logger.error(\"A local OS error occurred while connecting to %s: %s.\", self.name, os_error)\n else:\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n except HTTPError as e:\n # This block catches most of the httpx Exceptions and logs a general message.\n command.errors = [exc_to_str(e)]\n anta_log_exception(e, f\"An error occurred while issuing an eAPI request to {self.name}\", logger)\n logger.debug(\"%s: %s\", self.name, command)\n
Copy files to and from the device using asyncssh.scp().
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 Parameters\n ----------\n sources\n List of files to copy to or from the device.\n destination\n Local or remote destination when copying the files. Can be a folder.\n direction\n 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() function is invalid: %s\", direction)\n\n return\n await asyncssh.scp(src, dst)\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
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 = AntaCommand(command=\"show version\")\n await self._collect(show_version)\n if not show_version.collected:\n logger.warning(\"Cannot get hardware information from device %s\", self.name)\n else:\n self.hw_model = show_version.json_output.get(\"modelName\", None)\n if self.hw_model is None:\n logger.critical(\"Cannot parse 'show version' returned by device %s\", self.name)\n # in some cases it is possible that 'modelName' comes back empty\n # and it is nice to get a meaninfule error message\n elif self.hw_model == \"\":\n logger.critical(\"Got an empty 'modelName' in the 'show version' returned by device %s\", self.name)\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
Bases: dict[str, AntaDevice]
dict[str, AntaDevice]
Inventory abstraction for ANTA framework.
devices: list[AntaDevice]\n
List of AntaDevice in this inventory.
__setitem__(key: str, value: AntaDevice) -> None\n
Set a device in the inventory.
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
add_device(device: AntaDevice) -> None\n
Add a device to final inventory.
Device object to be added.
def add_device(self, device: AntaDevice) -> None:\n \"\"\"Add a device to final inventory.\n\n Parameters\n ----------\n device\n Device object to be added.\n\n \"\"\"\n self[device.name] = device\n
connect_inventory() -> None\n
Run refresh() coroutines for all AntaDevice objects in this inventory.
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
get_inventory(\n *,\n established_only: bool = False,\n tags: set[str] | None = None,\n devices: set[str] | None = None\n) -> AntaInventory\n
Return a filtered inventory.
established_only
Whether or not to include only established devices.
Tags to filter devices.
devices
Names to filter devices.
AntaInventory
An inventory with filtered AntaDevice objects.
def get_inventory(self, *, established_only: bool = False, tags: set[str] | None = None, devices: set[str] | None = None) -> AntaInventory:\n \"\"\"Return a filtered inventory.\n\n Parameters\n ----------\n established_only\n Whether or not to include only established devices.\n tags\n Tags to filter devices.\n devices\n Names to filter devices.\n\n Returns\n -------\n AntaInventory\n An inventory with filtered AntaDevice objects.\n \"\"\"\n\n def _filter_devices(device: AntaDevice) -> bool:\n \"\"\"Select the devices based on the inputs `tags`, `devices` and `established_only`.\"\"\"\n if tags is not None and all(tag not in tags for tag in device.tags):\n return False\n if devices is None or device.name in devices:\n return bool(not established_only or device.established)\n return False\n\n filtered_devices: list[AntaDevice] = list(filter(_filter_devices, self.values()))\n result = AntaInventory()\n for device in filtered_devices:\n result.add_device(device)\n return result\n
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.
The inventory devices are AsyncEOSDevice instances.
Path to device inventory YAML file.
Username to use to connect to devices.
Password to use to connect to devices.
Enable password to use if required.
Whether or not the commands need to be run in enable mode towards the devices.
Disable cache globally.
InventoryRootKeyError
Root key of inventory is missing.
InventoryIncorrectSchemaError
Inventory file is not following AntaInventory Schema.
@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 Parameters\n ----------\n filename\n Path to device inventory YAML file.\n username\n Username to use to connect to devices.\n password\n Password to use to connect to devices.\n enable_password\n Enable password to use if required.\n timeout\n Timeout value in seconds for outgoing API calls.\n enable\n Whether or not the commands need to be run in enable mode towards the devices.\n insecure\n Disable SSH Host Key validation.\n disable_cache\n Disable cache globally.\n\n Raises\n ------\n InventoryRootKeyError\n Root key of inventory is missing.\n InventoryIncorrectSchemaError\n 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
Manage Exception in Inventory module.
Bases: Exception
Exception
Error when user data does not follow ANTA schema.
Error raised when inventory root key is not found.
Device inventory input model.
anta/inventory/models.py
def yaml(self) -> str:\n \"\"\"Return a YAML representation string of this model.\n\n Returns\n -------\n str\n The YAML representation string of this model.\n \"\"\"\n # TODO: Pydantic and YAML serialization/deserialization is not supported natively.\n # This could be improved.\n # https://github.com/pydantic/pydantic/issues/1043\n # Explore if this worth using this: https://github.com/NowanIlfideme/pydantic-yaml\n return yaml.safe_dump(yaml.safe_load(self.model_dump_json(serialize_as_any=True, exclude_unset=True)), indent=2, width=math.inf)\n
Host entry of AntaInventoryInput.
Hostname | IPvAnyAddress
IP Address or FQDN of the device.
Port | None
Custom eAPI port to use.
Custom name of the device.
Tags of the device.
Disable cache for this device.
Network entry of AntaInventoryInput.
network
IPvAnyNetwork
Subnet to use for scanning.
Tags of the devices in this network.
Disable cache for all devices in this network.
IP Range entry of AntaInventoryInput.
start
IPvAnyAddress
IPv4 or IPv6 address for the beginning of the range.
stop
IPv4 or IPv6 address for the end of the range.
Tags of the devices in this IP range.
Disable cache for all devices in this IP range.
Markdown report generator for ANTA test results.
ANTAReport(mdfile: TextIOWrapper, results: ResultManager)\n
Bases: MDReportBase
MDReportBase
Generate the # ANTA Report section of the markdown report.
# ANTA Report
mdfile
TextIOWrapper
An open file object to write the markdown data into.
The ResultsManager instance containing all test results.
generate_section() -> None\n
anta/reporter/md_reporter.py
def generate_section(self) -> None:\n \"\"\"Generate the `# ANTA Report` section of the markdown report.\"\"\"\n self.write_heading(heading_level=1)\n toc = MD_REPORT_TOC\n self.mdfile.write(toc + \"\\n\\n\")\n
MDReportBase(mdfile: TextIOWrapper, results: ResultManager)\n
Base class for all sections subclasses.
Every subclasses must implement the generate_section method that uses the ResultManager object to generate and write content to the provided markdown file.
generate_section
generate_heading_name() -> str\n
Generate a formatted heading name based on the class name.
Formatted header name.
ANTAReport
ANTA Report
TestResultsSummary
Test Results Summary
def generate_heading_name(self) -> str:\n \"\"\"Generate a formatted heading name based on the class name.\n\n Returns\n -------\n str\n Formatted header name.\n\n Example\n -------\n - `ANTAReport` will become `ANTA Report`.\n - `TestResultsSummary` will become `Test Results Summary`.\n \"\"\"\n class_name = self.__class__.__name__\n\n # Split the class name into words, keeping acronyms together\n words = re.findall(r\"[A-Z]?[a-z]+|[A-Z]+(?=[A-Z][a-z]|\\d|\\W|$)|\\d+\", class_name)\n\n # Capitalize each word, but keep acronyms in all caps\n formatted_words = [word if word.isupper() else word.capitalize() for word in words]\n\n return \" \".join(formatted_words)\n
generate_rows() -> Generator[str, None, None]\n
Generate the rows of a markdown table for a specific report section.
Subclasses can implement this method to generate the content of the table rows.
def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of a markdown table for a specific report section.\n\n Subclasses can implement this method to generate the content of the table rows.\n \"\"\"\n msg = \"Subclasses should implement this method\"\n raise NotImplementedError(msg)\n
Abstract method to generate a specific section of the markdown report.
Must be implemented by subclasses.
@abstractmethod\ndef generate_section(self) -> None:\n \"\"\"Abstract method to generate a specific section of the markdown report.\n\n Must be implemented by subclasses.\n \"\"\"\n msg = \"Must be implemented by subclasses\"\n raise NotImplementedError(msg)\n
safe_markdown(text: str | None) -> str\n
Escape markdown characters in the text to prevent markdown rendering issues.
The text to escape markdown characters from.
The text with escaped markdown characters.
def safe_markdown(self, text: str | None) -> str:\n \"\"\"Escape markdown characters in the text to prevent markdown rendering issues.\n\n Parameters\n ----------\n text\n The text to escape markdown characters from.\n\n Returns\n -------\n str\n The text with escaped markdown characters.\n \"\"\"\n # Custom field from a TestResult object can be None\n if text is None:\n return \"\"\n\n # Replace newlines with spaces to keep content on one line\n text = text.replace(\"\\n\", \" \")\n\n # Replace backticks with single quotes\n return text.replace(\"`\", \"'\")\n
write_heading(heading_level: int) -> None\n
Write a markdown heading to the markdown file.
The heading name used is the class name.
heading_level
int
The level of the heading (1-6).
## Test Results Summary
def write_heading(self, heading_level: int) -> None:\n \"\"\"Write a markdown heading to the markdown file.\n\n The heading name used is the class name.\n\n Parameters\n ----------\n heading_level\n The level of the heading (1-6).\n\n Example\n -------\n `## Test Results Summary`\n \"\"\"\n # Ensure the heading level is within the valid range of 1 to 6\n heading_level = max(1, min(heading_level, 6))\n heading_name = self.generate_heading_name()\n heading = \"#\" * heading_level + \" \" + heading_name\n self.mdfile.write(f\"{heading}\\n\\n\")\n
write_table(\n table_heading: list[str], *, last_table: bool = False\n) -> None\n
Write a markdown table with a table heading and multiple rows to the markdown file.
table_heading
List of strings to join for the table heading.
last_table
Flag to determine if it\u2019s the last table of the markdown file to avoid unnecessary new line. Defaults to False.
def write_table(self, table_heading: list[str], *, last_table: bool = False) -> None:\n \"\"\"Write a markdown table with a table heading and multiple rows to the markdown file.\n\n Parameters\n ----------\n table_heading\n List of strings to join for the table heading.\n last_table\n Flag to determine if it's the last table of the markdown file to avoid unnecessary new line. Defaults to False.\n \"\"\"\n self.mdfile.write(\"\\n\".join(table_heading) + \"\\n\")\n for row in self.generate_rows():\n self.mdfile.write(row)\n if not last_table:\n self.mdfile.write(\"\\n\")\n
Class responsible for generating a Markdown report based on the provided ResultManager object.
It aggregates different report sections, each represented by a subclass of MDReportBase, and sequentially generates their content into a markdown file.
The generate class method will loop over all the section subclasses and call their generate_section method. The final report will be generated in the same order as the sections list of the method.
generate
sections
generate(results: ResultManager, md_filename: Path) -> None\n
Generate and write the various sections of the markdown report.
md_filename
The path to the markdown file to write the report into.
@classmethod\ndef generate(cls, results: ResultManager, md_filename: Path) -> None:\n \"\"\"Generate and write the various sections of the markdown report.\n\n Parameters\n ----------\n results\n The ResultsManager instance containing all test results.\n md_filename\n The path to the markdown file to write the report into.\n \"\"\"\n try:\n with md_filename.open(\"w\", encoding=\"utf-8\") as mdfile:\n sections: list[MDReportBase] = [\n ANTAReport(mdfile, results),\n TestResultsSummary(mdfile, results),\n SummaryTotals(mdfile, results),\n SummaryTotalsDeviceUnderTest(mdfile, results),\n SummaryTotalsPerCategory(mdfile, results),\n TestResults(mdfile, results),\n ]\n for section in sections:\n section.generate_section()\n except OSError as exc:\n message = f\"OSError caught while writing the Markdown file '{md_filename.resolve()}'.\"\n anta_log_exception(exc, message, logger)\n raise\n
SummaryTotals(\n mdfile: TextIOWrapper, results: ResultManager\n)\n
Generate the ### Summary Totals section of the markdown report.
### Summary Totals
Generate the rows of the summary totals table.
def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals table.\"\"\"\n yield (\n f\"| {self.results.get_total_results()} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SUCCESS})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.SKIPPED})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.FAILURE})} \"\n f\"| {self.results.get_total_results({AntaTestStatus.ERROR})} |\\n\"\n )\n
def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n
SummaryTotalsDeviceUnderTest(\n mdfile: TextIOWrapper, results: ResultManager\n)\n
Generate the ### Summary Totals Devices Under Tests section of the markdown report.
### Summary Totals Devices Under Tests
Generate the rows of the summary totals device under test table.
def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals device under test table.\"\"\"\n for device, stat in self.results.device_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n categories_skipped = \", \".join(sorted(convert_categories(list(stat.categories_skipped))))\n categories_failed = \", \".join(sorted(convert_categories(list(stat.categories_failed))))\n yield (\n f\"| {device} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} | {stat.tests_error_count} \"\n f\"| {categories_skipped or '-'} | {categories_failed or '-'} |\\n\"\n )\n
def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Devices Under Tests` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n
SummaryTotalsPerCategory(\n mdfile: TextIOWrapper, results: ResultManager\n)\n
Generate the ### Summary Totals Per Category section of the markdown report.
### Summary Totals Per Category
Generate the rows of the summary totals per category table.
def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the summary totals per category table.\"\"\"\n for category, stat in self.results.sorted_category_stats.items():\n total_tests = stat.tests_success_count + stat.tests_skipped_count + stat.tests_failure_count + stat.tests_error_count\n yield (\n f\"| {category} | {total_tests} | {stat.tests_success_count} | {stat.tests_skipped_count} | {stat.tests_failure_count} \"\n f\"| {stat.tests_error_count} |\\n\"\n )\n
def generate_section(self) -> None:\n \"\"\"Generate the `### Summary Totals Per Category` section of the markdown report.\"\"\"\n self.write_heading(heading_level=3)\n self.write_table(table_heading=self.TABLE_HEADING)\n
TestResults(mdfile: TextIOWrapper, results: ResultManager)\n
Generates the ## Test Results section of the markdown report.
## Test Results
Generate the rows of the all test results table.
def generate_rows(self) -> Generator[str, None, None]:\n \"\"\"Generate the rows of the all test results table.\"\"\"\n for result in self.results.get_results(sort_by=[\"name\", \"test\"]):\n messages = self.safe_markdown(\", \".join(result.messages))\n categories = \", \".join(convert_categories(result.categories))\n yield (\n f\"| {result.name or '-'} | {categories or '-'} | {result.test or '-'} \"\n f\"| {result.description or '-'} | {self.safe_markdown(result.custom_field) or '-'} | {result.result or '-'} | {messages or '-'} |\\n\"\n )\n
Generate the ## Test Results section of the markdown report.
def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n self.write_table(table_heading=self.TABLE_HEADING, last_table=True)\n
TestResultsSummary(\n mdfile: TextIOWrapper, results: ResultManager\n)\n
Generate the ## Test Results Summary section of the markdown report.
def generate_section(self) -> None:\n \"\"\"Generate the `## Test Results Summary` section of the markdown report.\"\"\"\n self.write_heading(heading_level=2)\n
AntaTest(\n device: AntaDevice,\n inputs: dict[str, Any] | AntaTest.Input | None = None,\n eos_data: list[dict[Any, Any] | str] | None = None,\n)\n
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.
The following is an example of an AntaTest subclass implementation:
class VerifyReachability(AntaTest):\n '''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 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
AntaDevice instance on which the test will be run.
dict[str, Any] | Input | None
Dictionary of attributes used to instantiate the AntaTest.Input instance.
list[dict[Any, Any] | str] | None
Populate outputs of the test commands instead of collecting from devices. This list must have the same length and order than the instance_commands instance attribute.
blocked: bool\n
Check if CLI commands contain a blocked keyword.
collected: bool\n
Return True if all commands for this test have been collected.
failed_commands: list[AntaCommand]\n
Return a list of all the commands that have failed.
module: str\n
Return the Python module in which this AntaTest class is defined.
Class defining inputs for a test in ANTA.
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
Runtime filters to map tests with list of tags or devices.
Tag of devices on which to run the test.
Test inputs model to overwrite result fields.
Implement generic hashing for AntaTest.Input.
This will work in most cases but this does not consider 2 lists with different ordering as equal.
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
anta_test(\n function: F,\n) -> Callable[..., Coroutine[Any, Any, TestResult]]\n
Decorate the test() method in child classes.
This decorator implements (in this order):
@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 Parameters\n ----------\n self\n The test instance.\n eos_data\n 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\n Any keyword argument to pass to the test.\n\n Returns\n -------\n TestResult\n The TestResult instance attribute populated with error status if any.\n\n \"\"\"\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 AntaTest.update_progress()\n return self.result\n\n if cmds := self.failed_commands:\n unsupported_commands = [f\"'{c.command}' is not supported on {self.device.hw_model}\" for c in cmds if not c.supported]\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 else:\n self.result.is_error(message=\"\\n\".join([f\"{c.command} has failed: {', '.join(c.errors)}\" for c in cmds]))\n AntaTest.update_progress()\n return self.result\n\n try:\n function(self, **kwargs)\n except Exception as e: # noqa: BLE001\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 # TODO: find a correct way to time test execution\n AntaTest.update_progress()\n return self.result\n\n return wrapper\n
collect() -> None\n
Collect outputs of all commands of this test class from the device of this test instance.
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, collection_id=self.name)\n except Exception as e: # noqa: BLE001\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
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.
def render(self, template: AntaTemplate) -> list[AntaCommand]:\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 _ = template\n msg = f\"AntaTemplate are provided but render() method has not been implemented for {self.module}.{self.__class__.__name__}\"\n raise NotImplementedError(msg)\n
save_commands_data(\n eos_data: list[dict[str, Any] | str]\n) -> None\n
Populate output of all AntaCommand instances in instance_commands.
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
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.
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 reason\")\n
@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 reason\")\n ```\n\n \"\"\"\n
CLI commands are protected to avoid execution of critical commands such as reload or write erase.
reload
write erase
^reload\\s*\\w*
^conf\\w*\\s*(terminal|session)*
^wr\\w*\\s*\\w+
Class to define a command.
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.
Device command.
version
Literal[1, 'latest']
eAPI version - valid values are 1 or \u201clatest\u201d.
revision
Revision | None
eAPI revision of the command. Valid values are 1 to 99. Revision has precedence over version.
ofmt
Literal['json', 'text']
eAPI output - json or text.
dict[str, Any] | str | None
Output of the command. Only defined if there was no errors.
template
AntaTemplate | None
AntaTemplate object used to render this command.
errors
If the command execution fails, eAPI returns a list of strings detailing the error(s).
params
AntaParamsBaseModel
Pydantic Model containing the variables values used to render the template.
Enable or disable caching for this AntaCommand if the AntaDevice supports it.
Return True if the command has been collected, False otherwise.
A command that has not been collected could have returned an error. See error property.
error: bool\n
Return True if the command returned an error, False otherwise.
json_output: dict[str, Any]\n
Get the command output as JSON.
requires_privileges: bool\n
Return True if the command requires privileged mode, False otherwise.
RuntimeError
If the command has not been collected and has not returned an error. AntaDevice.collect() must be called before this property.
supported: bool\n
Return True if the command is supported on the device hardware platform, False otherwise.
text_output: str\n
Get the command output as a string.
uid: str\n
Generate a unique identifier for this command.
AntaTemplate(\n template: str,\n version: Literal[1, \"latest\"] = \"latest\",\n revision: Revision | None = None,\n ofmt: Literal[\"json\", \"text\"] = \"json\",\n *,\n use_cache: bool = True\n)\n
Class to define a command template as Python f-string.
Can render a command from parameters.
Python f-string. Example: \u2018show vlan {vlan_id}\u2019.
Revision of the command. Valid values are 1 to 99. Revision has precedence over version.
Enable or disable caching for this AntaTemplate if the AntaDevice supports it.
Return the representation of the class.
Copying pydantic model style, excluding params_schema
params_schema
def __repr__(self) -> str:\n \"\"\"Return the representation of the class.\n\n Copying pydantic model style, excluding `params_schema`\n \"\"\"\n return \" \".join(f\"{a}={v!r}\" for a, v in vars(self).items() if a != \"params_schema\")\n
render(**params: str | int | bool) -> AntaCommand\n
Render an AntaCommand from an AntaTemplate instance.
Keep the parameters used in the AntaTemplate instance.
str | int | bool
Dictionary of variables with string values to render the Python f-string.
{}
The rendered AntaCommand. This AntaCommand instance have a template attribute that references this AntaTemplate instance.
AntaTemplateRenderError
If a parameter is missing to render the AntaTemplate instance.
def render(self, **params: str | int | bool) -> AntaCommand:\n \"\"\"Render an AntaCommand from an AntaTemplate instance.\n\n Keep the parameters used in the AntaTemplate instance.\n\n Parameters\n ----------\n params\n Dictionary of variables with string values to render the Python f-string.\n\n Returns\n -------\n AntaCommand\n The rendered AntaCommand.\n This AntaCommand instance have a template attribute that references this\n AntaTemplate instance.\n\n Raises\n ------\n AntaTemplateRenderError\n If a parameter is missing to render the AntaTemplate instance.\n \"\"\"\n try:\n command = self.template.format(**params)\n except (KeyError, SyntaxError) as e:\n raise AntaTemplateRenderError(self, e.args[0]) from e\n return AntaCommand(\n command=command,\n ofmt=self.ofmt,\n version=self.version,\n revision=self.revision,\n template=self,\n params=self.params_schema(**params),\n use_cache=self.use_cache,\n )\n
Report management for ANTA.
ReportJinja(template_path: pathlib.Path)\n
Report builder based on a Jinja2 template.
render(\n data: list[dict[str, Any]],\n *,\n trim_blocks: bool = True,\n lstrip_blocks: bool = True\n) -> str\n
Build a report based on a Jinja2 template.
Report is built based on a J2 template provided by user. Data structure sent to template is:
>>> print(ResultManager.json)\n[\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n]\n
list[dict[str, Any]]
List of results from ResultManager.results.
ResultManager.results
trim_blocks
enable trim_blocks for J2 rendering.
lstrip_blocks
enable lstrip_blocks for J2 rendering.
Rendered template
anta/reporter/__init__.py
def render(self, data: list[dict[str, Any]], *, trim_blocks: bool = True, lstrip_blocks: bool = True) -> str:\n \"\"\"Build a report based on a Jinja2 template.\n\n Report is built based on a J2 template provided by user.\n Data structure sent to template is:\n\n Example\n -------\n ```\n >>> print(ResultManager.json)\n [\n {\n name: ...,\n test: ...,\n result: ...,\n messages: [...]\n categories: ...,\n description: ...,\n }\n ]\n ```\n\n Parameters\n ----------\n data\n List of results from `ResultManager.results`.\n trim_blocks\n enable trim_blocks for J2 rendering.\n lstrip_blocks\n enable lstrip_blocks for J2 rendering.\n\n Returns\n -------\n str\n Rendered template\n\n \"\"\"\n with self.template_path.open(encoding=\"utf-8\") as file_:\n template = Template(file_.read(), trim_blocks=trim_blocks, lstrip_blocks=lstrip_blocks)\n\n return template.render({\"data\": data})\n
TableReport Generate a Table based on TestResult.
Headers(\n device: str = \"Device\",\n test_case: str = \"Test Name\",\n number_of_success: str = \"# of success\",\n number_of_failure: str = \"# of failure\",\n number_of_skipped: str = \"# of skipped\",\n number_of_errors: str = \"# of errors\",\n list_of_error_nodes: str = \"List of failed or error nodes\",\n list_of_error_tests: str = \"List of failed or error test cases\",\n)\n
Headers for the table report.
report_all(\n manager: ResultManager, title: str = \"All tests results\"\n) -> Table\n
Create a table report with all tests for one or all devices.
Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category
manager
title
Title for the report. Defaults to \u2018All tests results\u2019.
'All tests results'
Table
A fully populated rich Table.
def report_all(self, manager: ResultManager, title: str = \"All tests results\") -> Table:\n \"\"\"Create a table report with all tests for one or all devices.\n\n Create table with full output: Device | Test Name | Test Status | Message(s) | Test description | Test category\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n title\n Title for the report. Defaults to 'All tests results'.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\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 def add_line(result: TestResult) -> None:\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(convert_categories(result.categories))\n table.add_row(str(result.name), result.test, state, message, result.description, categories)\n\n for result in manager.results:\n add_line(result)\n return table\n
report_summary_devices(\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table\n
Create a table report with result aggregated per device.
Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases
List of device names to include. None to select all devices.
Title of the report.
'Summary per device'
def report_summary_devices(\n self,\n manager: ResultManager,\n devices: list[str] | None = None,\n title: str = \"Summary per device\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per device.\n\n Create table with full output: Device | # of success | # of skipped | # of failure | # of errors | List of failed or error test cases\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n devices\n List of device names to include. None to select all devices.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.device,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_tests,\n ]\n table = self._build_headers(headers=headers, table=table)\n for device, stats in sorted(manager.device_stats.items()):\n if devices is None or device in devices:\n table.add_row(\n device,\n str(stats.tests_success_count),\n str(stats.tests_skipped_count),\n str(stats.tests_failure_count),\n str(stats.tests_error_count),\n \", \".join(stats.tests_failure),\n )\n return table\n
report_summary_tests(\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table\n
Create a table report with result aggregated per test.
Create table with full output: Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes
List of test names to include. None to select all tests.
'Summary per test'
def report_summary_tests(\n self,\n manager: ResultManager,\n tests: list[str] | None = None,\n title: str = \"Summary per test\",\n) -> Table:\n \"\"\"Create a table report with result aggregated per test.\n\n Create table with full output:\n Test Name | # of success | # of skipped | # of failure | # of errors | List of failed or error nodes\n\n Parameters\n ----------\n manager\n A ResultManager instance.\n tests\n List of test names to include. None to select all tests.\n title\n Title of the report.\n\n Returns\n -------\n Table\n A fully populated rich `Table`.\n \"\"\"\n table = Table(title=title, show_lines=True)\n headers = [\n self.Headers.test_case,\n self.Headers.number_of_success,\n self.Headers.number_of_skipped,\n self.Headers.number_of_failure,\n self.Headers.number_of_errors,\n self.Headers.list_of_error_nodes,\n ]\n table = self._build_headers(headers=headers, table=table)\n for test, stats in sorted(manager.test_stats.items()):\n if tests is None or test in tests:\n table.add_row(\n test,\n str(stats.devices_success_count),\n str(stats.devices_skipped_count),\n str(stats.devices_failure_count),\n str(stats.devices_error_count),\n \", \".join(stats.devices_failure),\n )\n return table\n
options:\n filters: [\"!^_[^_]\", \"!^__len__\"]\n
ResultManager()\n
Helper to manage Test Results and generate reports.
Create Inventory:
inventory_anta = AntaInventory.parse(\n filename='examples/inventory.yml',\n username='ansible',\n password='ansible',\n)\n
Create Result Manager:
manager = ResultManager()\n
Run tests for all connected devices:
for device in inventory_anta.get_inventory().devices:\n manager.add(\n VerifyNTP(device=device).test()\n )\n manager.add(\n VerifyEOSVersion(device=device).test(version='4.28.3M')\n )\n
Print result in native format:
manager.results\n[\n TestResult(\n name=\"pf1\",\n test=\"VerifyZeroTouch\",\n categories=[\"configuration\"],\n description=\"Verifies ZeroTouch is disabled\",\n result=\"success\",\n messages=[],\n custom_field=None,\n ),\n TestResult(\n name=\"pf1\",\n test='VerifyNTP',\n categories=[\"software\"],\n categories=['system'],\n description='Verifies if NTP is synchronised.',\n result='failure',\n messages=[\"The device is not synchronized with the configured NTP server(s): 'NTP is disabled.'\"],\n custom_field=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:
If the status of the added test is error, the status is untouched and the error_status is set to True.
json: str\n
Get a JSON representation of the results.
results: list[TestResult]\n
Get the list of TestResult.
cached
results_by_status: dict[AntaTestStatus, list[TestResult]]\n
A cached property that returns the results grouped by status.
sorted_category_stats: dict[str, CategoryStats]\n
A property that returns the category_stats dictionary sorted by key name.
__len__() -> int\n
Implement len method to count number of results.
anta/result_manager/__init__.py
def __len__(self) -> int:\n \"\"\"Implement __len__ method to count number of results.\"\"\"\n return len(self._result_entries)\n
add(result: TestResult) -> None\n
Add a result to the ResultManager instance.
The result is added to the internal list of results and the overall status of the ResultManager instance is updated based on the added test status.
TestResult to add to the ResultManager instance.
def add(self, result: TestResult) -> None:\n \"\"\"Add a result to the ResultManager instance.\n\n The result is added to the internal list of results and the overall status\n of the ResultManager instance is updated based on the added test status.\n\n Parameters\n ----------\n result\n TestResult to add to the ResultManager instance.\n \"\"\"\n self._result_entries.append(result)\n self._update_status(result.result)\n self._update_stats(result)\n\n # Every time a new result is added, we need to clear the cached property\n self.__dict__.pop(\"results_by_status\", None)\n
filter(hide: set[AntaTestStatus]) -> ResultManager\n
Get a filtered ResultManager based on test status.
hide
set[AntaTestStatus]
Set of AntaTestStatus enum members to select tests to hide based on their status.
A filtered ResultManager.
def filter(self, hide: set[AntaTestStatus]) -> ResultManager:\n \"\"\"Get a filtered ResultManager based on test status.\n\n Parameters\n ----------\n hide\n Set of AntaTestStatus enum members to select tests to hide based on their status.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n possible_statuses = set(AntaTestStatus)\n manager = ResultManager()\n manager.results = self.get_results(possible_statuses - hide)\n return manager\n
filter_by_devices(devices: set[str]) -> ResultManager\n
Get a filtered ResultManager that only contains specific devices.
Set of device names to filter the results.
def filter_by_devices(self, devices: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific devices.\n\n Parameters\n ----------\n devices\n Set of device names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.name in devices]\n return manager\n
filter_by_tests(tests: set[str]) -> ResultManager\n
Get a filtered ResultManager that only contains specific tests.
Set of test names to filter the results.
def filter_by_tests(self, tests: set[str]) -> ResultManager:\n \"\"\"Get a filtered ResultManager that only contains specific tests.\n\n Parameters\n ----------\n tests\n Set of test names to filter the results.\n\n Returns\n -------\n ResultManager\n A filtered `ResultManager`.\n \"\"\"\n manager = ResultManager()\n manager.results = [result for result in self._result_entries if result.test in tests]\n return manager\n
get_devices() -> set[str]\n
Get the set of all the device names.
Set of device names.
def get_devices(self) -> set[str]:\n \"\"\"Get the set of all the device names.\n\n Returns\n -------\n set[str]\n Set of device names.\n \"\"\"\n return {str(result.name) for result in self._result_entries}\n
get_results(\n status: set[AntaTestStatus] | None = None,\n sort_by: list[str] | None = None,\n) -> list[TestResult]\n
Get the results, optionally filtered by status and sorted by TestResult fields.
If no status is provided, all results are returned.
status
set[AntaTestStatus] | None
Optional set of AntaTestStatus enum members to filter the results.
sort_by
Optional list of TestResult fields to sort the results.
list[TestResult]
List of results.
def get_results(self, status: set[AntaTestStatus] | None = None, sort_by: list[str] | None = None) -> list[TestResult]:\n \"\"\"Get the results, optionally filtered by status and sorted by TestResult fields.\n\n If no status is provided, all results are returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n sort_by\n Optional list of TestResult fields to sort the results.\n\n Returns\n -------\n list[TestResult]\n List of results.\n \"\"\"\n # Return all results if no status is provided, otherwise return results for multiple statuses\n results = self._result_entries if status is None else list(chain.from_iterable(self.results_by_status.get(status, []) for status in status))\n\n if sort_by:\n accepted_fields = TestResult.model_fields.keys()\n if not set(sort_by).issubset(set(accepted_fields)):\n msg = f\"Invalid sort_by fields: {sort_by}. Accepted fields are: {list(accepted_fields)}\"\n raise ValueError(msg)\n results = sorted(results, key=lambda result: [getattr(result, field) for field in sort_by])\n\n return results\n
get_status(*, ignore_error: bool = False) -> str\n
Return the current status including error_status if ignore_error is False.
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
get_tests() -> set[str]\n
Get the set of all the test names.
Set of test names.
def get_tests(self) -> set[str]:\n \"\"\"Get the set of all the test names.\n\n Returns\n -------\n set[str]\n Set of test names.\n \"\"\"\n return {str(result.test) for result in self._result_entries}\n
get_total_results(\n status: set[AntaTestStatus] | None = None,\n) -> int\n
Get the total number of results, optionally filtered by status.
If no status is provided, the total number of results is returned.
Total number of results.
def get_total_results(self, status: set[AntaTestStatus] | None = None) -> int:\n \"\"\"Get the total number of results, optionally filtered by status.\n\n If no status is provided, the total number of results is returned.\n\n Parameters\n ----------\n status\n Optional set of AntaTestStatus enum members to filter the results.\n\n Returns\n -------\n int\n Total number of results.\n \"\"\"\n if status is None:\n # Return the total number of results\n return sum(len(results) for results in self.results_by_status.values())\n\n # Return the total number of results for multiple statuses\n return sum(len(self.results_by_status.get(status, [])) for status in status)\n
Describe the result of a test from a single device.
Name of the device where the test was run.
Name of the test run on the device.
List of categories the TestResult belongs to. Defaults to the AntaTest categories.
Description of the TestResult. Defaults to the AntaTest description.
AntaTestStatus
Result of the test. Must be one of the AntaTestStatus Enum values: unset, success, failure, error or skipped.
Messages to report after the test, if any.
Custom field to store a string for flexibility in integrating with ANTA.
is_error(message: str | None = None) -> None\n
Set status to error.
message
Optional message related to the test.
anta/result_manager/models.py
def is_error(self, message: str | None = None) -> None:\n \"\"\"Set status to error.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.ERROR, message)\n
is_failure(message: str | None = None) -> None\n
Set status to failure.
def is_failure(self, message: str | None = None) -> None:\n \"\"\"Set status to failure.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.FAILURE, message)\n
is_skipped(message: str | None = None) -> None\n
Set status to skipped.
def is_skipped(self, message: str | None = None) -> None:\n \"\"\"Set status to skipped.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.SKIPPED, message)\n
is_success(message: str | None = None) -> None\n
Set status to success.
def is_success(self, message: str | None = None) -> None:\n \"\"\"Set status to success.\n\n Parameters\n ----------\n message\n Optional message related to the test.\n\n \"\"\"\n self._set_status(AntaTestStatus.SUCCESS, message)\n
ANTA runner function.
adjust_rlimit_nofile() -> tuple[int, int]\n
Adjust the maximum number of open file descriptors for the ANTA process.
The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable.
If the ANTA_NOFILE environment variable is not set or is invalid, DEFAULT_NOFILE is used.
ANTA_NOFILE
DEFAULT_NOFILE
tuple[int, int]
The new soft and hard limits for open file descriptors.
anta/runner.py
def adjust_rlimit_nofile() -> tuple[int, int]:\n \"\"\"Adjust the maximum number of open file descriptors for the ANTA process.\n\n The limit is set to the lower of the current hard limit and the value of the ANTA_NOFILE environment variable.\n\n If the `ANTA_NOFILE` environment variable is not set or is invalid, `DEFAULT_NOFILE` is used.\n\n Returns\n -------\n tuple[int, int]\n The new soft and hard limits for open file descriptors.\n \"\"\"\n try:\n nofile = int(os.environ.get(\"ANTA_NOFILE\", DEFAULT_NOFILE))\n except ValueError as exception:\n logger.warning(\"The ANTA_NOFILE environment variable value is invalid: %s\\nDefault to %s.\", exc_to_str(exception), DEFAULT_NOFILE)\n nofile = DEFAULT_NOFILE\n\n limits = resource.getrlimit(resource.RLIMIT_NOFILE)\n logger.debug(\"Initial limit numbers for open file descriptors for the current ANTA process: Soft Limit: %s | Hard Limit: %s\", limits[0], limits[1])\n nofile = min(limits[1], nofile)\n logger.debug(\"Setting soft limit for open file descriptors for the current ANTA process to %s\", nofile)\n resource.setrlimit(resource.RLIMIT_NOFILE, (nofile, limits[1]))\n return resource.getrlimit(resource.RLIMIT_NOFILE)\n
get_coroutines(\n selected_tests: defaultdict[\n AntaDevice, set[AntaTestDefinition]\n ],\n manager: ResultManager,\n) -> list[Coroutine[Any, Any, TestResult]]\n
Get the coroutines for the ANTA run.
selected_tests
defaultdict[AntaDevice, set[AntaTestDefinition]]
A mapping of devices to the tests to run. The selected tests are generated by the prepare_tests function.
prepare_tests
A ResultManager
list[Coroutine[Any, Any, TestResult]]
The list of coroutines to run.
def get_coroutines(selected_tests: defaultdict[AntaDevice, set[AntaTestDefinition]], manager: ResultManager) -> list[Coroutine[Any, Any, TestResult]]:\n \"\"\"Get the coroutines for the ANTA run.\n\n Parameters\n ----------\n selected_tests\n A mapping of devices to the tests to run. The selected tests are generated by the `prepare_tests` function.\n manager\n A ResultManager\n\n Returns\n -------\n list[Coroutine[Any, Any, TestResult]]\n The list of coroutines to run.\n \"\"\"\n coros = []\n for device, test_definitions in selected_tests.items():\n for test in test_definitions:\n try:\n test_instance = test.test(device=device, inputs=test.inputs)\n manager.add(test_instance.result)\n coros.append(test_instance.test())\n except Exception as e: # noqa: PERF203, BLE001\n # An AntaTest instance is potentially user-defined code.\n # We need to catch everything and exit gracefully with an error message.\n message = \"\\n\".join(\n [\n f\"There is an error when creating test {test.test.__module__}.{test.test.__name__}.\",\n f\"If this is not a custom test implementation: {GITHUB_SUGGESTION}\",\n ],\n )\n anta_log_exception(e, message, logger)\n return coros\n
log_cache_statistics(devices: list[AntaDevice]) -> None\n
Log cache statistics for each device in the inventory.
list[AntaDevice]
List of devices in the inventory.
def log_cache_statistics(devices: list[AntaDevice]) -> None:\n \"\"\"Log cache statistics for each device in the inventory.\n\n Parameters\n ----------\n devices\n List of devices in the inventory.\n \"\"\"\n for device in devices:\n if device.cache_statistics is not None:\n msg = (\n f\"Cache statistics for '{device.name}': \"\n f\"{device.cache_statistics['cache_hits']} hits / {device.cache_statistics['total_commands_sent']} \"\n f\"command(s) ({device.cache_statistics['cache_hit_ratio']})\"\n )\n logger.info(msg)\n else:\n logger.info(\"Caching is not enabled on %s\", device.name)\n
main(\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False\n) -> None\n
Run ANTA.
Use this as an entrypoint to the test framework in your script. ResultManager object gets updated with the test results.
ResultManager object to populate with the test results.
inventory
AntaInventory object that includes the device(s).
AntaCatalog object that includes the list of tests.
Devices on which to run tests. None means all devices. These may come from the --device / -d CLI option in NRFU.
--device / -d
Tests to run against devices. None means all tests. These may come from the --test / -t CLI option in NRFU.
--test / -t
Tags to filter devices from the inventory. These may come from the --tags CLI option in NRFU.
--tags
Include only established device(s).
dry_run
Build the list of coroutine to run and stop before test execution.
@cprofile()\nasync def main( # noqa: PLR0913\n manager: ResultManager,\n inventory: AntaInventory,\n catalog: AntaCatalog,\n devices: set[str] | None = None,\n tests: set[str] | None = None,\n tags: set[str] | None = None,\n *,\n established_only: bool = True,\n dry_run: bool = False,\n) -> None:\n \"\"\"Run ANTA.\n\n Use this as an entrypoint to the test framework in your script.\n ResultManager object gets updated with the test results.\n\n Parameters\n ----------\n manager\n ResultManager object to populate with the test results.\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n devices\n Devices on which to run tests. None means all devices. These may come from the `--device / -d` CLI option in NRFU.\n tests\n Tests to run against devices. None means all tests. These may come from the `--test / -t` CLI option in NRFU.\n tags\n Tags to filter devices from the inventory. These may come from the `--tags` CLI option in NRFU.\n established_only\n Include only established device(s).\n dry_run\n Build the list of coroutine to run and stop before test execution.\n \"\"\"\n # Adjust the maximum number of open file descriptors for the ANTA process\n limits = adjust_rlimit_nofile()\n\n if not catalog.tests:\n logger.info(\"The list of tests is empty, exiting\")\n return\n\n with Catchtime(logger=logger, message=\"Preparing ANTA NRFU Run\"):\n # Setup the inventory\n selected_inventory = inventory if dry_run else await setup_inventory(inventory, tags, devices, established_only=established_only)\n if selected_inventory is None:\n return\n\n with Catchtime(logger=logger, message=\"Preparing the tests\"):\n selected_tests = prepare_tests(selected_inventory, catalog, tests, tags)\n if selected_tests is None:\n return\n final_tests_count = sum(len(tests) for tests in selected_tests.values())\n\n run_info = (\n \"--- ANTA NRFU Run Information ---\\n\"\n f\"Number of devices: {len(inventory)} ({len(selected_inventory)} established)\\n\"\n f\"Total number of selected tests: {final_tests_count}\\n\"\n f\"Maximum number of open file descriptors for the current ANTA process: {limits[0]}\\n\"\n \"---------------------------------\"\n )\n\n logger.info(run_info)\n\n if final_tests_count > limits[0]:\n logger.warning(\n \"The number of concurrent tests is higher than the open file descriptors limit for this ANTA process.\\n\"\n \"Errors may occur while running the tests.\\n\"\n \"Please consult the ANTA FAQ.\"\n )\n\n coroutines = get_coroutines(selected_tests, manager)\n\n if dry_run:\n logger.info(\"Dry-run mode, exiting before running the tests.\")\n for coro in coroutines:\n coro.close()\n return\n\n if AntaTest.progress is not None:\n AntaTest.nrfu_task = AntaTest.progress.add_task(\"Running NRFU Tests...\", total=len(coroutines))\n\n with Catchtime(logger=logger, message=\"Running ANTA tests\"):\n await asyncio.gather(*coroutines)\n\n log_cache_statistics(selected_inventory.devices)\n
prepare_tests(\n inventory: AntaInventory,\n catalog: AntaCatalog,\n tests: set[str] | None,\n tags: set[str] | None,\n) -> (\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n)\n
Prepare the tests to run.
Tests to run against devices. None means all tests.
Tags to filter devices from the inventory.
defaultdict[AntaDevice, set[AntaTestDefinition]] | None
A mapping of devices to the tests to run or None if there are no tests to run.
def prepare_tests(\n inventory: AntaInventory, catalog: AntaCatalog, tests: set[str] | None, tags: set[str] | None\n) -> defaultdict[AntaDevice, set[AntaTestDefinition]] | None:\n \"\"\"Prepare the tests to run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n catalog\n AntaCatalog object that includes the list of tests.\n tests\n Tests to run against devices. None means all tests.\n tags\n Tags to filter devices from the inventory.\n\n Returns\n -------\n defaultdict[AntaDevice, set[AntaTestDefinition]] | None\n A mapping of devices to the tests to run or None if there are no tests to run.\n \"\"\"\n # Build indexes for the catalog. If `tests` is set, filter the indexes based on these tests\n catalog.build_indexes(filtered_tests=tests)\n\n # Using a set to avoid inserting duplicate tests\n device_to_tests: defaultdict[AntaDevice, set[AntaTestDefinition]] = defaultdict(set)\n\n total_test_count = 0\n\n # Create the device to tests mapping from the tags\n for device in inventory.devices:\n if tags:\n # If there are CLI tags, execute tests with matching tags for this device\n if not (matching_tags := tags.intersection(device.tags)):\n # The device does not have any selected tag, skipping\n continue\n device_to_tests[device].update(catalog.get_tests_by_tags(matching_tags))\n else:\n # If there is no CLI tags, execute all tests that do not have any tags\n device_to_tests[device].update(catalog.tag_to_tests[None])\n\n # Then add the tests with matching tags from device tags\n device_to_tests[device].update(catalog.get_tests_by_tags(device.tags))\n\n total_test_count += len(device_to_tests[device])\n\n if total_test_count == 0:\n msg = (\n f\"There are no tests{f' matching the tags {tags} ' if tags else ' '}to run in the current test catalog and device inventory, please verify your inputs.\"\n )\n logger.warning(msg)\n return None\n\n return device_to_tests\n
setup_inventory(\n inventory: AntaInventory,\n tags: set[str] | None,\n devices: set[str] | None,\n *,\n established_only: bool\n) -> AntaInventory | None\n
Set up the inventory for the ANTA run.
Devices on which to run tests. None means all devices.
If True use return only devices where a connection is established.
AntaInventory | None
The filtered inventory or None if there are no devices to run tests on.
async def setup_inventory(inventory: AntaInventory, tags: set[str] | None, devices: set[str] | None, *, established_only: bool) -> AntaInventory | None:\n \"\"\"Set up the inventory for the ANTA run.\n\n Parameters\n ----------\n inventory\n AntaInventory object that includes the device(s).\n tags\n Tags to filter devices from the inventory.\n devices\n Devices on which to run tests. None means all devices.\n established_only\n If True use return only devices where a connection is established.\n\n Returns\n -------\n AntaInventory | None\n The filtered inventory or None if there are no devices to run tests on.\n \"\"\"\n if len(inventory) == 0:\n logger.info(\"The inventory is empty, exiting\")\n return None\n\n # Filter the inventory based on the CLI provided tags and devices if any\n selected_inventory = inventory.get_inventory(tags=tags, devices=devices) if tags or devices else inventory\n\n with Catchtime(logger=logger, message=\"Connecting to devices\"):\n # Connect to the devices\n await selected_inventory.connect_inventory()\n\n # Remove devices that are unreachable\n selected_inventory = selected_inventory.get_inventory(established_only=established_only)\n\n # If there are no devices in the inventory after filtering, exit\n if not selected_inventory.devices:\n msg = f'No reachable device {f\"matching the tags {tags} \" if tags else \"\"}was found.{f\" Selected devices: {devices} \" if devices is not None else \"\"}'\n logger.warning(msg)\n return None\n\n return selected_inventory\n
Verifies the management CVX global status.
anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n
anta/tests/cvx.py
class VerifyManagementCVX(AntaTest):\n \"\"\"Verifies the management CVX global status.\n\n Expected Results\n ----------------\n * Success: The test will pass if the management CVX global status matches the expected status.\n * Failure: The test will fail if the management CVX global status does not match the expected status.\n\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyManagementCVX:\n enabled: true\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyManagementCVX test.\"\"\"\n\n enabled: bool\n \"\"\"Whether management CVX must be enabled (True) or disabled (False).\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyManagementCVX.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n cluster_status = command_output[\"clusterStatus\"]\n if (cluster_state := cluster_status.get(\"enabled\")) != self.inputs.enabled:\n self.result.is_failure(f\"Management CVX status is not valid: {cluster_state}\")\n
enabled
Verify if all MCS client mounts are in mountStateMountComplete.
anta.tests.cvx:\n- VerifyMcsClientMounts:\n
class VerifyMcsClientMounts(AntaTest):\n \"\"\"Verify if all MCS client mounts are in mountStateMountComplete.\n\n Expected Results\n ----------------\n * Success: The test will pass if the MCS mount status on MCS Clients are mountStateMountComplete.\n * Failure: The test will fail even if one switch's MCS client mount status is not mountStateMountComplete.\n\n Examples\n --------\n ```yaml\n anta.tests.cvx:\n - VerifyMcsClientMounts:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"cvx\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management cvx mounts\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyMcsClientMounts.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n mount_states = command_output[\"mountStates\"]\n mcs_mount_state_detected = False\n for mount_state in mount_states:\n if not mount_state[\"type\"].startswith(\"Mcs\"):\n continue\n mcs_mount_state_detected = True\n if (state := mount_state[\"state\"]) != \"mountStateMountComplete\":\n self.result.is_failure(f\"MCS Client mount states are not valid: {state}\")\n\n if not mcs_mount_state_detected:\n self.result.is_failure(\"MCS Client mount states are not present\")\n
Verifies the AAA accounting console method lists for different accounting types (system, exec, commands, dot1x).
anta.tests.aaa:\n - VerifyAcctConsoleMethods:\n methods:\n - local\n - none\n - logging\n types:\n - system\n - exec\n - commands\n - dot1x\n
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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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
methods
list[AAAAuthMethod]
types
set[Literal['commands', 'exec', 'system', 'dot1x']]
Verifies the AAA accounting default method lists for different accounting types (system, exec, commands, dot1x).
anta.tests.aaa:\n - VerifyAcctDefaultMethods:\n methods:\n - local\n - none\n - logging\n types:\n - system\n - exec\n - commands\n - dot1x\n
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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods accounting\", revision=1)]\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
Verifies the AAA authentication method lists for different authentication types (login, enable, dot1x).
anta.tests.aaa:\n - VerifyAuthenMethods:\n methods:\n - local\n - none\n - logging\n types:\n - login\n - enable\n - dot1x\n
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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authentication\", revision=1)]\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
set[Literal['login', 'enable', 'dot1x']]
Verifies the AAA authorization method lists for different authorization types (commands, exec).
anta.tests.aaa:\n - VerifyAuthzMethods:\n methods:\n - local\n - none\n - logging\n types:\n - commands\n - exec\n
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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show aaa methods authorization\", revision=1)]\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
set[Literal['commands', 'exec']]
Verifies if the provided TACACS server group(s) are configured.
anta.tests.aaa:\n - VerifyTacacsServerGroups:\n groups:\n - TACACS-GROUP1\n - TACACS-GROUP2\n
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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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
groups
Verifies TACACS servers are configured for a specified VRF.
anta.tests.aaa:\n - VerifyTacacsServers:\n servers:\n - 10.10.10.21\n - 10.10.10.22\n vrf: MGMT\n
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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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
servers
list[IPv4Address]
vrf
'default'
Verifies TACACS source-interface for a specified VRF.
anta.tests.aaa:\n - VerifyTacacsSourceIntf:\n intf: Management0\n vrf: MGMT\n
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 categories: ClassVar[list[str]] = [\"aaa\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show tacacs\", revision=1)]\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
intf
Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs.
anta.tests.avt:\n - VerifyAVTPathHealth:\n
anta/tests/avt.py
class VerifyAVTPathHealth(AntaTest):\n \"\"\"Verifies the status of all Adaptive Virtual Topology (AVT) paths for all VRFs.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for all VRFs are active and valid.\n * Failure: The test will fail if the AVT path is not configured or if any AVT path under any VRF is either inactive or invalid.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTPathHealth:\n ```\n \"\"\"\n\n description = \"Verifies the status of all AVT paths for all VRFs.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTPathHealth.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output.get(\"vrfs\", {})\n\n # Check if AVT is configured\n if not command_output:\n self.result.is_failure(\"Adaptive virtual topology paths are not configured.\")\n return\n\n # Iterate over each VRF\n for vrf, vrf_data in command_output.items():\n # Iterate over each AVT path\n for profile, avt_path in vrf_data.get(\"avts\", {}).items():\n for path, flags in avt_path.get(\"avtPaths\", {}).items():\n # Get the status of the AVT path\n valid = flags[\"flags\"][\"valid\"]\n active = flags[\"flags\"][\"active\"]\n\n # Check the status of the AVT path\n if not valid and not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid and not active.\")\n elif not valid:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is invalid.\")\n elif not active:\n self.result.is_failure(f\"AVT path {path} for profile {profile} in VRF {vrf} is not active.\")\n
Verifies the Adaptive Virtual Topology (AVT) role of a device.
anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n
class VerifyAVTRole(AntaTest):\n \"\"\"Verifies the Adaptive Virtual Topology (AVT) role of a device.\n\n Expected Results\n ----------------\n * Success: The test will pass if the AVT role of the device matches the expected role.\n * Failure: The test will fail if the AVT is not configured or if the AVT role does not match the expected role.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTRole:\n role: edge\n ```\n \"\"\"\n\n description = \"Verifies the AVT role of a device.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show adaptive-virtual-topology path\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTRole test.\"\"\"\n\n role: str\n \"\"\"Expected AVT role of the device.\"\"\"\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTRole.\"\"\"\n # Initialize the test result as success\n self.result.is_success()\n\n # Get the command output\n command_output = self.instance_commands[0].json_output\n\n # Check if the AVT role matches the expected role\n if self.inputs.role != command_output.get(\"role\"):\n self.result.is_failure(f\"Expected AVT role as `{self.inputs.role}`, but found `{command_output.get('role')}` instead.\")\n
role
Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF.
anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n
class VerifyAVTSpecificPath(AntaTest):\n \"\"\"Verifies the status and type of an Adaptive Virtual Topology (AVT) path for a specified VRF.\n\n Expected Results\n ----------------\n * Success: The test will pass if all AVT paths for the specified VRF are active, valid, and match the specified type (direct/multihop) if provided.\n If multiple paths are configured, the test will pass only if all the paths are valid and active.\n * Failure: The test will fail if no AVT paths are configured for the specified VRF, or if any configured path is not active, valid,\n or does not match the specified type.\n\n Examples\n --------\n ```yaml\n anta.tests.avt:\n - VerifyAVTSpecificPath:\n avt_paths:\n - avt_name: CONTROL-PLANE-PROFILE\n vrf: default\n destination: 10.101.255.2\n next_hop: 10.101.255.1\n path_type: direct\n ```\n \"\"\"\n\n description = \"Verifies the status and type of an AVT path for a specified VRF.\"\n categories: ClassVar[list[str]] = [\"avt\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show adaptive-virtual-topology path vrf {vrf} avt {avt_name} destination {destination}\")\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyAVTSpecificPath test.\"\"\"\n\n avt_paths: list[AVTPaths]\n \"\"\"List of AVT paths to verify.\"\"\"\n\n class AVTPaths(BaseModel):\n \"\"\"Model for the details of AVT paths.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"The VRF for the AVT path. Defaults to 'default' if not provided.\"\"\"\n avt_name: str\n \"\"\"Name of the adaptive virtual topology.\"\"\"\n destination: IPv4Address\n \"\"\"The IPv4 address of the AVT peer.\"\"\"\n next_hop: IPv4Address\n \"\"\"The IPv4 address of the next hop for the AVT peer.\"\"\"\n path_type: str | None = None\n \"\"\"The type of the AVT path. If not provided, both 'direct' and 'multihop' paths are considered.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input AVT path/peer.\"\"\"\n return [template.render(vrf=path.vrf, avt_name=path.avt_name, destination=path.destination) for path in self.inputs.avt_paths]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyAVTSpecificPath.\"\"\"\n # Assume the test is successful until a failure is detected\n self.result.is_success()\n\n # Process each command in the instance\n for command, input_avt in zip(self.instance_commands, self.inputs.avt_paths):\n # Extract the command output and parameters\n vrf = command.params.vrf\n avt_name = command.params.avt_name\n peer = str(command.params.destination)\n\n command_output = command.json_output.get(\"vrfs\", {})\n\n # If no AVT is configured, mark the test as failed and skip to the next command\n if not command_output:\n self.result.is_failure(f\"AVT configuration for peer '{peer}' under topology '{avt_name}' in VRF '{vrf}' is not found.\")\n continue\n\n # Extract the AVT paths\n avt_paths = get_value(command_output, f\"{vrf}.avts.{avt_name}.avtPaths\")\n next_hop, input_path_type = str(input_avt.next_hop), input_avt.path_type\n\n nexthop_path_found = path_type_found = False\n\n # Check each AVT path\n for path, path_data in avt_paths.items():\n # If the path does not match the expected next hop, skip to the next path\n if path_data.get(\"nexthopAddr\") != next_hop:\n continue\n\n nexthop_path_found = True\n path_type = \"direct\" if get_value(path_data, \"flags.directPath\") else \"multihop\"\n\n # If the path type does not match the expected path type, skip to the next path\n if input_path_type and path_type != input_path_type:\n continue\n\n path_type_found = True\n valid = get_value(path_data, \"flags.valid\")\n active = get_value(path_data, \"flags.active\")\n\n # Check the path status and type against the expected values\n if not all([valid, active]):\n failure_reasons = []\n if not get_value(path_data, \"flags.active\"):\n failure_reasons.append(\"inactive\")\n if not get_value(path_data, \"flags.valid\"):\n failure_reasons.append(\"invalid\")\n # Construct the failure message prefix\n failed_log = f\"AVT path '{path}' for topology '{avt_name}' in VRF '{vrf}'\"\n self.result.is_failure(f\"{failed_log} is {', '.join(failure_reasons)}.\")\n\n # If no matching next hop or path type was found, mark the test as failed\n if not nexthop_path_found or not path_type_found:\n self.result.is_failure(\n f\"No '{input_path_type}' path found with next-hop address '{next_hop}' for AVT peer '{peer}' under topology '{avt_name}' in VRF '{vrf}'.\"\n )\n
avt_paths
list[AVTPaths]
avt_name
IPv4Address
next_hop
path_type
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.
anta.tests.bfd:\n - VerifyBFDPeersHealth:\n down_threshold: 2\n
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 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\", revision=1),\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
down_threshold
Field(default=None, gt=0)
Verifies the timers of the IPv4 BFD peers in the specified VRF.
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
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 categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\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 tx_interval = bfd_peers.tx_interval\n rx_interval = bfd_peers.rx_interval\n multiplier = bfd_peers.multiplier\n\n # Check if BFD peer configured\n bfd_output = get_value(\n self.instance_commands[0].json_output,\n f\"vrfs..{vrf}..ipv4Neighbors..{peer}..peerStats..\",\n separator=\"..\",\n )\n if not bfd_output:\n failures[peer] = {vrf: \"Not Configured\"}\n continue\n\n # Convert interval timer(s) into milliseconds to be consistent with the inputs.\n bfd_details = bfd_output.get(\"peerStatsDetail\", {})\n op_tx_interval = bfd_details.get(\"operTxInterval\") // 1000\n op_rx_interval = bfd_details.get(\"operRxInterval\") // 1000\n detect_multiplier = bfd_details.get(\"detectMult\")\n intervals_ok = op_tx_interval == tx_interval and op_rx_interval == rx_interval and detect_multiplier == multiplier\n\n # Check timers of BFD peer\n if not intervals_ok:\n failures[peer] = {\n vrf: {\n \"tx_interval\": op_tx_interval,\n \"rx_interval\": op_rx_interval,\n \"multiplier\": detect_multiplier,\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
bfd_peers
list[BFDPeer]
peer_address
tx_interval
BfdInterval
rx_interval
multiplier
BfdMultiplier
Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered.
anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n
class VerifyBFDPeersRegProtocols(AntaTest):\n \"\"\"Verifies that IPv4 BFD peer(s) have the specified protocol(s) registered.\n\n Expected Results\n ----------------\n * Success: The test will pass if IPv4 BFD peers are registered with the specified protocol(s).\n * Failure: The test will fail if IPv4 BFD peers are not found or the specified protocol(s) are not registered for the BFD peer(s).\n\n Examples\n --------\n ```yaml\n anta.tests.bfd:\n - VerifyBFDPeersRegProtocols:\n bfd_peers:\n - peer_address: 192.0.255.7\n vrf: default\n protocols:\n - bgp\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bfd\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bfd peers detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBFDPeersRegProtocols 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 protocols: list[BfdProtocol]\n \"\"\"List of protocols to be verified.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBFDPeersRegProtocols.\"\"\"\n # Initialize failure messages\n failures: dict[Any, Any] = {}\n\n # Iterating over BFD peers, extract the parameters and command output\n for bfd_peer in self.inputs.bfd_peers:\n peer = str(bfd_peer.peer_address)\n vrf = bfd_peer.vrf\n protocols = bfd_peer.protocols\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 registered protocols\n difference = set(protocols) - set(get_value(bfd_output, \"peerStatsDetail.apps\"))\n\n if difference:\n failures[peer] = {vrf: sorted(difference)}\n\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BFD peers are not configured or have non-registered protocol(s):\\n{failures}\")\n
protocols
list[BfdProtocol]
Verifies if the IPv4 BFD peer\u2019s sessions are UP and remote disc is non-zero in the specified VRF.
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
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 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\", revision=1)]\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
Verifies there is no difference between the running-config and the startup-config.
anta.tests.configuration:\n - VerifyRunningConfigDiffs:\n
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 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
Verifies the given regular expression patterns are present in the running-config.
Since this uses regular expression searches on the whole running-config, it can drastically impact performance and should only be used if no other test is available.
If possible, try using another ANTA test that is more specific.
anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n
class VerifyRunningConfigLines(AntaTest):\n \"\"\"Verifies the given regular expression patterns are present in the running-config.\n\n !!! warning\n Since this uses regular expression searches on the whole running-config, it can\n drastically impact performance and should only be used if no other test is available.\n\n If possible, try using another ANTA test that is more specific.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the patterns are found in the running-config.\n * Failure: The test will fail if any of the patterns are NOT found in the running-config.\n\n Examples\n --------\n ```yaml\n anta.tests.configuration:\n - VerifyRunningConfigLines:\n regex_patterns:\n - \"^enable password.*$\"\n - \"bla bla\"\n ```\n \"\"\"\n\n description = \"Search the Running-Config for the given RegEx patterns.\"\n categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show running-config\", ofmt=\"text\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyRunningConfigLines test.\"\"\"\n\n regex_patterns: list[RegexString]\n \"\"\"List of regular expressions.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRunningConfigLines.\"\"\"\n failure_msgs = []\n command_output = self.instance_commands[0].text_output\n\n for pattern in self.inputs.regex_patterns:\n re_search = re.compile(pattern, flags=re.MULTILINE)\n\n if not re_search.search(command_output):\n failure_msgs.append(f\"'{pattern}'\")\n\n if not failure_msgs:\n self.result.is_success()\n else:\n self.result.is_failure(\"Following patterns were not found: \" + \",\".join(failure_msgs))\n
regex_patterns
list[RegexString]
Verifies ZeroTouch is disabled.
anta.tests.configuration:\n - VerifyZeroTouch:\n
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 categories: ClassVar[list[str]] = [\"configuration\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show zerotouch\", revision=1)]\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
Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.
This test performs the following checks for each specified LLDP neighbor:
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
anta/tests/connectivity.py
class VerifyLLDPNeighbors(AntaTest):\n \"\"\"Verifies the connection status of the specified LLDP (Link Layer Discovery Protocol) neighbors.\n\n This test performs the following checks for each specified LLDP neighbor:\n\n 1. Confirming matching ports on both local and neighboring devices.\n 2. Ensuring compatibility of device names and interface identifiers.\n 3. Verifying neighbor configurations match expected values per interface; extra neighbors are ignored.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the provided LLDP neighbors are present and correctly 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 in the LLDP table.\n - The system name or port of the LLDP neighbor does not match the expected 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 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\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLLDPNeighbors test.\"\"\"\n\n neighbors: list[LLDPNeighbor]\n \"\"\"List of LLDP neighbors.\"\"\"\n Neighbor: ClassVar[type[Neighbor]] = Neighbor\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLLDPNeighbors.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output[\"lldpNeighbors\"]\n for neighbor in self.inputs.neighbors:\n if neighbor.port not in output:\n self.result.is_failure(f\"{neighbor} - Port not found\")\n continue\n\n if len(lldp_neighbor_info := output[neighbor.port][\"lldpNeighborInfo\"]) == 0:\n self.result.is_failure(f\"{neighbor} - No LLDP neighbors\")\n continue\n\n # Check if the system name and neighbor port matches\n match_found = any(\n info[\"systemName\"] == neighbor.neighbor_device and info[\"neighborInterfaceInfo\"][\"interfaceId_v2\"] == neighbor.neighbor_port\n for info in lldp_neighbor_info\n )\n if not match_found:\n failure_msg = [f\"{info['systemName']}/{info['neighborInterfaceInfo']['interfaceId_v2']}\" for info in lldp_neighbor_info]\n self.result.is_failure(f\"{neighbor} - Wrong LLDP neighbors: {', '.join(failure_msg)}\")\n
neighbors
list[LLDPNeighbor]
Test network reachability to one or many destination IP(s).
anta.tests.connectivity:\n - VerifyReachability:\n hosts:\n - source: Management0\n destination: 1.1.1.1\n vrf: MGMT\n df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\n
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 df_bit: True\n size: 100\n - source: Management0\n destination: 8.8.8.8\n vrf: MGMT\n df_bit: True\n size: 100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"connectivity\"]\n # Template uses '{size}{df_bit}' without space since df_bit includes leading space when enabled\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"ping vrf {vrf} {destination} source {source} size {size}{df_bit} repeat {repeat}\", revision=1)\n ]\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 Host: ClassVar[type[Host]] = Host\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each host in the input list.\"\"\"\n commands = []\n for host in self.inputs.hosts:\n # df_bit includes leading space when enabled, empty string when disabled\n df_bit = \" df-bit\" if host.df_bit else \"\"\n command = template.render(destination=host.destination, source=host.source, vrf=host.vrf, repeat=host.repeat, size=host.size, df_bit=df_bit)\n commands.append(command)\n return commands\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyReachability.\"\"\"\n self.result.is_success()\n\n for command, host in zip(self.instance_commands, self.inputs.hosts):\n if f\"{host.repeat} received\" not in command.json_output[\"messages\"][0]:\n self.result.is_failure(f\"{host} - Unreachable\")\n
list[Host]
Model for a remote host to ping.
source
IPv4Address | Interface
repeat
2
size
100
df_bit
anta/input_models/connectivity.py
class Host(BaseModel):\n \"\"\"Model for a remote host to ping.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\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 size: int = 100\n \"\"\"Specify datagram size. Defaults to 100.\"\"\"\n df_bit: bool = False\n \"\"\"Enable do not fragment bit in IP header. Defaults to False.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the Host for reporting.\n\n Examples\n --------\n Host 10.1.1.1 (src: 10.2.2.2, vrf: mgmt, size: 100B, repeat: 2)\n\n \"\"\"\n df_status = \", df-bit: enabled\" if self.df_bit else \"\"\n return f\"Host {self.destination} (src: {self.source}, vrf: {self.vrf}, size: {self.size}B, repeat: {self.repeat}{df_status})\"\n
LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information.
Interface
neighbor_device
neighbor_port
class LLDPNeighbor(BaseModel):\n \"\"\"LLDP (Link Layer Discovery Protocol) model representing the port details and neighbor information.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n port: Interface\n \"\"\"The LLDP port for the local device.\"\"\"\n neighbor_device: str\n \"\"\"The system name of the LLDP neighbor device.\"\"\"\n neighbor_port: Interface\n \"\"\"The LLDP port on the neighboring device.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the LLDPNeighbor for reporting.\n\n Examples\n --------\n Port Ethernet1 (Neighbor: DC1-SPINE2, Neighbor Port: Ethernet2)\n\n \"\"\"\n return f\"Port {self.port} (Neighbor: {self.neighbor_device}, Neighbor Port: {self.neighbor_port})\"\n
Alias for the LLDPNeighbor model to maintain backward compatibility.
When initialized, it will emit a deprecation warning and call the LLDPNeighbor model.
TODO: Remove this class in ANTA v2.0.0.
class Neighbor(LLDPNeighbor): # pragma: no cover\n \"\"\"Alias for the LLDPNeighbor model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the LLDPNeighbor model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n
__init__(**data: Any) -> None\n
def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the LLDPNeighbor class, emitting a depreciation warning.\"\"\"\n warn(\n message=\"Neighbor model is deprecated and will be removed in ANTA v2.0.0. Use the LLDPNeighbor model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n
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
anta.tests.field_notices:\n - VerifyFieldNotice44Resolution:\n
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 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\", revision=1)]\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 break\n else:\n self.result.is_failure(\"Aboot component not found\")\n return\n\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
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
anta.tests.field_notices:\n - VerifyFieldNotice72Resolution:\n
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 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\", revision=1)]\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_failure(\"Error in running test - Component FixedSystemvrm1 not found in 'show version'\")\n
Verifies if hardware flow tracking is running and an input tracker is active.
This test optionally verifies the tracker interval/timeout and exporter configuration.
anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n
anta/tests/flow_tracking.py
class VerifyHardwareFlowTrackerStatus(AntaTest):\n \"\"\"Verifies if hardware flow tracking is running and an input tracker is active.\n\n This test optionally verifies the tracker interval/timeout and exporter configuration.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware flow tracking is running and an input tracker is active.\n * Failure: The test will fail if hardware flow tracking is not running, an input tracker is not active,\n or the tracker interval/timeout and exporter configuration does not match the expected values.\n\n Examples\n --------\n ```yaml\n anta.tests.flow_tracking:\n - VerifyFlowTrackingHardware:\n trackers:\n - name: FLOW-TRACKER\n record_export:\n on_inactive_timeout: 70000\n on_interval: 300000\n exporters:\n - name: CV-TELEMETRY\n local_interface: Loopback0\n template_interval: 3600000\n ```\n \"\"\"\n\n description = (\n \"Verifies if hardware flow tracking is running and an input tracker is active. Optionally verifies the tracker interval/timeout and exporter configuration.\"\n )\n categories: ClassVar[list[str]] = [\"flow tracking\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show flow tracking hardware tracker {name}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyHardwareFlowTrackerStatus test.\"\"\"\n\n trackers: list[FlowTracker]\n \"\"\"List of flow trackers to verify.\"\"\"\n\n class FlowTracker(BaseModel):\n \"\"\"Detail of a flow tracker.\"\"\"\n\n name: str\n \"\"\"Name of the flow tracker.\"\"\"\n\n record_export: RecordExport | None = None\n \"\"\"Record export configuration for the flow tracker.\"\"\"\n\n exporters: list[Exporter] | None = None\n \"\"\"List of exporters for the flow tracker.\"\"\"\n\n class RecordExport(BaseModel):\n \"\"\"Record export configuration.\"\"\"\n\n on_inactive_timeout: int\n \"\"\"Timeout in milliseconds for exporting records when inactive.\"\"\"\n\n on_interval: int\n \"\"\"Interval in milliseconds for exporting records.\"\"\"\n\n class Exporter(BaseModel):\n \"\"\"Detail of an exporter.\"\"\"\n\n name: str\n \"\"\"Name of the exporter.\"\"\"\n\n local_interface: str\n \"\"\"Local interface used by the exporter.\"\"\"\n\n template_interval: int\n \"\"\"Template interval in milliseconds for the exporter.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each hardware tracker.\"\"\"\n return [template.render(name=tracker.name) for tracker in self.inputs.trackers]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareFlowTrackerStatus.\"\"\"\n self.result.is_success()\n for command, tracker_input in zip(self.instance_commands, self.inputs.trackers):\n hardware_tracker_name = command.params.name\n record_export = tracker_input.record_export.model_dump() if tracker_input.record_export else None\n exporters = [exporter.model_dump() for exporter in tracker_input.exporters] if tracker_input.exporters else None\n command_output = command.json_output\n\n # Check if hardware flow tracking is configured\n if not command_output.get(\"running\"):\n self.result.is_failure(\"Hardware flow tracking is not running.\")\n return\n\n # Check if the input hardware tracker is configured\n tracker_info = command_output[\"trackers\"].get(hardware_tracker_name)\n if not tracker_info:\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not configured.\")\n continue\n\n # Check if the input hardware tracker is active\n if not tracker_info.get(\"active\"):\n self.result.is_failure(f\"Hardware flow tracker `{hardware_tracker_name}` is not active.\")\n continue\n\n # Check the input hardware tracker timeouts\n failure_msg = \"\"\n if record_export:\n record_export_failure = validate_record_export(record_export, tracker_info)\n if record_export_failure:\n failure_msg += record_export_failure\n\n # Check the input hardware tracker exporters' configuration\n if exporters:\n exporters_failure = validate_exporters(exporters, tracker_info)\n if exporters_failure:\n failure_msg += exporters_failure\n\n if failure_msg:\n self.result.is_failure(f\"{hardware_tracker_name}: {failure_msg}\\n\")\n
trackers
list[FlowTracker]
record_export
RecordExport | None
exporters
list[Exporter] | None
on_inactive_timeout
on_interval
local_interface
template_interval
validate_exporters(\n exporters: list[dict[str, str]],\n tracker_info: dict[str, str],\n) -> str\n
Validate the exporter configurations against the tracker info.
list[dict[str, str]]
The list of expected exporter configurations.
tracker_info
dict[str, str]
The actual tracker info from the command output.
Failure message if any exporter configuration does not match.
def validate_exporters(exporters: list[dict[str, str]], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the exporter configurations against the tracker info.\n\n Parameters\n ----------\n exporters\n The list of expected exporter configurations.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n Failure message if any exporter configuration does not match.\n \"\"\"\n failed_log = \"\"\n for exporter in exporters:\n exporter_name = exporter[\"name\"]\n actual_exporter_info = tracker_info[\"exporters\"].get(exporter_name)\n if not actual_exporter_info:\n failed_log += f\"\\nExporter `{exporter_name}` is not configured.\"\n continue\n\n expected_exporter_data = {\"local interface\": exporter[\"local_interface\"], \"template interval\": exporter[\"template_interval\"]}\n actual_exporter_data = {\"local interface\": actual_exporter_info[\"localIntf\"], \"template interval\": actual_exporter_info[\"templateInterval\"]}\n\n if expected_exporter_data != actual_exporter_data:\n failed_msg = get_failed_logs(expected_exporter_data, actual_exporter_data)\n failed_log += f\"\\nExporter `{exporter_name}`: {failed_msg}\"\n return failed_log\n
validate_record_export(\n record_export: dict[str, str],\n tracker_info: dict[str, str],\n) -> str\n
Validate the record export configuration against the tracker info.
The expected record export configuration.
A failure message if the record export configuration does not match, otherwise blank string.
def validate_record_export(record_export: dict[str, str], tracker_info: dict[str, str]) -> str:\n \"\"\"Validate the record export configuration against the tracker info.\n\n Parameters\n ----------\n record_export\n The expected record export configuration.\n tracker_info\n The actual tracker info from the command output.\n\n Returns\n -------\n str\n A failure message if the record export configuration does not match, otherwise blank string.\n \"\"\"\n failed_log = \"\"\n actual_export = {\"inactive timeout\": tracker_info.get(\"inactiveTimeout\"), \"interval\": tracker_info.get(\"activeInterval\")}\n expected_export = {\"inactive timeout\": record_export.get(\"on_inactive_timeout\"), \"interval\": record_export.get(\"on_interval\")}\n if actual_export != expected_export:\n failed_log = get_failed_logs(expected_export, actual_export)\n return failed_log\n
Verifies if a GreenT (GRE Encapsulated Telemetry) policy other than the default is created.
anta.tests.greent:\n - VerifyGreenTCounters:\n
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 description = \"Verifies if a GreenT policy other than the default is created.\"\n categories: ClassVar[list[str]] = [\"greent\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show monitor telemetry postcard policy profile\", revision=1)]\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
Verifies if the GreenT (GRE Encapsulated Telemetry) counters are incremented.
anta.tests.greent:\n - VerifyGreenT:\n
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 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\", revision=1)]\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
Verifies there are no adverse drops on DCS-7280 and DCS-7500 family switches (Arad/Jericho chips).
anta.tests.hardware:\n - VerifyAdverseDrops:\n
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 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\", revision=1)]\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
Verifies the status of power supply fans and all fan trays.
anta.tests.hardware:\n - VerifyEnvironmentCooling:\n states:\n - ok\n
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\", revision=1)]\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
states
Verifies the power supplies status.
anta.tests.hardware:\n - VerifyEnvironmentPower:\n states:\n - ok\n
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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment power\", revision=1)]\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
Verifies the device\u2019s system cooling status.
anta.tests.hardware:\n - VerifyEnvironmentSystemCooling:\n
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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment cooling\", revision=1)]\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
Verifies if the device temperature is within acceptable limits.
anta.tests.hardware:\n - VerifyTemperature:\n
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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature\", revision=1)]\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
Verifies if all the transceivers come from approved manufacturers.
anta.tests.hardware:\n - VerifyTransceiversManufacturers:\n manufacturers:\n - Not Present\n - Arista Networks\n - Arastra, Inc.\n
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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show inventory\", revision=2)]\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
manufacturers
Verifies if all the transceivers are operating at an acceptable temperature.
anta.tests.hardware:\n - VerifyTransceiversTemperature:\n
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 categories: ClassVar[list[str]] = [\"hardware\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show system environment temperature transceiver\", revision=1)]\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
Verifies if Proxy-ARP is enabled for the provided list of interface(s).
anta.tests.interfaces:\n - VerifyIPProxyARP:\n interfaces:\n - Ethernet1\n - Ethernet2\n
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 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}\", revision=2)]\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 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
interfaces
Verifies there are no illegal LACP packets in all port channels.
anta.tests.interfaces:\n - VerifyIllegalLACP:\n
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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show lacp counters all-ports\", revision=1)]\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
Verifies that the interfaces packet discard counters are equal to zero.
anta.tests.interfaces:\n - VerifyInterfaceDiscards:\n
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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters discards\", revision=1)]\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
Verifies there are no interfaces in the errdisabled state.
anta.tests.interfaces:\n - VerifyInterfaceErrDisabled:\n
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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces status\", revision=1)]\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
Verifies that the interfaces error counters are equal to zero.
anta.tests.interfaces:\n - VerifyInterfaceErrors:\n
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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces counters errors\", revision=1)]\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
Verifies if an interface is configured with a correct primary and list of optional secondary IPv4 addresses.
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
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 description = \"Verifies the interface IPv4 addresses.\"\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip interface {interface}\", revision=2)]\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 [template.render(interface=interface.name) for interface in self.inputs.interfaces]\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 for interface in self.inputs.interfaces:\n if interface.name == intf:\n input_interface_detail = interface\n break\n else:\n self.result.is_failure(f\"Could not find `{intf}` in the input interfaces. {GITHUB_SUGGESTION}\")\n continue\n\n input_primary_ip = str(input_interface_detail.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 (param_secondary_ips := input_interface_detail.secondary_ips) is not None:\n input_secondary_ips = sorted([str(network) for network in param_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
list[InterfaceDetail]
primary_ip
IPv4Network
secondary_ips
list[IPv4Network] | None
Verifies that the utilization of interfaces is below a certain threshold.
Load interval (default to 5 minutes) is defined in device configuration. This test has been implemented for full-duplex interfaces only.
anta.tests.interfaces:\n - VerifyInterfaceUtilization:\n threshold: 70.0\n
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 This test has been implemented for full-duplex interfaces only.\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 * Error: The test will error out if the device has at least one non full-duplex interface.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfaceUtilization:\n threshold: 70.0\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show interfaces counters rates\", revision=1),\n AntaCommand(command=\"show interfaces\", revision=1),\n ]\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 # The utilization logic has been implemented for full-duplex interfaces only\n if ((duplex := (interface := interfaces[\"interfaces\"][intf]).get(\"duplex\", None)) is not None and duplex != duplex_full) or (\n (members := interface.get(\"memberInterfaces\", None)) is not None and any(stats[\"duplex\"] != duplex_full for stats in members.values())\n ):\n self.result.is_failure(f\"Interface {intf} or one of its member interfaces is not Full-Duplex. VerifyInterfaceUtilization has not been implemented.\")\n return\n\n if (bandwidth := interfaces[\"interfaces\"][intf][\"bandwidth\"]) == 0:\n self.logger.debug(\"Interface %s has been ignored due to null bandwidth value\", intf)\n continue\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
threshold
Percent
75.0
Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces.
anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n
class VerifyInterfacesSpeed(AntaTest):\n \"\"\"Verifies the speed, lanes, auto-negotiation status, and mode as full duplex for interfaces.\n\n - If the auto-negotiation status is set to True, verifies that auto-negotiation is successful, the mode is full duplex and the speed/lanes match the input.\n - If the auto-negotiation status is set to False, verifies that the mode is full duplex and the speed/lanes match the input.\n\n Expected Results\n ----------------\n * Success: The test will pass if an interface is configured correctly with the specified speed, lanes, auto-negotiation status, and mode as full duplex.\n * Failure: The test will fail if an interface is not found, if the speed, lanes, and auto-negotiation status do not match the input, or mode is not full duplex.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyInterfacesSpeed:\n interfaces:\n - name: Ethernet2\n auto: False\n speed: 10\n - name: Eth3\n auto: True\n speed: 100\n lanes: 1\n - name: Eth2\n auto: False\n speed: 2.5\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\")]\n\n class Input(AntaTest.Input):\n \"\"\"Inputs for the VerifyInterfacesSpeed test.\"\"\"\n\n interfaces: list[InterfaceDetail]\n \"\"\"List of interfaces to be tested\"\"\"\n\n class InterfaceDetail(BaseModel):\n \"\"\"Detail of an interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"The name of the interface.\"\"\"\n auto: bool\n \"\"\"The auto-negotiation status of the interface.\"\"\"\n speed: float = Field(ge=1, le=1000)\n \"\"\"The speed of the interface in Gigabits per second. Valid range is 1 to 1000.\"\"\"\n lanes: None | int = Field(None, ge=1, le=8)\n \"\"\"The number of lanes in the interface. Valid range is 1 to 8. This field is optional.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesSpeed.\"\"\"\n self.result.is_success()\n command_output = self.instance_commands[0].json_output\n\n # Iterate over all the interfaces\n for interface in self.inputs.interfaces:\n intf = interface.name\n\n # Check if interface exists\n if not (interface_output := get_value(command_output, f\"interfaces.{intf}\")):\n self.result.is_failure(f\"Interface `{intf}` is not found.\")\n continue\n\n auto_negotiation = interface_output.get(\"autoNegotiate\")\n actual_lanes = interface_output.get(\"lanes\")\n\n # Collecting actual interface details\n actual_interface_output = {\n \"auto negotiation\": auto_negotiation if interface.auto is True else None,\n \"duplex mode\": interface_output.get(\"duplex\"),\n \"speed\": interface_output.get(\"bandwidth\"),\n \"lanes\": actual_lanes if interface.lanes is not None else None,\n }\n\n # Forming expected interface details\n expected_interface_output = {\n \"auto negotiation\": \"success\" if interface.auto is True else None,\n \"duplex mode\": \"duplexFull\",\n \"speed\": interface.speed * BPS_GBPS_CONVERSIONS,\n \"lanes\": interface.lanes,\n }\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n for output in [actual_interface_output, expected_interface_output]:\n # Convert speed to Gbps for readability\n if output[\"speed\"] is not None:\n output[\"speed\"] = f\"{custom_division(output['speed'], BPS_GBPS_CONVERSIONS)}Gbps\"\n failed_log = get_failed_logs(expected_interface_output, actual_interface_output)\n self.result.is_failure(f\"For interface {intf}:{failed_log}\\n\")\n
EthernetInterface
auto
speed
float
Field(ge=1, le=1000)
lanes
None | int
Field(None, ge=1, le=8)
Verifies the operational states of specified interfaces to ensure they match expected configurations.
This test performs the following checks for each specified interface:
line_protocol_status
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
class VerifyInterfacesStatus(AntaTest):\n \"\"\"Verifies the operational states of specified interfaces to ensure they match expected configurations.\n\n This test performs the following checks for each specified interface:\n\n 1. If `line_protocol_status` is defined, both `status` and `line_protocol_status` are verified for the specified interface.\n 2. If `line_protocol_status` is not provided but the `status` is \"up\", it is assumed that both the status and line protocol should be \"up\".\n 3. If the interface `status` is not \"up\", only the interface's status is validated, with no line protocol check performed.\n\n Expected Results\n ----------------\n * Success: If the interface status and line protocol status matches the expected operational state for all specified interfaces.\n * Failure: If any of the following occur:\n - The specified interface is not configured.\n - The specified interface status and line protocol status does not match the expected operational state for any interface.\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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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 InterfaceState: ClassVar[type[InterfaceState]] = InterfaceState\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyInterfacesStatus.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output\n for interface in self.inputs.interfaces:\n if (intf_status := get_value(command_output[\"interfaceDescriptions\"], interface.name, separator=\"..\")) is None:\n self.result.is_failure(f\"{interface.name} - Not configured\")\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 actual_state = f\"Expected: {interface.status}/{interface.line_protocol_status}, Actual: {status}/{proto}\"\n self.result.is_failure(f\"{interface.name} - {actual_state}\")\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\"):\n self.result.is_failure(f\"{interface.name} - Expected: up/up, Actual: {status}/{proto}\")\n elif interface.status != status:\n self.result.is_failure(f\"{interface.name} - Expected: {interface.status}, Actual: {status}\")\n
list[InterfaceState]
Verifies the IP virtual router MAC address.
anta.tests.interfaces:\n - VerifyIpVirtualRouterMac:\n mac_address: 00:1c:73:00:dc:01\n
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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip virtual-router\", revision=2)]\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
mac_address
MacAddress
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.
anta.tests.interfaces:\n - VerifyL2MTU:\n mtu: 1500\n ignored_interfaces:\n - Management1\n - Vxlan1\n specific_mtu:\n - Ethernet1/1: 1500\n
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 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\", revision=1)]\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
mtu
9214
ignored_interfaces
Field(default=['Management', 'Loopback', 'Vxlan', 'Tunnel'])
specific_mtu
list[dict[str, int]]
Field(default=[])
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.
anta.tests.interfaces:\n - VerifyL3MTU:\n mtu: 1500\n ignored_interfaces:\n - Vxlan1\n specific_mtu:\n - Ethernet1: 2500\n
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 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\", revision=1)]\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
1500
Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces.
anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n
class VerifyLACPInterfacesStatus(AntaTest):\n \"\"\"Verifies the Link Aggregation Control Protocol (LACP) status of the provided interfaces.\n\n - Verifies that the interface is a member of the LACP port channel.\n - Ensures that the synchronization is established.\n - Ensures the interfaces are in the correct state for collecting and distributing traffic.\n - Validates that LACP settings, such as timeouts, are correctly configured. (i.e The long timeout mode, also known as \"slow\" mode, is the default setting.)\n\n Expected Results\n ----------------\n * Success: The test will pass if the provided interfaces are bundled in port channel and all specified parameters are correct.\n * Failure: The test will fail if any interface is not bundled in port channel or any of specified parameter is not correct.\n\n Examples\n --------\n ```yaml\n anta.tests.interfaces:\n - VerifyLACPInterfacesStatus:\n interfaces:\n - name: Ethernet1\n portchannel: Port-Channel100\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show lacp interface {interface}\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyLACPInterfacesStatus test.\"\"\"\n\n interfaces: list[LACPInterface]\n \"\"\"List of LACP member interface.\"\"\"\n\n class LACPInterface(BaseModel):\n \"\"\"Model for an LACP member interface.\"\"\"\n\n name: EthernetInterface\n \"\"\"Ethernet interface to validate.\"\"\"\n portchannel: PortChannelInterface\n \"\"\"Port Channel in which the interface is bundled.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each interface in the input list.\"\"\"\n return [template.render(interface=interface.name) for interface in self.inputs.interfaces]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyLACPInterfacesStatus.\"\"\"\n self.result.is_success()\n\n # Member port verification parameters.\n member_port_details = [\"activity\", \"aggregation\", \"synchronization\", \"collecting\", \"distributing\", \"timeout\"]\n\n # Iterating over command output for different interfaces\n for command, input_entry in zip(self.instance_commands, self.inputs.interfaces):\n interface = input_entry.name\n portchannel = input_entry.portchannel\n\n # Verify if a PortChannel is configured with the provided interface\n if not (interface_details := get_value(command.json_output, f\"portChannels.{portchannel}.interfaces.{interface}\")):\n self.result.is_failure(f\"Interface '{interface}' is not configured to be a member of LACP '{portchannel}'.\")\n continue\n\n # Verify the interface is bundled in port channel.\n actor_port_status = interface_details.get(\"actorPortStatus\")\n if actor_port_status != \"bundled\":\n message = f\"For Interface {interface}:\\nExpected `bundled` as the local port status, but found `{actor_port_status}` instead.\\n\"\n self.result.is_failure(message)\n continue\n\n # Collecting actor and partner port details\n actor_port_details = interface_details.get(\"actorPortState\", {})\n partner_port_details = interface_details.get(\"partnerPortState\", {})\n\n # Collecting actual interface details\n actual_interface_output = {\n \"actor_port_details\": {param: actor_port_details.get(param, \"NotFound\") for param in member_port_details},\n \"partner_port_details\": {param: partner_port_details.get(param, \"NotFound\") for param in member_port_details},\n }\n\n # Forming expected interface details\n expected_details = {param: param != \"timeout\" for param in member_port_details}\n expected_interface_output = {\"actor_port_details\": expected_details, \"partner_port_details\": expected_details}\n\n # Forming failure message\n if actual_interface_output != expected_interface_output:\n message = f\"For Interface {interface}:\\n\"\n actor_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"actor_port_details\", {}), actual_interface_output.get(\"actor_port_details\", {})\n )\n partner_port_failed_log = get_failed_logs(\n expected_interface_output.get(\"partner_port_details\", {}), actual_interface_output.get(\"partner_port_details\", {})\n )\n\n if actor_port_failed_log:\n message += f\"Actor port details:{actor_port_failed_log}\\n\"\n if partner_port_failed_log:\n message += f\"Partner port details:{partner_port_failed_log}\\n\"\n\n self.result.is_failure(message)\n
list[LACPInterface]
portchannel
PortChannelInterface
Verifies that the device has the expected number of loopback interfaces and all are operational.
anta.tests.interfaces:\n - VerifyLoopbackCount:\n number: 3\n
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 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\", revision=1)]\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: # pragma: no branch\n self.result.is_failure(f\"The following Loopbacks are not up: {down_loopback_interfaces}\")\n
number
PositiveInteger
Verifies there are no inactive ports in all port channels.
anta.tests.interfaces:\n - VerifyPortChannels:\n
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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show port-channel\", revision=1)]\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
Verifies the status of all SVIs.
anta.tests.interfaces:\n - VerifySVI:\n
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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip interface brief\", revision=1)]\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
Verifies there are no interface storm-control drop counters.
anta.tests.interfaces:\n - VerifyStormControlDrops:\n
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 categories: ClassVar[list[str]] = [\"interfaces\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show storm-control\", revision=1)]\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
Model for an interface state.
Literal['up', 'down', 'adminDown']
Literal['up', 'down', 'testing', 'unknown', 'dormant', 'notPresent', 'lowerLayerDown'] | None
anta/input_models/interfaces.py
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
Verifies if LANZ (Latency Analyzer) is enabled.
anta.tests.lanz:\n - VerifyLANZ:\n
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 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\", revision=1)]\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
Verifies if AAA accounting logs are generated.
anta.tests.logging:\n - VerifyLoggingAccounting:\n
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 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
Verifies there are no syslog messages with a severity of ERRORS or higher.
anta.tests.logging:\n - VerifyLoggingErrors:\n
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 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
Verifies if logs are generated with the device FQDN.
This test performs the following checks:
EOS logging buffer should be set to severity level informational or higher for this test to work.
informational
anta.tests.logging:\n - VerifyLoggingHostname:\n
class VerifyLoggingHostname(AntaTest):\n \"\"\"Verifies if logs are generated with the device FQDN.\n\n This test performs the following checks:\n\n 1. Retrieves the device's configured FQDN\n 2. Sends a test log message at the **informational** level\n 3. Retrieves the most recent logs (last 30 seconds)\n 4. Verifies that the test message includes the complete FQDN of the device\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the device's complete FQDN.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The log message does not include the device's FQDN\n - The FQDN in the log message doesn't match the configured FQDN\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingHostname:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show hostname\", revision=1),\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingHostname validation\", ofmt=\"text\"),\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
Verifies logging hosts (syslog servers) for a specified VRF.
anta.tests.logging:\n - VerifyLoggingHosts:\n hosts:\n - 1.1.1.1\n - 2.2.2.2\n vrf: default\n
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 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
Verifies if logs are generated.
anta.tests.logging:\n - VerifyLoggingLogsGeneration:\n
class VerifyLoggingLogsGeneration(AntaTest):\n \"\"\"Verifies if logs are generated.\n\n This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message was successfully logged\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are being generated and the test message is found in recent logs.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The logging system is not capturing new messages\n - No logs are being generated\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingLogsGeneration:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingLogsGeneration validation\", ofmt=\"text\"),\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
Verifies if logging persistent is enabled and logs are saved in flash.
anta.tests.logging:\n - VerifyLoggingPersistent:\n
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 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
Verifies logging source-interface for a specified VRF.
anta.tests.logging:\n - VerifyLoggingSourceIntf:\n interface: Management0\n vrf: default\n
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 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 VerifyLoggingSourceIntf 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 VerifyLoggingSourceIntf.\"\"\"\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
interface
Verifies if logs are generated with the appropriate timestamp.
2024-01-25T15:30:45.123456+00:00
anta.tests.logging:\n - VerifyLoggingTimestamp:\n
class VerifyLoggingTimestamp(AntaTest):\n \"\"\"Verifies if logs are generated with the appropriate timestamp.\n\n This test performs the following checks:\n\n 1. Sends a test log message at the **informational** level\n 2. Retrieves the most recent logs (last 30 seconds)\n 3. Verifies that the test message is present with a high-resolution RFC3339 timestamp format\n - Example format: `2024-01-25T15:30:45.123456+00:00`\n - Includes microsecond precision\n - Contains timezone offset\n\n !!! warning\n EOS logging buffer should be set to severity level `informational` or higher for this test to work.\n\n Expected Results\n ----------------\n * Success: If logs are generated with the correct high-resolution RFC3339 timestamp format.\n * Failure: If any of the following occur:\n - The test message is not found in recent logs\n - The timestamp format does not match the expected RFC3339 format\n\n Examples\n --------\n ```yaml\n anta.tests.logging:\n - VerifyLoggingTimestamp:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"logging\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"send log level informational message ANTA VerifyLoggingTimestamp validation\", ofmt=\"text\"),\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
_get_logging_states(\n logger: logging.Logger, command_output: str\n) -> str\n
Parse show logging output and gets operational logging states used in the tests in this module.
show logging
The logger object.
command_output
The show logging output.
The operational logging states.
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 Parameters\n ----------\n logger\n The logger object.\n command_output\n The `show logging` output.\n\n Returns\n -------\n str\n 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
This section describes all the available tests provided by the ANTA package.
Here are the tests that we currently provide:
All these tests can be imported in a catalog to be used by the ANTA CLI or in your own framework.
Verifies there are no MLAG config-sanity inconsistencies.
anta.tests.mlag:\n - VerifyMlagConfigSanity:\n
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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag config-sanity\", revision=1)]\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 command_output[\"mlagActive\"] 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
Verifies the dual-primary detection and its parameters of the MLAG configuration.
anta.tests.mlag:\n - VerifyMlagDualPrimary:\n detection_delay: 200\n errdisabled: True\n recovery_delay: 60\n recovery_delay_non_mlag: 0\n
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 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\", revision=2)]\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
detection_delay
errdisabled
recovery_delay
recovery_delay_non_mlag
Verifies there are no inactive or active-partial MLAG ports.
anta.tests.mlag:\n - VerifyMlagInterfaces:\n
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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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
Verify the MLAG (Multi-Chassis Link Aggregation) primary priority.
anta.tests.mlag:\n - VerifyMlagPrimaryPriority:\n primary_priority: 3276\n
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 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\", revision=2)]\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
primary_priority
MlagPriority
Verifies the reload-delay parameters of the MLAG configuration.
anta.tests.mlag:\n - VerifyMlagReloadDelay:\n reload_delay: 300\n reload_delay_non_mlag: 330\n
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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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
reload_delay
reload_delay_non_mlag
Verifies the health status of the MLAG configuration.
anta.tests.mlag:\n - VerifyMlagStatus:\n
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 categories: ClassVar[list[str]] = [\"mlag\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show mlag\", revision=2)]\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
Verifies the IGMP snooping global status.
anta.tests.multicast:\n - VerifyIGMPSnoopingGlobal:\n enabled: True\n
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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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
Verifies the IGMP snooping status for the provided VLANs.
anta.tests.multicast:\n - VerifyIGMPSnoopingVlans:\n vlans:\n 10: False\n 12: False\n
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 categories: ClassVar[list[str]] = [\"multicast\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip igmp snooping\", revision=1)]\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
vlans
dict[Vlan, bool]
Verifies the path and telemetry state of all paths under router path-selection.
The expected states are \u2018IPsec established\u2019, \u2018Resolved\u2019 for path and \u2018active\u2019 for telemetry.
anta.tests.path_selection:\n - VerifyPathsHealth:\n
anta/tests/path_selection.py
class VerifyPathsHealth(AntaTest):\n \"\"\"Verifies the path and telemetry state of all paths under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if all path states under router path-selection are either 'IPsec established' or 'Resolved'\n and their telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if any path state is not 'IPsec established' or 'Resolved',\n or the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifyPathsHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show path-selection paths\", revision=1)]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyPathsHealth.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"dpsPeers\"]\n\n # If no paths are configured for router path-selection, the test fails\n if not command_output:\n self.result.is_failure(\"No path configured for router path-selection.\")\n return\n\n # Check the state of each path\n for peer, peer_data in command_output.items():\n for group, group_data in peer_data[\"dpsGroups\"].items():\n for path_data in group_data[\"dpsPaths\"].values():\n path_state = path_data[\"state\"]\n session = path_data[\"dpsSessions\"][\"0\"][\"active\"]\n\n # If the path state of any path is not 'ipsecEstablished' or 'routeResolved', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for peer {peer} in path-group {group} is `{path_state}`.\")\n\n # If the telemetry state of any path is inactive, the test fails\n elif not session:\n self.result.is_failure(f\"Telemetry state for peer {peer} in path-group {group} is `inactive`.\")\n
Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection.
anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n
class VerifySpecificPath(AntaTest):\n \"\"\"Verifies the path and telemetry state of a specific path for an IPv4 peer under router path-selection.\n\n The expected states are 'IPsec established', 'Resolved' for path and 'active' for telemetry.\n\n Expected Results\n ----------------\n * Success: The test will pass if the path state under router path-selection is either 'IPsec established' or 'Resolved'\n and telemetry state as 'active'.\n * Failure: The test will fail if router path-selection is not configured or if the path state is not 'IPsec established' or 'Resolved',\n or if the telemetry state is 'inactive'.\n\n Examples\n --------\n ```yaml\n anta.tests.path_selection:\n - VerifySpecificPath:\n paths:\n - peer: 10.255.0.1\n path_group: internet\n source_address: 100.64.3.2\n destination_address: 100.64.1.2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"path-selection\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show path-selection paths peer {peer} path-group {group} source {source} destination {destination}\", revision=1)\n ]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificPath test.\"\"\"\n\n paths: list[RouterPath]\n \"\"\"List of router paths to verify.\"\"\"\n\n class RouterPath(BaseModel):\n \"\"\"Detail of a router path.\"\"\"\n\n peer: IPv4Address\n \"\"\"Static peer IPv4 address.\"\"\"\n\n path_group: str\n \"\"\"Router path group name.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of path.\"\"\"\n\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of path.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each router path.\"\"\"\n return [\n template.render(peer=path.peer, group=path.path_group, source=path.source_address, destination=path.destination_address) for path in self.inputs.paths\n ]\n\n @skip_on_platforms([\"cEOSLab\", \"vEOS-lab\"])\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificPath.\"\"\"\n self.result.is_success()\n\n # Check the state of each path\n for command in self.instance_commands:\n peer = command.params.peer\n path_group = command.params.group\n source = command.params.source\n destination = command.params.destination\n command_output = command.json_output.get(\"dpsPeers\", [])\n\n # If the peer is not configured for the path group, the test fails\n if not command_output:\n self.result.is_failure(f\"Path `peer: {peer} source: {source} destination: {destination}` is not configured for path-group `{path_group}`.\")\n continue\n\n # Extract the state of the path\n path_output = get_value(command_output, f\"{peer}..dpsGroups..{path_group}..dpsPaths\", separator=\"..\")\n path_state = next(iter(path_output.values())).get(\"state\")\n session = get_value(next(iter(path_output.values())), \"dpsSessions.0.active\")\n\n # If the state of the path is not 'ipsecEstablished' or 'routeResolved', or the telemetry state is 'inactive', the test fails\n if path_state not in [\"ipsecEstablished\", \"routeResolved\"]:\n self.result.is_failure(f\"Path state for `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `{path_state}`.\")\n elif not session:\n self.result.is_failure(\n f\"Telemetry state for path `peer: {peer} source: {source} destination: {destination}` in path-group {path_group} is `inactive`.\"\n )\n
paths
list[RouterPath]
peer
path_group
source_address
destination_address
Verifies that the device is using the provided Ternary Content-Addressable Memory (TCAM) profile.
anta.tests.profiles:\n - VerifyTcamProfile:\n profile: vxlan-routing\n
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 description = \"Verifies the device TCAM profile.\"\n categories: ClassVar[list[str]] = [\"profiles\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hardware tcam profile\", revision=1)]\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
profile
Verifies the device is using the expected UFT (Unified Forwarding Table) mode.
anta.tests.profiles:\n - VerifyUnifiedForwardingTableMode:\n mode: 3\n
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 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\", revision=1)]\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
mode
Literal[0, 1, 2, 3, 4, 'flexible']
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.
anta.tests.ptp:\n - VerifyPtpGMStatus:\n gmid: 0xec:46:70:ff:fe:00:ff:a9\n
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 * Skipped: The test will be skipped if PTP is not configured on the device.\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 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\", revision=2)]\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_skipped(\"PTP is not configured\")\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
gmid
Verifies that the device was locked to the upstream Precision Time Protocol (PTP) Grandmaster (GM) in the last minute.
anta.tests.ptp:\n - VerifyPtpLockStatus:\n
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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpLockStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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
Verifies that the device is configured as a Precision Time Protocol (PTP) Boundary Clock (BC).
anta.tests.ptp:\n - VerifyPtpModeStatus:\n
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 * Skipped: The test will be skipped if PTP is not configured on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpModeStatus:\n ```\n \"\"\"\n\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\", revision=2)]\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_skipped(\"PTP is not configured\")\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
Verifies that the Precision Time Protocol (PTP) timing offset is within \u00b1 1000ns from the master clock.
anta.tests.ptp:\n - VerifyPtpOffset:\n
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 on the device.\n\n Examples\n --------\n ```yaml\n anta.tests.ptp:\n - VerifyPtpOffset:\n ```\n \"\"\"\n\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\", revision=1)]\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
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.
anta.tests.ptp:\n - VerifyPtpPortModeStatus:\n
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 description = \"Verifies the PTP interfaces state.\"\n categories: ClassVar[list[str]] = [\"ptp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ptp\", revision=2)]\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
Verifies if the advertised communities of BGP peers are standard, extended, and large in the specified VRF.
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
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 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\", revision=3)]\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
bgp_peers
list[BgpPeer]
Verifies the advertised and received routes of BGP peers.
The route type should be \u2018valid\u2019 and \u2018active\u2019 for a specified VRF.
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
class VerifyBGPExchangedRoutes(AntaTest):\n \"\"\"Verifies the advertised and received routes of BGP peers.\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show bgp neighbors {peer} advertised-routes vrf {vrf}\", revision=3),\n AntaTemplate(template=\"show bgp neighbors {peer} routes vrf {vrf}\", revision=3),\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 [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\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 = command.params.peer\n vrf = command.params.vrf\n for input_entry in self.inputs.bgp_peers:\n if str(input_entry.peer_address) == peer and input_entry.vrf == vrf:\n advertised_routes = input_entry.advertised_routes\n received_routes = input_entry.received_routes\n break\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
list[BgpNeighbor]
advertised_routes
list[IPv4Network]
received_routes
Verifies the four octet asn capabilities of a BGP peer in a specified VRF.
anta.tests.routing:\n bgp:\n - VerifyBGPPeerASNCap:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n
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 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\", revision=3)]\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
Verifies the count of BGP peers for given address families.
This test performs the following checks for each specified address family:
check_peer_state
Established
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
class VerifyBGPPeerCount(AntaTest):\n \"\"\"Verifies the count of BGP peers for given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Confirms that the specified VRF is configured.\n 2. Counts the number of peers that are:\n - If `check_peer_state` is set to True, Counts the number of BGP peers that are in the `Established` state and\n have successfully negotiated the specified AFI/SAFI\n - If `check_peer_state` is set to False, skips validation of the `Established` state and AFI/SAFI negotiation.\n\n Expected Results\n ----------------\n * Success: If the count of BGP peers matches the expected count with `check_peer_state` enabled/disabled.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - The BGP peer count does not match expected value with `check_peer_state` enabled/disabled.\"\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp summary vrf all\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerCount test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'num_peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.num_peers is None:\n msg = f\"{af} 'num_peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerCount.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n peers_data = vrf_output.get(\"peers\", {}).values()\n if not address_family.check_peer_state:\n # Count the number of peers without considering the state and negotiated AFI/SAFI check if the count matches the expected count\n peer_count = sum(1 for peer_data in peers_data if address_family.eos_key in peer_data)\n else:\n # Count the number of established peers with negotiated AFI/SAFI\n peer_count = sum(\n 1\n for peer_data in peers_data\n if peer_data.get(\"peerState\") == \"Established\" and get_value(peer_data, f\"{address_family.eos_key}.afiSafiState\") == \"negotiated\"\n )\n\n # Check if the count matches the expected count\n if address_family.num_peers != peer_count:\n self.result.is_failure(f\"{address_family} - Expected: {address_family.num_peers}, Actual: {peer_count}\")\n
address_families
list[BgpAddressFamily]
Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s).
By default, all drop statistics counters will be checked for any non-zero values. An optional list of specific drop statistics can be provided for granular testing.
anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n
class VerifyBGPPeerDropStats(AntaTest):\n \"\"\"Verifies BGP NLRI drop statistics for the provided BGP IPv4 peer(s).\n\n By default, all drop statistics counters will be checked for any non-zero values.\n An optional list of specific drop statistics can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's drop statistic(s) are zero.\n * Failure: The test will fail if the BGP peer's drop statistic(s) are non-zero/Not Found or peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerDropStats:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n drop_stats:\n - inDropAsloop\n - prefixEvpnDroppedUnsupportedRouteType\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerDropStats 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 drop_stats: list[BgpDropStats] | None = None\n \"\"\"Optional list of drop statistics to be verified. If not provided, test will verifies all the drop statistics.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerDropStats.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n drop_statistics = input_entry.drop_stats\n\n # Verify BGP peer\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify BGP peer's drop stats\n drop_stats_output = peer_detail.get(\"dropStats\", {})\n\n # In case drop stats not provided, It will check all drop statistics\n if not drop_statistics:\n drop_statistics = drop_stats_output\n\n # Verify BGP peer's drop stats\n drop_stats_not_ok = {\n drop_stat: drop_stats_output.get(drop_stat, \"Not Found\") for drop_stat in drop_statistics if drop_stats_output.get(drop_stat, \"Not Found\")\n }\n if any(drop_stats_not_ok):\n failures[peer] = {vrf: drop_stats_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero NLRI drop statistics counters:\\n{failures}\")\n
drop_stats
list[BgpDropStats] | None
Verifies the MD5 authentication and state of IPv4 BGP peers in a specified VRF.
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
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 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\", revision=3)]\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
Verifies the multiprotocol capabilities of a BGP peer in a specified VRF.
Supports strict: True to verify that only the specified capabilities are configured, requiring an exact match.
strict: True
anta.tests.routing:\n bgp:\n - VerifyBGPPeerMPCaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n strict: False\n capabilities:\n - ipv4Unicast\n
class VerifyBGPPeerMPCaps(AntaTest):\n \"\"\"Verifies the multiprotocol capabilities of a BGP peer in a specified VRF.\n\n Supports `strict: True` to verify that only the specified capabilities are configured, requiring an exact match.\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 strict: False\n capabilities:\n - ipv4Unicast\n ```\n \"\"\"\n\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\", revision=3)]\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 strict: bool = False\n \"\"\"If True, requires exact matching of provided capabilities. Defaults to False.\"\"\"\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 # Fetching the capabilities output.\n bgp_output = get_value(bgp_output, \"neighborCapabilities.multiprotocolCaps\")\n\n if bgp_peer.strict and sorted(capabilities) != sorted(bgp_output):\n failure[\"bgp_peers\"][peer][vrf] = {\n \"status\": f\"Expected only `{', '.join(capabilities)}` capabilities should be listed but found `{', '.join(bgp_output)}` instead.\"\n }\n failures = deep_update(failures, failure)\n continue\n\n # Check each capability\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
capabilities
list[MultiProtocolCaps]
Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s).
anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n
class VerifyBGPPeerRouteLimit(AntaTest):\n \"\"\"Verifies the maximum routes and optionally verifies the maximum routes warning limit for the provided BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's maximum routes and, if provided, the maximum routes warning limit are equal to the given limits.\n * Failure: The test will fail if the BGP peer's maximum routes do not match the given limit, or if the maximum routes warning limit is provided\n and does not match the given limit, or if the peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteLimit:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n maximum_routes: 12000\n warning_limit: 10000\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerRouteLimit 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 maximum_routes: int = Field(ge=0, le=4294967294)\n \"\"\"The maximum allowable number of BGP routes, `0` means unlimited.\"\"\"\n warning_limit: int = Field(default=0, ge=0, le=4294967294)\n \"\"\"Optional maximum routes warning limit. If not provided, it defaults to `0` meaning no warning limit.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerRouteLimit.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n maximum_routes = input_entry.maximum_routes\n warning_limit = input_entry.warning_limit\n failure: dict[Any, Any] = {}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify maximum routes configured.\n if (actual_routes := peer_detail.get(\"maxTotalRoutes\", \"Not Found\")) != maximum_routes:\n failure[\"Maximum total routes\"] = actual_routes\n\n # Verify warning limit if given.\n if warning_limit and (actual_warning_limit := peer_detail.get(\"totalRoutesWarnLimit\", \"Not Found\")) != warning_limit:\n failure[\"Warning limit\"] = actual_warning_limit\n\n # Updated failures if any.\n if failure:\n failures[peer] = {vrf: failure}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peer(s) are not configured or maximum routes and maximum routes warning limit is not correct:\\n{failures}\")\n
maximum_routes
Field(ge=0, le=4294967294)
warning_limit
Field(default=0, ge=0, le=4294967294)
Verifies the route refresh capabilities of a BGP peer in a specified VRF.
anta.tests.routing:\n bgp:\n - VerifyBGPPeerRouteRefreshCap:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n
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 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\", revision=3)]\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
Verifies BGP update error counters for the provided BGP IPv4 peer(s).
By default, all update error counters will be checked for any non-zero values. An optional list of specific update error counters can be provided for granular testing.
Note: For \u201cdisabledAfiSafi\u201d error counter field, checking that it\u2019s not \u201cNone\u201d versus 0.
anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n
class VerifyBGPPeerUpdateErrors(AntaTest):\n \"\"\"Verifies BGP update error counters for the provided BGP IPv4 peer(s).\n\n By default, all update error counters will be checked for any non-zero values.\n An optional list of specific update error counters can be provided for granular testing.\n\n Note: For \"disabledAfiSafi\" error counter field, checking that it's not \"None\" versus 0.\n\n Expected Results\n ----------------\n * Success: The test will pass if the BGP peer's update error counter(s) are zero/None.\n * Failure: The test will fail if the BGP peer's update error counter(s) are non-zero/not None/Not Found or\n peer is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBGPPeerUpdateErrors:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n update_error_filter:\n - inUpdErrWithdraw\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeerUpdateErrors 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 update_errors: list[BgpUpdateError] | None = None\n \"\"\"Optional list of update error counters to be verified. If not provided, test will verifies all the update error counters.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeerUpdateErrors.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = command.params.peer\n vrf = command.params.vrf\n update_error_counters = input_entry.update_errors\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Getting the BGP peer's error counters output.\n error_counters_output = peer_detail.get(\"peerInUpdateErrors\", {})\n\n # In case update error counters not provided, It will check all the update error counters.\n if not update_error_counters:\n update_error_counters = error_counters_output\n\n # verifying the error counters.\n error_counters_not_ok = {\n (\"disabledAfiSafi\" if error_counter == \"disabledAfiSafi\" else error_counter): value\n for error_counter in update_error_counters\n if (value := error_counters_output.get(error_counter, \"Not Found\")) != \"None\" and value != 0\n }\n if error_counters_not_ok:\n failures[peer] = {vrf: error_counters_not_ok}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following BGP peers are not configured or have non-zero update error counters:\\n{failures}\")\n
update_errors
list[BgpUpdateError] | None
Verifies the health of BGP peers for given address families.
negotiated
check_tcp_queues
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 check_tcp_queues: false\n
class VerifyBGPPeersHealth(AntaTest):\n \"\"\"Verifies the health of BGP peers for given address families.\n\n This test performs the following checks for each specified address family:\n\n 1. Validates that the VRF is configured.\n 2. Checks if there are any peers for the given AFI/SAFI.\n 3. For each relevant peer:\n - Verifies that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Checks that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified address families and their peers.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - No peers are found for a given AFI/SAFI.\n - Any BGP session is not in the `Established` state.\n - The AFI/SAFI state is not 'negotiated' for any peer.\n - Any TCP message queue (input or output) is not empty when `check_tcp_queues` is `True` (default).\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 check_tcp_queues: false\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPPeersHealth test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPPeersHealth.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n # Check if any peers are found for this AFI/SAFI\n relevant_peers = [\n peer for peer in vrf_output.get(\"peerList\", []) if get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\") is not None\n ]\n\n if not relevant_peers:\n self.result.is_failure(f\"{address_family} - No peers found\")\n continue\n\n for peer in relevant_peers:\n # Check if the BGP session is established\n if peer[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session state is not established; State: {peer['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer['peerAddress']} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n
Verifies the health of specific BGP peer(s) for given address families.
This test performs the following checks for each specified address family and peer:
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
class VerifyBGPSpecificPeers(AntaTest):\n \"\"\"Verifies the health of specific BGP peer(s) for given address families.\n\n This test performs the following checks for each specified address family and peer:\n\n 1. Confirms that the specified VRF is configured.\n 2. For each specified peer:\n - Verifies that the peer is found in the BGP configuration.\n - Checks that the BGP session is in the `Established` state.\n - Confirms that the AFI/SAFI state is `negotiated`.\n - Ensures that both input and output TCP message queues are empty.\n Can be disabled by setting `check_tcp_queues` to `False`.\n\n Expected Results\n ----------------\n * Success: If all checks pass for all specified peers in all address families.\n * Failure: If any of the following occur:\n - The specified VRF is not configured.\n - A specified peer is not found in the BGP configuration.\n - The BGP session for a peer is not in the `Established` state.\n - The AFI/SAFI state is not `negotiated` for a peer.\n - Any TCP message queue (input or output) is not empty for a peer when `check_tcp_queues` is `True` (default).\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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show bgp neighbors vrf all\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBGPSpecificPeers test.\"\"\"\n\n address_families: list[BgpAddressFamily]\n \"\"\"List of BGP address families.\"\"\"\n BgpAfi: ClassVar[type[BgpAfi]] = BgpAfi\n\n @field_validator(\"address_families\")\n @classmethod\n def validate_address_families(cls, address_families: list[BgpAddressFamily]) -> list[BgpAddressFamily]:\n \"\"\"Validate that 'peers' field is provided in each address family.\"\"\"\n for af in address_families:\n if af.peers is None:\n msg = f\"{af} 'peers' field missing in the input\"\n raise ValueError(msg)\n return address_families\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBGPSpecificPeers.\"\"\"\n self.result.is_success()\n\n output = self.instance_commands[0].json_output\n\n for address_family in self.inputs.address_families:\n # Check if the VRF is configured\n if (vrf_output := get_value(output, f\"vrfs.{address_family.vrf}\")) is None:\n self.result.is_failure(f\"{address_family} - VRF not configured\")\n continue\n\n for peer in address_family.peers:\n peer_ip = str(peer)\n\n # Check if the peer is found\n if (peer_data := get_item(vrf_output[\"peerList\"], \"peerAddress\", peer_ip)) is None:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Not configured\")\n continue\n\n # Check if the BGP session is established\n if peer_data[\"state\"] != \"Established\":\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session state is not established; State: {peer_data['state']}\")\n\n # Check if the AFI/SAFI state is negotiated\n capability_status = get_value(peer_data, f\"neighborCapabilities.multiprotocolCaps.{address_family.eos_key}\")\n if not capability_status:\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated\")\n\n if capability_status and not _check_bgp_neighbor_capability(capability_status):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - AFI/SAFI state is not negotiated; {format_data(capability_status)}\")\n\n # Check the TCP session message queues\n inq = peer_data[\"peerTcpInfo\"][\"inputQueueLength\"]\n outq = peer_data[\"peerTcpInfo\"][\"outputQueueLength\"]\n if address_family.check_tcp_queues and (inq != 0 or outq != 0):\n self.result.is_failure(f\"{address_family} Peer: {peer_ip} - Session has non-empty message queues; InQ: {inq}, OutQ: {outq}\")\n
Verifies if the BGP peers are configured with the correct hold and keep-alive timers in the specified VRF.
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
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 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\", revision=3)]\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
hold_time
Field(ge=3, le=7200)
keep_alive_time
Field(ge=0, le=3600)
Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s).
anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n
class VerifyBgpRouteMaps(AntaTest):\n \"\"\"Verifies BGP inbound and outbound route-maps of BGP IPv4 peer(s).\n\n Expected Results\n ----------------\n * Success: The test will pass if the correct route maps are applied in the correct direction (inbound or outbound) for IPv4 BGP peers in the specified VRF.\n * Failure: The test will fail if BGP peers are not configured or any neighbor has an incorrect or missing route map in either the inbound or outbound direction.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n bgp:\n - VerifyBgpRouteMaps:\n bgp_peers:\n - peer_address: 172.30.11.1\n vrf: default\n inbound_route_map: RM-MLAG-PEER-IN\n outbound_route_map: RM-MLAG-PEER-OUT\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp neighbors {peer} vrf {vrf}\", revision=3)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyBgpRouteMaps 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 inbound_route_map: str | None = None\n \"\"\"Inbound route map applied, defaults to None.\"\"\"\n outbound_route_map: str | None = None\n \"\"\"Outbound route map applied, defaults to None.\"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpPeer class.\n\n At least one of 'inbound' or 'outbound' route-map must be provided.\n \"\"\"\n if not (self.inbound_route_map or self.outbound_route_map):\n msg = \"At least one of 'inbound_route_map' or 'outbound_route_map' must be provided.\"\n raise ValueError(msg)\n return self\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each BGP peer in the input list.\"\"\"\n return [template.render(peer=str(bgp_peer.peer_address), vrf=bgp_peer.vrf) for bgp_peer in self.inputs.bgp_peers]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyBgpRouteMaps.\"\"\"\n failures: dict[Any, Any] = {}\n\n for command, input_entry in zip(self.instance_commands, self.inputs.bgp_peers):\n peer = str(input_entry.peer_address)\n vrf = input_entry.vrf\n inbound_route_map = input_entry.inbound_route_map\n outbound_route_map = input_entry.outbound_route_map\n failure: dict[Any, Any] = {vrf: {}}\n\n # Verify BGP peer.\n if not (peer_list := get_value(command.json_output, f\"vrfs.{vrf}.peerList\")) or (peer_detail := get_item(peer_list, \"peerAddress\", peer)) is None:\n failures[peer] = {vrf: \"Not configured\"}\n continue\n\n # Verify Inbound route-map\n if inbound_route_map and (inbound_map := peer_detail.get(\"routeMapInbound\", \"Not Configured\")) != inbound_route_map:\n failure[vrf].update({\"Inbound route-map\": inbound_map})\n\n # Verify Outbound route-map\n if outbound_route_map and (outbound_map := peer_detail.get(\"routeMapOutbound\", \"Not Configured\")) != outbound_route_map:\n failure[vrf].update({\"Outbound route-map\": outbound_map})\n\n if failure[vrf]:\n failures[peer] = failure\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(\n f\"The following BGP peers are not configured or has an incorrect or missing route map in either the inbound or outbound direction:\\n{failures}\"\n )\n
inbound_route_map
outbound_route_map
Verifies the EVPN Type-2 routes for a given IPv4 or MAC address and VNI.
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
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 categories: ClassVar[list[str]] = [\"bgp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show bgp evpn route-type mac-ip {address} vni {vni}\", revision=2)]\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=str(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 = 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
vxlan_endpoints
list[VxlanEndpoint]
address
IPv4Address | MacAddress
vni
Vni
Model for a BGP address family.
afi
Afi
safi
Safi | None
num_peers
PositiveInt | None
peers
list[IPv4Address | IPv6Address] | None
anta/input_models/routing/bgp.py
class BgpAddressFamily(BaseModel):\n \"\"\"Model for a BGP address family.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n afi: Afi\n \"\"\"BGP Address Family Identifier (AFI).\"\"\"\n safi: Safi | None = None\n \"\"\"BGP Subsequent Address Family Identifier (SAFI). Required when `afi` is `ipv4` or `ipv6`.\"\"\"\n vrf: str = \"default\"\n \"\"\"Optional VRF when `afi` is `ipv4` or `ipv6`. Defaults to `default`.\n\n If the input `afi` is NOT `ipv4` or `ipv6` (e.g. `evpn`, `vpn-ipv4`, etc.), the `vrf` must be `default`.\n\n These AFIs operate at a global level and do not use the VRF concept in the same way as IPv4/IPv6.\n \"\"\"\n num_peers: PositiveInt | None = None\n \"\"\"Number of expected established BGP peers with negotiated AFI/SAFI. Required field in the `VerifyBGPPeerCount` test.\"\"\"\n peers: list[IPv4Address | IPv6Address] | None = None\n \"\"\"List of expected IPv4/IPv6 BGP peers supporting the AFI/SAFI. Required field in the `VerifyBGPSpecificPeers` test.\"\"\"\n check_tcp_queues: bool = True\n \"\"\"Flag to check if the TCP session queues are empty for a BGP peer. Defaults to `True`.\n\n Can be disabled in the `VerifyBGPPeersHealth` and `VerifyBGPSpecificPeers` tests.\n \"\"\"\n check_peer_state: bool = False\n \"\"\"Flag to check if the peers are established with negotiated AFI/SAFI. Defaults to `False`.\n\n Can be enabled in the `VerifyBGPPeerCount` tests.\n \"\"\"\n\n @model_validator(mode=\"after\")\n def validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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 @property\n def eos_key(self) -> str:\n \"\"\"AFI/SAFI EOS key representation.\"\"\"\n # Pydantic handles the validation of the AFI/SAFI combination, so we can ignore error handling here.\n return AFI_SAFI_EOS_KEY[(self.afi, self.safi)]\n\n def __str__(self) -> str:\n \"\"\"Return a string representation of the BgpAddressFamily model. Used in failure messages.\n\n Examples\n --------\n - AFI:ipv4 SAFI:unicast VRF:default\n - AFI:evpn\n \"\"\"\n base_string = f\"AFI: {self.afi}\"\n if self.safi is not None:\n base_string += f\" SAFI: {self.safi}\"\n if self.afi in [\"ipv4\", \"ipv6\"]:\n base_string += f\" VRF: {self.vrf}\"\n return base_string\n
validate_inputs() -> Self\n
Validate the inputs provided to the BgpAddressFamily class.
If afi is either ipv4 or ipv6, safi must be provided.
ipv4
ipv6
If afi is not ipv4 or ipv6, safi must NOT be provided and vrf must be default.
default
@model_validator(mode=\"after\")\ndef validate_inputs(self) -> Self:\n \"\"\"Validate the inputs provided to the BgpAddressFamily 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
Alias for the BgpAddressFamily model to maintain backward compatibility.
When initialized, it will emit a deprecation warning and call the BgpAddressFamily model.
class BgpAfi(BgpAddressFamily): # pragma: no cover\n \"\"\"Alias for the BgpAddressFamily model to maintain backward compatibility.\n\n When initialized, it will emit a deprecation warning and call the BgpAddressFamily model.\n\n TODO: Remove this class in ANTA v2.0.0.\n \"\"\"\n\n def __init__(self, **data: Any) -> None: # noqa: ANN401\n \"\"\"Initialize the BgpAfi class, emitting a deprecation warning.\"\"\"\n warn(\n message=\"BgpAfi model is deprecated and will be removed in ANTA v2.0.0. Use the BgpAddressFamily model instead.\",\n category=DeprecationWarning,\n stacklevel=2,\n )\n super().__init__(**data)\n
Verifies the configured routing protocol model.
anta.tests.routing:\n generic:\n - VerifyRoutingProtocolModel:\n model: multi-agent\n
anta/tests/routing/generic.py
class VerifyRoutingProtocolModel(AntaTest):\n \"\"\"Verifies the configured routing protocol model.\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 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
model
Literal['multi-agent', 'ribd']
'multi-agent'
Verifies that the provided routes are present in the routing table of a specified VRF.
anta.tests.routing:\n generic:\n - VerifyRoutingTableEntry:\n vrf: default\n routes:\n - 10.1.0.1\n - 10.1.0.2\n
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 categories: ClassVar[list[str]] = [\"routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaTemplate(template=\"show ip route vrf {vrf} {route}\", revision=4),\n AntaTemplate(template=\"show ip route vrf {vrf}\", revision=4),\n ]\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 collect: Literal[\"one\", \"all\"] = \"one\"\n \"\"\"Route collect behavior: one=one route per command, all=all routes in vrf per command. Defaults to `one`\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for the input vrf.\"\"\"\n if template == VerifyRoutingTableEntry.commands[0] and self.inputs.collect == \"one\":\n return [template.render(vrf=self.inputs.vrf, route=route) for route in self.inputs.routes]\n\n if template == VerifyRoutingTableEntry.commands[1] and self.inputs.collect == \"all\":\n return [template.render(vrf=self.inputs.vrf)]\n\n return []\n\n @staticmethod\n @cache\n def ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyRoutingTableEntry.\"\"\"\n commands_output_route_ips = set()\n\n for command in self.instance_commands:\n command_output_vrf = command.json_output[\"vrfs\"][self.inputs.vrf]\n commands_output_route_ips |= {self.ip_interface_ip(route) for route in command_output_vrf[\"routes\"]}\n\n missing_routes = [str(route) for route in self.inputs.routes if route not in commands_output_route_ips]\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
routes
collect
Literal['one', 'all']
'one'
ip_interface_ip(route: str) -> IPv4Address\n
Return the IP address of the provided ip route with mask.
@staticmethod\n@cache\ndef ip_interface_ip(route: str) -> IPv4Address:\n \"\"\"Return the IP address of the provided ip route with mask.\"\"\"\n return IPv4Interface(route).ip\n
Verifies the size of the IP routing table of the default VRF.
anta.tests.routing:\n generic:\n - VerifyRoutingTableSize:\n minimum: 2\n maximum: 20\n
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 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: PositiveInteger\n \"\"\"Expected minimum routing table size.\"\"\"\n maximum: PositiveInteger\n \"\"\"Expected maximum routing table size.\"\"\"\n\n @model_validator(mode=\"after\")\n def check_min_max(self) -> Self:\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
minimum
maximum
Verifies ISIS Interfaces are running in correct mode.
anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n
anta/tests/routing/isis.py
class VerifyISISInterfaceMode(AntaTest):\n \"\"\"Verifies ISIS Interfaces are running in correct mode.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces are running in correct mode.\n * Failure: The test will fail if any of the listed interfaces is not running in correct mode.\n * Skipped: The test will be skipped if no ISIS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISInterfaceMode:\n interfaces:\n - name: Loopback0\n mode: passive\n # vrf is set to default by default\n - name: Ethernet2\n mode: passive\n level: 2\n # vrf is set to default by default\n - name: Ethernet1\n mode: point-to-point\n vrf: default\n # level is set to 2 by default\n ```\n \"\"\"\n\n description = \"Verifies interface mode for IS-IS\"\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceState]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceState(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n mode: Literal[\"point-to-point\", \"broadcast\", \"passive\"]\n \"\"\"Number of IS-IS neighbors.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF where the interface should be configured\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISInterfaceMode.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # Check for p2p interfaces\n for interface in self.inputs.interfaces:\n interface_data = _get_interface_data(\n interface=interface.name,\n vrf=interface.vrf,\n command_output=command_output,\n )\n # Check for correct VRF\n if interface_data is not None:\n interface_type = get_value(dictionary=interface_data, key=\"interfaceType\", default=\"unset\")\n # Check for interfaceType\n if interface.mode == \"point-to-point\" and interface.mode != interface_type:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in {interface.mode} reporting {interface_type}\")\n # Check for passive\n elif interface.mode == \"passive\":\n json_path = f\"intfLevels.{interface.level}.passive\"\n if interface_data is None or get_value(dictionary=interface_data, key=json_path, default=False) is False:\n self.result.is_failure(f\"Interface {interface.name} in VRF {interface.vrf} is not running in passive mode\")\n else:\n self.result.is_failure(f\"Interface {interface.name} not found in VRF {interface.vrf}\")\n
level
Literal[1, 2]
Literal['point-to-point', 'broadcast', 'passive']
Verifies number of IS-IS neighbors per level and per interface.
anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n
class VerifyISISNeighborCount(AntaTest):\n \"\"\"Verifies number of IS-IS neighbors per level and per interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if the number of neighbors is correct.\n * Failure: The test will fail if the number of neighbors is incorrect.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborCount:\n interfaces:\n - name: Ethernet1\n level: 1\n count: 2\n - name: Ethernet2\n level: 2\n count: 1\n - name: Ethernet3\n count: 2\n # level is set to 2 by default\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis interface brief\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n interfaces: list[InterfaceCount]\n \"\"\"list of interfaces with their information.\"\"\"\n\n class InterfaceCount(BaseModel):\n \"\"\"Input model for the VerifyISISNeighborCount test.\"\"\"\n\n name: Interface\n \"\"\"Interface name to check.\"\"\"\n level: int = 2\n \"\"\"IS-IS level to check.\"\"\"\n count: int\n \"\"\"Number of IS-IS neighbors.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborCount.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n isis_neighbor_count = _get_isis_neighbors_count(command_output)\n if len(isis_neighbor_count) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n for interface in self.inputs.interfaces:\n eos_data = [ifl_data for ifl_data in isis_neighbor_count if ifl_data[\"interface\"] == interface.name and ifl_data[\"level\"] == interface.level]\n if not eos_data:\n self.result.is_failure(f\"No neighbor detected for interface {interface.name}\")\n continue\n if eos_data[0][\"count\"] != interface.count:\n self.result.is_failure(\n f\"Interface {interface.name}: \"\n f\"expected Level {interface.level}: count {interface.count}, \"\n f\"got Level {eos_data[0]['level']}: count {eos_data[0]['count']}\"\n )\n
list[InterfaceCount]
count
Verifies all IS-IS neighbors are in UP state.
anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n
class VerifyISISNeighborState(AntaTest):\n \"\"\"Verifies all IS-IS neighbors are in UP state.\n\n Expected Results\n ----------------\n * Success: The test will pass if all IS-IS neighbors are in UP state.\n * Failure: The test will fail if some IS-IS neighbors are not in UP state.\n * Skipped: The test will be skipped if no IS-IS neighbor is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISNeighborState:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis neighbors\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISNeighborState.\"\"\"\n command_output = self.instance_commands[0].json_output\n if _count_isis_neighbor(command_output) == 0:\n self.result.is_skipped(\"No IS-IS neighbor detected\")\n return\n self.result.is_success()\n not_full_neighbors = _get_not_full_isis_neighbors(command_output)\n if not_full_neighbors:\n self.result.is_failure(f\"Some neighbors are not in the correct state (UP): {not_full_neighbors}.\")\n
Verify that all expected Adjacency segments are correctly visible for each interface.
anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n
class VerifyISISSegmentRoutingAdjacencySegments(AntaTest):\n \"\"\"Verify that all expected Adjacency segments are correctly visible for each interface.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed interfaces have correct adjacencies.\n * Failure: The test will fail if any of the listed interfaces has not expected list of adjacencies.\n * Skipped: The test will be skipped if no ISIS SR Adjacency is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingAdjacencySegments:\n instances:\n - name: CORE-ISIS\n vrf: default\n segments:\n - interface: Ethernet2\n address: 10.0.1.3\n sid_origin: dynamic\n\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing adjacency-segments\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingAdjacencySegments test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n segments: list[Segment]\n \"\"\"List of Adjacency segments configured in this instance.\"\"\"\n\n class Segment(BaseModel):\n \"\"\"Segment model definition.\"\"\"\n\n interface: Interface\n \"\"\"Interface name to check.\"\"\"\n level: Literal[1, 2] = 2\n \"\"\"ISIS level configured for interface. Default is 2.\"\"\"\n sid_origin: Literal[\"dynamic\"] = \"dynamic\"\n \"\"\"Adjacency type\"\"\"\n address: IPv4Address\n \"\"\"IP address of remote end of segment.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingAdjacencySegments.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS is not configured on device\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routging.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n for input_segment in instance.segments:\n eos_segment = _get_adjacency_segment_data_by_neighbor(\n neighbor=str(input_segment.address),\n instance=instance.name,\n vrf=instance.vrf,\n command_output=command_output,\n )\n if eos_segment is None:\n failure_message.append(f\"Your segment has not been found: {input_segment}.\")\n\n elif (\n eos_segment[\"localIntf\"] != input_segment.interface\n or eos_segment[\"level\"] != input_segment.level\n or eos_segment[\"sidOrigin\"] != input_segment.sid_origin\n ):\n failure_message.append(f\"Your segment is not correct: Expected: {input_segment} - Found: {eos_segment}.\")\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n
segments
list[Segment]
sid_origin
Literal['dynamic']
'dynamic'
Verify dataplane of a list of ISIS-SR instances.
anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n
class VerifyISISSegmentRoutingDataplane(AntaTest):\n \"\"\"Verify dataplane of a list of ISIS-SR instances.\n\n Expected Results\n ----------------\n * Success: The test will pass if all instances have correct dataplane configured\n * Failure: The test will fail if one of the instances has incorrect dataplane configured\n * Skipped: The test will be skipped if ISIS is not running\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingDataplane:\n instances:\n - name: CORE-ISIS\n vrf: default\n dataplane: MPLS\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingDataplane test.\"\"\"\n\n instances: list[IsisInstance]\n\n class IsisInstance(BaseModel):\n \"\"\"ISIS Instance model definition.\"\"\"\n\n name: str\n \"\"\"ISIS instance name.\"\"\"\n vrf: str = \"default\"\n \"\"\"VRF name where ISIS instance is configured.\"\"\"\n dataplane: Literal[\"MPLS\", \"mpls\", \"unset\"] = \"MPLS\"\n \"\"\"Configured dataplane for the instance.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingDataplane.\"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n if len(command_output[\"vrfs\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n # initiate defaults\n failure_message = []\n skip_vrfs = []\n skip_instances = []\n\n # Check if VRFs and instances are present in output.\n for instance in self.inputs.instances:\n vrf_data = get_value(\n dictionary=command_output,\n key=f\"vrfs.{instance.vrf}\",\n default=None,\n )\n if vrf_data is None:\n skip_vrfs.append(instance.vrf)\n failure_message.append(f\"VRF {instance.vrf} is not configured to run segment routing.\")\n\n elif get_value(dictionary=vrf_data, key=f\"isisInstances.{instance.name}\", default=None) is None:\n skip_instances.append(instance.name)\n failure_message.append(f\"Instance {instance.name} is not found in vrf {instance.vrf}.\")\n\n # Check Adjacency segments\n for instance in self.inputs.instances:\n if instance.vrf not in skip_vrfs and instance.name not in skip_instances:\n eos_dataplane = get_value(dictionary=command_output, key=f\"vrfs.{instance.vrf}.isisInstances.{instance.name}.dataPlane\", default=None)\n if instance.dataplane.upper() != eos_dataplane:\n failure_message.append(f\"ISIS instance {instance.name} is not running dataplane {instance.dataplane} ({eos_dataplane})\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n
dataplane
Literal['MPLS', 'mpls', 'unset']
'MPLS'
Verify ISIS-SR tunnels computed by device.
anta.tests.routing:\nisis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n
class VerifyISISSegmentRoutingTunnels(AntaTest):\n \"\"\"Verify ISIS-SR tunnels computed by device.\n\n Expected Results\n ----------------\n * Success: The test will pass if all listed tunnels are computed on device.\n * Failure: The test will fail if one of the listed tunnels is missing.\n * Skipped: The test will be skipped if ISIS-SR is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n isis:\n - VerifyISISSegmentRoutingTunnels:\n entries:\n # Check only endpoint\n - endpoint: 1.0.0.122/32\n # Check endpoint and via TI-LFA\n - endpoint: 1.0.0.13/32\n vias:\n - type: tunnel\n tunnel_id: ti-lfa\n # Check endpoint and via IP routers\n - endpoint: 1.0.0.14/32\n vias:\n - type: ip\n nexthop: 1.1.1.1\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"isis\", \"segment-routing\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show isis segment-routing tunnel\", ofmt=\"json\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyISISSegmentRoutingTunnels test.\"\"\"\n\n entries: list[Entry]\n \"\"\"List of tunnels to check on device.\"\"\"\n\n class Entry(BaseModel):\n \"\"\"Definition of a tunnel entry.\"\"\"\n\n endpoint: IPv4Network\n \"\"\"Endpoint IP of the tunnel.\"\"\"\n vias: list[Vias] | None = None\n \"\"\"Optional list of path to reach endpoint.\"\"\"\n\n class Vias(BaseModel):\n \"\"\"Definition of a tunnel path.\"\"\"\n\n nexthop: IPv4Address | None = None\n \"\"\"Nexthop of the tunnel. If None, then it is not tested. Default: None\"\"\"\n type: Literal[\"ip\", \"tunnel\"] | None = None\n \"\"\"Type of the tunnel. If None, then it is not tested. Default: None\"\"\"\n interface: Interface | None = None\n \"\"\"Interface of the tunnel. If None, then it is not tested. Default: None\"\"\"\n tunnel_id: Literal[\"TI-LFA\", \"ti-lfa\", \"unset\"] | None = None\n \"\"\"Computation method of the tunnel. If None, then it is not tested. Default: None\"\"\"\n\n def _eos_entry_lookup(self, search_value: IPv4Network, entries: dict[str, Any], search_key: str = \"endpoint\") -> dict[str, Any] | None:\n return next(\n (entry_value for entry_id, entry_value in entries.items() if str(entry_value[search_key]) == str(search_value)),\n None,\n )\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyISISSegmentRoutingTunnels.\n\n This method performs the main test logic for verifying ISIS Segment Routing tunnels.\n It checks the command output, initiates defaults, and performs various checks on the tunnels.\n \"\"\"\n command_output = self.instance_commands[0].json_output\n self.result.is_success()\n\n # initiate defaults\n failure_message = []\n\n if len(command_output[\"entries\"]) == 0:\n self.result.is_skipped(\"IS-IS-SR is not running on device.\")\n return\n\n for input_entry in self.inputs.entries:\n eos_entry = self._eos_entry_lookup(search_value=input_entry.endpoint, entries=command_output[\"entries\"])\n if eos_entry is None:\n failure_message.append(f\"Tunnel to {input_entry} is not found.\")\n elif input_entry.vias is not None:\n failure_src = []\n for via_input in input_entry.vias:\n if not self._check_tunnel_type(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel type\")\n if not self._check_tunnel_nexthop(via_input, eos_entry):\n failure_src.append(\"incorrect nexthop\")\n if not self._check_tunnel_interface(via_input, eos_entry):\n failure_src.append(\"incorrect interface\")\n if not self._check_tunnel_id(via_input, eos_entry):\n failure_src.append(\"incorrect tunnel ID\")\n\n if failure_src:\n failure_message.append(f\"Tunnel to {input_entry.endpoint!s} is incorrect: {', '.join(failure_src)}\")\n\n if failure_message:\n self.result.is_failure(\"\\n\".join(failure_message))\n\n def _check_tunnel_type(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel type specified in `via_input` matches any of the tunnel types in `eos_entry`.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input tunnel type to check.\n eos_entry : dict[str, Any]\n The EOS entry containing the tunnel types.\n\n Returns\n -------\n bool\n True if the tunnel type matches any of the tunnel types in `eos_entry`, False otherwise.\n \"\"\"\n if via_input.type is not None:\n return any(\n via_input.type\n == get_value(\n dictionary=eos_via,\n key=\"type\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_nexthop(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel nexthop matches the given input.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel nexthop matches, False otherwise.\n \"\"\"\n if via_input.nexthop is not None:\n return any(\n str(via_input.nexthop)\n == get_value(\n dictionary=eos_via,\n key=\"nexthop\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_interface(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel interface exists in the given EOS entry.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input via object.\n eos_entry : dict[str, Any]\n The EOS entry dictionary.\n\n Returns\n -------\n bool\n True if the tunnel interface exists, False otherwise.\n \"\"\"\n if via_input.interface is not None:\n return any(\n via_input.interface\n == get_value(\n dictionary=eos_via,\n key=\"interface\",\n default=\"undefined\",\n )\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n\n def _check_tunnel_id(self, via_input: VerifyISISSegmentRoutingTunnels.Input.Entry.Vias, eos_entry: dict[str, Any]) -> bool:\n \"\"\"Check if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias.\n\n Parameters\n ----------\n via_input : VerifyISISSegmentRoutingTunnels.Input.Entry.Vias\n The input vias to check.\n eos_entry : dict[str, Any])\n The EOS entry to compare against.\n\n Returns\n -------\n bool\n True if the tunnel ID matches any of the tunnel IDs in the EOS entry's vias, False otherwise.\n \"\"\"\n if via_input.tunnel_id is not None:\n return any(\n via_input.tunnel_id.upper()\n == get_value(\n dictionary=eos_via,\n key=\"tunnelId.type\",\n default=\"undefined\",\n ).upper()\n for eos_via in eos_entry[\"vias\"]\n )\n return True\n
entries
list[Entry]
endpoint
vias
list[Vias] | None
nexthop
IPv4Address | None
type
Literal['ip', 'tunnel'] | None
Interface | None
tunnel_id
Literal['TI-LFA', 'ti-lfa', 'unset'] | None
Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold.
anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n
anta/tests/routing/ospf.py
class VerifyOSPFMaxLSA(AntaTest):\n \"\"\"Verifies LSAs present in the OSPF link state database did not cross the maximum LSA Threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if all OSPF instances did not cross the maximum LSA Threshold.\n * Failure: The test will fail if some OSPF instances crossed the maximum LSA Threshold.\n * Skipped: The test will be skipped if no OSPF instance is found.\n\n Examples\n --------\n ```yaml\n anta.tests.routing:\n ospf:\n - VerifyOSPFMaxLSA:\n ```\n \"\"\"\n\n description = \"Verifies all OSPF instances did not cross the maximum LSA threshold.\"\n categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyOSPFMaxLSA.\"\"\"\n command_output = self.instance_commands[0].json_output\n ospf_instance_info = _get_ospf_max_lsa_info(command_output)\n if not ospf_instance_info:\n self.result.is_skipped(\"No OSPF instance found.\")\n return\n all_instances_within_threshold = all(instance[\"numLsa\"] <= instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100) for instance in ospf_instance_info)\n if all_instances_within_threshold:\n self.result.is_success()\n else:\n exceeded_instances = [\n instance[\"instance\"] for instance in ospf_instance_info if instance[\"numLsa\"] > instance[\"maxLsa\"] * (instance[\"maxLsaThreshold\"] / 100)\n ]\n self.result.is_failure(f\"OSPF Instances {exceeded_instances} crossed the maximum LSA threshold.\")\n
Verifies the number of OSPF neighbors in FULL state is the one we expect.
anta.tests.routing:\n ospf:\n - VerifyOSPFNeighborCount:\n number: 3\n
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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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
Verifies all OSPF neighbors are in FULL state.
anta.tests.routing:\n ospf:\n - VerifyOSPFNeighborState:\n
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 categories: ClassVar[list[str]] = [\"ospf\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip ospf neighbor\", revision=1)]\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
Verifies if eAPI HTTP server is disabled globally.
anta.tests.security:\n - VerifyAPIHttpStatus:\n
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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands\", revision=1)]\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
Verifies if eAPI HTTPS server SSL profile is configured and valid.
anta.tests.security:\n - VerifyAPIHttpsSSL:\n profile: default\n
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 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\", revision=1)]\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
Verifies if eAPI has the right number IPv4 ACL(s) configured for a specified VRF.
anta.tests.security:\n - VerifyAPIIPv4Acl:\n number: 3\n vrf: default\n
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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ip access-list summary\", revision=1)]\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
Verifies if eAPI has the right number IPv6 ACL(s) configured for a specified VRF.
anta.tests.security:\n - VerifyAPIIPv6Acl:\n number: 3\n vrf: default\n
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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management api http-commands ipv6 access-list summary\", revision=1)]\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
Verifies the eAPI SSL certificate expiry, common subject name, encryption algorithm and key size.
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
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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show management security ssl certificate\", revision=1),\n AntaCommand(command=\"show clock\", revision=1),\n ]\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) -> Self:\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 get_args(RsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for RSA encryption. Allowed sizes are {get_args(RsaKeySize)}.\"\n raise ValueError(msg)\n\n if self.encryption_algorithm == \"ECDSA\" and self.key_size not in get_args(EcdsaKeySize):\n msg = f\"`{self.certificate_name}` key size {self.key_size} is invalid for ECDSA encryption. Allowed sizes are {get_args(EcdsaKeySize)}.\"\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
certificates
list[APISSLCertificate]
certificate_name
expiry_threshold
common_name
encryption_algorithm
EncryptionAlgorithm
key_size
RsaKeySize | EcdsaKeySize
Verifies the login banner of a device.
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
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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner login\", revision=1)]\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
login_banner
Verifies the motd banner of a device.
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
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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show banner motd\", revision=1)]\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
motd_banner
Verifies hardware entropy generation is enabled on device.
anta.tests.security:\n - VerifyHardwareEntropy:\n
class VerifyHardwareEntropy(AntaTest):\n \"\"\"Verifies hardware entropy generation is enabled on device.\n\n Expected Results\n ----------------\n * Success: The test will pass if hardware entropy generation is enabled.\n * Failure: The test will fail if hardware entropy generation is not enabled.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyHardwareEntropy:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management security\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyHardwareEntropy.\"\"\"\n command_output = self.instance_commands[0].json_output\n\n # Check if hardware entropy generation is enabled.\n if not command_output.get(\"hardwareEntropyEnabled\"):\n self.result.is_failure(\"Hardware entropy generation is disabled.\")\n else:\n self.result.is_success()\n
Verifies all IPv4 security connections.
anta.tests.security:\n - VerifyIPSecConnHealth:\n
class VerifyIPSecConnHealth(AntaTest):\n \"\"\"Verifies all IPv4 security connections.\n\n Expected Results\n ----------------\n * Success: The test will pass if all the IPv4 security connections are established in all vrf.\n * Failure: The test will fail if IPv4 security is not configured or any of IPv4 security connections are not established in any vrf.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifyIPSecConnHealth:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ip security connection vrf all\")]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyIPSecConnHealth.\"\"\"\n self.result.is_success()\n failure_conn = []\n command_output = self.instance_commands[0].json_output[\"connections\"]\n\n # Check if IP security connection is configured\n if not command_output:\n self.result.is_failure(\"No IPv4 security connection configured.\")\n return\n\n # Iterate over all ipsec connections\n for conn_data in command_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n failure_conn.append(f\"source:{source} destination:{destination} vrf:{vrf}\")\n if failure_conn:\n failure_msg = \"\\n\".join(failure_conn)\n self.result.is_failure(f\"The following IPv4 security connections are not established:\\n{failure_msg}.\")\n
Verifies the configuration of IPv4 ACLs.
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
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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip access-lists {acl}\", revision=1)]\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) 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, acl in zip(self.instance_commands, self.inputs.ipv4_access_lists):\n # Collecting input ACL details\n acl_name = command_output.params.acl\n # Retrieve the expected entries from the inputs\n acl_entries = acl.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
ipv4_access_lists
list[IPv4ACL]
list[IPv4ACLEntry]
sequence
Field(ge=1, le=4294967295)
action
Verifies if the SSHD agent has the right number IPv4 ACL(s) configured for a specified VRF.
anta.tests.security:\n - VerifySSHIPv4Acl:\n number: 3\n vrf: default\n
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 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\", revision=1)]\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
Verifies if the SSHD agent has the right number IPv6 ACL(s) configured for a specified VRF.
anta.tests.security:\n - VerifySSHIPv6Acl:\n number: 3\n vrf: default\n
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 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\", revision=1)]\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
Verifies if the SSHD agent is disabled in the default VRF.
anta.tests.security:\n - VerifySSHStatus:\n
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 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 try:\n line = next(line for line in command_output.split(\"\\n\") if line.startswith(\"SSHD status\"))\n except StopIteration:\n self.result.is_failure(\"Could not find SSH status in returned output.\")\n return\n status = line.split()[-1]\n\n if status == \"disabled\":\n self.result.is_success()\n else:\n self.result.is_failure(line)\n
Verifies the state of IPv4 security connections for a specified peer.
It optionally allows for the verification of a specific path for a peer by providing source and destination addresses. If these addresses are not provided, it will verify all paths for the specified peer.
anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n
class VerifySpecificIPSecConn(AntaTest):\n \"\"\"Verifies the state of IPv4 security connections for a specified peer.\n\n It optionally allows for the verification of a specific path for a peer by providing source and destination addresses.\n If these addresses are not provided, it will verify all paths for the specified peer.\n\n Expected Results\n ----------------\n * Success: The test passes if the IPv4 security connection for a peer is established in the specified VRF.\n * Failure: The test fails if IPv4 security is not configured, a connection is not found for a peer, or the connection is not established in the specified VRF.\n\n Examples\n --------\n ```yaml\n anta.tests.security:\n - VerifySpecificIPSecConn:\n ip_security_connections:\n - peer: 10.255.0.1\n - peer: 10.255.0.2\n vrf: default\n connections:\n - source_address: 100.64.3.2\n destination_address: 100.64.2.2\n - source_address: 172.18.3.2\n destination_address: 172.18.2.2\n ```\n \"\"\"\n\n description = \"Verifies IPv4 security connections for a peer.\"\n categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show ip security connection vrf {vrf} path peer {peer}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySpecificIPSecConn test.\"\"\"\n\n ip_security_connections: list[IPSecPeers]\n \"\"\"List of IP4v security peers.\"\"\"\n\n class IPSecPeers(BaseModel):\n \"\"\"Details of IPv4 security peers.\"\"\"\n\n peer: IPv4Address\n \"\"\"IPv4 address of the peer.\"\"\"\n\n vrf: str = \"default\"\n \"\"\"Optional VRF for the IP security peer.\"\"\"\n\n connections: list[IPSecConn] | None = None\n \"\"\"Optional list of IPv4 security connections of a peer.\"\"\"\n\n class IPSecConn(BaseModel):\n \"\"\"Details of IPv4 security connections for a peer.\"\"\"\n\n source_address: IPv4Address\n \"\"\"Source IPv4 address of the connection.\"\"\"\n destination_address: IPv4Address\n \"\"\"Destination IPv4 address of the connection.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each input IP Sec connection.\"\"\"\n return [template.render(peer=conn.peer, vrf=conn.vrf) for conn in self.inputs.ip_security_connections]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySpecificIPSecConn.\"\"\"\n self.result.is_success()\n for command_output, input_peer in zip(self.instance_commands, self.inputs.ip_security_connections):\n conn_output = command_output.json_output[\"connections\"]\n peer = command_output.params.peer\n vrf = command_output.params.vrf\n conn_input = input_peer.connections\n\n # Check if IPv4 security connection is configured\n if not conn_output:\n self.result.is_failure(f\"No IPv4 security connection configured for peer `{peer}`.\")\n continue\n\n # If connection details are not provided then check all connections of a peer\n if conn_input is None:\n for conn_data in conn_output.values():\n state = next(iter(conn_data[\"pathDict\"].values()))\n if state != \"Established\":\n source = conn_data.get(\"saddr\")\n destination = conn_data.get(\"daddr\")\n vrf = conn_data.get(\"tunnelNs\")\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source} destination:{destination} vrf:{vrf}` for peer `{peer}` is `Established` \"\n f\"but found `{state}` instead.\"\n )\n continue\n\n # Create a dictionary of existing connections for faster lookup\n existing_connections = {\n (conn_data.get(\"saddr\"), conn_data.get(\"daddr\"), conn_data.get(\"tunnelNs\")): next(iter(conn_data[\"pathDict\"].values()))\n for conn_data in conn_output.values()\n }\n for connection in conn_input:\n source_input = str(connection.source_address)\n destination_input = str(connection.destination_address)\n\n if (source_input, destination_input, vrf) in existing_connections:\n existing_state = existing_connections[(source_input, destination_input, vrf)]\n if existing_state != \"Established\":\n self.result.is_failure(\n f\"Expected state of IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` \"\n f\"for peer `{peer}` is `Established` but found `{existing_state}` instead.\"\n )\n else:\n self.result.is_failure(\n f\"IPv4 security connection `source:{source_input} destination:{destination_input} vrf:{vrf}` for peer `{peer}` is not found.\"\n )\n
ip_security_connections
list[IPSecPeers]
connections
list[IPSecConn] | None
Verifies if Telnet is disabled in the default VRF.
anta.tests.security:\n - VerifyTelnetStatus:\n
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 categories: ClassVar[list[str]] = [\"security\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show management telnet\", revision=1)]\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
Verifies the DNS (Domain Name Service) name to IP address resolution.
anta.tests.services:\n - VerifyDNSLookup:\n domain_names:\n - arista.com\n - www.google.com\n - arista.ca\n
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 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}\", revision=1)]\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
domain_names
Verifies if the DNS (Domain Name Service) servers are correctly configured.
This test performs the following checks for each specified DNS Server:
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
class VerifyDNSServers(AntaTest):\n \"\"\"Verifies if the DNS (Domain Name Service) servers are correctly configured.\n\n This test performs the following checks for each specified DNS Server:\n\n 1. Confirming correctly registered with a valid IPv4 or IPv6 address with the designated VRF.\n 2. Ensuring an appropriate priority level.\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 any of the following conditions are met:\n - The provided DNS server is not configured.\n - The provided DNS server with designated VRF and priority does not match the expected information.\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 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\", revision=1)]\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 DnsServer: ClassVar[type[DnsServer]] = DnsServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyDNSServers.\"\"\"\n self.result.is_success()\n\n command_output = self.instance_commands[0].json_output[\"nameServerConfigs\"]\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 # Check if the DNS server is configured with specified VRF.\n if (output := get_dict_superset(command_output, input_dict)) is None:\n self.result.is_failure(f\"{server} - Not configured\")\n continue\n\n # Check if the DNS server priority matches with expected.\n if output[\"priority\"] != priority:\n self.result.is_failure(f\"{server} - Incorrect priority; Priority: {output['priority']}\")\n
dns_servers
list[DnsServer]
Verifies the errdisable recovery reason, status, and interval.
anta.tests.services:\n - VerifyErrdisableRecovery:\n reasons:\n - reason: acl\n interval: 30\n - reason: bpduguard\n interval: 30\n
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 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
reasons
list[ErrDisableReason]
reason
ErrDisableReasons
interval
ErrDisableInterval
Verifies the hostname of a device.
anta.tests.services:\n - VerifyHostname:\n hostname: s1-spine1\n
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 categories: ClassVar[list[str]] = [\"services\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show hostname\", revision=1)]\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
hostname
Model for a DNS server configuration.
server_address
IPv4Address | IPv6Address
priority
Field(ge=0, le=4)
anta/input_models/services.py
class DnsServer(BaseModel):\n \"\"\"Model for a DNS server configuration.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: IPv4Address | IPv6Address\n \"\"\"The IPv4 or IPv6 address of the DNS server.\"\"\"\n vrf: str = \"default\"\n \"\"\"The VRF instance in which the DNS server resides. Defaults to 'default'.\"\"\"\n priority: int = Field(ge=0, le=4)\n \"\"\"The priority level of the DNS server, ranging from 0 to 4. Lower values indicate a higher priority, with 0 being the highest and 4 the lowest.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Return a human-readable string representation of the DnsServer for reporting.\n\n Examples\n --------\n Server 10.0.0.1 (VRF: default, Priority: 1)\n \"\"\"\n return f\"Server {self.server_address} (VRF: {self.vrf}, Priority: {self.priority})\"\n
Verifies the SNMP contact of a device.
anta.tests.snmp:\n - VerifySnmpContact:\n contact: Jon@example.com\n
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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP contact is configured.\n if not (contact := get_value(self.instance_commands[0].json_output, \"contact.contact\")):\n self.result.is_failure(\"SNMP contact is not configured.\")\n return\n\n # Verifies the expected SNMP contact.\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
contact
Verifies the SNMP error counters.
By default, all error counters will be checked for any non-zero values. An optional list of specific error counters can be provided for granular testing.
```yaml anta.tests.snmp: - VerifySnmpErrorCounters: error_counters: - inVersionErrs - inBadCommunityNames
class VerifySnmpErrorCounters(AntaTest):\n \"\"\"Verifies the SNMP error counters.\n\n By default, all error counters will be checked for any non-zero values.\n An optional list of specific error counters can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP error counter(s) are zero/None.\n * Failure: The test will fail if the SNMP error counter(s) are non-zero/not None/Not Found or is not configured.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpErrorCounters:\n error_counters:\n - inVersionErrs\n - inBadCommunityNames\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpErrorCounters test.\"\"\"\n\n error_counters: list[SnmpErrorCounter] | None = None\n \"\"\"Optional list of SNMP error counters to be verified. If not provided, test will verifies all error counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpErrorCounters.\"\"\"\n error_counters = self.inputs.error_counters\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (snmp_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP error counters not provided, It will check all the error counters.\n if not error_counters:\n error_counters = list(get_args(SnmpErrorCounter))\n\n error_counters_not_ok = {counter: value for counter in error_counters if (value := snmp_counters.get(counter))}\n\n # Check if any failures\n if not error_counters_not_ok:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP error counters are not found or have non-zero error counters:\\n{error_counters_not_ok}\")\n
error_counters
list[SnmpErrorCounter] | None
Verifies if the SNMP agent has the right number IPv4 ACL(s) configured for a specified VRF.
anta.tests.snmp:\n - VerifySnmpIPv4Acl:\n number: 3\n vrf: default\n
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 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\", revision=1)]\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
Verifies if the SNMP agent has the right number IPv6 ACL(s) configured for a specified VRF.
anta.tests.snmp:\n - VerifySnmpIPv6Acl:\n number: 3\n vrf: default\n
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 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\", revision=1)]\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
Verifies the SNMP location of a device.
anta.tests.snmp:\n - VerifySnmpLocation:\n location: New York\n
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 categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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 # Verifies the SNMP location is configured.\n if not (location := get_value(self.instance_commands[0].json_output, \"location.location\")):\n self.result.is_failure(\"SNMP location is not configured.\")\n return\n\n # Verifies the expected SNMP location.\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
Verifies the SNMP PDU counters.
By default, all SNMP PDU counters will be checked for any non-zero values. An optional list of specific SNMP PDU(s) can be provided for granular testing.
anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n
class VerifySnmpPDUCounters(AntaTest):\n \"\"\"Verifies the SNMP PDU counters.\n\n By default, all SNMP PDU counters will be checked for any non-zero values.\n An optional list of specific SNMP PDU(s) can be provided for granular testing.\n\n Expected Results\n ----------------\n * Success: The test will pass if the SNMP PDU counter(s) are non-zero/greater than zero.\n * Failure: The test will fail if the SNMP PDU counter(s) are zero/None/Not Found.\n\n Examples\n --------\n ```yaml\n anta.tests.snmp:\n - VerifySnmpPDUCounters:\n pdus:\n - outTrapPdus\n - inGetNextPdus\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifySnmpPDUCounters test.\"\"\"\n\n pdus: list[SnmpPdu] | None = None\n \"\"\"Optional list of SNMP PDU counters to be verified. If not provided, test will verifies all PDU counters.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifySnmpPDUCounters.\"\"\"\n snmp_pdus = self.inputs.pdus\n command_output = self.instance_commands[0].json_output\n\n # Verify SNMP PDU counters.\n if not (pdu_counters := get_value(command_output, \"counters\")):\n self.result.is_failure(\"SNMP counters not found.\")\n return\n\n # In case SNMP PDUs not provided, It will check all the update error counters.\n if not snmp_pdus:\n snmp_pdus = list(get_args(SnmpPdu))\n\n failures = {pdu: value for pdu in snmp_pdus if (value := pdu_counters.get(pdu, \"Not Found\")) == \"Not Found\" or value == 0}\n\n # Check if any failures\n if not failures:\n self.result.is_success()\n else:\n self.result.is_failure(f\"The following SNMP PDU counters are not found or have zero PDU counters:\\n{failures}\")\n
pdus
list[SnmpPdu] | None
Verifies whether the SNMP agent is enabled in a specified VRF.
anta.tests.snmp:\n - VerifySnmpStatus:\n vrf: default\n
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 description = \"Verifies if the SNMP agent is enabled.\"\n categories: ClassVar[list[str]] = [\"snmp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show snmp\", revision=1)]\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
Verifies that all EOS extensions installed on the device are enabled for boot persistence.
anta.tests.software:\n - VerifyEOSExtensions:\n
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 categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [\n AntaCommand(command=\"show extensions\", revision=2),\n AntaCommand(command=\"show boot-extensions\", revision=1),\n ]\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
Verifies that the device is running one of the allowed EOS version.
anta.tests.software:\n - VerifyEOSVersion:\n versions:\n - 4.25.4M\n - 4.26.1F\n
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 description = \"Verifies the EOS version of the device.\"\n categories: ClassVar[list[str]] = [\"software\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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
versions
Verifies that he device is running one of the allowed TerminAttr version.
anta.tests.software:\n - VerifyTerminAttrVersion:\n versions:\n - v1.13.6\n - v1.8.0\n
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 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\", revision=1)]\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
Verifies there is no STP blocked ports.
anta.tests.stp:\n - VerifySTPBlockedPorts:\n
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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree blockedports\", revision=1)]\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
Verifies there is no errors in STP BPDU packets.
anta.tests.stp:\n - VerifySTPCounters:\n
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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree counters\", revision=1)]\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
Verifies that all interfaces are in a forwarding state for a provided list of VLAN(s).
anta.tests.stp:\n - VerifySTPForwardingPorts:\n vlans:\n - 10\n - 20\n
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 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\", revision=1)]\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 vlan_id = command.params.vlan\n if not (topologies := get_value(command.json_output, \"topologies\")):\n not_configured.append(vlan_id)\n else:\n interfaces_not_forwarding = []\n for value in topologies.values():\n if vlan_id and 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 forwarding state: {not_forwarding}\")\n if not not_configured and not interfaces_not_forwarding:\n self.result.is_success()\n
list[Vlan]
Verifies the configured STP mode for a provided list of VLAN(s).
anta.tests.stp:\n - VerifySTPMode:\n mode: rapidPvst\n vlans:\n - 10\n - 20\n
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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show spanning-tree vlan {vlan}\", revision=1)]\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 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
Literal['mstp', 'rstp', 'rapidPvst']
'mstp'
Verifies the STP root priority for a provided list of VLAN or MST instance ID(s).
anta.tests.stp:\n - VerifySTPRootPriority:\n priority: 32768\n instances:\n - 10\n - 20\n
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 categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree root detail\", revision=1)]\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
instances
Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold.
anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n
class VerifyStpTopologyChanges(AntaTest):\n \"\"\"Verifies the number of changes across all interfaces in the Spanning Tree Protocol (STP) topology is below a threshold.\n\n Expected Results\n ----------------\n * Success: The test will pass if the total number of changes across all interfaces is less than the specified threshold.\n * Failure: The test will fail if the total number of changes across all interfaces meets or exceeds the specified threshold,\n indicating potential instability in the topology.\n\n Examples\n --------\n ```yaml\n anta.tests.stp:\n - VerifyStpTopologyChanges:\n threshold: 10\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stp\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show spanning-tree topology status detail\", revision=1)]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStpTopologyChanges test.\"\"\"\n\n threshold: int\n \"\"\"The threshold number of changes in the STP topology.\"\"\"\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStpTopologyChanges.\"\"\"\n failures: dict[str, Any] = {\"topologies\": {}}\n\n command_output = self.instance_commands[0].json_output\n stp_topologies = command_output.get(\"topologies\", {})\n\n # verifies all available topologies except the \"NoStp\" topology.\n stp_topologies.pop(\"NoStp\", None)\n\n # Verify the STP topology(s).\n if not stp_topologies:\n self.result.is_failure(\"STP is not configured.\")\n return\n\n # Verifies the number of changes across all interfaces\n for topology, topology_details in stp_topologies.items():\n interfaces = {\n interface: {\"Number of changes\": num_of_changes}\n for interface, details in topology_details.get(\"interfaces\", {}).items()\n if (num_of_changes := details.get(\"numChanges\")) > self.inputs.threshold\n }\n if interfaces:\n failures[\"topologies\"][topology] = interfaces\n\n if failures[\"topologies\"]:\n self.result.is_failure(f\"The following STP topologies are not configured or number of changes not within the threshold:\\n{failures}\")\n else:\n self.result.is_success()\n
Verifies STUN client settings, including local IP/port and optionally public IP/port.
anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n
anta/tests/stun.py
class VerifyStunClient(AntaTest):\n \"\"\"Verifies STUN client settings, including local IP/port and optionally public IP/port.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN client is correctly configured with the specified IPv4 source address/port and public address/port.\n * Failure: The test will fail if the STUN client is not configured or if the IPv4 source address, public address, or port details are incorrect.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunClient:\n stun_clients:\n - source_address: 172.18.3.2\n public_address: 172.18.3.21\n source_port: 4500\n public_port: 6006\n - source_address: 100.64.3.2\n public_address: 100.64.3.21\n source_port: 4500\n public_port: 6006\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaTemplate(template=\"show stun client translations {source_address} {source_port}\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyStunClient test.\"\"\"\n\n stun_clients: list[ClientAddress]\n\n class ClientAddress(BaseModel):\n \"\"\"Source and public address/port details of STUN client.\"\"\"\n\n source_address: IPv4Address\n \"\"\"IPv4 source address of STUN client.\"\"\"\n source_port: Port = 4500\n \"\"\"Source port number for STUN client.\"\"\"\n public_address: IPv4Address | None = None\n \"\"\"Optional IPv4 public address of STUN client.\"\"\"\n public_port: Port | None = None\n \"\"\"Optional public port number for STUN client.\"\"\"\n\n def render(self, template: AntaTemplate) -> list[AntaCommand]:\n \"\"\"Render the template for each STUN translation.\"\"\"\n return [template.render(source_address=client.source_address, source_port=client.source_port) for client in self.inputs.stun_clients]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunClient.\"\"\"\n self.result.is_success()\n\n # Iterate over each command output and corresponding client input\n for command, client_input in zip(self.instance_commands, self.inputs.stun_clients):\n bindings = command.json_output[\"bindings\"]\n source_address = str(command.params.source_address)\n source_port = command.params.source_port\n\n # If no bindings are found for the STUN client, mark the test as a failure and continue with the next client\n if not bindings:\n self.result.is_failure(f\"STUN client transaction for source `{source_address}:{source_port}` is not found.\")\n continue\n\n # Extract the public address and port from the client input\n public_address = client_input.public_address\n public_port = client_input.public_port\n\n # Extract the transaction ID from the bindings\n transaction_id = next(iter(bindings.keys()))\n\n # Prepare the actual and expected STUN data for comparison\n actual_stun_data = {\n \"source ip\": get_value(bindings, f\"{transaction_id}.sourceAddress.ip\"),\n \"source port\": get_value(bindings, f\"{transaction_id}.sourceAddress.port\"),\n }\n expected_stun_data = {\"source ip\": source_address, \"source port\": source_port}\n\n # If public address is provided, add it to the actual and expected STUN data\n if public_address is not None:\n actual_stun_data[\"public ip\"] = get_value(bindings, f\"{transaction_id}.publicAddress.ip\")\n expected_stun_data[\"public ip\"] = str(public_address)\n\n # If public port is provided, add it to the actual and expected STUN data\n if public_port is not None:\n actual_stun_data[\"public port\"] = get_value(bindings, f\"{transaction_id}.publicAddress.port\")\n expected_stun_data[\"public port\"] = public_port\n\n # If the actual STUN data does not match the expected STUN data, mark the test as failure\n if actual_stun_data != expected_stun_data:\n failed_log = get_failed_logs(expected_stun_data, actual_stun_data)\n self.result.is_failure(f\"For STUN source `{source_address}:{source_port}`:{failed_log}\")\n
source_port
Port
4500
public_address
public_port
Verifies the STUN server status is enabled and running.
anta.tests.stun:\n - VerifyStunServer:\n
class VerifyStunServer(AntaTest):\n \"\"\"Verifies the STUN server status is enabled and running.\n\n Expected Results\n ----------------\n * Success: The test will pass if the STUN server status is enabled and running.\n * Failure: The test will fail if the STUN server is disabled or not running.\n\n Examples\n --------\n ```yaml\n anta.tests.stun:\n - VerifyStunServer:\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"stun\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show stun server status\", revision=1)]\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyStunServer.\"\"\"\n command_output = self.instance_commands[0].json_output\n status_disabled = not command_output.get(\"enabled\")\n not_running = command_output.get(\"pid\") == 0\n\n if status_disabled and not_running:\n self.result.is_failure(\"STUN server status is disabled and not running.\")\n elif status_disabled:\n self.result.is_failure(\"STUN server status is disabled.\")\n elif not_running:\n self.result.is_failure(\"STUN server is not running.\")\n else:\n self.result.is_success()\n
Verifies there are no agent crash reports.
anta.tests.system:\n - VerifyAgentLogs:\n
anta/tests/system.py
class VerifyAgentLogs(AntaTest):\n \"\"\"Verifies there are no agent crash reports.\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 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
Verifies whether the CPU utilization is below 75%.
anta.tests.system:\n - VerifyCPUUtilization:\n
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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show processes top once\", revision=1)]\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
Verifies if there are core dump files in the /var/core directory.
anta.tests.system:\n - VerifyCoreDump:\n
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 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\", revision=1)]\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
Verifies that no partition is utilizing more than 75% of its disk space.
anta.tests.system:\n - VerifyFileSystemUtilization:\n
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 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
Verifies whether the memory utilization is below 75%.
anta.tests.system:\n - VerifyMemoryUtilization:\n
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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show version\", revision=1)]\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
Verifies that the Network Time Protocol (NTP) is synchronized.
anta.tests.system:\n - VerifyNTP:\n
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 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
Verifies the Network Time Protocol (NTP) associations.
anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n
class VerifyNTPAssociations(AntaTest):\n \"\"\"Verifies the Network Time Protocol (NTP) associations.\n\n Expected Results\n ----------------\n * Success: The test will pass if the Primary NTP server (marked as preferred) has the condition 'sys.peer' and\n all other NTP servers have the condition 'candidate'.\n * Failure: The test will fail if the Primary NTP server (marked as preferred) does not have the condition 'sys.peer' or\n if any other NTP server does not have the condition 'candidate'.\n\n Examples\n --------\n ```yaml\n anta.tests.system:\n - VerifyNTPAssociations:\n ntp_servers:\n - server_address: 1.1.1.1\n preferred: True\n stratum: 1\n - server_address: 2.2.2.2\n stratum: 2\n - server_address: 3.3.3.3\n stratum: 2\n ```\n \"\"\"\n\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show ntp associations\")]\n\n class Input(AntaTest.Input):\n \"\"\"Input model for the VerifyNTPAssociations test.\"\"\"\n\n ntp_servers: list[NTPServer]\n \"\"\"List of NTP servers.\"\"\"\n NTPServer: ClassVar[type[NTPServer]] = NTPServer\n\n @AntaTest.anta_test\n def test(self) -> None:\n \"\"\"Main test function for VerifyNTPAssociations.\"\"\"\n self.result.is_success()\n\n if not (peers := get_value(self.instance_commands[0].json_output, \"peers\")):\n self.result.is_failure(\"No NTP peers configured\")\n return\n\n # Iterate over each NTP server.\n for ntp_server in self.inputs.ntp_servers:\n server_address = str(ntp_server.server_address)\n\n # We check `peerIpAddr` in the peer details - covering IPv4Address input, or the peer key - covering Hostname input.\n matching_peer = next((peer for peer, peer_details in peers.items() if (server_address in {peer_details[\"peerIpAddr\"], peer})), None)\n\n if not matching_peer:\n self.result.is_failure(f\"{ntp_server} - Not configured\")\n continue\n\n # Collecting the expected/actual NTP peer details.\n exp_condition = \"sys.peer\" if ntp_server.preferred else \"candidate\"\n exp_stratum = ntp_server.stratum\n act_condition = get_value(peers[matching_peer], \"condition\")\n act_stratum = get_value(peers[matching_peer], \"stratumLevel\")\n\n if act_condition != exp_condition or act_stratum != exp_stratum:\n self.result.is_failure(f\"{ntp_server} - Bad association; Condition: {act_condition}, Stratum: {act_stratum}\")\n
ntp_servers
list[NTPServer]
Verifies the last reload cause of the device.
anta.tests.system:\n - VerifyReloadCause:\n
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 categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show reload cause\", revision=1)]\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 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
Verifies if the device uptime is higher than the provided minimum uptime value.
anta.tests.system:\n - VerifyUptime:\n minimum: 86400\n
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 description = \"Verifies the device uptime.\"\n categories: ClassVar[list[str]] = [\"system\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show uptime\", revision=1)]\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
Model for a NTP server.
Hostname | IPv4Address
preferred
stratum
Field(ge=0, le=16)
anta/input_models/system.py
class NTPServer(BaseModel):\n \"\"\"Model for a NTP server.\"\"\"\n\n model_config = ConfigDict(extra=\"forbid\")\n server_address: Hostname | IPv4Address\n \"\"\"The NTP server address as an IPv4 address or hostname. The NTP server name defined in the running configuration\n of the device may change during DNS resolution, which is not handled in ANTA. Please provide the DNS-resolved server name.\n For example, 'ntp.example.com' in the configuration might resolve to 'ntp3.example.com' in the device output.\"\"\"\n preferred: bool = False\n \"\"\"Optional preferred for NTP server. If not provided, it defaults to `False`.\"\"\"\n stratum: int = Field(ge=0, le=16)\n \"\"\"NTP stratum level (0 to 15) where 0 is the reference clock and 16 indicates unsynchronized.\n Values should be between 0 and 15 for valid synchronization and 16 represents an out-of-sync state.\"\"\"\n\n def __str__(self) -> str:\n \"\"\"Representation of the NTPServer model.\"\"\"\n return f\"{self.server_address} (Preferred: {self.preferred}, Stratum: {self.stratum})\"\n
Verifies if the VLAN internal allocation policy is ascending or descending and if the VLANs are within the specified range.
anta.tests.vlan:\n - VerifyVlanInternalPolicy:\n policy: ascending\n start_vlan_id: 1006\n end_vlan_id: 4094\n
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 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\", revision=1)]\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
policy
Literal['ascending', 'descending']
start_vlan_id
Vlan
end_vlan_id
Verifies the interface vxlan1 source interface and UDP port.
anta.tests.vxlan:\n - VerifyVxlan1ConnSettings:\n source_interface: Loopback1\n udp_port: 4789\n
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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces\", revision=1)]\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
source_interface
VxlanSrcIntf
udp_port
Field(ge=1024, le=65335)
Verifies if the Vxlan1 interface is configured and \u2018up/up\u2019.
The name of this test has been updated from \u2018VerifyVxlan\u2019 for better representation.
anta.tests.vxlan:\n - VerifyVxlan1Interface:\n
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 description = \"Verifies the Vxlan1 interface status.\"\n categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show interfaces description\", revision=1)]\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
Verifies there are no VXLAN config-sanity inconsistencies.
anta.tests.vxlan:\n - VerifyVxlanConfigSanity:\n
class VerifyVxlanConfigSanity(AntaTest):\n \"\"\"Verifies there are no VXLAN config-sanity inconsistencies.\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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan config-sanity\", revision=1)]\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
Verifies the VNI-VLAN bindings of the Vxlan1 interface.
anta.tests.vxlan:\n - VerifyVxlanVniBinding:\n bindings:\n 10010: 10\n 10020: 20\n
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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vni\", revision=1)]\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
bindings
dict[Vni, Vlan]
Verifies the VTEP peers of the Vxlan1 interface.
anta.tests.vxlan:\n - VerifyVxlanVtep:\n vteps:\n - 10.1.1.5\n - 10.1.1.6\n
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 categories: ClassVar[list[str]] = [\"vxlan\"]\n commands: ClassVar[list[AntaCommand | AntaTemplate]] = [AntaCommand(command=\"show vxlan vtep\", revision=1)]\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
vteps
Module that provides predefined types for AntaTest.Input instances.
module-attribute
AAAAuthMethod = Annotated[\n str, AfterValidator(aaa_group_prefix)\n]\n
Afi = Literal[\n \"ipv4\",\n \"ipv6\",\n \"vpn-ipv4\",\n \"vpn-ipv6\",\n \"evpn\",\n \"rt-membership\",\n \"path-selection\",\n \"link-state\",\n]\n
BfdInterval = Annotated[int, Field(ge=50, le=60000)]\n
BfdMultiplier = Annotated[int, Field(ge=3, le=50)]\n
BfdProtocol = Literal[\n \"bgp\",\n \"isis\",\n \"lag\",\n \"ospf\",\n \"ospfv3\",\n \"pim\",\n \"route-input\",\n \"static-bfd\",\n \"static-route\",\n \"vrrp\",\n \"vxlan\",\n]\n
BgpDropStats = Literal[\n \"inDropAsloop\",\n \"inDropClusterIdLoop\",\n \"inDropMalformedMpbgp\",\n \"inDropOrigId\",\n \"inDropNhLocal\",\n \"inDropNhAfV6\",\n \"prefixDroppedMartianV4\",\n \"prefixDroppedMaxRouteLimitViolatedV4\",\n \"prefixDroppedMartianV6\",\n \"prefixDroppedMaxRouteLimitViolatedV6\",\n \"prefixLuDroppedV4\",\n \"prefixLuDroppedMartianV4\",\n \"prefixLuDroppedMaxRouteLimitViolatedV4\",\n \"prefixLuDroppedV6\",\n \"prefixLuDroppedMartianV6\",\n \"prefixLuDroppedMaxRouteLimitViolatedV6\",\n \"prefixEvpnDroppedUnsupportedRouteType\",\n \"prefixBgpLsDroppedReceptionUnsupported\",\n \"outDropV4LocalAddr\",\n \"outDropV6LocalAddr\",\n \"prefixVpnIpv4DroppedImportMatchFailure\",\n \"prefixVpnIpv4DroppedMaxRouteLimitViolated\",\n \"prefixVpnIpv6DroppedImportMatchFailure\",\n \"prefixVpnIpv6DroppedMaxRouteLimitViolated\",\n \"prefixEvpnDroppedImportMatchFailure\",\n \"prefixEvpnDroppedMaxRouteLimitViolated\",\n \"prefixRtMembershipDroppedLocalAsReject\",\n \"prefixRtMembershipDroppedMaxRouteLimitViolated\",\n]\n
BgpUpdateError = Literal[\n \"inUpdErrWithdraw\",\n \"inUpdErrIgnore\",\n \"inUpdErrDisableAfiSafi\",\n \"disabledAfiSafi\",\n \"lastUpdErrTime\",\n]\n
EcdsaKeySize = Literal[256, 384, 512]\n
EncryptionAlgorithm = Literal['RSA', 'ECDSA']\n
ErrDisableInterval = Annotated[int, Field(ge=30, le=86400)]\n
ErrDisableReasons = Literal[\n \"acl\",\n \"arp-inspection\",\n \"bpduguard\",\n \"dot1x-session-replace\",\n \"hitless-reload-down\",\n \"lacp-rate-limit\",\n \"link-flap\",\n \"no-internal-vlan\",\n \"portchannelguard\",\n \"portsec\",\n \"tapagg\",\n \"uplink-failure-detection\",\n]\n
EthernetInterface = Annotated[\n str,\n Field(pattern=\"^Ethernet[0-9]+(\\\\/[0-9]+)*$\"),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\n
Hostname = Annotated[\n str, Field(pattern=REGEXP_TYPE_HOSTNAME)\n]\n
Interface = Annotated[\n str,\n Field(pattern=REGEXP_TYPE_EOS_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\n
MlagPriority = Annotated[int, Field(ge=1, le=32767)]\n
MultiProtocolCaps = Annotated[\n str,\n BeforeValidator(\n bgp_multiprotocol_capabilities_abbreviations\n ),\n]\n
Percent = Annotated[float, Field(ge=0.0, le=100.0)]\n
Port = Annotated[int, Field(ge=1, le=65535)]\n
PortChannelInterface = Annotated[\n str,\n Field(pattern=REGEX_TYPE_PORTCHANNEL),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\n
PositiveInteger = Annotated[int, Field(ge=0)]\n
REGEXP_BGP_IPV4_MPLS_LABELS = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?label(s)?)\\\\b\"\n)\n
Match IPv4 MPLS Labels.
REGEXP_BGP_L2VPN_AFI = \"\\\\b(l2[\\\\s\\\\-]?vpn[\\\\s\\\\-]?evpn)\\\\b\"\n
Match L2VPN EVPN AFI.
REGEXP_EOS_BLACKLIST_CMDS = [\n \"^reload.*\",\n \"^conf\\\\w*\\\\s*(terminal|session)*\",\n \"^wr\\\\w*\\\\s*\\\\w+\",\n]\n
List of regular expressions to blacklist from eos commands.
REGEXP_INTERFACE_ID = '\\\\d+(\\\\/\\\\d+)*(\\\\.\\\\d+)?'\n
Match Interface ID lilke 1/1.1.
REGEXP_PATH_MARKERS = '[\\\\\\\\\\\\/\\\\s]'\n
Match directory path from string.
REGEXP_TYPE_EOS_INTERFACE = \"^(Dps|Ethernet|Fabric|Loopback|Management|Port-Channel|Tunnel|Vlan|Vxlan)[0-9]+(\\\\/[0-9]+)*(\\\\.[0-9]+)?$\"\n
Match EOS interface types like Ethernet1/1, Vlan1, Loopback1, etc.
REGEXP_TYPE_HOSTNAME = \"^(([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
Match hostname like my-hostname, my-hostname-1, my-hostname-1-2.
my-hostname
my-hostname-1
my-hostname-1-2
REGEXP_TYPE_VXLAN_SRC_INTERFACE = \"^(Loopback)([0-9]|[1-9][0-9]{1,2}|[1-7][0-9]{3}|8[01][0-9]{2}|819[01])$\"\n
Match Vxlan source interface like Loopback10.
REGEX_BGP_IPV4_MPLS_VPN = (\n \"\\\\b(ipv4[\\\\s\\\\-]?mpls[\\\\s\\\\-]?vpn)\\\\b\"\n)\n
Match IPv4 MPLS VPN.
REGEX_BGP_IPV4_UNICAST = (\n \"\\\\b(ipv4[\\\\s\\\\-]?uni[\\\\s\\\\-]?cast)\\\\b\"\n)\n
Match IPv4 Unicast.
REGEX_TYPE_PORTCHANNEL = '^Port-Channel[0-9]{1,6}$'\n
Match Port Channel interface like Port-Channel5.
RegexString = Annotated[str, AfterValidator(validate_regex)]\n
Revision = Annotated[int, Field(ge=1, le=99)]\n
RsaKeySize = Literal[2048, 3072, 4096]\n
Safi = Literal[\n \"unicast\", \"multicast\", \"labeled-unicast\", \"sr-te\"\n]\n
SnmpErrorCounter = Literal[\n \"inVersionErrs\",\n \"inBadCommunityNames\",\n \"inBadCommunityUses\",\n \"inParseErrs\",\n \"outTooBigErrs\",\n \"outNoSuchNameErrs\",\n \"outBadValueErrs\",\n \"outGeneralErrs\",\n]\n
SnmpPdu = Literal[\n \"inGetPdus\",\n \"inGetNextPdus\",\n \"inSetPdus\",\n \"outGetResponsePdus\",\n \"outTrapPdus\",\n]\n
Vlan = Annotated[int, Field(ge=0, le=4094)]\n
Vni = Annotated[int, Field(ge=1, le=16777215)]\n
VxlanSrcIntf = Annotated[\n str,\n Field(pattern=REGEXP_TYPE_VXLAN_SRC_INTERFACE),\n BeforeValidator(interface_autocomplete),\n BeforeValidator(interface_case_sensitivity),\n]\n
aaa_group_prefix(v: str) -> str\n
Prefix the AAA method with \u2018group\u2019 if it is known.
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
bgp_multiprotocol_capabilities_abbreviations(\n value: str,\n) -> str\n
Abbreviations for different BGP multiprotocol capabilities.
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 REGEXP_BGP_L2VPN_AFI: \"l2VpnEvpn\",\n REGEXP_BGP_IPV4_MPLS_LABELS: \"ipv4MplsLabels\",\n REGEX_BGP_IPV4_MPLS_VPN: \"ipv4MplsVpn\",\n REGEX_BGP_IPV4_UNICAST: \"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
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
et
eth
Ethernet
po
Port-Channel
lo
Loopback
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(REGEXP_INTERFACE_ID)\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 return next((f\"{full_name}{intf_id}\" for alias, full_name in alias_map.items() if v.lower().startswith(alias)), v)\n
interface_case_sensitivity(v: str) -> str\n
Reformat interface name to match expected case sensitivity.
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 v != \"\" and not v[0].isupper():\n return f\"{v[0].upper()}{v[1:]}\"\n return v\n
validate_regex(value: str) -> str\n
Validate that the input value is a valid regex format.
def validate_regex(value: str) -> str:\n \"\"\"Validate that the input value is a valid regex format.\"\"\"\n try:\n re.compile(value)\n except re.error as e:\n msg = f\"Invalid regex: {e}\"\n raise ValueError(msg) from e\n return value\n
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
Usage: anta check catalog [OPTIONS]\n\n Check that the catalog is valid.\n\nOptions:\n -c, --catalog FILE Path to the test catalog file [env var:\n ANTA_CATALOG; required]\n --catalog-format [yaml|json] Format of the catalog file, either 'yaml' or\n 'json' [env var: ANTA_CATALOG_FORMAT]\n --help Show this message and exit.\n
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:
debug
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.
The debug tools require a device from your inventory. Thus, you must use a valid ANTA Inventory.
You can use the run-cmd entrypoint to run a command, which includes the following options:
run-cmd
Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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
enable-password
This example illustrates how to run the show interfaces description command with a JSON format (default):
show interfaces description
JSON
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
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.
run-template
f-string
Usage: 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.\n\n 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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
This example uses the show vlan {vlan_id} command in a JSON format:
show vlan {vlan_id}
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
If multiple arguments of the same key are provided, only the last argument value will be kept in the template parameters.
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
ANTA CLI provides a set of entrypoints to facilitate remote command execution on EOS devices.
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
This command clears interface counters on EOS devices specified in your inventory.
Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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
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
This command collects all the commands specified in a commands-list file, which can be in either json or text format.
Usage: 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 FLOAT Global API timeout. This value will be used for\n all devices. [env var: ANTA_TIMEOUT; default:\n 30.0]\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 --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_2024-04-09_15_56_19]\n --help Show this message and exit.\n
The commands-list file should follow this structure:
---\njson_format:\n - show version\ntext_format:\n - show bfd peers\n
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
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.
/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.
Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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 [DEPRECATED] Ensure devices have 'aaa authorization\n exec default local' configured (required for SCP on\n EOS). THIS WILL CHANGE THE CONFIGURATION OF YOUR\n NETWORK.\n --help Show this message and exit.\n
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.
--output
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.
known_hosts
anta --insecure
The configuration aaa authorization exec default must be present on devices to be able to use SCP.
aaa authorization exec default
ANTA can automatically configure aaa authorization exec default local using the anta exec collect-tech-support --configure option but this option is deprecated and will be removed in ANTA 2.0.0.
aaa authorization exec default local
anta exec collect-tech-support --configure
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.
aaa authorization exec default none
aaa authorization exec default group tacacs+
The --latest option allows retrieval of a specific number of the most recent tech-support files.
--latest
By default all the tech-support files present on the devices are retrieved.
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.
The ANTA CLI offers multiple commands to access data from your local inventory.
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 (visit this page to learn more about tags). The --connected option allows to display only the devices where a connection has been established.
--connected
Usage: 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 FLOAT Global API timeout. This value will be used\n for all devices. [env var: ANTA_TIMEOUT;\n default: 30.0]\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 --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
By default, anta get inventory only provides information that doesn\u2019t rely on a device connection. If you are interested in obtaining connection-dependent details, like the hardware model, use the --connected option.
anta get inventory
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
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={'DC1-SPINE1', 'DC1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC1-SPINE2': AsyncEOSDevice(\n name='DC1-SPINE2',\n tags={'DC1', 'SPINE', 'DC1-SPINE2'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE1': AsyncEOSDevice(\n name='DC2-SPINE1',\n tags={'DC2', 'DC2-SPINE1', 'SPINE'},\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=False,\n insecure=False\n ),\n 'DC2-SPINE2': AsyncEOSDevice(\n name='DC2-SPINE2',\n tags={'DC2', 'DC2-SPINE2', 'SPINE'},\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=False,\n insecure=False\n )\n}\n
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.
from-ansible
get
$ anta get from-ansible --help\nUsage: anta get from-ansible [OPTIONS]\n\n Build ANTA inventory from an ansible inventory YAML file.\n\n NOTE: This command does not support inline vaulted variables. Make sure to\n comment them out.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var:\n ANTA_INVENTORY; required]\n --overwrite Do not prompt when overriding current inventory\n [env var: ANTA_GET_FROM_ANSIBLE_OVERWRITE]\n -g, --ansible-group TEXT Ansible group to filter\n --ansible-inventory FILE Path to your ansible inventory file to read\n [required]\n --help Show this message and exit.\n
Warnings
anta get from-ansible does not support inline vaulted variables, comment them out to generate your inventory. If the vaulted variable is necessary to build the inventory (e.g. ansible_host), it needs to be unvaulted for from-ansible command to work.\u201d
anta get from-ansible
ansible_host
The current implementation only considers devices directly attached to a specific Ansible group and does not support inheritance when using the --ansible-group option.
--ansible-group
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
anta --inventory
--overwrite
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:
---\nall:\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
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
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.
from-cvp
The current implementation only works with on-premises CloudVision instances, not with CloudVision as a Service (CVaaS).
Usage: anta get from-cvp [OPTIONS]\n\n Build ANTA inventory from CloudVision.\n\n NOTE: Only username/password authentication is supported for on-premises CloudVision instances.\n Token authentication for both on-premises and CloudVision as a Service (CVaaS) is not supported.\n\nOptions:\n -o, --output FILE Path to save inventory file [env var: ANTA_INVENTORY;\n required]\n --overwrite Do not prompt when overriding current inventory [env\n var: ANTA_GET_FROM_CVP_OVERWRITE]\n -host, --host TEXT CloudVision instance FQDN or IP [required]\n -u, --username TEXT CloudVision username [required]\n -p, --password TEXT CloudVision password [required]\n -c, --container TEXT CloudVision container where devices are configured\n --ignore-cert By default connection to CV will use HTTPS\n certificate, set this flag to disable it [env var:\n ANTA_GET_FROM_CVP_IGNORE_CERT]\n --help Show this message and exit.\n
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
The current implementation only considers devices directly attached to a specific container when using the --cvp-container option.
--cvp-container
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
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:
anta nrfu
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.
--catalog
--inventory
Issuing the command anta nrfu will run anta nrfu table without any option.
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. Refer to the dedicated page for more information.
Options --device and --test can be used to target one or multiple devices and/or tests to run in your environment. The options can be repeated. Example: anta nrfu --device leaf1a --device leaf1b --test VerifyUptime --test VerifyReloadCause.
--device
--test
anta nrfu --device leaf1a --device leaf1b --test VerifyUptime --test VerifyReloadCause
Option --hide can be used to hide test results in the output or report file based on their status. The option can be repeated. Example: anta nrfu --hide error --hide skipped.
--hide
anta nrfu --hide error --hide skipped
The text subcommand provides a straightforward text report for each test executed on all devices in your inventory.
Usage: anta nrfu text [OPTIONS]\n\n ANTA command to check network states with text result.\n\nOptions:\n --help Show this message and exit.\n
anta nrfu --device DC1-LEAF1A text\n
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.
Usage: anta nrfu table [OPTIONS]\n\n ANTA command to check network states with table result.\n\nOptions:\n --group-by [device|test] Group result by test or device.\n --help Show this message and exit.\n
The --group-by option show a summarized view of the test results per host or per test.
--group-by
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 --device spine1 table\n
anta nrfu --test VerifyZeroTouch table\n
The JSON rendering command in NRFU testing will generate an output of all test results in JSON format.
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 JSON 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. If specified, no output will be displayed in the terminal. This is useful for further processing or integration with other tools.
anta nrfu --tags LEAF json\n
The csv command in NRFU testing is useful for generating a CSV file with all tests result. This file can be easily analyzed and filtered by operator for reporting purposes.
csv
anta nrfu csv --help\nUsage: anta nrfu csv [OPTIONS]\n\n ANTA command to check network states with CSV result.\n\nOptions:\n --csv-output FILE Path to save report as a CSV file [env var:\n ANTA_NRFU_CSV_CSV_OUTPUT]\n --help Show this message and exit.\n
The md-report command in NRFU testing generates a comprehensive Markdown report containing various sections, including detailed statistics for devices and test categories.
md-report
anta nrfu md-report --help\n\nUsage: anta nrfu md-report [OPTIONS]\n\n ANTA command to check network state with Markdown report.\n\nOptions:\n --md-output FILE Path to save the report as a Markdown file [env var:\n ANTA_NRFU_MD_REPORT_MD_OUTPUT; required]\n --help Show this message and exit.\n
ANTA offers a CLI option for creating custom reports. This leverages the Jinja2 template system, allowing you to tailor reports to your specific needs.
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.
--template
The --output option allows you to choose the path where the final report will be saved.
anta nrfu --tags LEAF tpl-report --template ./custom_template.j2\n
The template ./custom_template.j2 is a simple Jinja2 template:
./custom_template.j2
{% 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
It is possible to run anta nrfu --dry-run to execute ANTA up to the point where it should communicate with the network to execute the tests. When using --dry-run, all inventory devices are assumed to be online. This can be useful to check how many tests would be run using the catalog and inventory.
anta nrfu --dry-run
--dry-run
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.
$ 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 selected inventory devices.\n
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_CATALOG=tests.yml\n
Then, run the CLI without options:
anta nrfu\n
All environment variables may not be needed for every commands. Refer to <command> --help for the comprehensive environment variables names.
<command> --help
Below are the environment variables usable with the anta nrfu command:
--enable
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.
ANTA CLI utilizes the following exit codes:
Exit code 0
Exit code 1
Exit code 2
Exit code 3
Exit code 4
To ignore the test status, use anta nrfu --ignore-status, and the exit code will always be 0.
anta nrfu --ignore-status
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.
anta nrfu --ignore-error
You can enable shell completion for the ANTA CLI:
If you use ZSH shell, add the following line in your ~/.zshrc:
~/.zshrc
eval \"$(_ANTA_COMPLETE=zsh_source anta)\" > /dev/null\n
With bash, add the following line in your ~/.bashrc:
~/.bashrc
eval \"$(_ANTA_COMPLETE=bash_source anta)\" > /dev/null\n
ANTA uses tags to define test-to-device mappings (tests run on devices with matching tags) and the --tags CLI option acts as a filter to execute specific test/device combinations.
Device tags can be defined in the inventory:
anta_inventory:\n hosts:\n - name: leaf1\n host: leaf1.anta.arista.com\n tags: [\"leaf\"]\n - name: leaf2\n host: leaf2.anta.arista.com\n tags: [\"leaf\"]\n - name: spine1\n host: spine1.anta.arista.com\n tags: [\"spine\"]\n
Each device also has its own name automatically added as a tag:
$ anta get inventory\nCurrent inventory content is:\n{\n 'leaf1': AsyncEOSDevice(\n name='leaf1',\n tags={'leaf', 'leaf1'}, <--\n [...]\n host='leaf1.anta.arista.com',\n [...]\n ),\n 'leaf2': AsyncEOSDevice(\n name='leaf2',\n tags={'leaf', 'leaf2'}, <--\n [...]\n host='leaf2.anta.arista.com',\n [...]\n ),\n 'spine1': AsyncEOSDevice(\n name='spine1',\n tags={'spine1', 'spine'}, <--\n [...]\n host='spine1.anta.arista.com',\n [...]\n )\n}\n
Tags can be defined in the test catalog to restrict tests to tagged devices:
anta.tests.system:\n - VerifyUptime:\n minimum: 10\n filters:\n tags: ['spine']\n - VerifyUptime:\n minimum: 9\n filters:\n tags: ['leaf']\n - VerifyReloadCause:\n filters:\n tags: ['spine', 'leaf']\n - VerifyCoredump:\n - VerifyAgentLogs:\n - VerifyCPUUtilization:\n - VerifyMemoryUtilization:\n - VerifyFileSystemUtilization:\n - VerifyNTP:\n\nanta.tests.mlag:\n - VerifyMlagStatus:\n filters:\n tags: ['leaf']\n\nanta.tests.interfaces:\n - VerifyL3MTU:\n mtu: 1500\n filters:\n tags: ['spine']\n
A tag used to filter a test can also be a device name
Use different input values for a specific test
Leverage tags to define different input values for a specific test. See the VerifyUptime example above.
VerifyUptime
tag
--tags leaf
leaf
--tags leaf,spine
spine
The following examples use the inventory and test catalog defined above.
Tests without tags are run on all devices. Tests with tags will only run on devices with matching tags.
$ anta nrfu table --group-by device\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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 27\n---------------------------------\n Summary per device\n\u250f\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\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 leaf1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 leaf2 \u2502 7 \u2502 1 \u2502 1 \u2502 0 \u2502 VerifyAgentLogs \u2502\n\u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n\u2502 spine1 \u2502 9 \u2502 0 \u2502 0 \u2502 0 \u2502 \u2502\n\u2514\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
With a tag specified, only tests matching this tag will be run on matching devices.
$ anta nrfu --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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (2 established)\nTotal number of selected tests: 6\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\n
In this case, only leaf devices defined in the inventory are used to run tests marked with the leaf in the test catalog.
It is possible to use multiple tags using the --tags tag1,tag2 syntax.
--tags tag1,tag2
$ anta nrfu --tags leaf,spine 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 - ANTA Inventory contains 3 devices (AsyncEOSDevice) \u2502\n\u2502 - Tests catalog contains 11 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--- ANTA NRFU Run Information ---\nNumber of devices: 3 (3 established)\nTotal number of selected tests: 15\n---------------------------------\n\nleaf1 :: VerifyReloadCause :: SUCCESS\nleaf1 :: VerifyMlagStatus :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf1 :: VerifyL3MTU :: SUCCESS\nleaf1 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyReloadCause :: SUCCESS\nleaf2 :: VerifyMlagStatus :: SKIPPED (MLAG is disabled)\nleaf2 :: VerifyUptime :: SUCCESS\nleaf2 :: VerifyL3MTU :: SUCCESS\nleaf2 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyReloadCause :: SUCCESS\nspine1 :: VerifyMlagStatus :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\nspine1 :: VerifyL3MTU :: SUCCESS\nspine1 :: VerifyUptime :: SUCCESS\n
As most ANTA commands accommodate tag filtering, this command is useful for enumerating all tags configured in the inventory. Running the anta get tags command will return a list of all tags configured in the inventory.
anta get tags
Usage: 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 FLOAT Global API timeout. This value will be used for all\n devices. [env var: ANTA_TIMEOUT; default: 30.0]\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 --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
To get the list of all configured tags in the inventory, run the following command:
$ anta get tags\nTags found:\n[\n \"leaf\",\n \"leaf1\",\n \"leaf2\",\n \"spine\",\n \"spine1\"\n]\n
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 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\", revision=1)]\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 break\n else:\n self.result.is_failure(\"Aboot component not found\")\n return\n\n self.result.is_success()\n incorrect_aboot_version = (\n (aboot_version.startswith(\"4.0.\") and int(aboot_version.split(\".\")[2]) < 7)\n or (aboot_version.startswith(\"4.1.\") and int(aboot_version.split(\".\")[2]) < 1)\n or (\n (aboot_version.startswith(\"6.0.\") and int(aboot_version.split(\".\")[2]) < 9)\n or (aboot_version.startswith(\"6.1.\") 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