refactor: the CARP state table lives with the driver that speaks CARP
CI / test (3.10) (push) Failing after 15s
CI / test (3.11) (push) Failing after 15s
CI / test (3.12) (push) Successful in 30s
CI / test (3.10) (pull_request) Successful in 30s
CI / test (3.11) (pull_request) Successful in 28s
CI / test (3.12) (pull_request) Successful in 29s

carp_virtual_ip() and its state table moved here from napalm-device-types
(4.10.1, NAPALM/napalm-device-types#39): under the agreed structure the base
package carries only the shape of the reading, and what a CARP state means is
protocol knowledge. Tests moved along (test_carp_states.py).
This commit is contained in:
Christian Manivong committed 2026-10-16 11:11:45 +02:00
1 parent 00172f7a69
commit c868d41322
3 files changed
+177 -5

No files matched your search

+65 -4
View File
@@ -9,17 +9,78 @@ VHID's MASTER. An address without a VHID stays on this node and is no virtual IP
each VHID's CARP password in it. The overview has none, so a secret never reaches
the caller.
What a state means (MASTER holds, BACKUP stands by) is the same on every CARP
firewall: :func:`napalm_device_types.virtual_ips.carp_virtual_ip`.
What a state means -- MASTER holds, BACKUP stands by -- is CARP protocol
knowledge, so it lives here with the driver that speaks CARP
(:func:`carp_virtual_ip`), not in napalm-device-types, which carries only the
shape of the reading.
"""
from __future__ import annotations
import ipaddress
from typing import Any
from typing import Any, Optional
from napalm_device_types.models import VirtualIpDict
from napalm_device_types.virtual_ips import carp_virtual_ip
#: CARP's state of a VHID on one member -> (running, holds).
_CARP_STATES: dict[str, tuple[bool, bool]] = {
"MASTER": (True, True),
"BACKUP": (True, False),
"INIT": (False, False),
"DISABLED": (False, False),
}
def _address(text: str) -> Optional[tuple[str, Optional[int]]]:
"""``"10.0.0.1/24"`` -> ``("10.0.0.1", 24)``; None for what is no address."""
host, slash, prefix = text.partition("/")
try:
address = ipaddress.ip_address(host)
except ValueError:
return None
if not slash:
return str(address), None
if not prefix.isdigit() or int(prefix) > address.max_prefixlen:
return None
return str(address), int(prefix)
def carp_virtual_ip(
*,
address: str,
prefix_length: Optional[int],
vhid: object,
state: Optional[str],
instance: str,
interface: Optional[str],
) -> Optional[VirtualIpDict]:
"""One address bound to a CARP VHID, as a virtual IP; None for what is no address.
Who holds it comes from CARP's *state* on this member, not from its
interfaces: MASTER holds it, BACKUP stands by, INIT and DISABLED neither run
nor hold. A state this does not know says nothing (``running`` and ``holds``
None), and the caller falls back on the interfaces. *address* may carry its
prefix (``"10.0.0.1/24"``), which then wins over *prefix_length*.
"""
parsed = _address(address.strip()) if address else None
if parsed is None:
return None
normalised, written = parsed
prefix = written if written is not None else prefix_length
if prefix is not None and not 0 <= prefix <= ipaddress.ip_address(normalised).max_prefixlen:
prefix = None
vhid_text = str(vhid).strip() if vhid is not None else ""
running, holds = _CARP_STATES.get((state or "").strip().upper(), (None, None))
return {
"mechanism": "carp",
"instance": instance or f"vhid {vhid_text}",
"address": normalised,
"prefix_length": prefix,
"interface": interface,
"group_key": f"vhid:{vhid_text}" if vhid_text.isdigit() else None,
"running": running,
"holds": holds,
}
def _interfaces(overview: Any) -> list[Any]: