Compare commits
6
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5c40019af | ||
|
|
36b7852bce | ||
|
|
31949eca0a | ||
|
|
7b491164a2 | ||
|
|
34b8f10ffa | ||
|
|
1ce0a0b6f2 |
@@ -70,6 +70,8 @@ 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.
|
||||
|
||||
A function class may use the **template form** — public method concrete, the
|
||||
device-specific part a `_hook` declared under `if TYPE_CHECKING` — *when the base
|
||||
@@ -225,6 +227,11 @@ class PfSenseDriver(FirewallDriver):
|
||||
def get_vpn_tunnels(self):
|
||||
# return Dict[str, VPNTunnelDict]
|
||||
...
|
||||
|
||||
def get_port_forwards(self):
|
||||
# return List[PortForwardDict] — forwards from the WAN only, never a
|
||||
# redirect between internal networks (shared with home gateways)
|
||||
...
|
||||
```
|
||||
|
||||
### Hypervisor
|
||||
@@ -238,7 +245,13 @@ class ProxmoxDriver(HypervisorDriver):
|
||||
# return List[VMDict]
|
||||
...
|
||||
|
||||
def snapshot_create(self, name, snapshot, description="", include_memory=False):
|
||||
def create_vm_snapshot(self, name, snapshot, description="", include_memory=False):
|
||||
...
|
||||
|
||||
def get_vm_cpu_types(self):
|
||||
# optional — return List[VMCpuTypeDict]: the CPU models a new VM may get
|
||||
# on this node, each with its cpuinfo flags and whether the node can run
|
||||
# it; the name goes to create_vm_from_cloud_init(cpu_type=...)
|
||||
...
|
||||
```
|
||||
|
||||
|
||||
@@ -38,6 +38,7 @@ instead of being restated on every role that happens to need it:
|
||||
* :class:`~napalm_device_types.dhcp.DhcpServerMixin`
|
||||
* :class:`~napalm_device_types.firewall_rules.FirewallRuleMixin`
|
||||
* :class:`~napalm_device_types.health_metrics.HealthMetricsMixin`
|
||||
* :class:`~napalm_device_types.host_reboot.HostRebootMixin`
|
||||
* :class:`~napalm_device_types.interface_filter.InterfaceFilterMixin`
|
||||
* :class:`~napalm_device_types.mac_acl.MacAclMixin`
|
||||
* :class:`~napalm_device_types.nat_vpn.NatVpnMixin`
|
||||
@@ -60,6 +61,7 @@ from napalm_device_types.hypervisor import HypervisorDriver
|
||||
from napalm_device_types.os import OSDriver
|
||||
from napalm_device_types.firewall_rules import FirewallRuleMixin
|
||||
from napalm_device_types.health_metrics import HealthMetricsMixin
|
||||
from napalm_device_types.host_reboot import HostRebootMixin
|
||||
from napalm_device_types.interface_filter import InterfaceFilterMixin
|
||||
from napalm_device_types.lag import add_lag_interfaces
|
||||
from napalm_device_types.mac_acl import MacAclMixin
|
||||
@@ -84,6 +86,7 @@ __all__ = [
|
||||
"FirewallDriver",
|
||||
"FirewallRuleMixin",
|
||||
"HealthMetricsMixin",
|
||||
"HostRebootMixin",
|
||||
"HypervisorDriver",
|
||||
"InterfaceFilterMixin",
|
||||
"MacAclMixin",
|
||||
|
||||
@@ -12,6 +12,7 @@ from typing import NamedTuple
|
||||
|
||||
from napalm.base import NetworkDriver
|
||||
|
||||
from napalm_device_types.host_reboot import HostRebootMixin
|
||||
from napalm_device_types.ping_sweep import PingSweepMixin
|
||||
|
||||
|
||||
@@ -47,7 +48,7 @@ class PortSpec(NamedTuple):
|
||||
mandatory: bool = False
|
||||
|
||||
|
||||
class DeviceTypeDriver(PingSweepMixin, NetworkDriver):
|
||||
class DeviceTypeDriver(PingSweepMixin, HostRebootMixin, NetworkDriver):
|
||||
"""Common base for all netOrk device-type drivers.
|
||||
|
||||
Sits between napalm.base.NetworkDriver and the type-specific abstract
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"""Restarting the device itself.
|
||||
|
||||
Declared under ``if TYPE_CHECKING``: a contract, not a placeholder. Only a
|
||||
driver that can actually restart its device defines ``reboot_host``, so
|
||||
``hasattr(driver, "reboot_host")`` tells a caller whether to offer it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
|
||||
class HostRebootMixin:
|
||||
if TYPE_CHECKING:
|
||||
|
||||
def reboot_host(self) -> None:
|
||||
"""
|
||||
Restarts the device this driver is connected to.
|
||||
|
||||
Returns once the device has accepted the request; the session is
|
||||
usually gone right after. Waiting for the device to come back is
|
||||
the caller's business (see ``REBOOT_SETTLE_SECONDS``).
|
||||
|
||||
A driver that manages other machines restarts its *own* host, never
|
||||
one of them: a hypervisor restarts the hypervisor, not a VM.
|
||||
|
||||
:raises RuntimeError: If the device refuses, e.g. an ESXi host that
|
||||
is not in maintenance mode while VMs are running.
|
||||
"""
|
||||
...
|
||||
@@ -21,6 +21,7 @@ from napalm_device_types.models import (
|
||||
StorageTargetDict,
|
||||
StorageVolumeDict,
|
||||
VMConfigDict,
|
||||
VMCpuTypeDict,
|
||||
VMDict,
|
||||
VMProvisionResultDict,
|
||||
VMStatusDict,
|
||||
@@ -43,6 +44,11 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
ROLE: str = "hypervisor"
|
||||
TYPE_LABEL: str = "Hypervisor"
|
||||
|
||||
#: What cloud-init installs and starts on a VM provisioned through this
|
||||
#: driver, so the hypervisor can read the guest's IP address back.
|
||||
GUEST_AGENT_PACKAGES: tuple[str, ...] = ("qemu-guest-agent",)
|
||||
GUEST_AGENT_RUNCMD: tuple[str, ...] = ("systemctl enable --now qemu-guest-agent",)
|
||||
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -59,7 +65,8 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
Each entry contains:
|
||||
|
||||
* name (string) - VM display name
|
||||
* vmid (int) - hypervisor-internal numeric ID
|
||||
* vmid (string) - hypervisor-internal ID (Proxmox: ``"100"``,
|
||||
VMware: the instance UUID)
|
||||
* status (string) - ``"running"``, ``"stopped"``, ``"paused"``, ``"suspended"``
|
||||
* vcpus (int) - number of virtual CPUs assigned
|
||||
* memory (int) - configured RAM in megabytes
|
||||
@@ -73,7 +80,7 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
[
|
||||
{
|
||||
"name": "web01",
|
||||
"vmid": 100,
|
||||
"vmid": "100",
|
||||
"status": "running",
|
||||
"vcpus": 4,
|
||||
"memory": 8192,
|
||||
@@ -84,7 +91,7 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
},
|
||||
{
|
||||
"name": "db-backup",
|
||||
"vmid": 101,
|
||||
"vmid": "101",
|
||||
"status": "stopped",
|
||||
"vcpus": 2,
|
||||
"memory": 4096,
|
||||
@@ -101,13 +108,14 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
"""
|
||||
Returns the full hardware configuration of a virtual machine.
|
||||
|
||||
:param name: VM name or numeric VMID as a string.
|
||||
:param name: VM name or its ``vmid``.
|
||||
:raises ValueError: If no VM with the given name/ID exists.
|
||||
|
||||
The returned dictionary contains:
|
||||
|
||||
* name (string) - VM display name
|
||||
* vmid (int) - hypervisor-internal numeric ID
|
||||
* vmid (string) - hypervisor-internal ID (Proxmox: ``"100"``,
|
||||
VMware: the instance UUID)
|
||||
* vcpus (int) - number of virtual CPUs
|
||||
* memory (int) - RAM in megabytes
|
||||
* os_type (string) - guest OS type hint (e.g. ``"l26"``, ``"win11"``, ``"other"``)
|
||||
@@ -131,11 +139,21 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
* description (string) - free-text notes / description
|
||||
* tags (list of strings) - organisational tags
|
||||
|
||||
Optional, present only where the hypervisor exposes them:
|
||||
|
||||
* os_name (string) - human-readable guest OS
|
||||
* cpu_type (string) - emulated CPU model
|
||||
* sockets (int), cores_per_socket (int) - vCPU topology
|
||||
* firmware (string) - ``"bios"`` or ``"efi"``
|
||||
* machine (string) - machine type / virtual hardware version
|
||||
* passthrough (list) - host devices handed to the VM, each with
|
||||
``slot``, ``kind`` (``"pci"``/``"usb"``) and ``config``
|
||||
|
||||
Example::
|
||||
|
||||
{
|
||||
"name": "web01",
|
||||
"vmid": 100,
|
||||
"vmid": "100",
|
||||
"vcpus": 4,
|
||||
"memory": 8192,
|
||||
"os_type": "l26",
|
||||
@@ -174,7 +192,7 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
|
||||
The method blocks until the hypervisor reports the VM as running.
|
||||
|
||||
:param name: VM name or numeric VMID as a string.
|
||||
:param name: VM name or its ``vmid``.
|
||||
:raises ValueError: If no VM with the given name/ID exists.
|
||||
:raises RuntimeError: If the VM cannot be started (e.g. resource limit).
|
||||
|
||||
@@ -192,7 +210,7 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
and the method blocks until the VM is stopped. With ``force=True``
|
||||
the VM is immediately powered off (equivalent to pulling the plug).
|
||||
|
||||
:param name: VM name or numeric VMID as a string.
|
||||
:param name: VM name or its ``vmid``.
|
||||
:param force: ``True`` for immediate power-off, ``False`` for graceful shutdown.
|
||||
:raises ValueError: If no VM with the given name/ID exists.
|
||||
:raises RuntimeError: If the VM is already stopped.
|
||||
@@ -211,7 +229,7 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
With ``force=False`` (default) a graceful ACPI reboot is requested.
|
||||
With ``force=True`` the VM is reset immediately without OS shutdown.
|
||||
|
||||
:param name: VM name or numeric VMID as a string.
|
||||
:param name: VM name or its ``vmid``.
|
||||
:param force: ``True`` for an immediate reset, ``False`` for graceful reboot.
|
||||
:raises ValueError: If no VM with the given name/ID exists.
|
||||
:raises RuntimeError: If the VM is not currently running.
|
||||
@@ -228,7 +246,7 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
Suspends (pauses) a running virtual machine, preserving its in-memory
|
||||
state. The VM can be resumed with :meth:`start_vm`.
|
||||
|
||||
:param name: VM name or numeric VMID as a string.
|
||||
:param name: VM name or its ``vmid``.
|
||||
:raises ValueError: If no VM with the given name/ID exists.
|
||||
:raises RuntimeError: If the VM is not currently running.
|
||||
|
||||
@@ -246,7 +264,7 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
"""
|
||||
Returns all snapshots of a virtual machine.
|
||||
|
||||
:param name: VM name or numeric VMID as a string.
|
||||
:param name: VM name or its ``vmid``.
|
||||
:raises ValueError: If no VM with the given name/ID exists.
|
||||
|
||||
Each entry contains:
|
||||
@@ -286,7 +304,7 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
"""
|
||||
Creates a snapshot of a virtual machine.
|
||||
|
||||
:param name: VM name or numeric VMID as a string.
|
||||
:param name: VM name or its ``vmid``.
|
||||
:param snapshot: Name for the new snapshot.
|
||||
:param description: Optional human-readable description.
|
||||
:param include_memory: Whether to include the current RAM state
|
||||
@@ -306,7 +324,7 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
"""
|
||||
Deletes a snapshot of a virtual machine.
|
||||
|
||||
:param name: VM name or numeric VMID as a string.
|
||||
:param name: VM name or its ``vmid``.
|
||||
:param snapshot: Name of the snapshot to delete.
|
||||
:raises ValueError: If the VM or snapshot does not exist.
|
||||
:raises RuntimeError: If other snapshots depend on this one (must delete children first).
|
||||
@@ -324,7 +342,7 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
The VM is stopped (if running), reverted, and then left in the state
|
||||
the snapshot recorded (running or stopped depending on ``has_memory``).
|
||||
|
||||
:param name: VM name or numeric VMID as a string.
|
||||
:param name: VM name or its ``vmid``.
|
||||
:param snapshot: Name of the snapshot to roll back to.
|
||||
:raises ValueError: If the VM or snapshot does not exist.
|
||||
:raises RuntimeError: If the rollback fails.
|
||||
@@ -445,6 +463,7 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
ssh_public_keys: List[str] | None = None,
|
||||
disk_resize_gb: int | None = None,
|
||||
storage: str | None = None,
|
||||
cpu_type: str | None = None,
|
||||
download_timeout: int = 300,
|
||||
timeout: int = 180,
|
||||
) -> VMProvisionResultDict:
|
||||
@@ -486,6 +505,13 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
disk on (a name returned by ``get_image_storages()``). If None,
|
||||
the driver auto-detects the first enabled, node-available storage
|
||||
whose content includes "images".
|
||||
cpu_type (string | None) - virtual CPU model for the VM (a name
|
||||
returned by ``get_vm_cpu_types()``). If None, the driver uses the
|
||||
entry that listing marks ``default``. A driver without
|
||||
``get_vm_cpu_types()`` offers no choice and must reject any
|
||||
value other than None with ValueError; one that has it raises
|
||||
ValueError for a name it does not list or that is not
|
||||
``available`` on this node, before creating anything.
|
||||
download_timeout (int) - maximum seconds to wait for the image download
|
||||
(skipped entirely if already cached on the hypervisor). Default 300.
|
||||
timeout (int) - maximum seconds to wait for the remaining provisioning
|
||||
@@ -601,3 +627,23 @@ class HypervisorDriver(PackageManagementMixin, HealthMetricsMixin, DeviceTypeDri
|
||||
as ``create_vm_from_cloud_init``'s ``storage`` argument.
|
||||
"""
|
||||
...
|
||||
|
||||
def get_vm_cpu_types(self) -> List[VMCpuTypeDict]:
|
||||
"""
|
||||
List the virtual CPU models a new VM may be given on the node it will
|
||||
be created on.
|
||||
|
||||
Optional: a hypervisor whose VMs have no per-VM CPU model (VMware
|
||||
sets CPU compatibility per cluster) does not implement it, and a
|
||||
caller then offers no choice.
|
||||
|
||||
Every model the driver knows is listed, also those this node's CPU
|
||||
cannot run -- marked ``available: False`` -- so a picker can show why
|
||||
an option is missing. Exactly one entry is ``default``: the model
|
||||
``create_vm_from_cloud_init`` uses when ``cpu_type`` is None.
|
||||
|
||||
Returns:
|
||||
List[VMCpuTypeDict] - each entry's ``name`` is directly usable as
|
||||
``create_vm_from_cloud_init``'s ``cpu_type`` argument.
|
||||
"""
|
||||
...
|
||||
|
||||
@@ -298,6 +298,21 @@ class NATTranslationDict(TypedDict):
|
||||
age: float
|
||||
|
||||
|
||||
class PortForwardDict(TypedDict):
|
||||
"""A port the WAN side can reach, forwarded to a host inside.
|
||||
|
||||
Shared by firewalls and home gateways (``NatVpnMixin.get_port_forwards``).
|
||||
"""
|
||||
|
||||
name: str
|
||||
protocol: str # "TCP" or "UDP"
|
||||
external_port: int
|
||||
internal_ip: str
|
||||
internal_port: int
|
||||
enabled: bool
|
||||
remote_host: NotRequired[str] # restrict forward to a specific remote source
|
||||
|
||||
|
||||
class SecurityZoneDict(TypedDict):
|
||||
interfaces: List[str]
|
||||
policy: str
|
||||
@@ -470,16 +485,6 @@ class WANStatusDict(TypedDict):
|
||||
link_status: NotRequired[str] # physical line state, e.g. "Up" / "Down"
|
||||
|
||||
|
||||
class PortForwardDict(TypedDict):
|
||||
name: str
|
||||
protocol: str # "TCP" or "UDP"
|
||||
external_port: int
|
||||
internal_ip: str
|
||||
internal_port: int
|
||||
enabled: bool
|
||||
remote_host: NotRequired[str] # restrict forward to a specific remote source
|
||||
|
||||
|
||||
class HostDict(TypedDict):
|
||||
mac: str
|
||||
ip: str
|
||||
@@ -512,7 +517,7 @@ class VMNICDict(TypedDict):
|
||||
|
||||
class VMDict(TypedDict):
|
||||
name: str
|
||||
vmid: int
|
||||
vmid: str
|
||||
status: str
|
||||
vcpus: int
|
||||
memory: int
|
||||
@@ -522,9 +527,17 @@ class VMDict(TypedDict):
|
||||
node: str
|
||||
|
||||
|
||||
class VMPassthroughDict(TypedDict):
|
||||
"""A host device handed through to a VM (PCI, USB)."""
|
||||
|
||||
slot: str # hypervisor's device key, e.g. "hostpci0"
|
||||
kind: str # "pci" or "usb"
|
||||
config: str # hypervisor's own description of the device
|
||||
|
||||
|
||||
class VMConfigDict(TypedDict):
|
||||
name: str
|
||||
vmid: int
|
||||
vmid: str
|
||||
vcpus: int
|
||||
memory: int
|
||||
os_type: str
|
||||
@@ -533,6 +546,14 @@ class VMConfigDict(TypedDict):
|
||||
nics: List[VMNICDict]
|
||||
description: str
|
||||
tags: List[str]
|
||||
# Hardware details not every hypervisor exposes; absent when unknown.
|
||||
os_name: NotRequired[str] # human-readable guest OS, e.g. "Ubuntu Linux (64-bit)"
|
||||
cpu_type: NotRequired[str] # e.g. "host", "kvm64"
|
||||
sockets: NotRequired[int]
|
||||
cores_per_socket: NotRequired[int]
|
||||
firmware: NotRequired[str] # "bios" or "efi"
|
||||
machine: NotRequired[str] # machine type / virtual hardware version
|
||||
passthrough: NotRequired[List[VMPassthroughDict]]
|
||||
|
||||
|
||||
class StorageVolumeDict(TypedDict):
|
||||
@@ -873,8 +894,8 @@ class NetworkTargetDict(TypedDict):
|
||||
is surfaced via ``fixed_vlan_tag`` instead, for display purposes.
|
||||
"""
|
||||
|
||||
name: str # Bridge or vnet name, usable directly as NICConfigDict.bridge
|
||||
kind: str # "bridge" or "vnet"
|
||||
name: str # Bridge, vnet or port group name, usable directly as NICConfigDict.bridge
|
||||
kind: str # "bridge", "vnet" or "portgroup" (VMware: VLAN fixed like a vnet's)
|
||||
vlan_aware: bool # True if a NICConfigDict.vlan_tag may be set on top of this target
|
||||
fixed_vlan_tag: NotRequired[int | None] # vnet only: the VLAN ID already baked into it
|
||||
|
||||
@@ -891,6 +912,24 @@ class StorageTargetDict(TypedDict):
|
||||
available_gb: float # Free capacity in gigabytes
|
||||
|
||||
|
||||
class VMCpuTypeDict(TypedDict):
|
||||
"""A virtual CPU model a new VM may be given (``create_vm_from_cloud_init``'s
|
||||
``cpu_type`` argument), judged against the specific node the VM will be
|
||||
created on.
|
||||
|
||||
``features`` uses the flag names of Linux's ``/proc/cpuinfo`` (``avx``,
|
||||
``avx2``, ``aes`` ...), so a caller can ask "does this model give the guest
|
||||
AVX?" without knowing the hypervisor's model names. A model that passes the
|
||||
host CPU through lists that CPU's own flags.
|
||||
"""
|
||||
|
||||
name: str # Model name, usable directly as create_vm_from_cloud_init(cpu_type=...)
|
||||
description: str # One line on what the model is for, for a picker
|
||||
features: List[str] # cpuinfo flags the guest is guaranteed to see
|
||||
available: bool # False when this node's CPU cannot run the model
|
||||
default: bool # The model create_vm_from_cloud_init uses when cpu_type is None
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Ping sweep (shared across device types)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Address translation and VPN tunnels.
|
||||
|
||||
A home gateway does a subset of what a firewall does, and these two readers
|
||||
are where the sets overlap exactly.
|
||||
A home gateway does a subset of what a firewall does, and these readers are
|
||||
where the sets overlap exactly.
|
||||
|
||||
Declared under ``if TYPE_CHECKING``: these are contracts, not placeholders.
|
||||
Nothing exists at runtime until a concrete driver implements it, so mixing
|
||||
@@ -13,7 +13,7 @@ from __future__ import annotations
|
||||
|
||||
from typing import Dict, List, TYPE_CHECKING
|
||||
|
||||
from napalm_device_types.models import NATTranslationDict, VPNTunnelDict
|
||||
from napalm_device_types.models import NATTranslationDict, PortForwardDict, VPNTunnelDict
|
||||
|
||||
|
||||
class NatVpnMixin:
|
||||
@@ -47,6 +47,46 @@ class NatVpnMixin:
|
||||
"""
|
||||
...
|
||||
|
||||
def get_port_forwards(self) -> List[PortForwardDict]:
|
||||
"""
|
||||
Returns the port forwards that let traffic in from the WAN.
|
||||
|
||||
A port forward here means destination NAT on an interface facing
|
||||
the internet: whoever reaches the external port is let through to
|
||||
``internal_ip``. A redirect between internal networks is
|
||||
destination NAT as well, but it is **not** a port forward and must
|
||||
be left out -- callers read every entry as "this host is reachable
|
||||
from outside". So are rules that only exempt traffic from
|
||||
redirection.
|
||||
|
||||
Each entry contains:
|
||||
|
||||
* name (string) - the rule's description/name
|
||||
* protocol (string) - ``"TCP"`` or ``"UDP"``; a rule for both is
|
||||
two entries. ``"ANY"`` forwards every protocol
|
||||
* external_port (int) - the WAN-side port; the first of a range,
|
||||
``0`` for every port (a whole host forwarded)
|
||||
* internal_ip (string) - the host the traffic is forwarded to
|
||||
* internal_port (int) - the port on that host
|
||||
* enabled (bool) - whether the rule is currently active
|
||||
* remote_host (string, optional) - restricts the forward to a specific
|
||||
remote source address; empty/absent means "any"
|
||||
|
||||
Example::
|
||||
|
||||
[
|
||||
{
|
||||
"name": "Webserver HTTPS",
|
||||
"protocol": "TCP",
|
||||
"external_port": 443,
|
||||
"internal_ip": "192.168.1.10",
|
||||
"internal_port": 443,
|
||||
"enabled": True,
|
||||
}
|
||||
]
|
||||
"""
|
||||
...
|
||||
|
||||
def get_vpn_tunnels(self) -> Dict[str, VPNTunnelDict]:
|
||||
"""
|
||||
Returns the status of VPN tunnels.
|
||||
|
||||
@@ -6,8 +6,9 @@ wireless access point in a single consumer device (e.g. AVM FritzBox,
|
||||
ISP-supplied DSL/cable routers). This base class merges the relevant
|
||||
subsets of :class:`~napalm_device_types.firewall.FirewallDriver` and
|
||||
:class:`~napalm_device_types.access_point.AccessPointDriver` plus
|
||||
gateway-specific operations (WAN status, port forwarding, connected
|
||||
hosts).
|
||||
gateway-specific operations (WAN status, connected hosts). Port
|
||||
forwarding is shared with firewalls, in
|
||||
:class:`~napalm_device_types.nat_vpn.NatVpnMixin`.
|
||||
|
||||
Usage::
|
||||
|
||||
@@ -25,7 +26,6 @@ from napalm_device_types.health_metrics import HealthMetricsMixin
|
||||
from napalm_device_types.dhcp import DhcpServerMixin
|
||||
from napalm_device_types.models import (
|
||||
HostDict,
|
||||
PortForwardDict,
|
||||
RadioStatusDict,
|
||||
SSIDDict,
|
||||
WANStatusDict,
|
||||
@@ -85,36 +85,6 @@ class ResidentialGatewayDriver(NatVpnMixin, HealthMetricsMixin, DhcpServerMixin,
|
||||
"""
|
||||
...
|
||||
|
||||
def get_port_forwards(self) -> List[PortForwardDict]:
|
||||
"""
|
||||
Returns the configured port forwarding (port mapping) rules.
|
||||
|
||||
Each entry contains:
|
||||
|
||||
* name (string) - the rule's description/name
|
||||
* protocol (string) - ``"TCP"`` or ``"UDP"``
|
||||
* external_port (int) - the WAN-side port
|
||||
* internal_ip (string) - the LAN host the traffic is forwarded to
|
||||
* internal_port (int) - the LAN-side port
|
||||
* enabled (bool) - whether the rule is currently active
|
||||
* remote_host (string, optional) - restricts the forward to a specific
|
||||
remote source address; empty/absent means "any"
|
||||
|
||||
Example::
|
||||
|
||||
[
|
||||
{
|
||||
"name": "Webserver HTTPS",
|
||||
"protocol": "TCP",
|
||||
"external_port": 443,
|
||||
"internal_ip": "192.168.1.10",
|
||||
"internal_port": 443,
|
||||
"enabled": True,
|
||||
}
|
||||
]
|
||||
"""
|
||||
...
|
||||
|
||||
def get_hosts(self) -> List[HostDict]:
|
||||
"""
|
||||
Returns the list of hosts known to the gateway (LAN clients).
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "napalm-device-types"
|
||||
version = "1.0.0"
|
||||
version = "2.0.0"
|
||||
description = "Abstract device-type base classes for NAPALM drivers"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""reboot_host: the contract for restarting the device itself.
|
||||
|
||||
netOrk used to restart a host by sending ``/sbin/reboot`` through a driver's
|
||||
private ``_send_command``. A driver that talks to an API instead has no such
|
||||
method, and the caller swallowed the resulting AttributeError, so the reboot
|
||||
"succeeded" without happening. A declared contract lets netOrk ask first.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from napalm_device_types import DeviceTypeDriver, HostRebootMixin
|
||||
|
||||
|
||||
def test_every_device_type_driver_carries_the_declaration():
|
||||
assert issubclass(DeviceTypeDriver, HostRebootMixin)
|
||||
|
||||
|
||||
def test_declared_not_implemented():
|
||||
"""hasattr is netOrk's capability probe; only a driver that implements it
|
||||
may answer True."""
|
||||
assert not hasattr(DeviceTypeDriver, "reboot_host")
|
||||
|
||||
|
||||
def test_declaration_documents_the_contract():
|
||||
source = inspect.getsource(HostRebootMixin)
|
||||
assert "def reboot_host(self) -> None" in source
|
||||
assert "RuntimeError" in source
|
||||
@@ -0,0 +1,39 @@
|
||||
"""get_port_forwards: what the WAN side may reach inside, on any gateway.
|
||||
|
||||
The reader used to be declared on ``ResidentialGatewayDriver`` only, as if a
|
||||
port forward were a home-router feature. A firewall forwards ports just the
|
||||
same -- OPNsense calls it destination NAT -- and the two consumers that ask
|
||||
(is this host reachable from the internet, which CVEs are exposed) need the
|
||||
answer from both. The declaration therefore lives where the two roles overlap,
|
||||
next to the NAT translations reader.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
|
||||
from napalm_device_types import FirewallDriver, ResidentialGatewayDriver
|
||||
from napalm_device_types.nat_vpn import NatVpnMixin
|
||||
|
||||
|
||||
def test_a_firewall_and_a_gateway_share_the_declaration():
|
||||
assert issubclass(FirewallDriver, NatVpnMixin)
|
||||
assert issubclass(ResidentialGatewayDriver, NatVpnMixin)
|
||||
assert "def get_port_forwards(self) -> List[PortForwardDict]" in inspect.getsource(NatVpnMixin)
|
||||
|
||||
|
||||
def test_it_is_declared_once():
|
||||
assert "def get_port_forwards" not in inspect.getsource(ResidentialGatewayDriver)
|
||||
|
||||
|
||||
def test_absent_until_a_driver_implements_it():
|
||||
assert not hasattr(FirewallDriver, "get_port_forwards")
|
||||
assert not hasattr(ResidentialGatewayDriver, "get_port_forwards")
|
||||
|
||||
|
||||
def test_the_contract_says_what_counts():
|
||||
"""A redirect between two internal networks is destination NAT too, and
|
||||
would make an internal host look reachable from the internet."""
|
||||
source = inspect.getsource(NatVpnMixin)
|
||||
assert "from the WAN" in source
|
||||
assert "between internal networks" in source
|
||||
@@ -0,0 +1,58 @@
|
||||
"""A new VM's virtual CPU model can be chosen, from a list the hypervisor offers.
|
||||
|
||||
Proxmox gives a VM created without a ``cpu`` argument the ``kvm64`` model,
|
||||
which has no AVX -- and MongoDB 5.0 and later will not start without it. Which
|
||||
model is right depends on the cluster (``host`` cannot live-migrate between
|
||||
different CPUs, ``x86-64-v3`` does not start on a CPU older than Haswell), so
|
||||
the caller chooses, from entries that say what each model provides and whether
|
||||
the node at hand can run it.
|
||||
|
||||
The declarations sit under ``TYPE_CHECKING`` (see test_role_contracts), so the
|
||||
signature is read from the source rather than from the class.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
import inspect
|
||||
from typing import List, get_type_hints
|
||||
|
||||
import napalm_device_types.hypervisor as hypervisor_module
|
||||
from napalm_device_types import HypervisorDriver
|
||||
from napalm_device_types.models import VMCpuTypeDict
|
||||
|
||||
|
||||
def _declared(name: str) -> ast.FunctionDef:
|
||||
tree = ast.parse(inspect.getsource(hypervisor_module))
|
||||
for node in ast.walk(tree):
|
||||
if isinstance(node, ast.FunctionDef) and node.name == name:
|
||||
return node
|
||||
raise AssertionError(f"HypervisorDriver does not declare {name}()")
|
||||
|
||||
|
||||
class TestCreateVmTakesACpuType:
|
||||
def test_cpu_type_is_an_optional_keyword(self):
|
||||
fn = _declared("create_vm_from_cloud_init")
|
||||
kwonly = {arg.arg: default for arg, default in zip(fn.args.kwonlyargs, fn.args.kw_defaults)}
|
||||
assert "cpu_type" in kwonly
|
||||
default = kwonly["cpu_type"]
|
||||
assert isinstance(default, ast.Constant) and default.value is None
|
||||
|
||||
|
||||
class TestCpuTypeListing:
|
||||
def test_is_declared(self):
|
||||
assert _declared("get_vm_cpu_types").returns is not None
|
||||
|
||||
def test_absent_until_a_driver_implements_it(self):
|
||||
"""netOrk probes capabilities with hasattr; a hypervisor without a
|
||||
choice of CPU model must not seem to offer one."""
|
||||
assert not hasattr(HypervisorDriver, "get_vm_cpu_types")
|
||||
|
||||
def test_entry_shape(self):
|
||||
assert get_type_hints(VMCpuTypeDict) == {
|
||||
"name": str,
|
||||
"description": str,
|
||||
"features": List[str],
|
||||
"available": bool,
|
||||
"default": bool,
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
"""A VM's ``vmid`` is a string on every hypervisor.
|
||||
|
||||
Proxmox numbers its guests, but VMware identifies them by UUID or MoRef
|
||||
(``"vm-42"``). An ``int`` in the contract forced netOrk to call ``int()`` on
|
||||
whatever came back, which cannot represent the second kind at all. The
|
||||
provisioning dicts already carried ``vmid`` as a string; these pin the
|
||||
read-side dicts to the same type.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import get_type_hints
|
||||
|
||||
import pytest
|
||||
from napalm_device_types.models import (
|
||||
VMConfigDict,
|
||||
VMDict,
|
||||
VMProvisionResultDict,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("model", [VMDict, VMConfigDict, VMProvisionResultDict])
|
||||
def test_vmid_is_a_string(model):
|
||||
assert get_type_hints(model)["vmid"] is str
|
||||
|
||||
|
||||
class TestVMConfigCarriesWhatAHardwareViewShows:
|
||||
"""netOrk's VM hardware view used to read Proxmox's raw config through the
|
||||
driver's private API. The contract has to carry those details so a second
|
||||
hypervisor can fill the same view -- optionally, since not every platform
|
||||
has every one of them."""
|
||||
|
||||
OPTIONAL = {
|
||||
"os_name",
|
||||
"cpu_type",
|
||||
"sockets",
|
||||
"cores_per_socket",
|
||||
"firmware",
|
||||
"machine",
|
||||
"passthrough",
|
||||
}
|
||||
|
||||
def test_hardware_details_are_optional_fields(self):
|
||||
assert self.OPTIONAL <= VMConfigDict.__optional_keys__
|
||||
|
||||
def test_contract_core_stays_required(self):
|
||||
assert "vmid" in VMConfigDict.__required_keys__
|
||||
assert "disks" in VMConfigDict.__required_keys__
|
||||
|
||||
def test_passthrough_entry_shape(self):
|
||||
from napalm_device_types.models import VMPassthroughDict
|
||||
|
||||
assert get_type_hints(VMPassthroughDict) == {"slot": str, "kind": str, "config": str}
|
||||
|
||||
|
||||
class TestGuestAgentDeclaration:
|
||||
"""netOrk's cloud-init installed qemu-guest-agent on every new VM. A VMware
|
||||
guest reports its IP through open-vm-tools instead; the hypervisor says
|
||||
which, and netOrk stops hard-coding one of them."""
|
||||
|
||||
def test_default_is_qemu_guest_agent(self):
|
||||
from napalm_device_types import HypervisorDriver
|
||||
|
||||
assert HypervisorDriver.GUEST_AGENT_PACKAGES == ("qemu-guest-agent",)
|
||||
assert HypervisorDriver.GUEST_AGENT_RUNCMD == ("systemctl enable --now qemu-guest-agent",)
|
||||
|
||||
def test_attributes_are_not_methods(self):
|
||||
from napalm_device_types import HypervisorDriver
|
||||
|
||||
assert not callable(vars(HypervisorDriver)["GUEST_AGENT_PACKAGES"])
|
||||
Reference in New Issue
Block a user