CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 25s
CI / test (3.12) (push) Successful in 26s
CI / test (3.10) (pull_request) Successful in 25s
CI / test (3.11) (pull_request) Successful in 24s
CI / test (3.12) (pull_request) Successful in 26s
netOrk reads an empty update list as "no updates" and closes every patch clock on the host. A refused sudo, a failing pkg, syspatch or freebsd-update, or a pkg database pkg cannot read used to come back as that empty list. Each of these now raises. Only pkg's "is not installed" still means no packages. BsdDriver takes on napalm-device-types' HostStatusMixin. On FreeBSD it reports a host whose installed kernel differs from the running one as needing a reboot (device-types 4.1). OpenBSD stays unknown. Closes #4
127 lines
5.3 KiB
Python
127 lines
5.3 KiB
Python
"""FreeBSD: kenv for the hardware, freebsd-version for base and kernel."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from napalm_device_types import FingerprintRule
|
|
|
|
from napalm_bsd import parse
|
|
from napalm_bsd.base import BASE_SYSTEM, BsdDriver
|
|
|
|
|
|
class FreeBSDDriver(BsdDriver):
|
|
"""NAPALM driver for FreeBSD hosts, over SSH."""
|
|
|
|
TYPE_LABEL = "FreeBSD"
|
|
VENDOR = "FreeBSD"
|
|
DRIVER_NAME = "freebsd"
|
|
SNMP_FINGERPRINT = [FingerprintRule("freebsd", weight=5.0)]
|
|
# OpenSSH in FreeBSD's base names it in its banner ("OpenSSH_9.9 FreeBSD-20250219").
|
|
SSH_FINGERPRINT = [FingerprintRule("freebsd", weight=3.0)]
|
|
|
|
HOSTNAME_COMMAND = "sysctl -n kern.hostname"
|
|
# Userland and running kernel; both carry the patch level (15.1-RELEASE-p4).
|
|
OS_VERSION_COMMAND = "freebsd-version -u"
|
|
KERNEL_COMMAND = "freebsd-version -r"
|
|
# kenv takes one variable per call, and a missing one fails the call.
|
|
PLATFORM_COMMAND = (
|
|
"for k in maker product serial; do "
|
|
'printf "%s=%s\\n" "$k" "$(kenv -q smbios.system.$k)"; done'
|
|
)
|
|
# -W: FreeBSD cuts long destinations to the column width otherwise.
|
|
ROUTES_COMMAND = "netstat -rnW"
|
|
# sockstat names the process of every socket it can see; root sees them all.
|
|
LISTENING_COMMAND = "sockstat -46lq -P tcp,udp"
|
|
|
|
def _parse_listening(self, output: str, *, attributed: bool) -> list[dict]:
|
|
return parse.sockstat(output)
|
|
|
|
PKG_QUERY = "pkg query '%n\t%v\t%R\t%sb\t%c'"
|
|
# Bootstraps pkg on a classic system that never had it, instead of asking.
|
|
INSTALL_COMMAND = "env ASSUME_ALWAYS_YES=yes pkg install -y {name}"
|
|
UNINSTALL_COMMAND = "pkg delete -y {name}"
|
|
# Root reads every daemon's pidfile; one-shot scripts have no status.
|
|
SERVICE_STATUS_COMMAND = (
|
|
"for s in $(service -e); do n=${s##*/}; "
|
|
'printf "%s\\t%s\\n" "$n" "$(service $n status 2>&1 | head -1)"; done'
|
|
)
|
|
SERVICE_ACTION_COMMAND = "service {name} {action}"
|
|
SNMPD_CONF = "/usr/local/etc/snmp/snmpd.conf"
|
|
# The rc script drops to the snmpd user, so the file stays readable (644).
|
|
SNMPD_START = "sysrc snmpd_enable=YES && service snmpd restart"
|
|
|
|
def _parse_services(self, output: str) -> list[dict]:
|
|
return parse.service_status(output)
|
|
|
|
def _has_pkg(self) -> bool:
|
|
"""pkg is bootstrapped; a hand-installed classic system may have only the stub.
|
|
|
|
Only the stub's "not installed" means no pkg; any other failure (a
|
|
database pkg cannot read) raises rather than read as "no packages".
|
|
"""
|
|
result = self.run_command("pkg -N")
|
|
if result.exit_code == 0:
|
|
return True
|
|
message = (result.stderr or result.stdout).strip()
|
|
if "is not installed" in message:
|
|
return False
|
|
raise RuntimeError(f"pkg -N: {message or f'exit {result.exit_code}'}")
|
|
|
|
def get_packages(self) -> list[dict]:
|
|
return parse.pkg_query(self._out(self.PKG_QUERY)) if self._has_pkg() else []
|
|
|
|
def get_available_updates(self) -> list[dict]:
|
|
"""Package updates from ``pkg upgrade -n``, and fetched base-system updates.
|
|
|
|
``security`` comes from VuXML (``pkg audit``): True for a package it
|
|
lists as vulnerable, False for one it does not, None when the audit
|
|
could not run. On pkgbase the base system is packages from the
|
|
FreeBSD-base repository; a classic base reports one entry (#799).
|
|
"""
|
|
updates: list[dict] = []
|
|
if self._has_pkg():
|
|
upgrade = self._read("pkg upgrade -n", privileged=True, timeout=300)
|
|
updates = parse.pkg_upgrades(upgrade.stdout)
|
|
audit = self.run_command("pkg audit -Fq", privileged=True, timeout=120)
|
|
vulnerable = parse.pkg_audit(audit.stdout)
|
|
# 1 is "vulnerable packages found" and any error alike; only a list
|
|
# of packages makes it a verdict.
|
|
known = audit.exit_code == 0 or (audit.exit_code == 1 and bool(vulnerable))
|
|
for update in updates:
|
|
update["security"] = (update["name"] in vulnerable) if known else None
|
|
if "FreeBSD-base" in self._out("pkg repos -l").split():
|
|
return updates
|
|
return updates + self._base_updates()
|
|
|
|
def _base_updates(self) -> list[dict]:
|
|
"""A classic base system's updates, as freebsd-update fetched them.
|
|
|
|
``updatesready`` exits 0 when fetched updates wait to be installed and
|
|
2 when there are none; it does not fetch, which ``freebsd-update cron``
|
|
does daily where it is enabled.
|
|
"""
|
|
ready = self._read(
|
|
"freebsd-update --not-running-from-cron updatesready",
|
|
privileged=True,
|
|
timeout=60,
|
|
ok=(0, 2),
|
|
)
|
|
if ready.exit_code == 2:
|
|
return []
|
|
return [
|
|
{
|
|
"name": BASE_SYSTEM,
|
|
"current_version": self._out("freebsd-version -u"),
|
|
"new_version": "fetched by freebsd-update",
|
|
"origin": "freebsd-update",
|
|
"security": None,
|
|
}
|
|
]
|
|
|
|
def _os_version(self) -> str:
|
|
version = super()._os_version()
|
|
return f"FreeBSD {version}" if version else ""
|
|
|
|
def _hardware(self) -> tuple[str, str, str]:
|
|
smbios = self._platform()
|
|
return smbios.get("maker", ""), smbios.get("product", ""), smbios.get("serial", "")
|