Skip to content

ANTA Device API

DeviceVersion

Bases: Protocol

Contract implemented by software version representations attached to an AntaDevice.

__str__

__str__() -> str

Return the normalized software version string.

Source code in anta/device.py
72
73
74
def __str__(self) -> str:
    """Return the normalized software version string."""
    raise NotImplementedError

to_dict

to_dict() -> dict[str, str | int]

Return the version components as a JSON-compatible dictionary.

Source code in anta/device.py
76
77
78
def to_dict(self) -> dict[str, str | int]:
    """Return the version components as a JSON-compatible dictionary."""
    raise NotImplementedError

AntaDeviceCapabilities dataclass

AntaDeviceCapabilities(supports_session_auth: bool = False, supports_ssl: bool = False)

Declares the optional features a device implementation supports.

Subclasses of AntaDevice set this as a ClassVar to advertise which ANTA capabilities they implement. The base default is all-False.

Attributes:

Name Type Description
supports_session_auth bool

Whether the device supports eAPI cookie-session authentication.

supports_ssl bool

Whether the device accepts SSL parameters from an ANTA inventory.

SSLParameters dataclass

SSLParameters(ciphers: str | None = None, verify: bool = False, check_hostname: bool = False)

Parameters used to build an SSL context for an HTTPS eAPI connection.

Attributes:

Name Type Description
ciphers str | None

OpenSSL cipher list. None uses the Python defaults.

verify bool

Whether to verify the peer certificate. Defaults to False.

check_hostname bool

Whether to verify that the certificate matches the device hostname. Defaults to False and requires verify=True.

create_ssl_context

create_ssl_context(*, trust_env: bool = True) -> SSLContext

Build an HTTPX-compatible SSL context from these parameters.

Parameters:

Name Type Description Default
trust_env bool

Use the SSL certificate environment variables supported by HTTPX.

True

Returns:

Type Description
SSLContext

Configured SSL context.

Raises:

Type Description
ValueError

If the cipher list is invalid or selects no supported cipher.

Source code in anta/device.py
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
def create_ssl_context(self, *, trust_env: bool = True) -> ssl.SSLContext:
    """Build an HTTPX-compatible SSL context from these parameters.

    Parameters
    ----------
    trust_env
        Use the SSL certificate environment variables supported by HTTPX.

    Returns
    -------
    ssl.SSLContext
        Configured SSL context.

    Raises
    ------
    ValueError
        If the cipher list is invalid or selects no supported cipher.
    """
    context = create_ssl_context(verify=self.verify, trust_env=trust_env)
    context.check_hostname = self.check_hostname
    if self.ciphers is not None:
        try:
            context.set_ciphers(self.ciphers)
        except ssl.SSLError as exc:
            msg = f"Invalid SSL cipher list: {self.ciphers!r}"
            raise ValueError(msg) from exc
    return context

AntaDevice

AntaDevice(name: str, tags: set[str] | None = None, *, disable_cache: bool = False)

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 | None

Hardware model of the device.

version DeviceVersion | None

Software version of the device, if available.

tags set[str]

Tags for this device.

cache AntaCache | None

In-memory cache for this device (None if cache is disabled).

cache_locks defaultdict[str, Lock] | None

Dictionary mapping keys to asyncio locks to guarantee exclusive access to the cache if not disabled. Deprecated, will be removed in ANTA v2.0.0, use self.cache.locks instead.

max_connections int | None

For informational/logging purposes only. Can be used by the runner to verify that the total potential connections of a run do not exceed the system file descriptor limit. This does not affect the actual device configuration. None if not available.

capabilities AntaDeviceCapabilities

Class-level declaration of which optional features this device type supports. Subclasses override this to advertise their capabilities.

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

cache_statistics property

cache_statistics: dict[str, Any] | None

Return the device cache statistics for logging purposes.

max_connections property

max_connections: int | None

Maximum number of concurrent connections allowed by the device. Can be overridden by subclasses, returns None if not available.

version property writable

version: DeviceVersion | None

Software version of the device, if available.

_collect abstractmethod async

_collect(command: AntaCommand, *, collection_id: str | None = None) -> None

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
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
@abstractmethod
async def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None:
    """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
    ----------
    command
        The command to collect.
    collection_id
        An identifier used to build the eAPI request ID.
    """

collect async

