From b5c830acfef6b0916169e65393d46fdb70e189d3 Mon Sep 17 00:00:00 2001 From: Christian Manivong Date: Thu, 8 Oct 2026 07:54:13 +0200 Subject: [PATCH] feat(vm-provision): declare the guest agent per guest OS, Linux only Follows napalm-device-types 3.0, which replaces GUEST_AGENT_PACKAGES / GUEST_AGENT_RUNCMD with GUEST_AGENTS = {guest_os: (packages, runcmd)}. VMware lists Linux with open-vm-tools, as before, and refuses any other guest_os before anything is created: BSD guests on VMware are optional for now (NetOrk/netork#801). The NoCloud network-config moved to napalm-device-types (`provisioning.network_config`), which Proxmox now uses as well; the seed module's copy is gone, and the tests check that the generic one is used. --- CHANGELOG.md | 6 ++++++ README.md | 4 +++- napalm_vmware/provision/seed.py | 14 -------------- napalm_vmware/provisioning.py | 19 +++++++++++++------ pyproject.toml | 2 +- tests/test_drivers.py | 10 ++++++++-- tests/test_provision_seed.py | 21 ++++++++++----------- tests/test_provisioning.py | 12 ++++++++++++ 8 files changed, 53 insertions(+), 35 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5875c46..45546f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `reboot_host` on ESXi, refused outside maintenance mode. - Declares `open-vm-tools` as the guest agent for provisioned VMs. +### Changed +- The guest agent is declared per guest OS (`GUEST_AGENTS`, napalm-device-types + 3.0); VMware provisions Linux guests only and refuses any other `guest_os`. +- The NoCloud network-config comes from napalm-device-types + (`provisioning.network_config`), which Proxmox uses as well. + ## [0.1.0] - 2026-09-24 ### Added diff --git a/README.md b/README.md index f908950..3649ce3 100644 --- a/README.md +++ b/README.md @@ -123,7 +123,9 @@ offers for Proxmox (qcow2/raw): A port group fixes its VLAN, so `get_network_targets` reports each one with `kind="portgroup"`, `vlan_aware=False` and its `fixed_vlan_tag`; a NIC asking for a different `vlan_tag` is refused. The guest agent netOrk installs is -`open-vm-tools` (`GUEST_AGENT_PACKAGES`), which reports the IP address back. +`open-vm-tools` (`GUEST_AGENTS`), which reports the IP address back. Only +Linux guests are provisioned; `guest_os` other than `"linux"` is refused +(BSD guests on VMware: NetOrk/netork#801). Unverified until #305: that each distribution's cloud kernel carries the PVSCSI and VMXNET3 drivers. diff --git a/napalm_vmware/provision/seed.py b/napalm_vmware/provision/seed.py index 2074a5f..ce2494b 100644 --- a/napalm_vmware/provision/seed.py +++ b/napalm_vmware/provision/seed.py @@ -31,20 +31,6 @@ def meta_data(hostname: str, instance_id: str) -> str: return yaml.safe_dump({"instance-id": instance_id, "local-hostname": hostname}, sort_keys=False) -def network_config(nics: list[tuple[str, bool]]) -> dict[str, Any] | None: - """Netplan v2 config: DHCP on every ``(mac, dhcp)`` NIC that asks for it. - - ``None`` when no NIC does -- cloud-init then leaves networking alone - rather than being told to configure nothing. - """ - ethernets = { - f"nic{i}": {"match": {"macaddress": mac}, "dhcp4": True} - for i, (mac, dhcp) in enumerate(nics) - if dhcp - } - return {"version": 2, "ethernets": ethernets} if ethernets else None - - def nocloud_iso(user: str, meta: str, network: dict[str, Any] | None) -> bytes: """An ISO 9660 image (Rock Ridge + Joliet) holding the seed files.""" files = {"user-data": user, "meta-data": meta} diff --git a/napalm_vmware/provisioning.py b/napalm_vmware/provisioning.py index 0ae6f3c..c9cd65a 100644 --- a/napalm_vmware/provisioning.py +++ b/napalm_vmware/provisioning.py @@ -22,6 +22,7 @@ from napalm_device_types.models import ( VMProvisionResultDict, VMStatusDict, ) +from napalm_device_types.provisioning import GuestAgents, network_config from pyVmomi import vim from napalm_vmware import paths @@ -35,7 +36,7 @@ from napalm_vmware.provision.image import ImageCache from napalm_vmware.provision.ovf import ovf_descriptor from napalm_vmware.provision.placement import Inventory as PlacementInventory from napalm_vmware.provision.placement import Placement, choose_placement -from napalm_vmware.provision.seed import meta_data, network_config, nocloud_iso, user_data +from napalm_vmware.provision.seed import meta_data, nocloud_iso, user_data logger = logging.getLogger(__name__) @@ -48,11 +49,14 @@ _STATUS = {"poweredOn": "running", "poweredOff": "stopped", "suspended": "suspen class VmwareProvisioningMixin: """Mixed into both drivers ahead of :class:`VmwareBaseDriver`.""" - GUEST_AGENT_PACKAGES: tuple[str, ...] = ("open-vm-tools",) - # The unit is open-vm-tools on Debian/Ubuntu and vmtoolsd on EL/Fedora. - GUEST_AGENT_RUNCMD: tuple[str, ...] = ( - "systemctl enable --now open-vm-tools || systemctl enable --now vmtoolsd", - ) + # Linux only: BSD guests on VMware are optional (NetOrk/netork#801). The + # unit is open-vm-tools on Debian/Ubuntu and vmtoolsd on EL/Fedora. + GUEST_AGENTS: GuestAgents = { + "linux": ( + ("open-vm-tools",), + ("systemctl enable --now open-vm-tools || systemctl enable --now vmtoolsd",), + ), + } hostname: str _port: int @@ -148,9 +152,12 @@ class VmwareProvisioningMixin: disk_resize_gb: int | None = None, storage: str | None = None, cpu_type: str | None = None, + guest_os: str = "linux", download_timeout: int = 300, timeout: int = 180, ) -> VMProvisionResultDict: + if guest_os not in self.GUEST_AGENTS: + raise ValueError(f"VMware provisions Linux guests only, not {guest_os!r}") if cpu_type is not None: # CPU compatibility is a cluster setting here (EVC), not a per-VM # one, and there is no get_vm_cpu_types() that could offer a model. diff --git a/pyproject.toml b/pyproject.toml index ca9251a..28bd025 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,7 @@ classifiers = [ ] dependencies = [ "napalm>=5.0.0", - "napalm-device-types>=2.0.0", + "napalm-device-types>=3.0.0", "pyvmomi>=8.0.1", # Provisioning: download the image, write the NoCloud seed ISO and its YAML. "requests>=2.31", diff --git a/tests/test_drivers.py b/tests/test_drivers.py index 250ac71..44ad13b 100644 --- a/tests/test_drivers.py +++ b/tests/test_drivers.py @@ -94,8 +94,14 @@ class TestIdentity: assert callable(getattr(cls, method, None)) @pytest.mark.parametrize("cls", DRIVERS) - def test_declares_its_guest_agent(self, cls): - assert cls.GUEST_AGENT_PACKAGES == ("open-vm-tools",) + def test_provisions_linux_guests_with_open_vm_tools(self, cls): + """Linux only: BSD guests on VMware are optional (NetOrk/netork#801).""" + assert cls.GUEST_AGENTS == { + "linux": ( + ("open-vm-tools",), + ("systemctl enable --now open-vm-tools || systemctl enable --now vmtoolsd",), + ), + } class TestOptionalArgs: diff --git a/tests/test_provision_seed.py b/tests/test_provision_seed.py index 4a557a5..00c2e3a 100644 --- a/tests/test_provision_seed.py +++ b/tests/test_provision_seed.py @@ -6,8 +6,11 @@ import io import pycdlib import yaml +from napalm_device_types import provisioning as generic -from napalm_vmware.provision.seed import meta_data, network_config, nocloud_iso, user_data +from napalm_vmware import provisioning as prov +from napalm_vmware.provision import seed +from napalm_vmware.provision.seed import meta_data, nocloud_iso, user_data class TestUserData: @@ -34,17 +37,13 @@ def test_meta_data(): class TestNetworkConfig: - def test_dhcp_nics_matched_by_mac(self): - cfg = network_config([("00:50:56:aa:bb:cc", True), ("00:50:56:aa:bb:dd", False)]) - assert cfg == { - "version": 2, - "ethernets": { - "nic0": {"match": {"macaddress": "00:50:56:aa:bb:cc"}, "dhcp4": True}, - }, - } + """Written by napalm-device-types, which Proxmox uses as well (NetOrk/netork#794).""" - def test_no_dhcp_nic_means_no_network_config(self): - assert network_config([("00:50:56:aa:bb:cc", False)]) is None + def test_the_seed_is_built_with_the_generic_network_config(self): + assert prov.network_config is generic.network_config + + def test_no_copy_of_it_is_left_here(self): + assert not hasattr(seed, "network_config") def _read(iso: bytes, name: str) -> str: diff --git a/tests/test_provisioning.py b/tests/test_provisioning.py index 4f81dca..8d447d4 100644 --- a/tests/test_provisioning.py +++ b/tests/test_provisioning.py @@ -152,6 +152,18 @@ class TestCreate: staged.cache.return_value.vmdk.assert_not_called() staged.import_ovf.assert_not_called() + @pytest.mark.parametrize("guest_os", ["freebsd", "openbsd"]) + def test_a_guest_other_than_linux_is_refused_before_anything_happens( + self, driver, staged, guest_os + ): + with pytest.raises(ValueError, match=guest_os): + _create(driver, guest_os=guest_os) + staged.cache.return_value.vmdk.assert_not_called() + staged.import_ovf.assert_not_called() + + def test_linux_is_the_default_guest(self, driver, staged): + assert _create(driver, guest_os="linux")["vmid"] == "uuid-99" + def test_placement_refusal_is_a_runtime_error(self, driver, staged): with patch.object(prov, "choose_placement", side_effect=ValueError("no host")): with pytest.raises(RuntimeError, match="no host"): -- 2.54.0