Source code for netgear_switch.cli_read

"""Model-driven CLI read operations over a ``CliSession``.

The CLI analogue of ``http_read.py``: dispatches each ``get_*`` op to its
FASTPATH ``show`` command (via ``protocols.cli.commands.cli_spec``) over a
``CliSession`` transport, and parses the returned text with the pure functions
in ``protocols.cli.parse``. Works unchanged against a real SSH/telnet/console
session or the in-process mock face (``virtual.faces.cli.VirtualCliFace``).

Unlike ``HttpReader``, construction is NOT gated on ``reads_verified``: the
reader is always usable directly (mock tests, and a future live-verified flip),
and it is the FACADE that refuses live CLI dispatch while ``reads_verified`` is
False (see ``_dispatch.cli_reads_supported`` / ``SyncSwitch._reader_for``).

Ops a FASTPATH model's CLI genuinely lacks raise ``UnsupportedCapabilityError``
honestly rather than fabricating: ``get_poe`` on the non-PoE M4300-24X (PoE port
count 0) is the only such carve-out -- every other ``show`` command exists on
every FASTPATH switch.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from .errors import UnsupportedCapabilityError
from .protocols.cli import parse
from .protocols.cli.commands import cli_spec
from .registry import MODELS

if TYPE_CHECKING:
    from .models import (
        DetectedModel,
        LLDPNeighbor,
        MacEntry,
        MgmtIpConfig,
        PoEStatus,
        PortStats,
        PortStatus,
        Sensor,
        ServiceStatus,
        SwitchUser,
        SyslogConfig,
        VLANInfo,
    )
    from .registry import SwitchModel
    from .transport.cli.session import CliSession


def _unsupported(model_key: str, op: str) -> UnsupportedCapabilityError:
    return UnsupportedCapabilityError(f"model {model_key!r} CLI does not expose {op}")


[docs] class CliReader: """Synchronous FASTPATH-CLI read facade over one switch.""" def __init__(self, session: CliSession, model: SwitchModel) -> None: self._spec = cli_spec(model) self.session = session self.model = model
[docs] def get_ports(self) -> list[PortStatus]: return parse.parse_port_status(self.session.run(self._spec.port_status_cmd))
[docs] def get_stats(self) -> list[PortStats]: # FASTPATH has no "all ports" octet-counter command; counters come from # a per-port ``show interface ethernet 1/0/<n>``. One round trip per # physical port, in port order. Iterate the ACTUAL physical ports the # switch reports (``show port all``) rather than ``range(1, port_count+1)``: # registry ``port_count`` is a nominal value that can exceed the real # physical-port count (e.g. m4300-24x registry port_count=28 but the # XSM4324CS has only 24 physical ports), which would otherwise issue # doomed queries for phantom ports and fabricate empty PortStats. Deriving # the port list from the port table is correct on any switch. out: list[PortStats] = [] for status in self.get_ports(): text = self.session.run(self._spec.interface_stats(status.port)) out.append(parse.parse_interface_counters(text, status.port)) return out
[docs] def get_vlans(self) -> list[VLANInfo]: brief = parse.parse_vlan_brief(self.session.run(self._spec.vlan_brief_cmd)) out: list[VLANInfo] = [] for vid, name in brief: detail = self.session.run(self._spec.vlan_detail(vid)) out.append(parse.parse_vlan_detail(detail, name=name)) return out
[docs] def get_pvids(self) -> list[tuple[int, int]]: return parse.parse_pvids(self.session.run(self._spec.pvid_cmd))
[docs] def get_macs(self) -> list[MacEntry]: if not self.model.has_mac_table: raise _unsupported(self.model.key, "a MAC/FDB table") return parse.parse_mac_table(self.session.run(self._spec.mac_table_cmd))
[docs] def get_lldp(self) -> list[LLDPNeighbor]: return parse.parse_lldp(self.session.run(self._spec.lldp_cmd))
[docs] def get_poe(self) -> list[PoEStatus]: if self.model.poe_port_count == 0: raise _unsupported(self.model.key, "PoE (model has no PSE ports)") return parse.parse_poe(self.session.run(self._spec.poe_cmd))
[docs] def get_sensors(self) -> list[Sensor]: return parse.parse_environment(self.session.run(self._spec.environment_cmd))
[docs] def get_mgmt_ip(self) -> MgmtIpConfig: return parse.parse_mgmt_ip(self.session.run(self._spec.network_cmd))
[docs] def get_services(self) -> list[ServiceStatus]: """Which management services are enabled, and on which ports. Three commands, because the switch splits it that way -- and the telnet one is ``show telnetcon``, not ``show telnet``. See ``parse.parse_services`` for why that distinction matters. """ return parse.parse_services( self.session.run(self._spec.http_service_cmd), self.session.run(self._spec.telnet_service_cmd), self.session.run(self._spec.ssh_service_cmd), )
[docs] def get_users(self) -> list[SwitchUser]: """The switch's local login accounts, from ``show users``. The access-mode wording differs between firmware images, so ``SwitchUser.access_mode`` keeps the raw text and ``privileged`` carries the normalised reading -- see ``parse.parse_users``. """ return parse.parse_users(self.session.run(self._spec.users_cmd))
[docs] def get_syslog(self) -> SyslogConfig: """Remote-logging configuration, from ``show logging`` + its host table. Two commands because the switch splits it that way: the globals live in ``show logging`` and the collectors in ``show logging hosts``. The host table's column set differs by firmware -- see ``parse.parse_syslog``. """ return parse.parse_syslog( self.session.run(self._spec.logging_cmd), self.session.run(self._spec.logging_hosts_cmd), )
[docs] def get_hostname(self) -> str: """The switch's host name, from ``show hosts``. See ``parse.parse_hostname`` for why this command and not ``show running-config``: the two report different values, and only this one agrees with SNMP's ``sysName``. """ return parse.parse_hostname(self.session.run(self._spec.hosts_cmd))
[docs] def identify(self) -> DetectedModel: return parse.parse_version(self.session.run(self._spec.version_cmd), MODELS)