collect(command: AntaCommand, *, collection_id: str | None = None) -> None

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’t 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’s 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
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
async def collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None:
    """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't 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's 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
    ----------
    command
        The command to collect.
    collection_id
        An identifier used to build the eAPI request ID.
    """
    if self.cache is not None and command.use_cache:
        async with self.cache.locks[command.uid]:
            cached_output = await self.cache.get(command.uid)

            if cached_output is not None:
                logger.debug("Cache hit for %s on %s", command.command, self.name)
                command.output = cached_output
            else:
                await self._collect(command=command, collection_id=collection_id)
                await self.cache.set(command.uid, command.output)
    else:
        await self._collect(command=command, collection_id=collection_id)

collect_commands async

collect_commands(commands: list[AntaCommand], *, collection_id: str | None = None) -> None

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
413
414
415
416
417
418
419
420
421
422
423
async def collect_commands(self, commands: list[AntaCommand], *, collection_id: str | None = None) -> None:
    """Collect multiple commands.

    Parameters
    ----------
    commands
        The commands to collect.
    collection_id
        An identifier used to build the eAPI request ID.
    """
    await asyncio.gather(*(self.collect(command=command, collection_id=collection_id) for command in commands))

copy async

copy(sources: list[Path], destination: Path, direction: Literal['to', 'from'] = 'from') -> None

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
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
async def copy(self, sources: list[Path], destination: Path, direction: Literal["to", "from"] = "from") -> None:
    """Copy files to and from the device, usually through SCP.

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

    Parameters
    ----------
    sources
        List of files to copy to or from the device.
    destination
        Local or remote destination when copying the files. Can be a folder.
    direction
        Defines if this coroutine copies files to or from the device.

    """
    _ = (sources, destination, direction)
    msg = f"copy() method has not been implemented in {self.__class__.__name__} definition"
    raise NotImplementedError(msg)

disconnect async

disconnect() -> None

Disconnect the device and close any open connections.

It is not mandatory to implement this for a valid AntaDevice subclass. If disconnection logic is not needed, implement this method as pass. Subclasses should document the concrete resources they close.

NOTE: In ANTA 2.0, this method will be made abstract and must be implemented by all subclasses.

Source code in anta/device.py
461
462
463
464
465
466
467
468
469
470
471
472
async def disconnect(self) -> None:
    """Disconnect the device and close any open connections.

    It is not mandatory to implement this for a valid AntaDevice subclass.
    If disconnection logic is not needed, implement this method as `pass`.
    Subclasses should document the concrete resources they close.

    NOTE:
        In ANTA 2.0, this method will be made abstract and must be implemented by all subclasses.
    """
    msg = f"disconnect() method has not been implemented in {self.__class__.__name__} definition"
    logger.warning(msg)

refresh abstractmethod async

refresh() -> None

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.

Implementations may additionally populate platform with an object implementing the DevicePlatform protocol. Consumers must remain compatible with implementations that only populate hw_model.

Source code in anta/device.py
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
@abstractmethod
async def refresh(self) -> None:
    """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.

    Implementations may additionally populate `platform` with an object implementing
    the `DevicePlatform` protocol. Consumers must remain compatible with
    implementations that only populate `hw_model`.
    """

AsyncEOSDevice

AsyncEOSDevice(host: str, username: str, password: str, name: str | None = None, enable_password: str | None = None, port: int | None = None, ssh_port: int = 22, tags: set[str] | None = None, timeout: float | None = None, proto: Literal['http', 'https'] = 'https', *, enable: bool = False, insecure: bool = False, disable_cache: bool = False, use_session_auth: bool = False, ssl_params: SSLParameters | None = None)

Bases: AntaDevice

Implementation of AntaDevice for EOS using the asynceapi library, which is built on HTTPX.

Call disconnect() to close the eAPI httpx client. Call refresh() to re-establish the eAPI connection; it automatically recreates _client if it has been closed.

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.

platform DevicePlatform | None

Structured platform identity discovered during refresh.

tags set[str]

Tags for this device.

enable bool

When True, commands are collected in privileged (enable) mode.

ssl_params SSLParameters | None

Per-device SSL parameters. None inherits the global SSL cipher setting.

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_password str | None

Password used to gain privileged access on EOS.

None
port int | None

eAPI port. Defaults to 80 is proto is ‘http’ or 443 if proto is ‘https’.

None
ssh_port int

SSH port.

22
tags set[str] | None

Tags for this device.

None
timeout float | None

Global timeout value in seconds for outgoing eAPI calls. None means no timeout.

None
proto Literal['http', 'https']

eAPI protocol. Value can be ‘http’ or ‘https’.

'https'
enable bool

Collect commands using privileged mode.

False
insecure bool

Disable SSH Host Key validation.

