Files
napalm-bsd/napalm_bsd/freebsd.py
T
christianmanivong e8468a292a
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 25s
CI / test (3.11) (pull_request) Successful in 24s
CI / test (3.12) (pull_request) Successful in 25s
feat: packages, services and updates
The write side NetOrk/netork#799 needs, on both drivers.

- Packages: get_packages (FreeBSD `pkg query` with the repository as
  source, nothing on a classic system without pkg; OpenBSD `pkg_info`),
  install_package / uninstall_package as root (`pkg install`/`pkg
  delete`, `pkg_add -I`/`pkg_delete`), names checked before anything is
  sent, a failure raised with what the tool printed.
- Updates: get_available_updates. FreeBSD `pkg upgrade -n`, with
  `security` from VuXML (`pkg audit`: True for a listed package, False for
  any other, None when the audit could not run); OpenBSD `pkg_add -u -n
  -v` (no security verdict). Base-system patches are one `base-system`
  entry, as decided in #799: OpenBSD's `syspatch -c`, and on a classic
  FreeBSD what `freebsd-update` has fetched; on pkgbase the base system is
  packages from FreeBSD-base.
- Services: get_services lists the enabled ones (FreeBSD `service ...
  status` as root, as root-only pidfiles hide daemons otherwise, without
  root when sudo refuses; OpenBSD `rcctl check`), manage_service runs
  start/stop/restart/enable/disable as root and returns success and
  output.

Fixtures recorded on the FreeBSD 15.1 and OpenBSD 7.9 VMs with genuinely
outdated packages (FreeBSD pointed at the latest branch, an OpenBSD
package taken back to its release build). Checked live on both, including
a service restart and installing and removing a package.
2026-10-08 09:59:04 +02:00

107 lines
4.4 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'"
INSTALL_COMMAND = "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}"
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."""
return self.run_command("pkg -N").exit_code == 0
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():
updates = parse.pkg_upgrades(self._out("pkg upgrade -n", privileged=True, timeout=300))
audit = self.run_command("pkg audit -Fq", privileged=True, timeout=120)
known = audit.exit_code in (0, 1) # 1: vulnerable packages found
vulnerable = parse.pkg_audit(audit.stdout) if known else set()
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.run_command(
"freebsd-update --not-running-from-cron updatesready", privileged=True, timeout=60
)
if ready.exit_code != 0:
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", "")