Source code for netgear_switch.transport.cli.console
"""Thin serial-console CLI transport (implements ``CliSession``), transport-only.
Reuses the SAME ``ShellDriver``/parsers as the SSH transport, carrying bytes over
a local serial line (``pyserial``, the ``[ssh]`` extra) instead of a network
socket. Used to reach a switch's physical console port. Cannot be exercised from
CI (no serial hardware); transport-only, parser-shared.
pyserial is imported lazily so ``import netgear_switch`` never depends on it.
"""
from __future__ import annotations
import contextlib
from typing import TYPE_CHECKING, Any
from .session import CliSession, CliTransportError, ShellDriver
if TYPE_CHECKING:
from ...protocols.cli.commands import CliModelSpec
# NETGEAR console ports default to 115200 8N1.
_DEFAULT_BAUD = 115200
_DEFAULT_TIMEOUT = 20.0
[docs]
class ConsoleCliTransport(CliSession):
"""A serial-console interactive-shell CLI session over ``pyserial``."""
def __init__(
self,
device: str,
username: str,
password: str,
spec: CliModelSpec,
*,
baudrate: int = _DEFAULT_BAUD,
timeout: float = _DEFAULT_TIMEOUT,
) -> None:
self._device = device
self._username = username
self._password = password
self._spec = spec
self._baudrate = baudrate
self._timeout = timeout
self._serial: Any = None
self._driver: ShellDriver | None = None
def _login(self, ser: Any) -> None:
# Prod the console (it may already be at a prompt) then answer the
# User:/Password: login the shared driver's setup() does not cover.
ser.write(b"\r\n")
ser.write(self._username.encode("latin-1") + b"\r\n")
ser.write(self._password.encode("latin-1") + b"\r\n")
[docs]
def connect(self) -> None:
try:
import serial
except ImportError as exc: # pragma: no cover - depends on optional extra
raise CliTransportError(
"console CLI transport requires pyserial (install the '[ssh]' extra)"
) from exc
try:
ser = serial.Serial(
self._device, baudrate=self._baudrate, timeout=self._timeout
)
self._login(ser)
except Exception as exc:
self.close()
raise CliTransportError(f"console open/login failed: {exc}") from exc
self._serial = ser
self._driver = ShellDriver(
ser.write,
ser.read,
enable_cmd=self._spec.enable_cmd,
paging_off_cmd=self._spec.paging_off_cmd,
enable_password=self._password,
)
self._driver.setup()
[docs]
def run(self, command: str) -> str:
if self._driver is None:
self.connect()
assert self._driver is not None
return self._driver.run(command)
[docs]
def run_scp_copy(self, command: str, scp_password: str) -> str:
if self._driver is None:
self.connect()
assert self._driver is not None
return self._driver.run_scp_copy(command, scp_password)
[docs]
def run_write_memory(self, command: str = "write memory", *, prestuff: bool) -> str:
if self._driver is None:
self.connect()
assert self._driver is not None
return self._driver.run_write_memory(command, prestuff=prestuff)
[docs]
def close(self) -> None:
if self._serial is not None:
with contextlib.suppress(Exception): # teardown must not raise
self._serial.close()
self._serial = None
self._driver = None