False
disable_cache bool

Disable caching for all commands for this device.

False
use_session_auth bool

Use eAPI cookie-session authentication for this device.

False
ssl_params SSLParameters | None

SSL parameters for HTTPS eAPI connections. None inherits ANTA_SSL_CIPHERS.

None

capabilities class-attribute instance-attribute

capabilities = AntaDeviceCapabilities(supports_session_auth=True, supports_ssl=True)

Features supported by this device type.

max_connections property

max_connections: int | None

Maximum number of concurrent connections allowed by the device. Returns None if not available.

ssl_params property

ssl_params: SSLParameters | None

Explicit per-device SSL parameters, or None when global defaults are inherited.

use_session_auth property

use_session_auth: bool

Whether eAPI cookie-session authentication is enabled for this device.

_collect async

_collect(command: AntaCommand, *, collection_id: str | None = None) -> None

Collect device command output from EOS using asynceapi.

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

Raises:

Type Description
RuntimeError

If the eAPI client is closed. Call refresh() first to reconnect.

Source code in anta/device.py
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
async def _collect(self, command: AntaCommand, *, collection_id: str | None = None) -> None:
    """Collect device command output from EOS using asynceapi.

    Supports outformat `json` and `text` as output structure.
    Gain privileged access using the `enable_password` attribute
    of the `AntaDevice` instance if populated.

    Parameters
    ----------
    command
        The command to collect.
    collection_id
        An identifier used to build the eAPI request ID.

    Raises
    ------
    RuntimeError
        If the eAPI client is closed. Call `refresh()` first to reconnect.
    """
    if self._client.is_closed:
        msg = f"Device {self.name}: httpx client is closed. Call refresh() to reconnect before collecting commands."
        raise RuntimeError(msg)
    async with self._command_semaphore:
        commands: list[EapiComplexCommand | EapiSimpleCommand] = []
        if self.enable and self._enable_password is not None:
            commands.append(
                {
                    "cmd": "enable",
                    "input": str(self._enable_password),
                },
            )
        elif self.enable:
            # No password
            commands.append(EapiComplexCommand(cmd="enable"))
        commands += [EapiComplexCommand(cmd=command.command, revision=command.revision)] if command.revision else [EapiComplexCommand(cmd=command.command)]
        try:
            response = await self._client.cli(
                commands=commands,
                ofmt=command.ofmt,
                version=command.version,
                req_id=f"ANTA-{collection_id}-{id(command)}" if collection_id else f"ANTA-{id(command)}",
            )
            # Do not keep response of 'enable' command
            command.output = response[-1]
        except asynceapi.EapiCommandError as e:
            # This block catches exceptions related to EOS issuing an error.
            self._handle_eapi_command_error(command, e)
        except EapiAuthenticationError as e:
            # This block catches authentication errors (HTTP 401) from eAPI when session auth is enabled.
            command.errors = [exc_to_str(e)]
            logger.error("Authentication failed while sending a command to %s: %s", self.name, e)
        except TimeoutException as e:
            # This block catches Timeout exceptions.
            command.errors = [exc_to_str(e)]
            timeouts = self._client.timeout.as_dict()
            logger.error(
                "%s occurred while sending a command to %s. Consider increasing the timeout.\nCurrent timeouts: Connect: %s | Read: %s | Write: %s | Pool: %s",
                exc_to_str(e),
                self.name,
                timeouts["connect"],
                timeouts["read"],
                timeouts["write"],
                timeouts["pool"],
            )
        except (ConnectError, OSError) as e:
            # This block catches OSError and socket issues related exceptions.
            command.errors = [exc_to_str(e)]
            self._handle_connect_error(e)
        except HTTPError as e:
            # This block catches most of the httpx Exceptions and logs a general message.
            command.errors = [exc_to_str(e)]
            anta_log_exception(e, f"An error occurred while issuing an eAPI request to {self.name}", logger)
        logger.debug("%s: %s", self.name, command)

copy async

copy(sources: list[Path], destination: Path, direction: Literal['to', 'from'] = 'from') -> None

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

