Commit Graph
120 Commits
Author SHA1 Message Date
christianmanivong 22e4377c2b Merge pull request 'chore: adopt the NAPALM driver standard (pilot)' (#52) from chore/driver-standard into main
driver / lint (push) Successful in 11s
driver / test (3.11) (push) Successful in 48s
driver / test (3.13) (push) Successful in 56s
driver / lowest (push) Successful in 36s
driver / security (push) Successful in 55s
driver / build (push) Successful in 26s
driver / publish (push) Skipped
driver / standard (push) Successful in 13s
driver / types (push) Successful in 43s
driver / test (3.10) (push) Successful in 51s
driver / test (3.12) (push) Successful in 49s
CI / driver (push) Successful in 6m28s
v4.21.1
2026-10-22 22:08:14 +00:00
christianmanivong 49830671c9 chore(release): 4.21.1
driver / test (3.10) (pull_request) Successful in 3m3s
driver / test (3.11) (pull_request) Successful in 49s
driver / test (3.13) (pull_request) Successful in 4m52s
driver / lowest (pull_request) Successful in 26s
driver / security (pull_request) Successful in 3m10s
driver / build (pull_request) Successful in 4m38s
CI / driver (pull_request) Successful in 21m52s
driver / standard (pull_request) Successful in 36s
driver / lint (pull_request) Successful in 1m12s
driver / types (pull_request) Successful in 1m12s
driver / test (3.12) (pull_request) Successful in 1m53s
driver / publish (pull_request) Skipped
2026-10-22 21:12:28 +02:00
christianmanivong 0475184684 chore: adopt the NAPALM driver standard
driver / test (3.10) (pull_request) Successful in 1m43s
driver / test (3.11) (pull_request) Failing after 9m18s
driver / test (3.13) (pull_request) Successful in 1m4s
driver / lowest (pull_request) Failing after 1m38s
driver / security (pull_request) Failing after 4m33s
driver / build (pull_request) Failing after 1m29s
CI / driver (pull_request) Failing after 24m6s
driver / standard (pull_request) Successful in 54s
driver / lint (pull_request) Successful in 50s
driver / types (pull_request) Failing after 1m17s
driver / test (3.12) (pull_request) Failing after 1m20s
driver / publish (pull_request) Skipped
- CI moves to .gitea/workflows/ci.yml and calls the shared driver workflow
  of NAPALM/driver-standard: conformance, ruff, strict mypy, tests on
  3.10-3.13 and at the lowest declared versions, bandit, pip-audit, build,
  and on a release tag the upload to PyPI.
- tests/quality_baseline.json records today's long and complex functions
  and the one file over 1000 lines; they may shrink, not grow.
- Coverage floor 95 % (measured 95.15 %).
- CHANGELOG.md starts here; project links point at git.netork.io; the
  license is the SPDX string PEP 639 wants.
2026-10-22 19:06:19 +02:00
christianmanivong b7350a9b2c fix: what bandit and the remaining lint rules found
- Long docstring and command lines are wrapped; recorded device output in
  tests/ keeps its lines (E501 is off there).
- host_firewall: the negation token is the named constant _NEGATION. bandit
  read token == "!" as a hardcoded password.
- ssh_host_keys: _known_hosts_file takes the expected key as an argument
  instead of asserting it; both callers had already returned when it is None.
- ssh_server: the staged path is on the device, not a local temp file
  (nosec B108).
- tests: raise ... from exc, zip(strict=True).
2026-10-22 19:05:11 +02:00
christianmanivong 08296d0d2e style: ruff format and the lint rules of the driver standard
Mechanical: ruff format, and ruff check --fix with the rule set every NAPALM
driver uses (E, F, I, UP, B at line length 100). typing.List/Dict/Optional
become builtin generics and X | None, imports are sorted, the coding cookies
are gone. Two tests that read the source text follow the new spelling.

What ruff cannot fix comes in the next commit.
2026-10-22 19:03:21 +02:00
christianmanivong ac8e7c4c11 Merge pull request 'feat: HostFirewallMixin, a Linux host's own netfilter rules (NetOrk/netork#1344)' (#51) from feat/host-firewall-1344 into main
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 28s
CI / test (3.13) (push) Successful in 25s
CI / test (3.12) (push) Successful in 38s
2026-10-21 21:26:36 +00:00
christianmanivong f77091c489 feat: HostFirewallMixin, a Linux host's own netfilter rules (NetOrk/netork#1344)
CI / test (3.10) (push) Successful in 40s
CI / test (3.11) (push) Successful in 25s
CI / test (3.13) (push) Successful in 51s
CI / test (3.10) (pull_request) Successful in 24s
CI / test (3.12) (pull_request) Successful in 27s
CI / test (3.13) (pull_request) Successful in 25s
CI / test (3.12) (push) Successful in 25s
CI / test (3.11) (pull_request) Successful in 26s
get_host_firewall() reads `nft -j list ruleset` as root and returns every
chain with its family, table, hook, priority and policy, and every rule as
matches and a verdict. Rules iptables-nft wrote show their extensions in nft's
JSON only as an opaque `xt`, so the tables iptables manages are taken from
iptables-save instead; a legacy iptables is kept beside nft's tables, since
both apply. A condition the parse does not model is an `unknown` match with
its text, never left out. Without root neither tool can be read, and
HostFirewallUnavailable is raised rather than a host without rules.

A driver supplies only _run_host_firewall_command(command, privileged=).
Version 4.21.0.
2026-10-21 23:10:44 +02:00
christianmanivong be3958e561 Merge pull request 'feat: ServicePackagesMixin, which packages a systemd service is (NetOrk/netork#1236)' (#50) from feat/service-packages-1236 into main
CI / test (3.10) (push) Successful in 31s
CI / test (3.11) (push) Successful in 28s
CI / test (3.13) (push) Successful in 36s
CI / test (3.12) (push) Successful in 36s
2026-10-19 16:56:15 +00:00
christianmanivong a21e365c4e fix: annotate the unit path set so mypy accepts service_packages (NetOrk/netork#1236)
CI / test (3.10) (push) Successful in 44s
CI / test (3.11) (push) Successful in 33s
CI / test (3.13) (push) Successful in 30s
CI / test (3.10) (pull_request) Successful in 42s
CI / test (3.12) (pull_request) Successful in 38s
CI / test (3.13) (pull_request) Successful in 35s
CI / test (3.12) (push) Successful in 32s
CI / test (3.11) (pull_request) Successful in 43s
2026-10-19 18:50:41 +02:00
christianmanivong cce26eff44 feat: ServicePackagesMixin, which packages a systemd service is (NetOrk/netork#1236)
CI / test (3.10) (push) Failing after 17s
CI / test (3.11) (push) Failing after 14s
CI / test (3.13) (push) Failing after 16s
CI / test (3.10) (pull_request) Failing after 17s
CI / test (3.12) (pull_request) Failing after 20s
CI / test (3.13) (pull_request) Failing after 26s
CI / test (3.12) (push) Failing after 15s
CI / test (3.11) (pull_request) Failing after 15s
get_service_packages() returns, per systemd service, the installed packages
it is made of: the one shipping its unit file and the one shipping the
binary its ExecStart runs -- not the libraries that binary links against.
One read-only command: systemctl show for every loaded service unit, then
one dpkg -S for all paths (each also across the /usr merge), or rpm -qf per
path. The report is framed; one cut short raises. A driver supplies
_run_service_packages_command, as for KernelFactsMixin.

Version 4.20.0.
2026-10-19 17:38:57 +02:00
christianmanivong 888a11ca6a Merge pull request 'feat: ssh_host_keys.HostKeyCheck, trust on first use (NetOrk/netork#1167)' (#49) from fix/ssh-host-key-tofu-1167 into main
CI / test (3.10) (push) Successful in 35s
CI / test (3.11) (push) Successful in 41s
CI / test (3.13) (push) Successful in 29s
CI / test (3.12) (push) Successful in 32s
2026-10-18 19:23:26 +00:00
christianmanivong 94bf399d34 feat: ssh_host_keys.HostKeyCheck, trust on first use (NetOrk/netork#1167)
CI / test (3.10) (push) Successful in 35s
CI / test (3.11) (push) Successful in 41s
CI / test (3.13) (push) Successful in 33s
CI / test (3.10) (pull_request) Successful in 29s
CI / test (3.12) (pull_request) Successful in 33s
CI / test (3.13) (pull_request) Successful in 40s
CI / test (3.12) (push) Successful in 38s
CI / test (3.11) (pull_request) Successful in 28s
Drivers get the host keys to expect in optional_args["ssh_host_keys"] (by port)
and report what they saw through optional_args["ssh_host_key_observer"].
With a known key, any other key is refused before authentication
(SshHostKeyMismatch); without one, the first key is accepted. One
implementation for paramiko (HostKeyCheck.connect) and netmiko
(HostKeyCheck.netmiko + observe). A caller or driver that ignores both keys
keeps working as before.

Version 4.19.0.
2026-10-18 21:23:25 +02:00
christianmanivong bb2229fd9e Merge pull request 'chore: release the service clusters as 4.18.0' (#48) from chore/4.18.0 into main
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 25s
CI / test (3.13) (push) Successful in 27s
CI / test (3.12) (push) Successful in 26s
2026-10-18 13:42:42 +00:00
christianmanivong 680500dd2e chore: release the service clusters as 4.18.0
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 25s
CI / test (3.13) (push) Successful in 27s
CI / test (3.10) (pull_request) Successful in 26s
CI / test (3.12) (pull_request) Successful in 27s
CI / test (3.13) (pull_request) Successful in 26s
CI / test (3.12) (push) Successful in 27s
CI / test (3.11) (pull_request) Successful in 25s
feat/service-clusters (#47) and feat/opnsense-public-contracts (#46) both
called themselves 4.17.0 and were merged one after the other: two trees
under one version. 4.17.0 stays the public contracts (83cd7db, pinned by
netOrk for #1113); the service clusters on top of it are 4.18.0.
2026-10-18 15:38:29 +02:00
christianmanivong a0e70d1686 Merge pull request 'feat: the configuration that says which services run together (4.17.0)' (#47) from feat/service-clusters into main
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 24s
CI / test (3.13) (push) Successful in 26s
CI / test (3.12) (push) Successful in 27s
2026-10-18 13:32:33 +00:00
christianmanivong 2bd7bcc77c feat: the configuration that says which services run together (4.17.0)
CI / test (3.10) (push) Successful in 29s
CI / test (3.11) (push) Successful in 24s
CI / test (3.13) (push) Successful in 27s
CI / test (3.10) (pull_request) Successful in 26s
CI / test (3.12) (pull_request) Successful in 27s
CI / test (3.13) (pull_request) Successful in 26s
CI / test (3.12) (push) Successful in 27s
CI / test (3.11) (pull_request) Successful in 25s
ServiceClustersMixin.get_service_clusters() -> ServiceClustersDict
{patroni, adguardhome_sync, bunkerweb}: per mechanism the clusters the
host's configuration says it belongs to, each with its members (address,
port, name, role as the mechanism words it, this_host, config_from) and the
unit or container it was read from (source).

- Patroni: scope, name and restapi.connect_address from its YAML, then the
  members and roles /cluster lists from the DCS.
- adguardhome-sync: ORIGIN_URL and REPLICA*_URL; the origin pushes to each
  replica.
- BunkerWeb: a worker's API_WHITELIST_IP names the manager it admits, a
  manager's BUNKERWEB_INSTANCES its workers.

Two hosts answering at one virtual IP prove nothing; this is the evidence
that they run one service (NetOrk/netork#1125). One generic command for
every Linux host, filtered on the host like the virtual IPs' one:
allowlisted fields only, URLs without user:password@, DCS endpoints stay
there. Root first, then without it; None per mechanism is "did not look".
2026-10-18 15:28:34 +02:00
christianmanivong 83cd7db761 Merge pull request 'feat: public contracts for update progress, DNS forwards and OSPF (4.17.0)' (#46) from feat/opnsense-public-contracts into main
CI / test (3.10) (push) Successful in 25s
CI / test (3.11) (push) Successful in 25s
CI / test (3.13) (push) Successful in 28s
CI / test (3.12) (push) Successful in 28s
2026-10-18 13:06:07 +00:00
Christian Manivong 1dd448e1d3 feat: public contracts for update progress, DNS forwards and OSPF (4.17.0)
CI / test (3.10) (push) Successful in 35s
CI / test (3.11) (push) Successful in 32s
CI / test (3.12) (push) Successful in 30s
CI / test (3.13) (push) Successful in 27s
CI / test (3.10) (pull_request) Successful in 27s
CI / test (3.12) (pull_request) Successful in 26s
CI / test (3.13) (pull_request) Successful in 26s
CI / test (3.11) (pull_request) Successful in 25s
An application followed a firewall's upgrade, edited its resolver's
forwards and read its OSPF state through the driver's private REST
helpers (NetOrk/netork#1113). Declared contracts, neutral shapes:

- UpdateMixin.get_update_progress() -> UpdateProgressDict {state, log}
  for an update apply_updates only starts.
- DnsForwardMixin: get_dns_forwards(), set_dns_forward(domain, server,
  port), remove_dns_forward(domain); the driver applies.
- OspfMixin.get_ospf() -> OspfDict {router_id, areas, interfaces}; the
  routing daemon's format is the driver's to translate.

All declared only.
2026-10-18 15:02:15 +02:00
christianmanivong 753ddc5eda Merge pull request 'feat: the firmware a vendor offers, as a fact (4.16.0)' (#45) from feat/firmware-offer into main
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 25s
CI / test (3.13) (push) Successful in 26s
CI / test (3.12) (push) Successful in 26s
2026-10-17 11:04:22 +00:00
christianmanivong 59fb1273e8 feat: the firmware a vendor offers, as a fact (4.16.0)
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 24s
CI / test (3.13) (push) Successful in 26s
CI / test (3.10) (pull_request) Successful in 25s
CI / test (3.12) (pull_request) Successful in 27s
CI / test (3.13) (pull_request) Successful in 25s
CI / test (3.12) (push) Successful in 27s
CI / test (3.11) (pull_request) Successful in 24s
FirmwareOfferMixin.get_firmware_offer() -> FirmwareOfferDict
{current_version, latest_version, newer, download_url?}: what runs and
the newest firmware the vendor offers, from its download catalogue or
the device's own update check. Comparing vendor version strings stays
with the driver, so it says whether the offer is newer; whether that is a
warning is the application's call (NetOrk/netork#1111). Declared only.
2026-10-17 13:00:43 +02:00
christianmanivong 4641063db6 Merge pull request 'feat: a missing license can say what the device withholds without it (4.15.0)' (#44) from feat/license-withholds into main
CI / test (3.10) (push) Successful in 29s
CI / test (3.11) (push) Successful in 27s
CI / test (3.13) (push) Successful in 37s
CI / test (3.12) (push) Successful in 32s
2026-10-17 09:41:28 +00:00
christianmanivong 634c142477 test: read the withholds hint the way Python 3.10 reports it
CI / test (3.10) (push) Successful in 35s
CI / test (3.11) (push) Successful in 34s
CI / test (3.13) (push) Successful in 34s
CI / test (3.10) (pull_request) Successful in 33s
CI / test (3.12) (pull_request) Successful in 31s
CI / test (3.13) (pull_request) Successful in 32s
CI / test (3.12) (push) Successful in 36s
CI / test (3.11) (pull_request) Successful in 30s
2026-10-17 11:36:32 +02:00
christianmanivong 2d1c40c87d feat: a missing license can say what the device withholds without it (4.15.0)
CI / test (3.10) (push) Failing after 19s
CI / test (3.11) (push) Successful in 25s
CI / test (3.13) (push) Successful in 28s
CI / test (3.10) (pull_request) Failing after 20s
CI / test (3.12) (pull_request) Successful in 30s
CI / test (3.13) (pull_request) Successful in 32s
CI / test (3.12) (push) Successful in 29s
CI / test (3.11) (pull_request) Successful in 26s
LicenseDict gets withholds: NotRequired[List[str]]: what a device keeps back
for want of a license while the rest of it answers. A Supermicro X10 with
only SFT-OOB-LIC answers Redfish but withholds its drives and firmware
inventory until DCMS is activated. That makes it partly licensed, not
unlicensed, and the caller can tell the two apart (NetOrk/netork#1111).
2026-10-17 11:32:10 +02:00
christianmanivong 87c9b06bf3 Merge pull request 'feat: licenses, a hypervisor's hosts and the package proxy as facts (4.14.0)' (#43) from feat/fact-getters-1111 into main
CI / test (3.10) (push) Successful in 32s
CI / test (3.11) (push) Successful in 26s
CI / test (3.13) (push) Successful in 35s
CI / test (3.12) (push) Successful in 36s
2026-10-17 07:19:33 +00:00
christianmanivong 2805f911c4 feat: licenses, a hypervisor's hosts and the package proxy as facts (4.14.0)
CI / test (3.10) (push) Successful in 32s
CI / test (3.11) (push) Successful in 32s
CI / test (3.12) (push) Successful in 35s
CI / test (3.13) (push) Successful in 35s
CI / test (3.10) (pull_request) Successful in 31s
CI / test (3.12) (pull_request) Successful in 30s
CI / test (3.13) (pull_request) Successful in 29s
CI / test (3.11) (pull_request) Successful in 27s
Drivers report facts and the application decides what is a warning
(docs/STRUCTURE.md, NetOrk/netork#1111). Three facts drivers so far
only reported as warning codes get contracts:

- LicenseMixin.get_licenses(): licenses and subscriptions with their
  status (a Proxmox subscription, a VMware edition, a BMC license a
  feature needs).
- HypervisorDriver.get_hypervisor_hosts(): each host connected, in
  maintenance, with configuration issues, under a central manager.
- PackageProxyMixin.get_package_proxy(): the proxy apt fetches through.

All declared only.
2026-10-17 09:12:20 +02:00
christianmanivong 6551008ad3 Merge pull request 'feat: fix routines as templates with the caller's values; deprecate run_device_action (4.13.0)' (#41) from feat/fix-templates-33 into main
CI / test (3.10) (push) Successful in 29s
CI / test (3.11) (push) Successful in 32s
CI / test (3.13) (push) Successful in 34s
CI / test (3.12) (push) Successful in 36s
2026-10-16 22:43:22 +00:00
christianmanivong 321537ff0a Merge pull request 'feat: SnmpHealthProfile, the OIDs a driver declares; deprecate HealthMetricsMixin (4.12.0)' (#40) from feat/snmp-health-profile-32 into main
CI / test (3.10) (push) Successful in 35s
CI / test (3.11) (push) Successful in 33s
CI / test (3.13) (push) Successful in 33s
CI / test (3.12) (push) Successful in 34s
2026-10-16 22:43:22 +00:00
christianmanivong 9324a5b8f1 feat: fix routines as templates with the caller's values; deprecate run_device_action (4.13.0)
CI / test (3.10) (push) Successful in 34s
CI / test (3.11) (push) Successful in 31s
CI / test (3.13) (push) Successful in 27s
CI / test (3.10) (pull_request) Successful in 27s
CI / test (3.12) (pull_request) Successful in 28s
CI / test (3.13) (pull_request) Successful in 27s
CI / test (3.12) (push) Successful in 31s
CI / test (3.11) (pull_request) Successful in 28s
run_device_action was a string dispatch drivers filled with an
application's remediations, values hard-coded inside (community
"public", the caller's address, a /24 firewall rule).

napalm_device_types.fixes declares them as templates instead. A
template runs declared steps in order and stops at the first failure
(run_fix_steps); the driver implements the steps, or overrides the
whole method where its device takes the change in one call; every
value comes from the caller.

- SnmpAgentMixin.configure_snmp_agent(settings): install, configure,
  firewall, start. The caller verifies with its own SNMP client.
  get_management_peer() (optional) says the address the device sees the
  management connection come from, for choosing allowed_sources.
- EnsurePackageMixin.ensure_package(name): install only what is missing.
- PackageProxyMixin.configure_package_proxy(url): declared.
- ContainerEngineMixin.grant_container_engine_access(user): declared.

None is mixed into a role base, so a capability check stays truthful.

Closes #33
2026-10-16 15:29:42 +02:00
christianmanivong b4c4ec0b0b feat: SnmpHealthProfile, the OIDs a driver declares; deprecate HealthMetricsMixin (4.12.0)
CI / test (3.10) (push) Successful in 28s
CI / test (3.11) (push) Successful in 27s
CI / test (3.13) (push) Successful in 30s
CI / test (3.10) (pull_request) Successful in 29s
CI / test (3.12) (pull_request) Successful in 35s
CI / test (3.13) (pull_request) Successful in 33s
CI / test (3.12) (push) Successful in 29s
CI / test (3.11) (pull_request) Successful in 26s
Drivers no longer collect over SNMP (docs/STRUCTURE.md). A driver whose
device publishes its health values under OIDs of its own declares them
as SNMP_HEALTH_PROFILE: per value either scalars asked in order or a
table folded by first, average or max, optionally with a pattern for
text answers; memory as total with used or free, or a percentage, in
KiB or bytes; and how to read the interfaces. The application polls
and computes.

HealthMetricsMixin.get_health_metrics now warns that it is deprecated;
it goes in 5.0.

Closes #32
2026-10-16 15:29:27 +02:00
christianmanivong 07b50f6a48 Merge pull request 'feat: an HA pair's DHCP peers and config sync as facts (4.11.0)' (#42) from feat/dhcp-ha-config-sync into main
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 23s
CI / test (3.12) (push) Successful in 26s
CI / test (3.13) (push) Successful in 26s
2026-10-16 13:22:28 +00:00
christianmanivong 90367c1370 feat: an HA pair's DHCP peers and config sync as facts (4.11.0)
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 24s
CI / test (3.13) (push) Successful in 26s
CI / test (3.10) (pull_request) Successful in 26s
CI / test (3.12) (pull_request) Successful in 27s
CI / test (3.13) (pull_request) Successful in 25s
CI / test (3.12) (push) Successful in 27s
CI / test (3.11) (pull_request) Successful in 24s
get_dhcp_ha (declared on DhcpServerMixin, optional) is what a DHCP
server's own HA configuration says: on or off, this server's peer name,
each peer's name, role as the server words it, and HA URL.
ConfigSyncMixin.get_config_sync is where a host pushes its configuration
and for which services, by the names get_services reports; sync items
without a service name stay visible under `unmapped`. A driver mixes it
in, no role promises it. Both raise when they could not read.

What the facts mean for a pair -- hot standby, which side to write to --
is the caller's (NetOrk/netork#1125).
2026-10-16 15:18:25 +02:00
christianmanivong 2739f10ecd Merge pull request 'fix: keep CARP state knowledge out of the base package (4.10.1)' (#39) from fix/carp-helper-to-driver into main
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 24s
CI / test (3.12) (push) Successful in 25s
CI / test (3.13) (push) Successful in 25s
2026-10-16 09:19:00 +00:00
christianmanivong 0330f17a67 fix: keep CARP state knowledge out of the base package (4.10.1)
CI / test (3.10) (push) Successful in 25s
CI / test (3.11) (push) Successful in 24s
CI / test (3.12) (push) Successful in 26s
CI / test (3.13) (push) Successful in 25s
CI / test (3.10) (pull_request) Successful in 26s
CI / test (3.12) (pull_request) Successful in 26s
CI / test (3.13) (pull_request) Successful in 26s
CI / test (3.11) (pull_request) Successful in 24s
carp_virtual_ip() and its state table are CARP protocol knowledge; under the
agreed structure (PR #37, docs/STRUCTURE.md) the base package carries only
contracts, data shapes and I/O-free reconcilers. They move to the driver that
speaks CARP (napalm-opnsense). The contract stays: VirtualIpsDict.carp and
VirtualIpDict.holds.

Nothing used the helper yet; it was released in 4.10.0 an hour ago.

netOrk #1121.
2026-10-16 11:11:16 +02:00
christianmanivong d3034bc8ea Merge pull request 'feat: CARP addresses as virtual IPs, with their holder in the reading (4.10.0)' (#38) from feat/carp-virtual-ips into main
CI / test (3.10) (push) Successful in 25s
CI / test (3.11) (push) Successful in 24s
CI / test (3.13) (push) Successful in 26s
CI / test (3.12) (push) Successful in 26s
2026-10-16 09:10:36 +00:00
christianmanivong 4f3b88db0e feat: CARP addresses as virtual IPs, with their holder in the reading (4.10.0)
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 24s
CI / test (3.13) (push) Successful in 25s
CI / test (3.10) (pull_request) Successful in 26s
CI / test (3.12) (pull_request) Successful in 26s
CI / test (3.13) (pull_request) Successful in 26s
CI / test (3.12) (push) Successful in 27s
CI / test (3.11) (pull_request) Successful in 24s
A firewall pair's CARP addresses are a third virtual IP mechanism. Unlike
vip-manager and keepalived, a CARP reading says who holds the address: a
firewall's interface list need not show it, but CARP's state names the MASTER.

- VirtualIpsDict gains carp (NotRequired), VirtualIpDict gains holds
  (NotRequired; None leaves the holder to the interfaces).
- carp_virtual_ip() shapes one address bound to a VHID: MASTER holds, BACKUP
  stands by, INIT and DISABLED neither run nor hold, an unknown state says
  nothing. The same for OPNsense and pfSense; a driver supplies the fields.

netOrk #1121.
2026-10-16 11:06:43 +02:00
christianmanivong fde6d717a8 feat: SystemIdentityMixin reads a host's SMBIOS identity (4.9.0)
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 25s
CI / test (3.13) (push) Successful in 27s
CI / test (3.12) (push) Successful in 27s
What ties a server to its BMC: the BMC reports the server's serial
number and system UUID, the host reads the same values from its DMI
tables. Both drivers that read DMI (napalm-linux, napalm-proxmox) had a
copy of the shell for it, and neither read the board serial.

SystemIdentityMixin.get_system_identity() reads product_uuid,
product_serial and board_serial in one framed command and returns them
with vendor placeholders empty: a Supermicro board's product serial is
0123456789 on every one of them, others read "To be filled by O.E.M."
or "Default string", and many cheap boards share the UUID
03000200-0400-0500-0006-000700080009. is_placeholder_identity() is the
same filter for a BMC driver's side. A driver supplies only
_run_system_identity_command(command, privileged=); the files are
root's.
2026-10-13 12:20:10 +02:00
christianmanivong a1da0337e2 feat: alternative fingerprint rules count once (4.8.0)
CI / test (3.10) (push) Successful in 28s
CI / test (3.11) (push) Successful in 25s
CI / test (3.13) (push) Successful in 26s
CI / test (3.12) (push) Successful in 27s
Discovery compares drivers by score divided by the most a driver could
score, and that maximum summed every positive rule. Rules that never
match together, such as one vendor's landing page or another's, made a
driver that knows several vendors lose to one that knows none:
napalm-redfish's Supermicro landing page scored 6 of 41 and lost to a
bare "linux" in the same BMC's SNMP sysDescr, 5 of 18.

FingerprintRule gets an optional group. rules_max_weight() sums the
positive rules and counts each group once, with its strongest rule.
Existing rules have no group and count as before.
2026-10-13 10:19:54 +02:00
christianmanivong 7afa590c0c fix: ship py.typed so type checkers read the package's types
CI / test (3.10) (push) Successful in 27s
CI / test (3.11) (push) Successful in 26s
CI / test (3.13) (push) Successful in 1m16s
CI / test (3.12) (push) Successful in 26s
The package declares "Typing :: Typed" but shipped no PEP 561 marker, so
mypy in driver repositories treated it as untyped. Add the marker, include
it as package data, and release 4.7.1.
2026-10-13 07:20:02 +02:00
christianmanivong 155b5bbbe4 feat!: settle the open review questions; release 4.7.0
## feat!: settle the open review questions on roles, diffs, SSH and SNMP

- roles: a driver that sets ROLE itself replaces the role it inherits,
  so a Windows driver on OSDriver is "windows", not also "linux".
- os: OSDriver's account listing is get_os_users; NAPALM's get_users has
  another shape and a runtime stub, so hasattr could not tell.
- README: NAPALM's own getters must come from the driver or a mixin
  listed before the first role base; its stubs win the MRO otherwise.
- firewall/DHCP diffs: an empty key, a key given twice, or one more than
  one live entry holds raises ValueError instead of overwriting by guess;
  live entries without a key are never matched.
- ssh_server: only host key algorithms the host has a key for count as
  staying; when the keys cannot be read, the host key list is not
  touched. sshd_config and its copy keep their file mode (GNU and BSD
  stat), 644 only for a new file.
- SNMP: 64-bit ifXTable octets and ifHighSpeed, with the 32-bit counters
  as fallback for agents without ifXTable.
- pyproject: declare typing-extensions, which models.py imports.

BREAKING CHANGE: OSDriver.get_users is now get_os_users; diffs raise
ValueError on keys that cannot name one entry; plan_ssh_hardening
removes no host key algorithm without host_keys.

## fix: tell "could not read" and "does not install" apart in update and VIP readers

- package_updates: the dnf security reader records dnf's exit status and
  raises when it is missing or non-zero, as the apt reader does; a failed
  dnf no longer reads as "no security advisories".
- host_status: an enabled dnf-automatic.timer counts as auto updates only
  with apply_updates = yes (dnf5's automatic.conf wins over dnf4's);
  dnf-automatic-install.timer always does. Unattended-upgrades counts only
  while the program is installed, not from a conffile left behind.
- virtual_ips: a host with systemctl but without systemd as PID 1 reads as
  "did not look" (None), not as "has none". keepalived blocks whose brace
  is on the next line are read.

BREAKING CHANGE: parse_dnf_security expects DNF_SECURITY_COMMAND's
__DNF_RC marker and raises ValueError without it.
2026-10-13 06:58:40 +02:00
christianmanivong e509445572 feat: add ManagementControllerDriver for server BMCs
A role base for baseboard management controllers (Supermicro, Dell
iDRAC, HPE iLO, Lenovo XCC), reached over Redfish or IPMI, not SSH.

It declares, under TYPE_CHECKING only: get_power_state, set_power_state
(Redfish ResetType actions), get_hardware_inventory (CPUs, DIMMs,
drives, PSUs, NICs, and the server's serial, system UUID and NIC MACs),
get_event_log, get_firmware_inventory and get_management_protocols.

reboot_host restarts the BMC itself (Manager.Reset), never the managed
server, which goes through set_power_state. No HealthMetricsMixin and no
command channel; sensors come through NAPALM's get_environment.
2026-10-13 06:47:19 +02:00
christianmanivong dd8977750d chore: Python 3.13, mypy --strict in CI, eight bug fixes, docs
Squashes three changes:

## chore: support Python 3.13 and make mypy --strict pass in CI

Add 3.13 to the CI matrix and classifiers, run mypy in CI, fix the
14 strict errors it reported, and correct the README's minimum Python.

## fix: eight bugs found in a full review, each with a regression test

- channel: run_on_transport lost output sshd sent after the exit status;
  it now reads stdout and stderr to EOF before returning.
- ssh_server: a failed way back (restore or reload) raised out of
  harden_ssh_server after the host was touched; it is now reported in
  the result, with rolled_back False.
- host_status: a newer RPM kernel (RHEL, Fedora) was never seen, because
  its release, not its arch, followed the last "-".
- package_updates: an apt line wrapped at a blank lost the blank, and
  with it the package.
- kernel: an empty payload (gzip or base64 missing) read as a report.
- listening: "[fe80::1]%eth0:546", as ss prints it, kept its brackets.
- ping_sweep: a reply whose "success" was not a dict aborted the sweep.
- lag: a member of unknown speed (-1) lowered the LAG's speed.

## docs: bring the README and docstrings in line with the code

The firewall worked example showed the runtime stubs the README forbids;
the class table missed PhoneDriver and MediaDriver; ConfigLifecycleMixin
is mixed in by drivers, not by a role base; services, mac_acl and
packages named a class, a key and an exception that do not exist.
2026-10-13 01:22:22 +02:00
christianmanivong e58b65a3db Merge pull request 'fix: never let root read the SSH configuration from stdin' (#30) from fix/ssh-write-without-sudo-stdin into main
CI / test (3.10) (push) Successful in 25s
CI / test (3.11) (push) Successful in 25s
CI / test (3.12) (push) Successful in 25s
2026-10-11 21:50:08 +00:00
christianmanivong 7ec427c743 fix: never let root read the SSH configuration from stdin
CI / test (3.10) (push) Successful in 23s
CI / test (3.11) (push) Successful in 22s
CI / test (3.10) (pull_request) Successful in 23s
CI / test (3.12) (pull_request) Successful in 24s
CI / test (3.12) (push) Successful in 24s
CI / test (3.11) (pull_request) Successful in 25s
A driver gains root with sudo -S and puts the password in front of stdin.
A sudo that does not prompt -- NOPASSWD, a cached timestamp -- leaves it
there, and the cat under sudo wrote it into the drop-in as its first line.
Seen on the first real run (NetOrk/netork#1027): sshd -t rejected the file
and the rollback removed it, but the password was in a 0644 file for a
moment and in the error the caller showed.

The content now goes without privilege to a mktemp file only the login user
can read; root installs that file (mode 644, root-owned) and removes it.
The fake host in the tests prepends the password to every privileged stdin,
as a non-prompting sudo does.

Bumps to 4.5.1. NetOrk/netork#1027, NetOrk/netork#1029
2026-10-11 23:48:51 +02:00
christianmanivong d43e15e8db Merge pull request 'feat: OpenSshServerMixin takes flagged algorithms out of what sshd offers' (#29) from feat/ssh-algorithm-fix into main
CI / test (3.10) (push) Successful in 23s
CI / test (3.11) (push) Successful in 22s
CI / test (3.12) (push) Successful in 23s
2026-10-11 21:14:06 +00:00
christianmanivong 23c352aa8a feat: OpenSshServerMixin takes flagged algorithms out of what sshd offers
CI / test (3.10) (push) Successful in 23s
CI / test (3.11) (push) Successful in 22s
CI / test (3.12) (push) Successful in 23s
CI / test (3.10) (pull_request) Successful in 23s
CI / test (3.12) (pull_request) Successful in 23s
CI / test (3.11) (pull_request) Successful in 22s
ssh-audit names algorithms an SSH server should stop offering. Taking them
out is the same on every OpenSSH host, so it lives here once: a driver
mixes in OpenSshServerMixin and supplies run_command.

harden_ssh_server reads the lists sshd really uses (sshd -T), removes the
requested algorithms and writes the rest to
/etc/ssh/sshd_config.d/00-netork-ssh-hardening.conf -- or, where
sshd_config has no Include of that directory, rewrites sshd_config after
a copy. It never removes the last algorithm of a list, nor, given the
managing client's lists, the last one that client speaks. sshd -t must
accept the result and sshd -T must show it; after the reload the caller's
verify opens a new session, and if that fails the still-open one puts the
previous state back and reloads again.

Bumps to 4.5.0. NetOrk/netork#1027
2026-10-11 23:12:28 +02:00
christianmanivong a710a85de6 Merge pull request 'feat: report how full /, /boot and /var are in the host status' (#28) from fix/volume-almost-full-862 into main
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 29s
CI / test (3.12) (push) Successful in 31s
2026-10-11 08:33:03 +00:00
christianmanivong c947250179 feat: report how full /, /boot and /var are in the host status
CI / test (3.10) (push) Successful in 26s
CI / test (3.11) (push) Successful in 23s
CI / test (3.10) (pull_request) Successful in 23s
CI / test (3.12) (pull_request) Successful in 25s
CI / test (3.12) (push) Successful in 23s
CI / test (3.11) (pull_request) Successful in 26s
A Proxmox host's root filesystem stood at 94 % with about 400 MB free for a
week; a kernel upgrade failed on it with "No space left on device" and left
dpkg halfway, and nothing had said the disk was full (NetOrk/netork#862).

HOST_STATUS_COMMAND now also runs POSIX `df -Pk / /boot /var`, without root.
get_host_status() returns `filesystems`: each filesystem once (a directory
that is not a mount of its own is the filesystem it lives on), with its
mountpoint, total, used and available bytes, or None when df said nothing.
`available` is what df says an ordinary user can still write, which on ext4
is less than total minus used. How full is too full is the caller's decision.

Version 4.4.0.
2026-10-11 09:06:00 +02:00
christianmanivong ff6dbe0206 Merge pull request 'fix: tell unattended-upgrade from Ubuntu's idle shutdown helper' (#27) from fix/unattended-upgrade-shutdown into main
CI / test (3.10) (push) Successful in 24s
CI / test (3.11) (push) Successful in 26s
CI / test (3.12) (push) Successful in 26s
2026-10-08 15:33:26 +00:00
christianmanivong 7aa731ad34 fix: tell unattended-upgrade from Ubuntu's idle shutdown helper
CI / test (3.10) (push) Successful in 24s
CI / test (3.11) (push) Successful in 23s
CI / test (3.10) (pull_request) Successful in 24s
CI / test (3.12) (pull_request) Successful in 25s
CI / test (3.12) (push) Successful in 24s
CI / test (3.11) (pull_request) Successful in 22s
On every Ubuntu host with unattended-upgrades, get_host_status() read the
package manager as busy, so an interrupted dpkg was never reported there.
Ubuntu keeps unattended-upgrade-shutdown --wait-for-signal running all the
time; its process name is cut to 15 characters, unattended-upgr, the same
as a real run's.

BUSY_CHECK now matches dpkg, apt, apt-get and aptitude by name and
unattended-upgrade by its command line. The tests run it through sh with a
stand-in ps, and check that the command does not match itself: over an exec
channel it is the command line of the shell that runs it.

Closes #26
2026-10-08 17:30:44 +02:00
christianmanivong 72679ff4f8 Merge pull request 'feat: report a dpkg left halfway and when the host's own automatic updates last succeeded' (#25) from feat/dpkg-interrupted into main
CI / test (3.10) (push) Successful in 24s
CI / test (3.11) (push) Successful in 23s
CI / test (3.12) (push) Successful in 24s
2026-10-08 14:12:35 +00:00