feat: read the virtual IPs vip-manager and keepalived declare on a host
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 24s
CI / test (3.12) (push) Successful in 26s
CI / test (3.10) (pull_request) Successful in 35s
CI / test (3.11) (pull_request) Successful in 36s
CI / test (3.12) (pull_request) Successful in 39s
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 24s
CI / test (3.12) (push) Successful in 26s
CI / test (3.10) (pull_request) Successful in 35s
CI / test (3.11) (pull_request) Successful in 36s
CI / test (3.12) (pull_request) Successful in 39s
A virtual IP sits on whichever member holds it right now; a standby's address list shows no trace of it. What every member has is the configuration that declares it, and reading that is the same on every Linux host -- so VirtualIpsMixin.get_virtual_ips() lives here and a driver only carries the command across (NetOrk/netork#829). - vip-manager: every vip-manager.service / vip-manager@*.service unit; the address from a flag (1.x's -ip=${VIP_IP} resolved through the unit's environment), the environment, or the --config YAML (5.x). - keepalived: the virtual_ipaddress(_excluded) entries of every vrrp_instance, with its interface and virtual_router_id; include is followed four levels deep, and a pattern that is no plain path glob is never handed to the root shell. The same files hold the etcd password and auth_pass, so an awk program filters on the host and prints allowlisted fields only. None per mechanism is "did not look", [] a host without it. The command is about 4 kB and goes on an exec channel. Version 4.2.0.
This commit is contained in:
@@ -52,6 +52,7 @@ instead of being restated on every role that happens to need it:
|
||||
* :class:`~napalm_device_types.services.ServiceControlMixin`
|
||||
* :class:`~napalm_device_types.systemd.SystemdServicesMixin`
|
||||
* :class:`~napalm_device_types.updates.UpdateMixin`
|
||||
* :class:`~napalm_device_types.virtual_ips.VirtualIpsMixin`
|
||||
|
||||
Introspection -- :func:`~napalm_device_types.roles.roles_of`,
|
||||
:func:`~napalm_device_types.roles.role_keys_of` and
|
||||
@@ -113,6 +114,11 @@ from napalm_device_types.systemd import (
|
||||
parse_systemd_services,
|
||||
)
|
||||
from napalm_device_types.updates import UpdateMixin
|
||||
from napalm_device_types.virtual_ips import (
|
||||
VIRTUAL_IPS_COMMAND,
|
||||
VirtualIpsMixin,
|
||||
parse_virtual_ips,
|
||||
)
|
||||
from napalm_device_types.residential_gateway import ResidentialGatewayDriver
|
||||
from napalm_device_types.storage import StorageDriver
|
||||
from napalm_device_types.switch import SwitchDriver
|
||||
@@ -170,6 +176,9 @@ __all__ = [
|
||||
"SystemdUnavailable",
|
||||
"parse_systemd_services",
|
||||
"UpdateMixin",
|
||||
"VIRTUAL_IPS_COMMAND",
|
||||
"VirtualIpsMixin",
|
||||
"parse_virtual_ips",
|
||||
"add_lag_interfaces",
|
||||
"driver_supports_ping",
|
||||
"normalize_cidr",
|
||||
|
||||
@@ -366,6 +366,35 @@ class ListeningSocketsDict(TypedDict):
|
||||
sockets: List[ListeningSocketDict]
|
||||
|
||||
|
||||
class VirtualIpDict(TypedDict):
|
||||
"""An address the host's HA configuration lets float between machines.
|
||||
|
||||
Declared, not observed: whether the host holds the address right now is in
|
||||
its interface addresses, not here. Only these fields leave the host -- never
|
||||
a password, an etcd endpoint or a notify script.
|
||||
"""
|
||||
|
||||
mechanism: str # "vip-manager" | "keepalived"
|
||||
instance: str # the vrrp_instance name, or the vip-manager unit without ".service"
|
||||
address: str # normalised: "10.7.224.10", "fd00::10"
|
||||
prefix_length: Optional[int] # None where the configuration gives none
|
||||
interface: Optional[str]
|
||||
group_key: Optional[str] # vip-manager's trigger-key, or "vrid:<n>" for keepalived
|
||||
running: Optional[bool] # the unit is active
|
||||
|
||||
|
||||
class VirtualIpsDict(TypedDict):
|
||||
"""What ``VirtualIpsMixin.get_virtual_ips`` read, per mechanism.
|
||||
|
||||
A list is what was looked at -- ``[]`` a host without that mechanism. None is
|
||||
"did not look": a configuration it could not read, an include it could not
|
||||
follow, a host without systemd. A consumer keeps what it knew for a None.
|
||||
"""
|
||||
|
||||
vip_manager: Optional[List[VirtualIpDict]]
|
||||
keepalived: Optional[List[VirtualIpDict]]
|
||||
|
||||
|
||||
class PortForwardDict(TypedDict):
|
||||
"""A port the WAN side can reach, forwarded to a host inside.
|
||||
|
||||
|
||||
@@ -0,0 +1,406 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Virtual IPs: the addresses a host's HA configuration lets float between machines.
|
||||
|
||||
vip-manager and keepalived put a service address on whichever member is master
|
||||
*right now*. The address list of a standby shows no trace of it; what every
|
||||
member has is the configuration that declares it. That is read the same way on
|
||||
every Linux host, so the command and its parse live here once, and a driver
|
||||
only carries the command across.
|
||||
|
||||
**Declared, not held.** The reading says which addresses the configuration
|
||||
lets onto this host. Whether the host holds one at the moment is in its
|
||||
interface addresses, which the caller already has.
|
||||
|
||||
**Filtered on the host.** The same files hold secrets -- vip-manager's etcd
|
||||
password, keepalived's ``auth_pass``, notify scripts with their arguments. The
|
||||
command therefore never prints a configuration: an awk program reads the unit's
|
||||
properties and files and prints allowlisted fields only. A secret cannot reach
|
||||
the caller's memory, its log or an exception text, because it never leaves the
|
||||
host.
|
||||
|
||||
**What is read.**
|
||||
|
||||
- *vip-manager*: every ``vip-manager.service`` and ``vip-manager@*.service`` unit
|
||||
systemd knows. The address comes from the first of: a command-line flag
|
||||
(``--ip``; 1.x's ``-ip=${VIP_IP}`` resolved through the unit's environment), the
|
||||
environment (``VIP_IP``, an ``EnvironmentFile`` over ``Environment``), the
|
||||
``--config`` YAML (5.x's ``ip``). Mask, interface and trigger key alike.
|
||||
- *keepalived*: ``keepalived.service``, its ``-f``/``--use-file`` or
|
||||
``/etc/keepalived/keepalived.conf``. The ``virtual_ipaddress`` and
|
||||
``virtual_ipaddress_excluded`` entries of every ``vrrp_instance``, with the
|
||||
instance's ``interface`` and ``virtual_router_id``. ``include`` is followed
|
||||
four levels deep, a relative pattern from the including file's directory. A
|
||||
pattern that is not a plain path glob is not followed: it would otherwise be
|
||||
handed to a root shell.
|
||||
|
||||
**None is "did not look".** A file that exists but cannot be read, an include
|
||||
that cannot be followed, a host without systemd: that mechanism is None, and the
|
||||
caller keeps what it knew. ``[]`` is a host that has none.
|
||||
|
||||
**Root, and without it.** The configuration may be root's alone. The whole
|
||||
script goes to the host as one ``sh -c`` argument; when the privileged call
|
||||
brings no report back, it runs again without privilege and reads what it can.
|
||||
|
||||
**An exec channel, not a terminal.** With its awk program the command is about
|
||||
4 kB, past the line an interactive shell reliably takes. A driver sends it the
|
||||
way it runs any command that needs no PTY -- napalm-linux's ``run_command``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from shlex import quote
|
||||
from typing import Callable, Dict, List, Optional, Set, Tuple, TYPE_CHECKING
|
||||
|
||||
from napalm_device_types.models import VirtualIpDict, VirtualIpsDict
|
||||
from napalm_device_types.terminal import strip_terminal_codes
|
||||
|
||||
_BEGIN = "VIPS_BEGIN"
|
||||
_END = "VIPS_END"
|
||||
_NO_SYSTEMD = "no-systemd"
|
||||
#: A record that says a mechanism was not fully read.
|
||||
_BLIND = frozenset({"unreadable", "too-deep"})
|
||||
_KEEPALIVED_CONF = "/etc/keepalived/keepalived.conf"
|
||||
_ENV_KEYS = "VIP_(IP|MASK|NETMASK|IFACE|INTERFACE|KEY|TRIGGER_KEY)"
|
||||
_CFG_KEYS = "(ip|netmask|mask|interface|iface|trigger-key|key)"
|
||||
|
||||
#: One line of awk, fed a unit's ``systemctl show`` output with ``-v m=<mechanism>``.
|
||||
#: It prints records, never a line of configuration: ``active <state>``,
|
||||
#: ``arg <flag> <value>``, ``env <VIP_*> <value>``, ``cfg <key> <value>``,
|
||||
#: ``instance <name>`` / ``interface`` / ``virtual_router_id`` / ``vip <addr> [dev <if>]``
|
||||
#: / ``end``, and ``unreadable``/``too-deep``/``missing <path>``. No ``>`` but in
|
||||
#: ``2>/dev/null``: comparisons are written the other way round. ``\047`` is a
|
||||
#: single quote, so the program itself needs none.
|
||||
_AWK = " ".join(
|
||||
(
|
||||
r'function q(p) { return "\047" p "\047" }',
|
||||
r'function ok(f) { if (f == "" || f ~ /\047/) { print "unreadable " f; return 0 }'
|
||||
r' if (system("test -r " q(f)) == 0) return 1;'
|
||||
r' if (system("test -e " q(f)) == 0) print "unreadable " f; else print "missing " f;'
|
||||
r" return 0 }",
|
||||
r"function rf(f, c, l, r, k) { if (!ok(f)) return;"
|
||||
r" while (0 < (r = (getline l < f))) {"
|
||||
rf' if (c == "cfg" && l ~ /^[ \t]*{_CFG_KEYS}[ \t]*:/) {{ sub(/^[ \t]*/, "", l); k = l;'
|
||||
r' sub(/[ \t]*:.*/, "", k); sub(/^[^:]*:[ \t]*/, "", l); print "cfg " k " " l }'
|
||||
rf' else if (c == "env" && l ~ /^[ \t]*(export[ \t]+)?{_ENV_KEYS}[ \t]*=/) {{'
|
||||
r' sub(/^[ \t]*(export[ \t]+)?/, "", l); k = l; sub(/[ \t]*=.*/, "", k);'
|
||||
r' sub(/^[^=]*=[ \t]*/, "", l); print "env " k " " l } }'
|
||||
r' if (r < 0) print "unreadable " f; close(f) }',
|
||||
r'function push(n, a) { st[++sd] = n; if (n == "vrrp_instance") print "instance " a }',
|
||||
r'function pop() { if (0 < sd) { if (st[sd] == "vrrp_instance") print "end"; sd-- } }',
|
||||
r"function stmt(w, nw, f, d, x, i) { if (!nw) return;"
|
||||
r' if (w[1] == "include") { if (1 < nw) inc(w[2], f, d); return }'
|
||||
r' if (sd == 1 && st[1] == "vrrp_instance") {'
|
||||
r' if (1 < nw && (w[1] == "interface" || w[1] == "virtual_router_id")) print w[1] " " w[2];'
|
||||
r" return }"
|
||||
r' if (sd == 2 && st[1] == "vrrp_instance" && st[2] ~ /^virtual_ipaddress(_excluded)?$/) {'
|
||||
r' x = "vip " w[1]; for (i = 2; i < nw; i++) if (w[i] == "dev") x = x " dev " w[i + 1];'
|
||||
r" print x } }",
|
||||
r"function rk(f, d, l, r, n, t, k, w, nw) {"
|
||||
r' if (4 < d || (f in seen)) { print "too-deep " f; return }'
|
||||
r" if (!ok(f)) return; seen[f] = 1;"
|
||||
r' while (0 < (r = (getline l < f))) { sub(/[#!].*/, "", l); gsub(/[{}]/, " & ", l);'
|
||||
r' n = split(l, t, " "); nw = 0; for (k = 1; k <= n; k++) {'
|
||||
r' if (t[k] == "{") { push(nw ? w[1] : "", 1 < nw ? w[2] : ""); nw = 0 }'
|
||||
r' else if (t[k] == "}") { stmt(w, nw, f, d); nw = 0; pop() } else w[++nw] = t[k] }'
|
||||
r" stmt(w, nw, f, d) }"
|
||||
r' if (r < 0) print "unreadable " f; close(f); delete seen[f] }',
|
||||
r"function inc(p, f, d, c, g, dir, fs, n, i) {"
|
||||
r' if (p !~ /^\//) { dir = f; sub(/[^\/]*$/, "", dir); p = dir p }'
|
||||
r' if (p !~ /^[A-Za-z0-9_.\/*?-]+$/) { print "unreadable " p; return }'
|
||||
r' c = "for g in " p "; do [ -e \"$g\" ] && echo \"$g\"; done"; n = 0;'
|
||||
r" while (0 < (c | getline g)) fs[++n] = g; close(c);"
|
||||
r" for (i = 1; i <= n; i++) rk(fs[i], d + 1) }",
|
||||
r'/^ActiveState=/ { print "active " substr($0, 13); next }',
|
||||
r'/^ExecStart=/ { s = $0; i = index(s, "argv[]="); if (!i) next; s = substr(s, i + 7);'
|
||||
r' j = index(s, " ;"); if (j) s = substr(s, 1, j - 1); n = split(s, t, " ");'
|
||||
r' for (k = 2; k <= n; k++) { f = t[k]; if (f !~ /^-/) continue; v = ""; x = index(f, "=");'
|
||||
r" if (x) { v = substr(f, x + 1); f = substr(f, 1, x - 1) }"
|
||||
r' else if (k < n && t[k + 1] !~ /^-/) v = t[++k]; sub(/^--?/, "", f);'
|
||||
r" if (f ~ /^(config|ip|netmask|mask|interface|iface|trigger-key|key|f|use-file)$/) {"
|
||||
r' print "arg " f " " v; a[f] = v } } next }',
|
||||
r'/^EnvironmentFiles=/ { s = substr($0, 18); sub(/ \(ignore_errors=[a-z]*\)$/, "", s);'
|
||||
r' if (s != "") ef[++ne] = s; next }',
|
||||
r'/^Environment=/ { n = split(substr($0, 13), t, " "); for (k = 1; k <= n; k++) {'
|
||||
rf' x = index(t[k], "="); if (x && substr(t[k], 1, x - 1) ~ /^{_ENV_KEYS}$/)'
|
||||
r' print "env " substr(t[k], 1, x - 1) " " substr(t[k], x + 1) } next }',
|
||||
rf'END {{ if (m == "keepalived") {{ p = "{_KEEPALIVED_CONF}";'
|
||||
r' if ("use-file" in a) p = a["use-file"]; if ("f" in a) p = a["f"]; rk(p, 0) }'
|
||||
r' else { for (k = 1; k <= ne; k++) rf(ef[k], "env");'
|
||||
r' if ("config" in a) rf(a["config"], "cfg") } }',
|
||||
)
|
||||
)
|
||||
|
||||
#: One line, POSIX ``sh``, read-only. The frame markers are printed in two halves
|
||||
#: so that a transport which echoes the command does not show them early.
|
||||
VIRTUAL_IPS_COMMAND = (
|
||||
"PATH=$PATH:/usr/sbin:/sbin; "
|
||||
"printf '%s%s\\n' VIPS_ BEGIN; "
|
||||
"if command -v systemctl >/dev/null 2>&1; then echo '[systemd]'; "
|
||||
"for u in $(systemctl list-units --all --plain --no-legend --type=service "
|
||||
"vip-manager.service 'vip-manager@*' keepalived.service 2>/dev/null | awk '{print $1}'); do "
|
||||
"case $u in keepalived*) m=keepalived;; *) m=vip-manager;; esac; "
|
||||
'echo "[$m $u]"; '
|
||||
'systemctl show -p ActiveState -p ExecStart -p EnvironmentFiles -p Environment "$u" '
|
||||
f"2>/dev/null | awk -v m=$m {quote(_AWK)}; done; "
|
||||
"else echo '[no-systemd]'; fi; "
|
||||
"printf '%s%s\\n' VIPS_ END"
|
||||
)
|
||||
|
||||
_COMMENT_RE = re.compile(r"\s+#.*$")
|
||||
_VARIABLE_RE = re.compile(r"^\$\{?(\w+)\}?$")
|
||||
_Record = Tuple[str, str]
|
||||
|
||||
|
||||
def _frame(output: str) -> List[str]:
|
||||
lines = [line.strip() for line in strip_terminal_codes(output).splitlines()]
|
||||
try:
|
||||
start = lines.index(_BEGIN)
|
||||
end = lines.index(_END, start)
|
||||
except ValueError:
|
||||
raise ValueError("no intact virtual IP report in the output") from None
|
||||
return lines[start + 1 : end]
|
||||
|
||||
|
||||
def _sections(lines: List[str]) -> List[Tuple[str, str, List[_Record]]]:
|
||||
"""``[<mechanism> <unit>]`` headers, each with its records as ``(kind, rest)``."""
|
||||
sections: List[Tuple[str, str, List[_Record]]] = []
|
||||
for line in lines:
|
||||
if line.startswith("[") and line.endswith("]"):
|
||||
mechanism, _, unit = line[1:-1].partition(" ")
|
||||
sections.append((mechanism, unit, []))
|
||||
elif line and sections:
|
||||
kind, _, rest = line.partition(" ")
|
||||
sections[-1][2].append((kind, rest.strip()))
|
||||
return sections
|
||||
|
||||
|
||||
def _clean(value: str) -> str:
|
||||
"""Without a trailing ``# comment`` and the quotes around it."""
|
||||
value = _COMMENT_RE.sub("", value.strip())
|
||||
if len(value) >= 2 and value[0] == value[-1] and value[0] in "\"'":
|
||||
value = value[1:-1]
|
||||
return value.strip()
|
||||
|
||||
|
||||
def _address(text: str) -> Optional[Tuple[str, Optional[int]]]:
|
||||
"""``"10.0.0.10/24"`` -> ``("10.0.0.10", 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 _prefix(mask: Optional[str]) -> Optional[int]:
|
||||
"""``"24"`` or ``"255.255.255.0"`` -> 24."""
|
||||
if not mask:
|
||||
return None
|
||||
if mask.isdigit():
|
||||
return int(mask) if int(mask) <= 128 else None
|
||||
try:
|
||||
return ipaddress.IPv4Network(f"0.0.0.0/{mask}").prefixlen
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
|
||||
def _running(records: List[_Record]) -> Optional[bool]:
|
||||
states = [rest for kind, rest in records if kind == "active"]
|
||||
return states[-1] == "active" if states else None
|
||||
|
||||
|
||||
def _vip_manager(unit: str, records: List[_Record]) -> List[VirtualIpDict]:
|
||||
values: Dict[str, Dict[str, str]] = {"arg": {}, "env": {}, "cfg": {}}
|
||||
for kind, rest in records:
|
||||
if kind in values:
|
||||
key, _, value = rest.partition(" ")
|
||||
values[kind][key] = value
|
||||
env = values["env"]
|
||||
|
||||
def resolve(value: str) -> Optional[str]:
|
||||
value = _clean(value)
|
||||
variable = _VARIABLE_RE.match(value)
|
||||
if variable:
|
||||
value = _clean(env.get(variable.group(1), ""))
|
||||
return value if value and "$" not in value else None
|
||||
|
||||
def pick(args: Tuple[str, ...], envs: Tuple[str, ...], cfgs: Tuple[str, ...]) -> Optional[str]:
|
||||
for kind, names in (("arg", args), ("env", envs), ("cfg", cfgs)):
|
||||
for name in names:
|
||||
if name in values[kind]:
|
||||
found = resolve(values[kind][name])
|
||||
if found:
|
||||
return found
|
||||
return None
|
||||
|
||||
ip = pick(("ip",), ("VIP_IP",), ("ip",))
|
||||
parsed = _address(ip) if ip else None
|
||||
if parsed is None:
|
||||
return []
|
||||
address, prefix = parsed
|
||||
mask = pick(("netmask", "mask"), ("VIP_NETMASK", "VIP_MASK"), ("netmask", "mask"))
|
||||
return [
|
||||
{
|
||||
"mechanism": "vip-manager",
|
||||
"instance": unit[: -len(".service")] if unit.endswith(".service") else unit,
|
||||
"address": address,
|
||||
"prefix_length": _prefix(mask) if mask else prefix,
|
||||
"interface": pick(
|
||||
("interface", "iface"), ("VIP_INTERFACE", "VIP_IFACE"), ("interface", "iface")
|
||||
),
|
||||
"group_key": pick(
|
||||
("trigger-key", "key"), ("VIP_TRIGGER_KEY", "VIP_KEY"), ("trigger-key", "key")
|
||||
),
|
||||
"running": _running(records),
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@dataclass
|
||||
class _Instance:
|
||||
name: str
|
||||
interface: Optional[str] = None
|
||||
vrid: str = ""
|
||||
vips: List[str] = field(default_factory=list)
|
||||
|
||||
|
||||
def _keepalived(unit: str, records: List[_Record]) -> List[VirtualIpDict]:
|
||||
running = _running(records)
|
||||
instances: List[_Instance] = []
|
||||
current: Optional[_Instance] = None
|
||||
for kind, rest in records:
|
||||
if kind == "instance":
|
||||
current = _Instance(rest or unit)
|
||||
instances.append(current)
|
||||
elif current is None:
|
||||
continue
|
||||
elif kind == "interface":
|
||||
current.interface = rest or None
|
||||
elif kind == "virtual_router_id":
|
||||
current.vrid = rest
|
||||
elif kind == "vip":
|
||||
current.vips.append(rest)
|
||||
elif kind == "end":
|
||||
current = None
|
||||
return [entry for instance in instances for entry in _instance_entries(instance, running)]
|
||||
|
||||
|
||||
def _instance_entries(instance: _Instance, running: Optional[bool]) -> List[VirtualIpDict]:
|
||||
entries: List[VirtualIpDict] = []
|
||||
for vip in instance.vips:
|
||||
tokens = vip.split()
|
||||
parsed = _address(tokens[0]) if tokens else None
|
||||
if parsed is None:
|
||||
continue
|
||||
address, prefix = parsed
|
||||
dev = tokens[tokens.index("dev") + 1] if "dev" in tokens[1:-1] else None
|
||||
entries.append(
|
||||
{
|
||||
"mechanism": "keepalived",
|
||||
"instance": instance.name,
|
||||
"address": address,
|
||||
"prefix_length": prefix if prefix is not None else _host_prefix(address),
|
||||
"interface": dev or instance.interface,
|
||||
"group_key": f"vrid:{instance.vrid}" if instance.vrid.isdigit() else None,
|
||||
"running": running,
|
||||
}
|
||||
)
|
||||
return entries
|
||||
|
||||
|
||||
def _host_prefix(address: str) -> int:
|
||||
"""keepalived's default for an entry without one: the single address."""
|
||||
return ipaddress.ip_address(address).max_prefixlen
|
||||
|
||||
|
||||
_READERS: Dict[str, Tuple[str, Callable[[str, List[_Record]], List[VirtualIpDict]]]] = {
|
||||
"vip-manager": ("vip_manager", _vip_manager),
|
||||
"keepalived": ("keepalived", _keepalived),
|
||||
}
|
||||
|
||||
|
||||
def _order(entry: VirtualIpDict) -> Tuple[int, int, str]:
|
||||
address = ipaddress.ip_address(entry["address"])
|
||||
return address.version, int(address), entry["instance"]
|
||||
|
||||
|
||||
def parse_virtual_ips(output: str) -> VirtualIpsDict:
|
||||
"""Parse what :data:`VIRTUAL_IPS_COMMAND` printed, each mechanism's entries
|
||||
sorted by address.
|
||||
|
||||
:raises ValueError: when the output carries no intact report.
|
||||
"""
|
||||
sections = _sections(_frame(output))
|
||||
if any(mechanism == _NO_SYSTEMD for mechanism, _, _ in sections):
|
||||
return {"vip_manager": None, "keepalived": None}
|
||||
found: Dict[str, List[VirtualIpDict]] = {"vip_manager": [], "keepalived": []}
|
||||
blind: Set[str] = set()
|
||||
for mechanism, unit, records in sections:
|
||||
if mechanism not in _READERS:
|
||||
continue
|
||||
key, read = _READERS[mechanism]
|
||||
if any(kind in _BLIND for kind, _ in records):
|
||||
blind.add(key)
|
||||
else:
|
||||
found[key].extend(read(unit, records))
|
||||
def result(key: str) -> Optional[List[VirtualIpDict]]:
|
||||
return None if key in blind else sorted(found[key], key=_order)
|
||||
|
||||
return {"vip_manager": result("vip_manager"), "keepalived": result("keepalived")}
|
||||
|
||||
|
||||
class VirtualIpsMixin:
|
||||
"""Adds :meth:`get_virtual_ips` to a driver that can run a command on a Linux host.
|
||||
|
||||
The template form (README, "Function classes"): the command and its parse are
|
||||
the same everywhere, so they are concrete here, and a driver supplies only
|
||||
:meth:`_run_virtual_ips_command` -- how a command reaches its host, and how it
|
||||
gains root there. Mixed in by the drivers that can, not by
|
||||
:class:`~napalm_device_types.os.OSDriver`, so ``hasattr(driver,
|
||||
"get_virtual_ips")`` stays a truthful answer.
|
||||
"""
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover - declared for type checkers only
|
||||
|
||||
def _run_virtual_ips_command(self, command: str, *, privileged: bool) -> str:
|
||||
"""Run *command* on the host and return what it printed; as root
|
||||
when *privileged*. The command is a single ``sh -c`` invocation of
|
||||
about 4 kB: send it on an exec channel, not typed into a shell."""
|
||||
...
|
||||
|
||||
def get_virtual_ips(self) -> VirtualIpsDict:
|
||||
"""
|
||||
Returns the virtual IPs the host's vip-manager and keepalived configuration
|
||||
declares -- whether or not the host holds them at the moment.
|
||||
|
||||
* vip_manager (list or None) - see :class:`~napalm_device_types.models.VirtualIpDict`
|
||||
* keepalived (list or None)
|
||||
|
||||
None for a mechanism means it could not be read; ``[]`` that the host has none.
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
"vip_manager": [
|
||||
{"mechanism": "vip-manager", "instance": "vip-manager",
|
||||
"address": "10.7.224.10", "prefix_length": 24, "interface": "ens7",
|
||||
"group_key": "/service/netork-db/leader", "running": True},
|
||||
],
|
||||
"keepalived": [],
|
||||
}
|
||||
|
||||
:raises ValueError: if neither reading carried an intact report.
|
||||
"""
|
||||
command = f"sh -c {quote(VIRTUAL_IPS_COMMAND)}"
|
||||
try:
|
||||
return parse_virtual_ips(self._run_virtual_ips_command(command, privileged=True))
|
||||
except ValueError:
|
||||
pass
|
||||
return parse_virtual_ips(self._run_virtual_ips_command(command, privileged=False))
|
||||
Reference in New Issue
Block a user