Merging upstream version 1.3.0.
Signed-off-by: Daniel Baumann <daniel@debian.org>
This commit is contained in:
parent
5b922100c9
commit
8a6a3342fc
337 changed files with 16571 additions and 4891 deletions
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2024 Arista Networks, Inc.
|
||||
# Copyright (c) 2024-2025 Arista Networks, Inc.
|
||||
# Use of this source code is governed by the Apache License 2.0
|
||||
# that can be found in the LICENSE file.
|
||||
# Initially written by Jeremy Schulman at https://github.com/jeremyschulman/aio-eapi
|
||||
|
|
22
asynceapi/_constants.py
Normal file
22
asynceapi/_constants.py
Normal file
|
@ -0,0 +1,22 @@
|
|||
# Copyright (c) 2024-2025 Arista Networks, Inc.
|
||||
# Use of this source code is governed by the Apache License 2.0
|
||||
# that can be found in the LICENSE file.
|
||||
"""Constants and Enums for the asynceapi package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import Enum
|
||||
|
||||
|
||||
class EapiCommandFormat(str, Enum):
|
||||
"""Enum for the eAPI command format.
|
||||
|
||||
NOTE: This could be updated to StrEnum when Python 3.11 is the minimum supported version in ANTA.
|
||||
"""
|
||||
|
||||
JSON = "json"
|
||||
TEXT = "text"
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""Override the __str__ method to return the value of the Enum, mimicking the behavior of StrEnum."""
|
||||
return self.value
|
36
asynceapi/_errors.py
Normal file
36
asynceapi/_errors.py
Normal file
|
@ -0,0 +1,36 @@
|
|||
# Copyright (c) 2024-2025 Arista Networks, Inc.
|
||||
# Use of this source code is governed by the Apache License 2.0
|
||||
# that can be found in the LICENSE file.
|
||||
"""Exceptions for the asynceapi package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from asynceapi._models import EapiResponse
|
||||
|
||||
|
||||
class EapiReponseError(Exception):
|
||||
"""Exception raised when an eAPI response contains errors.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
response : EapiResponse
|
||||
The eAPI response that contains the error.
|
||||
"""
|
||||
|
||||
def __init__(self, response: EapiResponse) -> None:
|
||||
"""Initialize the EapiReponseError exception."""
|
||||
self.response = response
|
||||
|
||||
# Build a descriptive error message
|
||||
message = "Error in eAPI response"
|
||||
|
||||
if response.error_code is not None:
|
||||
message += f" (code: {response.error_code})"
|
||||
|
||||
if response.error_message is not None:
|
||||
message += f": {response.error_message}"
|
||||
|
||||
super().__init__(message)
|
238
asynceapi/_models.py
Normal file
238
asynceapi/_models.py
Normal file
|
@ -0,0 +1,238 @@
|
|||
# Copyright (c) 2024-2025 Arista Networks, Inc.
|
||||
# Use of this source code is governed by the Apache License 2.0
|
||||
# that can be found in the LICENSE file.
|
||||
"""Models for the asynceapi package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from logging import getLogger
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
from uuid import uuid4
|
||||
|
||||
from ._constants import EapiCommandFormat
|
||||
from ._errors import EapiReponseError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Iterator
|
||||
|
||||
from ._types import EapiComplexCommand, EapiJsonOutput, EapiSimpleCommand, EapiTextOutput, JsonRpc
|
||||
|
||||
LOGGER = getLogger(__name__)
|
||||
|
||||
|
||||
# pylint: disable=too-many-instance-attributes
|
||||
@dataclass(frozen=True)
|
||||
class EapiRequest:
|
||||
"""Model for an eAPI request.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
commands : list[EapiSimpleCommand | EapiComplexCommand]
|
||||
A list of commands to execute.
|
||||
version : int | Literal["latest"]
|
||||
The eAPI version to use. Defaults to "latest".
|
||||
format : EapiCommandFormat
|
||||
The command output format. Defaults "json".
|
||||
timestamps : bool
|
||||
Include timestamps in the command output. Defaults to False.
|
||||
auto_complete : bool
|
||||
Enable command auto-completion. Defaults to False.
|
||||
expand_aliases : bool
|
||||
Expand command aliases. Defaults to False.
|
||||
stop_on_error : bool
|
||||
Stop command execution on first error. Defaults to True.
|
||||
id : int | str
|
||||
The request ID. Defaults to a random hex string.
|
||||
"""
|
||||
|
||||
commands: list[EapiSimpleCommand | EapiComplexCommand]
|
||||
version: int | Literal["latest"] = "latest"
|
||||
format: EapiCommandFormat = EapiCommandFormat.JSON
|
||||
timestamps: bool = False
|
||||
auto_complete: bool = False
|
||||
expand_aliases: bool = False
|
||||
stop_on_error: bool = True
|
||||
id: int | str = field(default_factory=lambda: uuid4().hex)
|
||||
|
||||
def to_jsonrpc(self) -> JsonRpc:
|
||||
"""Return the JSON-RPC dictionary payload for the request."""
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "runCmds",
|
||||
"params": {
|
||||
"version": self.version,
|
||||
"cmds": self.commands,
|
||||
"format": self.format,
|
||||
"timestamps": self.timestamps,
|
||||
"autoComplete": self.auto_complete,
|
||||
"expandAliases": self.expand_aliases,
|
||||
"stopOnError": self.stop_on_error,
|
||||
},
|
||||
"id": self.id,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EapiResponse:
|
||||
"""Model for an eAPI response.
|
||||
|
||||
Construct an EapiResponse from a JSON-RPC response dictionary using the `from_jsonrpc` class method.
|
||||
|
||||
Can be iterated over to access command results in order of execution.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
request_id : str
|
||||
The ID of the original request this response corresponds to.
|
||||
_results : dict[int, EapiCommandResult]
|
||||
Dictionary mapping request command indices to their respective results.
|
||||
error_code : int | None
|
||||
The JSON-RPC error code, if any.
|
||||
error_message : str | None
|
||||
The JSON-RPC error message, if any.
|
||||
"""
|
||||
|
||||
request_id: str
|
||||
_results: dict[int, EapiCommandResult] = field(default_factory=dict)
|
||||
error_code: int | None = None
|
||||
error_message: str | None = None
|
||||
|
||||
@property
|
||||
def success(self) -> bool:
|
||||
"""Return True if the response has no errors."""
|
||||
return self.error_code is None
|
||||
|
||||
@property
|
||||
def results(self) -> list[EapiCommandResult]:
|
||||
"""Get all results as a list. Results are ordered by the command indices in the request."""
|
||||
return list(self._results.values())
|
||||
|
||||
def __len__(self) -> int:
|
||||
"""Return the number of results."""
|
||||
return len(self._results)
|
||||
|
||||
def __iter__(self) -> Iterator[EapiCommandResult]:
|
||||
"""Enable iteration over the results. Results are yielded in the same order as provided in the request."""
|
||||
yield from self._results.values()
|
||||
|
||||
@classmethod
|
||||
def from_jsonrpc(cls, response: dict[str, Any], request: EapiRequest, *, raise_on_error: bool = False) -> EapiResponse:
|
||||
"""Build an EapiResponse from a JSON-RPC eAPI response.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
response
|
||||
The JSON-RPC eAPI response dictionary.
|
||||
request
|
||||
The corresponding EapiRequest.
|
||||
raise_on_error
|
||||
Raise an EapiReponseError if the response contains errors, by default False.
|
||||
|
||||
Returns
|
||||
-------
|
||||
EapiResponse
|
||||
The EapiResponse object.
|
||||
"""
|
||||
has_error = "error" in response
|
||||
response_data = response["error"]["data"] if has_error else response["result"]
|
||||
|
||||
# Handle case where we have fewer results than commands (stop_on_error=True)
|
||||
executed_count = min(len(response_data), len(request.commands))
|
||||
|
||||
# Process the results we have
|
||||
results = {}
|
||||
for i in range(executed_count):
|
||||
cmd = request.commands[i]
|
||||
cmd_str = cmd["cmd"] if isinstance(cmd, dict) else cmd
|
||||
data = response_data[i]
|
||||
|
||||
output = None
|
||||
errors = []
|
||||
success = True
|
||||
start_time = None
|
||||
duration = None
|
||||
|
||||
# Parse the output based on the data type, no output when errors are present
|
||||
if isinstance(data, dict):
|
||||
if "errors" in data:
|
||||
errors = data["errors"]
|
||||
success = False
|
||||
else:
|
||||
output = data["output"] if request.format == EapiCommandFormat.TEXT and "output" in data else data
|
||||
|
||||
# Add timestamps if available
|
||||
if request.timestamps and "_meta" in data:
|
||||
meta = data.pop("_meta")
|
||||
start_time = meta.get("execStartTime")
|
||||
duration = meta.get("execDuration")
|
||||
|
||||
elif isinstance(data, str):
|
||||
# Handle case where eAPI returns a JSON string response (serialized JSON) for certain commands
|
||||
try:
|
||||
from json import JSONDecodeError, loads
|
||||
|
||||
output = loads(data)
|
||||
except (JSONDecodeError, TypeError):
|
||||
# If it's not valid JSON, store as is
|
||||
LOGGER.warning("Invalid JSON response for command: %s. Storing as text: %s", cmd_str, data)
|
||||
output = data
|
||||
|
||||
results[i] = EapiCommandResult(
|
||||
command=cmd_str,
|
||||
output=output,
|
||||
errors=errors,
|
||||
success=success,
|
||||
start_time=start_time,
|
||||
duration=duration,
|
||||
)
|
||||
|
||||
# If stop_on_error is True and we have an error, indicate commands not executed
|
||||
if has_error and request.stop_on_error and executed_count < len(request.commands):
|
||||
for i in range(executed_count, len(request.commands)):
|
||||
cmd = request.commands[i]
|
||||
cmd_str = cmd["cmd"] if isinstance(cmd, dict) else cmd
|
||||
results[i] = EapiCommandResult(command=cmd_str, output=None, errors=["Command not executed due to previous error"], success=False, executed=False)
|
||||
|
||||
response_obj = cls(
|
||||
request_id=response["id"],
|
||||
_results=results,
|
||||
error_code=response["error"]["code"] if has_error else None,
|
||||
error_message=response["error"]["message"] if has_error else None,
|
||||
)
|
||||
|
||||
if raise_on_error and has_error:
|
||||
raise EapiReponseError(response_obj)
|
||||
|
||||
return response_obj
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class EapiCommandResult:
|
||||
"""Model for an eAPI command result.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
command : str
|
||||
The command that was executed.
|
||||
output : EapiJsonOutput | EapiTextOutput | None
|
||||
The command result output. None if the command returned errors.
|
||||
errors : list[str]
|
||||
A list of error messages, if any.
|
||||
success : bool
|
||||
True if the command was successful.
|
||||
executed : bool
|
||||
True if the command was executed. When `stop_on_error` is True in the request, some commands may not be executed.
|
||||
start_time : float | None
|
||||
Command execution start time in seconds. Uses Unix epoch format. `timestamps` must be True in the request.
|
||||
duration : float | None
|
||||
Command execution duration in seconds. `timestamps` must be True in the request.
|
||||
"""
|
||||
|
||||
command: str
|
||||
output: EapiJsonOutput | EapiTextOutput | None
|
||||
errors: list[str] = field(default_factory=list)
|
||||
success: bool = True
|
||||
executed: bool = True
|
||||
start_time: float | None = None
|
||||
duration: float | None = None
|
53
asynceapi/_types.py
Normal file
53
asynceapi/_types.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
# Copyright (c) 2024-2025 Arista Networks, Inc.
|
||||
# Use of this source code is governed by the Apache License 2.0
|
||||
# that can be found in the LICENSE file.
|
||||
"""Type definitions used for the asynceapi package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._constants import EapiCommandFormat
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
from typing import NotRequired, TypedDict
|
||||
else:
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
EapiJsonOutput = dict[str, Any]
|
||||
"""Type definition of an eAPI JSON output response."""
|
||||
EapiTextOutput = str
|
||||
"""Type definition of an eAPI text output response."""
|
||||
EapiSimpleCommand = str
|
||||
"""Type definition of an eAPI simple command. A simple command is the CLI command to run as a string."""
|
||||
|
||||
|
||||
class EapiComplexCommand(TypedDict):
|
||||
"""Type definition of an eAPI complex command. A complex command is a dictionary with the CLI command to run with additional parameters."""
|
||||
|
||||
cmd: str
|
||||
input: NotRequired[str]
|
||||
revision: NotRequired[int]
|
||||
|
||||
|
||||
class JsonRpc(TypedDict):
|
||||
"""Type definition of a JSON-RPC payload."""
|
||||
|
||||
jsonrpc: Literal["2.0"]
|
||||
method: Literal["runCmds"]
|
||||
params: JsonRpcParams
|
||||
id: NotRequired[int | str]
|
||||
|
||||
|
||||
class JsonRpcParams(TypedDict):
|
||||
"""Type definition of JSON-RPC parameters."""
|
||||
|
||||
version: NotRequired[int | Literal["latest"]]
|
||||
cmds: list[EapiSimpleCommand | EapiComplexCommand]
|
||||
format: NotRequired[EapiCommandFormat]
|
||||
autoComplete: NotRequired[bool]
|
||||
expandAliases: NotRequired[bool]
|
||||
timestamps: NotRequired[bool]
|
||||
stopOnError: NotRequired[bool]
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2024 Arista Networks, Inc.
|
||||
# Copyright (c) 2024-2025 Arista Networks, Inc.
|
||||
# Use of this source code is governed by the Apache License 2.0
|
||||
# that can be found in the LICENSE file.
|
||||
# Initially written by Jeremy Schulman at https://github.com/jeremyschulman/aio-eapi
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2024 Arista Networks, Inc.
|
||||
# Copyright (c) 2024-2025 Arista Networks, Inc.
|
||||
# Use of this source code is governed by the Apache License 2.0
|
||||
# that can be found in the LICENSE file.
|
||||
# Initially written by Jeremy Schulman at https://github.com/jeremyschulman/aio-eapi
|
||||
|
@ -10,9 +10,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._types import EapiComplexCommand, EapiJsonOutput, EapiSimpleCommand
|
||||
from .device import Device
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
@ -78,7 +79,7 @@ class SessionConfig:
|
|||
# Public Methods
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
async def status_all(self) -> dict[str, Any]:
|
||||
async def status_all(self) -> EapiJsonOutput:
|
||||
"""Get the status of all the session config on the device.
|
||||
|
||||
Run the following command on the device:
|
||||
|
@ -86,7 +87,7 @@ class SessionConfig:
|
|||
|
||||
Returns
|
||||
-------
|
||||
dict[str, Any]
|
||||
EapiJsonOutput
|
||||
Dictionary of native EOS eAPI response; see `status` method for
|
||||
details.
|
||||
|
||||
|
@ -116,9 +117,9 @@ class SessionConfig:
|
|||
}
|
||||
```
|
||||
"""
|
||||
return await self._cli("show configuration sessions detail") # type: ignore[return-value] # json outformat returns dict[str, Any]
|
||||
return await self._cli(command="show configuration sessions detail")
|
||||
|
||||
async def status(self) -> dict[str, Any] | None:
|
||||
async def status(self) -> EapiJsonOutput | None:
|
||||
"""Get the status of a session config on the device.
|
||||
|
||||
Run the following command on the device:
|
||||
|
@ -129,7 +130,7 @@ class SessionConfig:
|
|||
|
||||
Returns
|
||||
-------
|
||||
dict[str, Any] | None
|
||||
EapiJsonOutput | None
|
||||
Dictionary instance of the session status. If the session does not exist,
|
||||
then this method will return None.
|
||||
|
||||
|
@ -201,7 +202,7 @@ class SessionConfig:
|
|||
# prepare the initial set of command to enter the config session and
|
||||
# rollback clean if the `replace` argument is True.
|
||||
|
||||
commands: list[str | dict[str, Any]] = [self._cli_config_session]
|
||||
commands: list[EapiSimpleCommand | EapiComplexCommand] = [self._cli_config_session]
|
||||
if replace:
|
||||
commands.append(self.CLI_CFG_FACTORY_RESET)
|
||||
|
||||
|
@ -232,7 +233,7 @@ class SessionConfig:
|
|||
if timer:
|
||||
command += f" timer {timer}"
|
||||
|
||||
await self._cli(command)
|
||||
await self._cli(command=command)
|
||||
|
||||
async def abort(self) -> None:
|
||||
"""Abort the configuration session.
|
||||
|
@ -240,7 +241,7 @@ class SessionConfig:
|
|||
Run the following command on the device:
|
||||
# configure session <name> abort
|
||||
"""
|
||||
await self._cli(f"{self._cli_config_session} abort")
|
||||
await self._cli(command=f"{self._cli_config_session} abort")
|
||||
|
||||
async def diff(self) -> str:
|
||||
"""Return the "diff" of the session config relative to the running config.
|
||||
|
@ -257,7 +258,7 @@ class SessionConfig:
|
|||
----------
|
||||
* https://www.gnu.org/software/diffutils/manual/diffutils.txt
|
||||
"""
|
||||
return await self._cli(f"show session-config named {self.name} diffs", ofmt="text") # type: ignore[return-value] # text outformat returns str
|
||||
return await self._cli(command=f"show session-config named {self.name} diffs", ofmt="text")
|
||||
|
||||
async def load_file(self, filename: str, *, replace: bool = False) -> None:
|
||||
"""Load the configuration from <filename> into the session configuration.
|
||||
|
@ -281,12 +282,12 @@ class SessionConfig:
|
|||
If there are any issues with loading the configuration file then a
|
||||
RuntimeError is raised with the error messages content.
|
||||
"""
|
||||
commands: list[str | dict[str, Any]] = [self._cli_config_session]
|
||||
commands: list[EapiSimpleCommand | EapiComplexCommand] = [self._cli_config_session]
|
||||
if replace:
|
||||
commands.append(self.CLI_CFG_FACTORY_RESET)
|
||||
|
||||
commands.append(f"copy {filename} session-config")
|
||||
res: list[dict[str, Any]] = await self._cli(commands=commands) # type: ignore[assignment] # JSON outformat of multiple commands returns list[dict[str, Any]]
|
||||
res = await self._cli(commands=commands)
|
||||
checks_re = re.compile(r"error|abort|invalid", flags=re.IGNORECASE)
|
||||
messages = res[-1]["messages"]
|
||||
|
||||
|
@ -295,4 +296,4 @@ class SessionConfig:
|
|||
|
||||
async def write(self) -> None:
|
||||
"""Save the running config to the startup config by issuing the command "write" to the device."""
|
||||
await self._cli("write")
|
||||
await self._cli(command="write")
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2024 Arista Networks, Inc.
|
||||
# Copyright (c) 2024-2025 Arista Networks, Inc.
|
||||
# Use of this source code is governed by the Apache License 2.0
|
||||
# that can be found in the LICENSE file.
|
||||
# Initially written by Jeremy Schulman at https://github.com/jeremyschulman/aio-eapi
|
||||
|
@ -10,7 +10,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from socket import getservbyname
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from typing import TYPE_CHECKING, Any, Literal, overload
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Public Imports
|
||||
|
@ -20,12 +20,13 @@ import httpx
|
|||
# -----------------------------------------------------------------------------
|
||||
# Private Imports
|
||||
# -----------------------------------------------------------------------------
|
||||
from ._constants import EapiCommandFormat
|
||||
from .aio_portcheck import port_check_url
|
||||
from .config_session import SessionConfig
|
||||
from .errors import EapiCommandError
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Sequence
|
||||
from ._types import EapiComplexCommand, EapiJsonOutput, EapiSimpleCommand, EapiTextOutput, JsonRpc
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Exports
|
||||
|
@ -121,18 +122,139 @@ class Device(httpx.AsyncClient):
|
|||
"""
|
||||
return await port_check_url(self.base_url)
|
||||
|
||||
# Single command, JSON output, no suppression
|
||||
@overload
|
||||
async def cli(
|
||||
self,
|
||||
command: str | dict[str, Any] | None = None,
|
||||
commands: Sequence[str | dict[str, Any]] | None = None,
|
||||
ofmt: str | None = None,
|
||||
version: int | str | None = "latest",
|
||||
*,
|
||||
command: EapiSimpleCommand | EapiComplexCommand,
|
||||
commands: None = None,
|
||||
ofmt: Literal["json"] = "json",
|
||||
version: int | Literal["latest"] = "latest",
|
||||
suppress_error: Literal[False] = False,
|
||||
auto_complete: bool = False,
|
||||
expand_aliases: bool = False,
|
||||
req_id: int | str | None = None,
|
||||
) -> EapiJsonOutput: ...
|
||||
|
||||
# Multiple commands, JSON output, no suppression
|
||||
@overload
|
||||
async def cli(
|
||||
self,
|
||||
*,
|
||||
command: None = None,
|
||||
commands: list[EapiSimpleCommand | EapiComplexCommand],
|
||||
ofmt: Literal["json"] = "json",
|
||||
version: int | Literal["latest"] = "latest",
|
||||
suppress_error: Literal[False] = False,
|
||||
auto_complete: bool = False,
|
||||
expand_aliases: bool = False,
|
||||
req_id: int | str | None = None,
|
||||
) -> list[EapiJsonOutput]: ...
|
||||
|
||||
# Single command, TEXT output, no suppression
|
||||
@overload
|
||||
async def cli(
|
||||
self,
|
||||
*,
|
||||
command: EapiSimpleCommand | EapiComplexCommand,
|
||||
commands: None = None,
|
||||
ofmt: Literal["text"],
|
||||
version: int | Literal["latest"] = "latest",
|
||||
suppress_error: Literal[False] = False,
|
||||
auto_complete: bool = False,
|
||||
expand_aliases: bool = False,
|
||||
req_id: int | str | None = None,
|
||||
) -> EapiTextOutput: ...
|
||||
|
||||
# Multiple commands, TEXT output, no suppression
|
||||
@overload
|
||||
async def cli(
|
||||
self,
|
||||
*,
|
||||
command: None = None,
|
||||
commands: list[EapiSimpleCommand | EapiComplexCommand],
|
||||
ofmt: Literal["text"],
|
||||
version: int | Literal["latest"] = "latest",
|
||||
suppress_error: Literal[False] = False,
|
||||
auto_complete: bool = False,
|
||||
expand_aliases: bool = False,
|
||||
req_id: int | str | None = None,
|
||||
) -> list[EapiTextOutput]: ...
|
||||
|
||||
# Single command, JSON output, with suppression
|
||||
@overload
|
||||
async def cli(
|
||||
self,
|
||||
*,
|
||||
command: EapiSimpleCommand | EapiComplexCommand,
|
||||
commands: None = None,
|
||||
ofmt: Literal["json"] = "json",
|
||||
version: int | Literal["latest"] = "latest",
|
||||
suppress_error: Literal[True],
|
||||
auto_complete: bool = False,
|
||||
expand_aliases: bool = False,
|
||||
req_id: int | str | None = None,
|
||||
) -> EapiJsonOutput | None: ...
|
||||
|
||||
# Multiple commands, JSON output, with suppression
|
||||
@overload
|
||||
async def cli(
|
||||
self,
|
||||
*,
|
||||
command: None = None,
|
||||
commands: list[EapiSimpleCommand | EapiComplexCommand],
|
||||
ofmt: Literal["json"] = "json",
|
||||
version: int | Literal["latest"] = "latest",
|
||||
suppress_error: Literal[True],
|
||||
auto_complete: bool = False,
|
||||
expand_aliases: bool = False,
|
||||
req_id: int | str | None = None,
|
||||
) -> list[EapiJsonOutput] | None: ...
|
||||
|
||||
# Single command, TEXT output, with suppression
|
||||
@overload
|
||||
async def cli(
|
||||
self,
|
||||
*,
|
||||
command: EapiSimpleCommand | EapiComplexCommand,
|
||||
commands: None = None,
|
||||
ofmt: Literal["text"],
|
||||
version: int | Literal["latest"] = "latest",
|
||||
suppress_error: Literal[True],
|
||||
auto_complete: bool = False,
|
||||
expand_aliases: bool = False,
|
||||
req_id: int | str | None = None,
|
||||
) -> EapiTextOutput | None: ...
|
||||
|
||||
# Multiple commands, TEXT output, with suppression
|
||||
@overload
|
||||
async def cli(
|
||||
self,
|
||||
*,
|
||||
command: None = None,
|
||||
commands: list[EapiSimpleCommand | EapiComplexCommand],
|
||||
ofmt: Literal["text"],
|
||||
version: int | Literal["latest"] = "latest",
|
||||
suppress_error: Literal[True],
|
||||
auto_complete: bool = False,
|
||||
expand_aliases: bool = False,
|
||||
req_id: int | str | None = None,
|
||||
) -> list[EapiTextOutput] | None: ...
|
||||
|
||||
# Actual implementation
|
||||
async def cli(
|
||||
self,
|
||||
command: EapiSimpleCommand | EapiComplexCommand | None = None,
|
||||
commands: list[EapiSimpleCommand | EapiComplexCommand] | None = None,
|
||||
ofmt: Literal["json", "text"] = "json",
|
||||
version: int | Literal["latest"] = "latest",
|
||||
*,
|
||||
suppress_error: bool = False,
|
||||
auto_complete: bool = False,
|
||||
expand_aliases: bool = False,
|
||||
req_id: int | str | None = None,
|
||||
) -> list[dict[str, Any] | str] | dict[str, Any] | str | None:
|
||||
) -> EapiJsonOutput | EapiTextOutput | list[EapiJsonOutput] | list[EapiTextOutput] | None:
|
||||
"""Execute one or more CLI commands.
|
||||
|
||||
Parameters
|
||||
|
@ -143,6 +265,7 @@ class Device(httpx.AsyncClient):
|
|||
A list of commands to execute; results in a list of output responses.
|
||||
ofmt
|
||||
Either 'json' or 'text'; indicates the output format for the CLI commands.
|
||||
eAPI defaults to 'json'.
|
||||
version
|
||||
By default the eAPI will use "version 1" for all API object models.
|
||||
This driver will, by default, always set version to "latest" so
|
||||
|
@ -158,13 +281,13 @@ class Device(httpx.AsyncClient):
|
|||
|
||||
response = dev.cli(..., suppress_error=True)
|
||||
auto_complete
|
||||
Enabled/disables the command auto-compelete feature of the EAPI. Per the
|
||||
Enabled/disables the command auto-compelete feature of the eAPI. Per the
|
||||
documentation:
|
||||
Allows users to use shorthand commands in eAPI calls. With this
|
||||
parameter included a user can send 'sh ver' via eAPI to get the
|
||||
output of 'show version'.
|
||||
expand_aliases
|
||||
Enables/disables the command use of User defined alias. Per the
|
||||
Enables/disables the command use of user-defined alias. Per the
|
||||
documentation:
|
||||
Allowed users to provide the expandAliases parameter to eAPI
|
||||
calls. This allows users to use aliased commands via the API.
|
||||
|
@ -176,15 +299,34 @@ class Device(httpx.AsyncClient):
|
|||
|
||||
Returns
|
||||
-------
|
||||
list[dict[str, Any] | str] | dict[str, Any] | str | None
|
||||
One or List of output responses, per the description above.
|
||||
dict[str, Any]
|
||||
Single command, JSON output, suppress_error=False
|
||||
list[dict[str, Any]]
|
||||
Multiple commands, JSON output, suppress_error=False
|
||||
str
|
||||
Single command, TEXT output, suppress_error=False
|
||||
list[str]
|
||||
Multiple commands, TEXT output, suppress_error=False
|
||||
dict[str, Any] | None
|
||||
Single command, JSON output, suppress_error=True
|
||||
list[dict[str, Any]] | None
|
||||
Multiple commands, JSON output, suppress_error=True
|
||||
str | None
|
||||
Single command, TEXT output, suppress_error=True
|
||||
list[str] | None
|
||||
Multiple commands, TEXT output, suppress_error=True
|
||||
"""
|
||||
if not any((command, commands)):
|
||||
msg = "Required 'command' or 'commands'"
|
||||
raise RuntimeError(msg)
|
||||
|
||||
jsonrpc = self._jsonrpc_command(
|
||||
commands=[command] if command else commands, ofmt=ofmt, version=version, auto_complete=auto_complete, expand_aliases=expand_aliases, req_id=req_id
|
||||
commands=[command] if command else commands if commands else [],
|
||||
ofmt=ofmt,
|
||||
version=version,
|
||||
auto_complete=auto_complete,
|
||||
expand_aliases=expand_aliases,
|
||||
req_id=req_id,
|
||||
)
|
||||
|
||||
try:
|
||||
|
@ -197,14 +339,14 @@ class Device(httpx.AsyncClient):
|
|||
|
||||
def _jsonrpc_command(
|
||||
self,
|
||||
commands: Sequence[str | dict[str, Any]] | None = None,
|
||||
ofmt: str | None = None,
|
||||
version: int | str | None = "latest",
|
||||
commands: list[EapiSimpleCommand | EapiComplexCommand],
|
||||
ofmt: Literal["json", "text"] = "json",
|
||||
version: int | Literal["latest"] = "latest",
|
||||
*,
|
||||
auto_complete: bool = False,
|
||||
expand_aliases: bool = False,
|
||||
req_id: int | str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
) -> JsonRpc:
|
||||
"""Create the JSON-RPC command dictionary object.
|
||||
|
||||
Parameters
|
||||
|
@ -213,6 +355,7 @@ class Device(httpx.AsyncClient):
|
|||
A list of commands to execute; results in a list of output responses.
|
||||
ofmt
|
||||
Either 'json' or 'text'; indicates the output format for the CLI commands.
|
||||
eAPI defaults to 'json'.
|
||||
version
|
||||
By default the eAPI will use "version 1" for all API object models.
|
||||
This driver will, by default, always set version to "latest" so
|
||||
|
@ -241,25 +384,20 @@ class Device(httpx.AsyncClient):
|
|||
dict containing the JSON payload to run the command.
|
||||
|
||||
"""
|
||||
cmd: dict[str, Any] = {
|
||||
return {
|
||||
"jsonrpc": "2.0",
|
||||
"method": "runCmds",
|
||||
"params": {
|
||||
"version": version,
|
||||
"cmds": commands,
|
||||
"format": ofmt or self.EAPI_DEFAULT_OFMT,
|
||||
"format": EapiCommandFormat(ofmt),
|
||||
"autoComplete": auto_complete,
|
||||
"expandAliases": expand_aliases,
|
||||
},
|
||||
"id": req_id or id(self),
|
||||
}
|
||||
if auto_complete is not None:
|
||||
cmd["params"].update({"autoComplete": auto_complete})
|
||||
|
||||
if expand_aliases is not None:
|
||||
cmd["params"].update({"expandAliases": expand_aliases})
|
||||
|
||||
return cmd
|
||||
|
||||
async def jsonrpc_exec(self, jsonrpc: dict[str, Any]) -> list[dict[str, Any] | str]:
|
||||
async def jsonrpc_exec(self, jsonrpc: JsonRpc) -> list[EapiJsonOutput] | list[EapiTextOutput]:
|
||||
"""Execute the JSON-RPC dictionary object.
|
||||
|
||||
Parameters
|
||||
|
@ -315,7 +453,7 @@ class Device(httpx.AsyncClient):
|
|||
failed_cmd = commands[err_at]
|
||||
|
||||
raise EapiCommandError(
|
||||
passed=[get_output(cmd_data[cmd_i]) for cmd_i, cmd in enumerate(commands[:err_at])],
|
||||
passed=[get_output(cmd_data[i]) for i in range(err_at)],
|
||||
failed=failed_cmd["cmd"] if isinstance(failed_cmd, dict) else failed_cmd,
|
||||
errors=cmd_data[err_at]["errors"],
|
||||
errmsg=err_msg,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
# Copyright (c) 2024 Arista Networks, Inc.
|
||||
# Copyright (c) 2024-2025 Arista Networks, Inc.
|
||||
# Use of this source code is governed by the Apache License 2.0
|
||||
# that can be found in the LICENSE file.
|
||||
# Initially written by Jeremy Schulman at https://github.com/jeremyschulman/aio-eapi
|
||||
|
@ -6,13 +6,16 @@
|
|||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import httpx
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._types import EapiComplexCommand, EapiJsonOutput, EapiSimpleCommand, EapiTextOutput
|
||||
|
||||
|
||||
class EapiCommandError(RuntimeError):
|
||||
"""Exception class for EAPI command errors.
|
||||
"""Exception class for eAPI command errors.
|
||||
|
||||
Attributes
|
||||
----------
|
||||
|
@ -23,7 +26,14 @@ class EapiCommandError(RuntimeError):
|
|||
not_exec: a list of commands that were not executed
|
||||
"""
|
||||
|
||||
def __init__(self, failed: str, errors: list[str], errmsg: str, passed: list[str | dict[str, Any]], not_exec: list[dict[str, Any]]) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
failed: str,
|
||||
errors: list[str],
|
||||
errmsg: str,
|
||||
passed: list[EapiJsonOutput] | list[EapiTextOutput],
|
||||
not_exec: list[EapiSimpleCommand | EapiComplexCommand],
|
||||
) -> None:
|
||||
"""Initialize for the EapiCommandError exception."""
|
||||
self.failed = failed
|
||||
self.errmsg = errmsg
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue