Merge pull request 'feat(vm-provision): declare the guest agent per guest OS, Linux only' (#4) from feat/guest-os into main
This commit was merged in pull request #4.
This commit is contained in:
@@ -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.
|
- `reboot_host` on ESXi, refused outside maintenance mode.
|
||||||
- Declares `open-vm-tools` as the guest agent for provisioned VMs.
|
- 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
|
## [0.1.0] - 2026-09-24
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|||||||
@@ -123,7 +123,9 @@ offers for Proxmox (qcow2/raw):
|
|||||||
A port group fixes its VLAN, so `get_network_targets` reports each one with
|
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
|
`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
|
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
|
Unverified until #305: that each distribution's cloud kernel carries the
|
||||||
PVSCSI and VMXNET3 drivers.
|
PVSCSI and VMXNET3 drivers.
|
||||||
|
|||||||
@@ -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)
|
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:
|
def nocloud_iso(user: str, meta: str, network: dict[str, Any] | None) -> bytes:
|
||||||
"""An ISO 9660 image (Rock Ridge + Joliet) holding the seed files."""
|
"""An ISO 9660 image (Rock Ridge + Joliet) holding the seed files."""
|
||||||
files = {"user-data": user, "meta-data": meta}
|
files = {"user-data": user, "meta-data": meta}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ from napalm_device_types.models import (
|
|||||||
VMProvisionResultDict,
|
VMProvisionResultDict,
|
||||||
VMStatusDict,
|
VMStatusDict,
|
||||||
)
|
)
|
||||||
|
from napalm_device_types.provisioning import GuestAgents, network_config
|
||||||
from pyVmomi import vim
|
from pyVmomi import vim
|
||||||
|
|
||||||
from napalm_vmware import paths
|
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.ovf import ovf_descriptor
|
||||||
from napalm_vmware.provision.placement import Inventory as PlacementInventory
|
from napalm_vmware.provision.placement import Inventory as PlacementInventory
|
||||||
from napalm_vmware.provision.placement import Placement, choose_placement
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -48,11 +49,14 @@ _STATUS = {"poweredOn": "running", "poweredOff": "stopped", "suspended": "suspen
|
|||||||
class VmwareProvisioningMixin:
|
class VmwareProvisioningMixin:
|
||||||
"""Mixed into both drivers ahead of :class:`VmwareBaseDriver`."""
|
"""Mixed into both drivers ahead of :class:`VmwareBaseDriver`."""
|
||||||
|
|
||||||
GUEST_AGENT_PACKAGES: tuple[str, ...] = ("open-vm-tools",)
|
# Linux only: BSD guests on VMware are optional (NetOrk/netork#801). The
|
||||||
# The unit is open-vm-tools on Debian/Ubuntu and vmtoolsd on EL/Fedora.
|
# unit is open-vm-tools on Debian/Ubuntu and vmtoolsd on EL/Fedora.
|
||||||
GUEST_AGENT_RUNCMD: tuple[str, ...] = (
|
GUEST_AGENTS: GuestAgents = {
|
||||||
"systemctl enable --now open-vm-tools || systemctl enable --now vmtoolsd",
|
"linux": (
|
||||||
)
|
("open-vm-tools",),
|
||||||
|
("systemctl enable --now open-vm-tools || systemctl enable --now vmtoolsd",),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
hostname: str
|
hostname: str
|
||||||
_port: int
|
_port: int
|
||||||
@@ -148,9 +152,12 @@ class VmwareProvisioningMixin:
|
|||||||
disk_resize_gb: int | None = None,
|
disk_resize_gb: int | None = None,
|
||||||
storage: str | None = None,
|
storage: str | None = None,
|
||||||
cpu_type: str | None = None,
|
cpu_type: str | None = None,
|
||||||
|
guest_os: str = "linux",
|
||||||
download_timeout: int = 300,
|
download_timeout: int = 300,
|
||||||
timeout: int = 180,
|
timeout: int = 180,
|
||||||
) -> VMProvisionResultDict:
|
) -> 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:
|
if cpu_type is not None:
|
||||||
# CPU compatibility is a cluster setting here (EVC), not a per-VM
|
# 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.
|
# one, and there is no get_vm_cpu_types() that could offer a model.
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ classifiers = [
|
|||||||
]
|
]
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"napalm>=5.0.0",
|
"napalm>=5.0.0",
|
||||||
"napalm-device-types>=2.0.0",
|
"napalm-device-types>=3.0.0",
|
||||||
"pyvmomi>=8.0.1",
|
"pyvmomi>=8.0.1",
|
||||||
# Provisioning: download the image, write the NoCloud seed ISO and its YAML.
|
# Provisioning: download the image, write the NoCloud seed ISO and its YAML.
|
||||||
"requests>=2.31",
|
"requests>=2.31",
|
||||||
|
|||||||
@@ -94,8 +94,14 @@ class TestIdentity:
|
|||||||
assert callable(getattr(cls, method, None))
|
assert callable(getattr(cls, method, None))
|
||||||
|
|
||||||
@pytest.mark.parametrize("cls", DRIVERS)
|
@pytest.mark.parametrize("cls", DRIVERS)
|
||||||
def test_declares_its_guest_agent(self, cls):
|
def test_provisions_linux_guests_with_open_vm_tools(self, cls):
|
||||||
assert cls.GUEST_AGENT_PACKAGES == ("open-vm-tools",)
|
"""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:
|
class TestOptionalArgs:
|
||||||
|
|||||||
@@ -6,8 +6,11 @@ import io
|
|||||||
|
|
||||||
import pycdlib
|
import pycdlib
|
||||||
import yaml
|
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:
|
class TestUserData:
|
||||||
@@ -34,17 +37,13 @@ def test_meta_data():
|
|||||||
|
|
||||||
|
|
||||||
class TestNetworkConfig:
|
class TestNetworkConfig:
|
||||||
def test_dhcp_nics_matched_by_mac(self):
|
"""Written by napalm-device-types, which Proxmox uses as well (NetOrk/netork#794)."""
|
||||||
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},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
def test_no_dhcp_nic_means_no_network_config(self):
|
def test_the_seed_is_built_with_the_generic_network_config(self):
|
||||||
assert network_config([("00:50:56:aa:bb:cc", False)]) is None
|
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:
|
def _read(iso: bytes, name: str) -> str:
|
||||||
|
|||||||
@@ -152,6 +152,18 @@ class TestCreate:
|
|||||||
staged.cache.return_value.vmdk.assert_not_called()
|
staged.cache.return_value.vmdk.assert_not_called()
|
||||||
staged.import_ovf.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):
|
def test_placement_refusal_is_a_runtime_error(self, driver, staged):
|
||||||
with patch.object(prov, "choose_placement", side_effect=ValueError("no host")):
|
with patch.object(prov, "choose_placement", side_effect=ValueError("no host")):
|
||||||
with pytest.raises(RuntimeError, match="no host"):
|
with pytest.raises(RuntimeError, match="no host"):
|
||||||
|
|||||||
Reference in New Issue
Block a user