Files
christianmanivong aef58ca161
CI / test (3.10) (pull_request) Successful in 30s
CI / test (3.11) (pull_request) Successful in 32s
CI / test (3.12) (pull_request) Successful in 36s
CI / test (3.10) (push) Successful in 45s
CI / test (3.11) (push) Successful in 30s
CI / test (3.12) (push) Successful in 29s
feat: set up SNMP with net-snmp
run_device_action("fix_snmp") and get_snmp_config for both drivers, as
decided in NetOrk/netork#800: net-snmp from packages rather than the base
daemons, because it answers UCD-SNMP-MIB, which netOrk's health metrics
read.

- fix_snmp installs net-snmp, writes the configuration netOrk uses on
  Linux (v2c, community "public", agentAddress udp:161) through stdin as
  root, enables and restarts the agent (FreeBSD `service snmpd`, OpenBSD
  `rcctl ... netsnmpd`, as `snmpd` is OpenBSD's own daemon) and asks it
  for sysDescr. It stops at the first step that fails and says why.
- get_snmp_config reports a running net-snmp's community and port.
- FreeBSD's install_package bootstraps pkg on a classic system that never
  had it instead of waiting for an answer that never comes.

Checked live: FreeBSD from a bare system, OpenBSD again over an existing
setup. Memory, swap and load answer on both; FreeBSD's ssCpuIdle about a
minute after start; OpenBSD's CPU figures from net-snmp are wrong (#800).
2026-10-08 10:05:09 +02:00

111 lines
4.7 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."""
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", "")