Files
napalm-bsd/tests/test_parse.py
T
christianmanivong 5f53de0c25
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 27s
CI / test (3.12) (push) Successful in 37s
CI / test (3.10) (pull_request) Successful in 42s
CI / test (3.11) (pull_request) Successful in 42s
CI / test (3.12) (pull_request) Successful in 38s
feat: report listening sockets
get_listening_sockets() in the shape of napalm-device-types'
ListeningSocketsMixin, whose ss/cgroup reading is Linux's, and with its
rule: read as root first, and without root when that brings nothing back,
which the reading reports as `attributed: False`.

- FreeBSD: `sockstat -46lq -P tcp,udp`, which sees every socket either way.
- OpenBSD: `fstat -n` as root (the sockets nothing is connected to; port 0
  is unbound), `netstat -an` without root, as fstat shows a user only
  their own processes; those sockets come without a process.
- Addresses as ss prints them: `*` becomes 0.0.0.0 or :: by family, IPv6
  without brackets, a zone (`fe80::1%lo0`) becomes the interface. One entry
  per socket, the first process holding it.

No get_kernel_facts on purpose: KernelFactsMixin reports a Linux kernel's
modules and CONFIG_ options, which a BSD kernel does not have.

Fixtures recorded on the FreeBSD 15.1 and OpenBSD 7.9 VMs with test
listeners on 127.0.0.1 and ::1 (NetOrk/netork#798).
2026-10-08 09:26:21 +02:00

272 lines
10 KiB
Python

"""Parsers for BSD command output, on output recorded from real systems.
``freebsd-host``: a hand-installed FreeBSD 15.1 (classic base, no pkg).
``freebsd-vm``: the FreeBSD 15.1 BASIC-CLOUDINIT image (pkgbase).
``openbsd-vm``: the OpenBSD 7.9 cloud image from hcartiaux/openbsd-cloud-image.
Recorded for NetOrk/netork#793; ``COMMANDS.txt`` in each directory lists them.
"""
from __future__ import annotations
from pathlib import Path
import pytest
from napalm_bsd import parse
FIXTURES = Path(__file__).parent / "fixtures"
def fixture(system: str, name: str) -> str:
return (FIXTURES / system / name).read_text()
class TestIfconfig:
def test_freebsd_ethernet(self):
em0 = parse.ifconfig(fixture("freebsd-host", "ifconfig_a.txt"))["em0"]
assert em0["mac"] == "bc:24:11:20:b0:20"
assert em0["mtu"] == 1500
assert em0["enabled"] and em0["up"]
assert em0["speed"] == 1000.0
assert em0["ipv4"] == {"192.0.2.104": 24}
assert em0["ipv6"] == {"fe80::be24:11ff:fe20:b020": 64}
def test_freebsd_loopback(self):
lo0 = parse.ifconfig(fixture("freebsd-host", "ifconfig_a.txt"))["lo0"]
assert lo0["loopback"]
assert lo0["ipv4"] == {"127.0.0.1": 8}
assert lo0["ipv6"] == {"::1": 128, "fe80::1": 64}
def test_freebsd_vm_reports_ten_gigabit(self):
vtnet0 = parse.ifconfig(fixture("freebsd-vm", "ifconfig_a.txt"))["vtnet0"]
assert vtnet0["speed"] == 10000.0
assert "10.0.2.15" in vtnet0["ipv4"]
def test_openbsd_calls_the_mac_lladdr(self):
interfaces = parse.ifconfig(fixture("openbsd-vm", "ifconfig_a.txt"))
assert list(interfaces) == ["lo0", "vio0", "enc0", "pflog0"]
assert interfaces["vio0"]["mac"] == "bc:24:11:aa:bb:04"
assert interfaces["vio0"]["ipv4"] == {"10.0.2.15": 24}
assert interfaces["vio0"]["up"]
def test_an_interface_without_flags_or_mtu_is_down(self):
enc0 = parse.ifconfig(fixture("openbsd-vm", "ifconfig_a.txt"))["enc0"]
assert not enc0["enabled"] and not enc0["up"]
assert enc0["mtu"] == 0
assert enc0["mac"] == ""
class TestRoutes:
def test_freebsd_default_and_connected(self):
routes = parse.routes(fixture("freebsd-host", "netstat_rn.txt"))
assert routes == [
{
"network": "0.0.0.0/0",
"next_hop": "192.0.2.1",
"interface": "em0",
"protocol": "static",
"family": "ipv4",
},
{
"network": "192.0.2.0/24",
"next_hop": "",
"interface": "em0",
"protocol": "connected",
"family": "ipv4",
},
{
"network": "fe80::/64",
"next_hop": "",
"interface": "em0",
"protocol": "connected",
"family": "ipv6",
},
]
def test_openbsd_abbreviated_networks_are_written_out(self):
routes = parse.routes(fixture("openbsd-vm", "netstat_rn.txt"))
networks = {r["network"]: r for r in routes}
assert networks["0.0.0.0/0"]["next_hop"] == "10.0.2.2"
assert networks["10.0.2.0/24"]["protocol"] == "connected"
assert networks["10.0.2.0/24"]["next_hop"] == ""
assert all(r["interface"] != "lo0" for r in routes)
def test_host_and_broadcast_entries_are_not_routes(self):
routes = parse.routes(fixture("openbsd-vm", "netstat_rn.txt"))
assert "10.0.2.15/32" not in {r["network"] for r in routes}
assert "10.0.2.255/32" not in {r["network"] for r in routes}
class TestArp:
def test_freebsd(self):
entries = parse.arp(fixture("freebsd-host", "arp_an.txt"))
assert entries[0] == {
"interface": "em0",
"mac": "bc:24:11:3c:e2:d0",
"ip": "192.0.2.254",
"age": 1176.0,
}
assert entries[2]["age"] == -1.0 # permanent: its own address
def test_openbsd_table(self):
entries = parse.arp(fixture("openbsd-vm", "arp_an.txt"))
assert entries[0] == {
"interface": "vio0",
"mac": "52:55:0a:00:02:02",
"ip": "10.0.2.2",
"age": 18 * 60 + 58.0,
}
assert entries[2]["age"] == -1.0
@pytest.mark.parametrize(
("text", "seconds"), [("18m58s", 1138.0), ("1h2m3s", 3723.0), ("45s", 45.0)]
)
def test_openbsd_durations(self, text, seconds):
assert parse.duration(text) == seconds
class TestUsers:
def test_passwd_with_supplementary_groups(self):
users = parse.users(
fixture("freebsd-host", "cat_etc_passwd.txt"),
fixture("freebsd-host", "cat_etc_group.txt"),
)
root = next(u for u in users if u["username"] == "root")
assert root == {
"username": "root",
"uid": 0,
"gid": 0,
"home": "/root",
"shell": "/bin/sh",
"groups": ["wheel", "operator"],
}
assert any(u["username"] == "netork" for u in users)
def test_comment_lines_are_skipped(self):
assert (
parse.users("# $FreeBSD$\n#\nroot:*:0:0:Charlie &:/root:/bin/sh\n", "")[0]["username"]
== "root"
)
class TestProcesses:
@pytest.mark.parametrize("system", ["freebsd-host", "freebsd-vm", "openbsd-vm"])
def test_the_start_date_is_kept_whole(self, system):
"""lstart is five words; napalm-linux cut it to the weekday (napalm-linux#21)."""
processes = parse.processes(
fixture(system, "ps_axww_o_user_pid_ppid_cpu_mem_vsz_rss_stat_lstart_command.txt")
)
init = next(p for p in processes if p["pid"] == 1)
assert init["command"] == "/sbin/init"
assert init["user"] == "root"
assert init["ppid"] == 0
assert len(init["started"].split()) == 5
def test_fields(self):
processes = parse.processes(
fixture("openbsd-vm", "ps_axww_o_user_pid_ppid_cpu_mem_vsz_rss_stat_lstart_command.txt")
)
assert processes[0] == {
"pid": 1,
"ppid": 0,
"user": "root",
"cpu": 0.0,
"memory": 0.0,
"vsz": 956,
"rss": 504,
"tty": "",
"state": "I",
"started": "Wed Oct 7 15:59:49 2026",
"command": "/sbin/init",
}
class TestCrontab:
def test_system_crontab_names_the_user(self):
jobs = parse.crontab(fixture("freebsd-host", "cat_etc_crontab.txt"), system=True)
assert {"user": "root", "schedule": "0 * * * *", "command": "newsyslog"} in jobs
assert {
"user": "operator",
"schedule": "*/11 * * * *",
"command": "/usr/libexec/save-entropy",
} in jobs
def test_user_crontab(self):
jobs = parse.crontab(fixture("openbsd-vm", "crontab_l.txt"), user="root")
assert jobs
assert all(j["user"] == "root" for j in jobs)
assert all(len(j["schedule"].split()) == 5 for j in jobs)
def test_special_schedules_and_environment_lines(self):
text = "SHELL=/bin/sh\n@reboot root /usr/local/bin/start\n# comment\n"
assert parse.crontab(text, system=True) == [
{"user": "root", "schedule": "@reboot", "command": "/usr/local/bin/start"}
]
class TestBoottime:
def test_freebsd(self):
assert parse.boottime(fixture("freebsd-host", "sysctl_n_kern.boottime.txt")) == 1791405216
def test_openbsd(self):
assert parse.boottime(fixture("openbsd-vm", "sysctl_n_kern.boottime.txt")) == 1791406789
def test_garbage(self):
assert parse.boottime("sysctl: unknown oid") is None
class TestListeningSockets:
"""In the shape of napalm-device-types' ListeningSocketDict: ``0.0.0.0`` and
``::`` are every address of their family, as ``ss`` prints them."""
@staticmethod
def _keys(sockets):
return {(s["proto"], s["address"], s["port"], s["interface"]) for s in sockets}
def test_freebsd_sockstat(self):
sockets = parse.sockstat(fixture("freebsd-vm", "sudo_sockstat_46lq_P_tcp_udp.txt"))
assert self._keys(sockets) == {
("udp", "127.0.0.1", 7779, None),
("tcp", "::1", 7777, None),
("tcp", "127.0.0.1", 7778, None),
("tcp", "::", 22, None),
("tcp", "0.0.0.0", 22, None),
("udp", "::", 514, None),
("udp", "0.0.0.0", 514, None),
}
sshd = next(s for s in sockets if s["port"] == 22 and s["address"] == "0.0.0.0")
assert (sshd["process"], sshd["pid"], sshd["unit"], sshd["container_id"]) == (
"sshd",
1282,
None,
None,
)
def test_openbsd_fstat_keeps_only_listening_and_bound_sockets(self):
sockets = parse.fstat_sockets(fixture("openbsd-vm", "sudo_fstat_n.txt"))
keys = self._keys(sockets)
assert ("tcp", "0.0.0.0", 22, None) in keys
assert ("tcp", "::", 22, None) in keys
assert ("tcp", "::1", 25, None) in keys
assert ("tcp", "fe80::1", 25, "lo0") in keys
assert ("udp", "10.0.2.15", 68, None) in keys
assert ("udp", "127.0.0.1", 7779, None) in keys
# connected (ntpd, ssh sessions) and unbound (*:0) sockets are not listening
assert not any(s["port"] in (0, 123) or s["process"] == "sshd-session" for s in sockets)
smtpd = next(s for s in sockets if s["port"] == 25 and s["address"] == "127.0.0.1")
assert (smtpd["process"], smtpd["pid"]) == ("smtpd", 69044)
def test_openbsd_netstat_without_root_names_no_process(self):
sockets = parse.netstat_listening(fixture("openbsd-vm", "netstat_an.txt"))
keys = self._keys(sockets)
assert ("tcp", "0.0.0.0", 22, None) in keys
assert ("tcp", "::", 22, None) in keys
assert ("tcp", "fe80::1", 25, "lo0") in keys
assert ("udp", "127.0.0.1", 7779, None) in keys
assert all(s["process"] is None and s["pid"] is None for s in sockets)
assert not any(s["port"] == 123 for s in sockets) # ntpd's connected sockets
def test_a_socket_held_by_two_processes_is_listed_once(self):
text = "www nginx 901 6 tcp4 *:80 *:*\nwww nginx 900 6 tcp4 *:80 *:*\n"
assert [(s["pid"]) for s in parse.sockstat(text)] == [901]