feat: shutdown_host and begin_stop_vm, shutting down without waiting
driver / lint (pull_request) Successful in 18s
driver / standard (pull_request) Successful in 20s
driver / test (3.10) (pull_request) Failing after 6s
driver / types (pull_request) Successful in 1m4s
driver / test (3.12) (pull_request) Successful in 1m8s
driver / test (3.11) (pull_request) Successful in 1m13s
driver / test (3.13) (pull_request) Successful in 1m1s
driver / lowest (pull_request) Successful in 43s
driver / build (pull_request) Successful in 29s
driver / security (pull_request) Successful in 1m8s
CI / driver (pull_request) Failing after 2m41s
driver / publish (pull_request) Skipped

HostShutdownMixin.shutdown_host(), on DeviceTypeDriver: a device shuts
itself down cleanly through its own means; declared, so hasattr says
whether a driver can. There is no forced variant.

VmStopRequestMixin.begin_stop_vm(name): ask for a VM's clean shutdown
and return at once, for a caller that waits on a timer and reads
get_vm_status instead of blocking in stop_vm. No role demands it.

For netOrk's graph workflows, which switch a hypervisor off when CI has
nothing for it (NetOrk/netork#1636).
This commit is contained in:
christianmanivong committed 2026-10-27 00:36:54 +01:00
1 parent 87dbdfac66
commit 4d3574cd60
8 files changed
+142 -3

No files matched your search

+8
View File
@@ -7,6 +7,14 @@ recorded here; their history is the git log.
## [Unreleased]
### Added
- `HostShutdownMixin.shutdown_host()`, on `DeviceTypeDriver`: shut the device itself
down cleanly through its own means; declared, so `hasattr` says whether a driver
can. No forced variant (NetOrk/netork#1636).
- `VmStopRequestMixin.begin_stop_vm(name)`: ask for a VM's clean shutdown and return
at once, for callers that wait on a timer instead of in `stop_vm`. No role demands
it (NetOrk/netork#1636).
## [4.22.0] - 2026-10-23
### Added
+4 -2
View File
@@ -91,8 +91,10 @@ Behaviour shared across roles lives in a function class, exactly once, and a rol
is a thin bundle over them — `PackageManagementMixin`, `HealthMetricsMixin`,
`ServiceControlMixin`, `UpdateMixin`, `NatVpnMixin`, `MacAclMixin`, `FirewallRuleMixin`,
`DhcpServerMixin`, `PingSweepMixin`, `ConfigLifecycleMixin`, `InterfaceFilterMixin`.
`HostRebootMixin` (`reboot_host`) is mixed into `DeviceTypeDriver` itself, since any
device may be restartable; like the others it only declares.
`HostRebootMixin` (`reboot_host`) and `HostShutdownMixin` (`shutdown_host`) are mixed
into `DeviceTypeDriver` itself, since any device may be restartable or able to shut itself
down; like the others they only declare. `VmStopRequestMixin` (`begin_stop_vm`) asks for a
VM's clean shutdown without waiting for it — a hypervisor driver mixes it in when it can.
**Fixes are templates** (`napalm_device_types.fixes`, since 4.13). A change the
application wants made on a device runs as fixed steps the driver implements, with values
+6
View File
@@ -44,6 +44,7 @@ instead of being restated on every role that happens to need it:
* :class:`~napalm_device_types.health_metrics.HealthMetricsMixin`
* :class:`~napalm_device_types.host_status.HostStatusMixin`
* :class:`~napalm_device_types.host_reboot.HostRebootMixin`
* :class:`~napalm_device_types.host_shutdown.HostShutdownMixin`
* :class:`~napalm_device_types.interface_filter.InterfaceFilterMixin`
* :class:`~napalm_device_types.kernel.KernelFactsMixin`
* :class:`~napalm_device_types.listening.ListeningSocketsMixin`
@@ -58,6 +59,7 @@ instead of being restated on every role that happens to need it:
* :class:`~napalm_device_types.systemd.SystemdServicesMixin`
* :class:`~napalm_device_types.updates.UpdateMixin`
* :class:`~napalm_device_types.virtual_ips.VirtualIpsMixin`
* :class:`~napalm_device_types.vm_stop_request.VmStopRequestMixin`
Introspection -- :func:`~napalm_device_types.roles.roles_of`,
:func:`~napalm_device_types.roles.role_keys_of` and
@@ -106,6 +108,7 @@ from napalm_device_types.host_firewall import (
parse_host_firewall,
)
from napalm_device_types.host_reboot import HostRebootMixin
from napalm_device_types.host_shutdown import HostShutdownMixin
from napalm_device_types.host_status import HOST_STATUS_COMMAND, HostStatusMixin, parse_host_status
from napalm_device_types.hypervisor import HypervisorDriver
from napalm_device_types.interface_filter import InterfaceFilterMixin
@@ -214,6 +217,7 @@ from napalm_device_types.virtual_ips import (
VirtualIpsMixin,
parse_virtual_ips,
)
from napalm_device_types.vm_stop_request import VmStopRequestMixin
__all__ = [
"HypervisorHostDict",
@@ -269,6 +273,7 @@ __all__ = [
"HOST_STATUS_COMMAND",
"HostStatusMixin",
"HostRebootMixin",
"HostShutdownMixin",
"HypervisorDriver",
"InterfaceFilterMixin",
"MacAclMixin",
@@ -317,6 +322,7 @@ __all__ = [
"UpdateMixin",
"VIRTUAL_IPS_COMMAND",
"VirtualIpsMixin",
"VmStopRequestMixin",
"SERVICE_CLUSTERS_COMMAND",
"ServiceClustersMixin",
"parse_service_clusters",
+8 -1
View File
@@ -15,6 +15,7 @@ from napalm.base import NetworkDriver
from napalm_device_types.channel import CommandChannelMixin
from napalm_device_types.host_reboot import HostRebootMixin
from napalm_device_types.host_shutdown import HostShutdownMixin
from napalm_device_types.ping_sweep import PingSweepMixin
@@ -74,7 +75,13 @@ class PortSpec(NamedTuple):
mandatory: bool = False
class DeviceTypeDriver(PingSweepMixin, HostRebootMixin, CommandChannelMixin, NetworkDriver): # type: ignore[misc]
class DeviceTypeDriver(
PingSweepMixin,
HostRebootMixin,
HostShutdownMixin,
CommandChannelMixin,
NetworkDriver, # type: ignore[misc]
):
"""Common base for all netOrk device-type drivers.
Sits between napalm.base.NetworkDriver and the type-specific abstract
+32
View File
@@ -0,0 +1,32 @@
"""Shutting the device itself down cleanly.
Declared under ``if TYPE_CHECKING``: a contract, not a placeholder. Only a
driver that can shut its device down through the device's own means defines
``shutdown_host``, so ``hasattr(driver, "shutdown_host")`` tells a caller
whether to offer it. There is no forced variant here, by design: cutting the
power is the BMC's business, not a driver's.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
class HostShutdownMixin:
if TYPE_CHECKING:
def shutdown_host(self) -> None:
"""
Shuts down the device this driver is connected to, cleanly.
Returns once the device has accepted the request; the session is
usually gone right after. Whether the device really went off is
the caller's business, e.g. through its BMC.
A driver that manages other machines shuts down its *own* host,
never one of them: a hypervisor node shuts down itself, and its
guests are stopped the way the node stops them on shutdown.
:raises RuntimeError: If the device refuses the request.
"""
...
+29
View File
@@ -0,0 +1,29 @@
"""Asking for a VM's clean shutdown without waiting for it.
``HypervisorDriver.stop_vm`` blocks until the VM is stopped, within a limit
of the driver's own. A caller that keeps its waits as timers asks with
``begin_stop_vm`` and reads ``get_vm_status`` until the VM says it stopped.
Declared under ``if TYPE_CHECKING``: a driver mixes this in when its
hypervisor can take the request on its own; ``hasattr`` tells a caller
whether it did. No role base carries it.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
class VmStopRequestMixin:
if TYPE_CHECKING:
def begin_stop_vm(self, name: str) -> None:
"""
Asks the hypervisor to shut a virtual machine down cleanly (ACPI)
and returns as soon as the request was accepted.
:param name: VM name or its ``vmid``.
:raises ValueError: If no VM with the given name/ID exists.
:raises RuntimeError: If the hypervisor refuses the request.
"""
...
+27
View File
@@ -0,0 +1,27 @@
"""shutdown_host: the contract for shutting the device itself down cleanly.
netOrk's graph workflows switch a hypervisor off when it has nothing to do
(NetOrk/netork#1636): through its own API, never by pulling the power. Like
``reboot_host`` it is declared, not implemented, so ``hasattr`` tells netOrk
whether a driver can.
"""
from __future__ import annotations
import inspect
from napalm_device_types import DeviceTypeDriver, HostShutdownMixin
def test_every_device_type_driver_carries_the_declaration():
assert issubclass(DeviceTypeDriver, HostShutdownMixin)
def test_declared_not_implemented():
assert not hasattr(DeviceTypeDriver, "shutdown_host")
def test_declaration_documents_the_contract():
source = inspect.getsource(HostShutdownMixin)
assert "def shutdown_host(self) -> None" in source
assert "RuntimeError" in source
+28
View File
@@ -0,0 +1,28 @@
"""begin_stop_vm: ask for a VM's clean shutdown and return at once.
``stop_vm`` blocks until the VM is stopped, with a limit of the driver's own.
A caller that keeps its waits as timers (netOrk's graph workflows,
NetOrk/netork#1636) asks with ``begin_stop_vm`` and reads ``get_vm_status``
until the VM is stopped. A driver mixes it in when it can; no role demands it.
"""
from __future__ import annotations
import inspect
from napalm_device_types import HypervisorDriver, VmStopRequestMixin
def test_no_role_demands_it():
assert not issubclass(HypervisorDriver, VmStopRequestMixin)
def test_declared_not_implemented():
assert not hasattr(VmStopRequestMixin, "begin_stop_vm")
def test_declaration_documents_the_contract():
source = inspect.getsource(VmStopRequestMixin)
assert "def begin_stop_vm(self, name: str) -> None" in source
assert "ValueError" in source
assert "RuntimeError" in source