Source code for netgear_switch.virtual.faces.mibview

# src/netgear_switch/virtual/faces/mibview.py
"""Pure OID responder over VirtualSwitchState.oid_map().

No pysnmp, no network: a sorted (oid_tuple, snmp_type, value) list answering
exact-match GET and lexicographic GETNEXT with bisect. Task 15 wires this into
the real pysnmp command responder.
"""

from __future__ import annotations

import bisect
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from ..state import VirtualSwitchState

_Entry = tuple[tuple[int, ...], str, str]  # (oid_tuple, snmp_type, value)


def _oid_to_tuple(oid: str) -> tuple[int, ...]:
    return tuple(int(part) for part in oid.lstrip(".").split("."))


[docs] class StateMibView: """Sorted view of a switch's OID map supporting GET and GETNEXT.""" def __init__(self, state: VirtualSwitchState) -> None: self._state = state self._load() def _load(self) -> None: entries: list[_Entry] = [ (_oid_to_tuple(oid), snmp_type, value) for oid, (snmp_type, value) in self._state.oid_map().items() ] entries.sort(key=lambda e: e[0]) self._entries = entries self._oids = [e[0] for e in entries] # parallel key list for bisect
[docs] def get(self, oid: tuple[int, ...]) -> _Entry | None: # Callers MUST check `is_implemented(oid)` first (see faces/snmp.py): # this flat bisect has no notion of "this whole subtree is not # registered on this model" -- a None here means only "no instance at # this exact OID within an implemented subtree" (-> NoSuchInstance). i = bisect.bisect_left(self._oids, oid) if i < len(self._oids) and self._oids[i] == oid: return self._entries[i] return None # caller maps None -> NoSuchInstance
[docs] def get_next(self, oid: tuple[int, ...]) -> _Entry | None: # Same caveat as `get` above: callers check `is_implemented(oid)` # first, since a bare bisect_right would otherwise happily jump into # a completely unrelated (but implemented) subtree when `oid` itself # is under an unregistered one. # bisect_right -> index of the first OID strictly greater than `oid`. i = bisect.bisect_right(self._oids, oid) if i < len(self._oids): return self._entries[i] return None # caller maps None -> endOfMibView
[docs] def rebuild(self) -> None: """Recompute the sorted view from current state (call after a write).""" self._load()
[docs] def apply_write(self, oid: str, value: int | bytes | str) -> None: """Mutate the underlying state then rebuild so reads reflect the write.""" self._state.apply_write(oid, value) self.rebuild()
[docs] def apply_write_uncommitted(self, oid: str, value: int | bytes | str) -> None: """Mutate the underlying state WITHOUT rebuilding the sorted view. For an atomic multi-varbind SET (``faces/snmp.py``'s ``write_variables``): the (relatively expensive) ``rebuild()`` is deferred until the whole PDU has committed successfully, once, rather than once per varbind. Callers MUST call ``rebuild()`` themselves once every varbind in the PDU has applied without error. """ self._state.apply_write(oid, value)
[docs] def snapshot_state(self) -> VirtualSwitchState: """Snapshot the underlying state, for atomic multi-varbind SET rollback. See ``VirtualSwitchState.snapshot``/``restore_state``. """ return self._state.snapshot()
[docs] def restore_state(self, snapshot: VirtualSwitchState) -> None: """Restore the underlying state in place from a prior ``snapshot_state()`` result, discarding any writes applied since. The sorted view itself needs no rebuild after a restore: if the caller took the snapshot before making any changes and only ever reaches this on a failed atomic SET, the state (and thus the view) is back to exactly what it was before that SET began. """ self._state.restore(snapshot)
[docs] def is_writable_oid(self, oid: str) -> bool: """Passthrough to ``VirtualSwitchState.is_writable_oid`` (see there).""" return self._state.is_writable_oid(oid)
[docs] def is_implemented(self, oid: tuple[int, ...]) -> bool: """False if ``oid`` falls under a subtree root this model's SNMP agent has no registration for at all (e.g. the RFC3621 PoE MIB on a non-PoE model) -- see ``VirtualSwitchState.is_oid_implemented``. ``faces/snmp.py`` checks this BEFORE calling ``get``/``get_next``: a real agent answers ``noSuchObject`` for such a request rather than this view's flat bisect silently finding whatever unrelated OID happens to sort next. """ oid_str = ".".join(str(x) for x in oid) return self._state.is_oid_implemented(oid_str)