feat: shutdown_host and begin_stop_vm, shutting down without waiting #55
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
...
|
||||
@@ -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.
|
||||
"""
|
||||
...
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user