289 lines
10 KiB
Python
289 lines
10 KiB
Python
# Copyright (c) 2024 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
|
|
"""asynceapi.SessionConfig definition."""
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# System Imports
|
|
# -----------------------------------------------------------------------------
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import TYPE_CHECKING, Any
|
|
|
|
if TYPE_CHECKING:
|
|
from .device import Device
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Exports
|
|
# -----------------------------------------------------------------------------
|
|
|
|
__all__ = ["SessionConfig"]
|
|
|
|
# -----------------------------------------------------------------------------
|
|
#
|
|
# CODE BEGINS
|
|
#
|
|
# -----------------------------------------------------------------------------
|
|
|
|
|
|
class SessionConfig:
|
|
"""
|
|
Send configuration to a device using the EOS session mechanism.
|
|
|
|
This is the preferred way of managing configuration changes.
|
|
|
|
Notes
|
|
-----
|
|
This class definition is used by the parent Device class definition as
|
|
defined by `config_session`. A Caller can use the SessionConfig directly
|
|
as well, but it is not required.
|
|
"""
|
|
|
|
CLI_CFG_FACTORY_RESET = "rollback clean-config"
|
|
|
|
def __init__(self, device: Device, name: str) -> None:
|
|
"""
|
|
Create a new instance of SessionConfig.
|
|
|
|
The session config instance bound
|
|
to the given device instance, and using the session `name`.
|
|
|
|
Parameters
|
|
----------
|
|
device: The associated device instance
|
|
name: The name of the config session
|
|
"""
|
|
self._device = device
|
|
self._cli = device.cli
|
|
self._name = name
|
|
self._cli_config_session = f"configure session {self.name}"
|
|
|
|
# -------------------------------------------------------------------------
|
|
# properties for read-only attributes
|
|
# -------------------------------------------------------------------------
|
|
|
|
@property
|
|
def name(self) -> str:
|
|
"""Return read-only session name attribute."""
|
|
return self._name
|
|
|
|
@property
|
|
def device(self) -> Device:
|
|
"""Return read-only device instance attribute."""
|
|
return self._device
|
|
|
|
# -------------------------------------------------------------------------
|
|
# Public Methods
|
|
# -------------------------------------------------------------------------
|
|
|
|
async def status_all(self) -> dict[str, Any]:
|
|
"""
|
|
Get the status of all the session config on the device.
|
|
|
|
Run the following command on the device:
|
|
# show configuration sessions detail
|
|
|
|
Returns
|
|
-------
|
|
Dict object of native EOS eAPI response; see `status` method for
|
|
details.
|
|
|
|
Examples
|
|
--------
|
|
{
|
|
"maxSavedSessions": 1,
|
|
"maxOpenSessions": 5,
|
|
"sessions": {
|
|
"jeremy1": {
|
|
"instances": {},
|
|
"state": "pending",
|
|
"commitUser": "",
|
|
"description": ""
|
|
},
|
|
"ansible_167510439362": {
|
|
"instances": {},
|
|
"state": "completed",
|
|
"commitUser": "joe.bob",
|
|
"description": "",
|
|
"completedTime": 1675104396.4500246
|
|
}
|
|
}
|
|
}
|
|
"""
|
|
return await self._cli("show configuration sessions detail") # type: ignore[return-value] # json outformat returns dict[str, Any]
|
|
|
|
async def status(self) -> dict[str, Any] | None:
|
|
"""
|
|
Get the status of a session config on the device.
|
|
|
|
Run the following command on the device:
|
|
# show configuration sessions detail
|
|
|
|
And return only the status dictionary for this session. If you want
|
|
all sessions, then use the `status_all` method.
|
|
|
|
Returns
|
|
-------
|
|
Dict instance of the session status. If the session does not exist,
|
|
then this method will return None.
|
|
|
|
The native eAPI results from JSON output, see example:
|
|
|
|
Examples
|
|
--------
|
|
all results:
|
|
{
|
|
"maxSavedSessions": 1,
|
|
"maxOpenSessions": 5,
|
|
"sessions": {
|
|
"jeremy1": {
|
|
"instances": {},
|
|
"state": "pending",
|
|
"commitUser": "",
|
|
"description": ""
|
|
},
|
|
"ansible_167510439362": {
|
|
"instances": {},
|
|
"state": "completed",
|
|
"commitUser": "joe.bob",
|
|
"description": "",
|
|
"completedTime": 1675104396.4500246
|
|
}
|
|
}
|
|
}
|
|
|
|
if the session name was 'jeremy1', then this method would return
|
|
{
|
|
"instances": {},
|
|
"state": "pending",
|
|
"commitUser": "",
|
|
"description": ""
|
|
}
|
|
"""
|
|
res = await self.status_all()
|
|
return res["sessions"].get(self.name)
|
|
|
|
async def push(self, content: list[str] | str, *, replace: bool = False) -> None:
|
|
"""
|
|
Send the configuration content to the device.
|
|
|
|
If `replace` is true, then the command "rollback clean-config" is issued
|
|
before sending the configuration content.
|
|
|
|
Parameters
|
|
----------
|
|
content:
|
|
The text configuration CLI commands, as a list of strings, that
|
|
will be sent to the device. If the parameter is a string, and not
|
|
a list, then split the string across linebreaks. In either case
|
|
any empty lines will be discarded before they are send to the
|
|
device.
|
|
replace:
|
|
When True, the content will replace the existing configuration
|
|
on the device.
|
|
"""
|
|
# if given s string, we need to break it up into individual command
|
|
# lines.
|
|
|
|
if isinstance(content, str):
|
|
content = content.splitlines()
|
|
|
|
# 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]
|
|
if replace:
|
|
commands.append(self.CLI_CFG_FACTORY_RESET)
|
|
|
|
# add the Caller's commands, filtering out any blank lines. any command
|
|
# lines (!) are still included.
|
|
|
|
commands.extend(filter(None, content))
|
|
|
|
await self._cli(commands=commands)
|
|
|
|
async def commit(self, timer: str | None = None) -> None:
|
|
"""
|
|
Commit the session config.
|
|
|
|
Run the following command on the device:
|
|
# configure session <name>
|
|
# commit
|
|
|
|
If the timer is specified, format is "hh:mm:ss", then a commit timer is
|
|
started. A second commit action must be made to confirm the config
|
|
session before the timer expires; otherwise the config-session is
|
|
automatically aborted.
|
|
"""
|
|
command = f"{self._cli_config_session} commit"
|
|
|
|
if timer:
|
|
command += f" timer {timer}"
|
|
|
|
await self._cli(command)
|
|
|
|
async def abort(self) -> None:
|
|
"""
|
|
Abort the configuration session.
|
|
|
|
Run the following command on the device:
|
|
# configure session <name> abort
|
|
"""
|
|
await self._cli(f"{self._cli_config_session} abort")
|
|
|
|
async def diff(self) -> str:
|
|
"""
|
|
Return the "diff" of the session config relative to the running config.
|
|
|
|
Run the following command on the device:
|
|
# show session-config named <name> diffs
|
|
|
|
Returns
|
|
-------
|
|
Return a string in diff-patch format.
|
|
|
|
References
|
|
----------
|
|
* 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
|
|
|
|
async def load_file(self, filename: str, *, replace: bool = False) -> None:
|
|
"""
|
|
Load the configuration from <filename> into the session configuration.
|
|
|
|
If the replace parameter is True then the file contents will replace the existing session config (load-replace).
|
|
|
|
Parameters
|
|
----------
|
|
filename:
|
|
The name of the configuration file. The caller is required to
|
|
specify the filesystem, for example, the
|
|
filename="flash:thisfile.cfg"
|
|
|
|
replace:
|
|
When True, the contents of the file will completely replace the
|
|
session config for a load-replace behavior.
|
|
|
|
Raises
|
|
------
|
|
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]
|
|
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]]
|
|
checks_re = re.compile(r"error|abort|invalid", flags=re.I)
|
|
messages = res[-1]["messages"]
|
|
|
|
if any(map(checks_re.search, messages)):
|
|
raise RuntimeError("".join(messages))
|
|
|
|
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")
|