feat(vm-provision): declare the guest agent per guest OS, Linux only #4

Merged
christianmanivong merged 1 commits from feat/guest-os into main 2026-10-08 06:05:06 +00:00
8 changed files with 53 additions and 35 deletions
Showing only changes of commit b5c830acfe - Show all commits
+6
View File
@@ -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
+3 -1
View File
@@ -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.
-14
View File
@@ -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}
+13 -6
View File
@@ -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.
+1 -1
View File
@@ -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",
+8 -2
View File
@@ -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:
+10 -11
View File
@@ -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:
+12
View File
@@ -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"):