FreeBSDDriver (`freebsd`) and OpenBSDDriver (`openbsd`) on a shared BsdDriver, for the BSD VMs netOrk provisions and BSD hosts added by hand (NetOrk/netork#798). - SSH over exec channels (napalm-device-types' run_on_transport): no PTY, stdout and stderr apart, real exit codes; run_command()/open_stream() are public. Root through `sudo -S` with the password on stdin, or `sudo -n` without one. - Facts: hostname, release and running kernel (freebsd-version / uname), hardware from SMBIOS (kenv) or hw.* sysctls, uptime from kern.boottime. - Interfaces and addresses (ifconfig -a: ether/lladdr, hex netmasks, scoped IPv6), routes (netstat -rn, OpenBSD's abbreviated networks written out, host/broadcast/loopback entries left out), ARP (FreeBSD's sentences, OpenBSD's table), users and groups, processes (lstart read as one five-word field), cron jobs (system crontab and the login user's). - Parsers are pure functions, tested on output recorded from FreeBSD 15.1 (hand-installed and cloud image) and OpenBSD 7.9 (NetOrk/netork#793), with internal addresses replaced by documentation ranges.
216 lines
7.6 KiB
Python
216 lines
7.6 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
|