Source code for netgear_switch.protocols.snmp.write

"""Pure SNMP write encoding: SET varbinds and Q-BRIDGE bitmap read-modify-write.

No I/O and transport-agnostic. ``SetVarbind`` carries a net-snmp-style type
letter (``i`` INTEGER, ``u`` Gauge32/unsigned, ``s`` string, ``x`` hex/octets,
``a`` IpAddress) that both transports map onto their own SET call. The bitmap
helpers do a read-modify-write so only the target port's bit changes, leaving
trunks and other access ports untouched (design spec §6).
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import TYPE_CHECKING

from ...models import VlanMode
from .parse import decode_port_bitmap

if TYPE_CHECKING:
    from collections.abc import Iterable

    from ...registry import SwitchModel

SET_TYPE_LETTERS: frozenset[str] = frozenset({"i", "u", "s", "x", "a"})


[docs] @dataclass(frozen=True) class SetVarbind: """One SNMP SET varbind: full numeric OID, value, and net-snmp type letter.""" oid: str value: int | str | bytes type_letter: str def __post_init__(self) -> None: if self.type_letter not in SET_TYPE_LETTERS: raise ValueError( f"unknown SET type letter {self.type_letter!r}; " f"expected one of {sorted(SET_TYPE_LETTERS)}" )
[docs] def encode_port_bitmap(ports: Iterable[int], width_bytes: int = 8) -> bytes: """Inverse of ``parse.decode_port_bitmap``: a port set -> a wire bitmap. Bit 7 (MSB) of byte 0 is port 1. The buffer grows past ``width_bytes`` if a port number needs it, so callers never pre-size for the actual port count. """ data = bytearray(width_bytes) for p in ports: byte_idx, bit = divmod(p - 1, 8) while byte_idx >= len(data): data.append(0) data[byte_idx] |= 0x80 >> bit return bytes(data)
[docs] def set_port_bit( current: bytes | str, port: int, present: bool, *, width_bytes: int | None = None ) -> bytes: """Read-modify-write one port's bit in a VLAN bitmap; all others preserved. Preserves the input bitmap's byte width to avoid wire-length mismatches on SET for >64-port switches. ``width_bytes``, if given (e.g. a model-derived width from ``vlan_bitmap_width``), is honoured too: the result is at least 8 bytes, at least as wide as the input, and at least ``width_bytes`` wide. """ # Compute the current bitmap's width in bytes if isinstance(current, bytes): current_width = len(current) else: current_width = len(current.encode("latin-1")) ports = set(decode_port_bitmap(current)) if present: ports.add(port) else: ports.discard(port) target_width = max(8, current_width, width_bytes or 0) return encode_port_bitmap(ports, width_bytes=target_width)
[docs] def membership_bitmaps( *, mode: VlanMode, port: int, egress: bytes | str, untagged: bytes | str, width_bytes: int | None = None, ) -> tuple[bytes, bytes]: """Compute (new_egress, new_untagged) for one port's VLAN membership change. UNTAGGED -> egress bit on + untagged bit on; TAGGED -> egress on, untagged off; EXCLUDED -> both off. Read-modify-write on the current bitmaps, so every other port's membership is preserved. ``width_bytes`` is forwarded to ``set_port_bit`` for both columns (see ``vlan_bitmap_width``). """ in_egress = mode in (VlanMode.UNTAGGED, VlanMode.TAGGED) in_untagged = mode is VlanMode.UNTAGGED return ( set_port_bit(egress, port, in_egress, width_bytes=width_bytes), set_port_bit(untagged, port, in_untagged, width_bytes=width_bytes), )
[docs] def vlan_bitmap_width(model: SwitchModel) -> int: """Wire byte-width of ``model``'s dot1q VLAN egress/untagged bitmaps. ``dot1qVlanStaticEgressPorts``/``UntaggedPorts`` are packed 8 ports/byte, MSB-first (port 1 = bit 7 of byte 0). The Q-BRIDGE MIB's own default PortList width is 8 bytes (64 ports); a model with more ports needs a wider bitmap or the SET's wire length won't match what the device expects. """ return max(8, (model.port_count + 7) // 8)