The SSH connection is established transiently and closed safely via an async with context.

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
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
async def copy(self, sources: list[Path], destination: Path, direction: Literal["to", "from"] = "from") -> None:
    """Copy files to and from the device using asyncssh.scp().

    The SSH connection is established transiently and closed safely via an `async with` context.

    Parameters
    ----------
    sources
        List of files to copy to or from the device.
    destination
        Local or remote destination when copying the files. Can be a folder.
    direction
        Defines if this coroutine copies files to or from the device.

    """
    async with asyncssh.connect(
        host=self._ssh_opts.host,
        port=self._ssh_opts.port,
        tunnel=self._ssh_opts.tunnel,
        family=self._ssh_opts.family,
        local_addr=self._ssh_opts.local_addr,
        options=self._ssh_opts,
    ) as conn:
        src: list[tuple[SSHClientConnection, Path]] | list[Path]
        dst: tuple[SSHClientConnection, Path] | Path
        if direction == "from":
            src = [(conn, file) for file in sources]
            dst = destination
            for file in sources:
                message = f"Copying '{file}' from device {self.name} to '{destination}' locally"
                logger.info(message)

        elif direction == "to":
            src = sources
            dst = conn, destination
            for file in src:
                message = f"Copying '{file}' to device {self.name} to '{destination}' remotely"
                logger.info(message)

        else:
            logger.critical("'direction' argument to copy() function is invalid: %s", direction)

            return
        await asyncssh.scp(src, dst)

disconnect async

disconnect() -> None

Close the eAPI httpx client.

Safe to call even if the client is already closed. Use refresh() to reconnect.

Source code in anta/device.py
907
908
909
910
911
912
913
914
915
916
917
async def disconnect(self) -> None:
    """Close the eAPI httpx client.

    Safe to call even if the client is already closed.
    Use `refresh()` to reconnect.
    """
    logger.debug("Disconnecting device %s", self.name)
    if not self._client.is_closed:
        await self._client.aclose()
    self.is_online = False
    self.established = False

refresh async

refresh() -> None

Update attributes of an AsyncEOSDevice instance.

If the eAPI client has been closed (e.g. after a disconnect() call), it is automatically recreated before attempting to reach the device.

Updates the following attributes:

  • is_online: True when the eAPI HTTP endpoint responds successfully.
  • established: True when show version succeeds and provides a valid hardware model.
  • hw_model: Hardware model parsed from show version.
  • platform: Structured system and module identity parsed from EOS inventory commands.
  • version: EOS version parsed from show version, or None when unavailable or invalid.
Source code in anta/device.py
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
async def refresh(self) -> None:
    """Update attributes of an AsyncEOSDevice instance.

    If the eAPI client has been closed (e.g. after a `disconnect()` call), it is
    automatically recreated before attempting to reach the device.

    Updates the following attributes:

    - `is_online`: True when the eAPI HTTP endpoint responds successfully.
    - `established`: True when `show version` succeeds and provides a valid hardware model.
    - `hw_model`: Hardware model parsed from `show version`.
    - `platform`: Structured system and module identity parsed from EOS inventory commands.
    - `version`: EOS version parsed from `show version`, or `None` when unavailable or invalid.
    """
    logger.debug("Refreshing device %s", self.name)
    self.version = None
    self.platform = None
    if self._client.is_closed:
        logger.debug("Recreating closed httpx client for device %s", self.name)
        self._client = self._create_client()
    try:
        self.is_online = await self._client.check_api_endpoint()
    except (EapiAuthenticationError, HTTPError) as e:
        self.is_online = False
        self.established = False
        logger.warning("An error occurred while attempting to connect to device %s: %s", self.name, exc_to_str(e))
        return

    show_version = AntaCommand(command="show version")
    await self._collect(show_version)
    if not show_version.collected:
        self.established = False
        logger.warning("Cannot get hardware information from device %s", self.name)
        return

    show_version_output = show_version.json_output
    model_name = show_version_output.get("modelName")
    self.hw_model = model_name if isinstance(model_name, str) else None
    version_result = parse_eos_version(show_version_output.get("version"))
    if isinstance(version_result, ParseFail):
        logger.warning("Cannot parse EOS version for device %s: %s (%s)", self.name, version_result.reason.value, version_result.detail)
    else:
        self.version = version_result.value
    self.established = True
    platform_result = parse_eos_platform(self.hw_model)
    if isinstance(platform_result, ParseFail):
        _log_platform_parse_failure(platform_result, self.name)
        self.established = False
        return

    platform = platform_result.value
    self.platform = platform
    if platform.type is PlatformType.UNKNOWN:
        logger.debug("System model %s on device %s has an unknown platform type", platform.model, self.name)

    # TODO(ANTA 1.10): Confirm eager module collection is acceptable before release. If benchmarks
    # show meaningful refresh overhead, consider an inventory-level `required_device_facts` opt-in.
    if platform.type is PlatformType.CHASSIS:
        await self._refresh_platform_modules(platform)