Files
napalm-vmware/tests/test_parse_vm_config.py
Christian Manivong c8e472b4c6 feat: NAPALM drivers for VMware ESXi and vCenter
Two drivers from one package, both in the hypervisor role:

- vmware_esxi talks to one host directly: facts, vmnics and vmkernel
  NICs, CDP/LLDP neighbours, sensors, VMs, datastores and port groups.
- vmware_vcenter talks to a vCenter: every VM of every host it manages,
  with the host as the VM's node, plus distributed port groups. It
  reports no interfaces of its own; the hosts' NICs belong to the hosts.

Both implement the HypervisorDriver VM contract: get_vms, get_vm_config,
start/stop/reboot/suspend_vm and the four snapshot methods, and emit raw
device warnings (maintenance mode, disconnected host, config issues,
host managed by a vCenter, free license making the API read-only).

Every read is a PropertyCollector query for the explicit paths in
paths.py, converted by to_plain() into dicts and lists; the parsers
only ever see that. tools/harvest.py dumps exactly those paths to JSON
and tools/sanitize.py scrubs the dump, so a real host can become a test
fixture without code changes. A VM's vmid is its instance UUID, which
survives vMotion and re-registration; a MoRef does not.

Tested against govmomi's vcsim in ESXi and vCenter mode, including real
power and snapshot tasks. Not yet tested against real hardware.
2026-09-24 09:07:30 +02:00

197 lines
6.5 KiB
Python

"""get_vm_config and snapshot trees."""
from __future__ import annotations
import pytest
from napalm_vmware.parse.networks import NetworkIndex, network_index
from napalm_vmware.parse.snapshots import find_snapshot, snapshot_list
from napalm_vmware.parse.vm_config import vm_config
@pytest.fixture
def vm(vcenter_data):
return vcenter_data["objects"]["VirtualMachine"][0]
@pytest.fixture
def index(vcenter_data):
objs = vcenter_data["objects"]
return network_index(objs["HostSystem"], objs["DistributedVirtualPortgroup"])
class TestVmConfig:
def test_core_fields_from_vcsim(self, vm, index):
cfg = vm_config(vm, index)
assert cfg["name"] == "DC0_H0_VM0"
assert cfg["vmid"] == vm["config.instanceUuid"]
assert cfg["vcpus"] == vm["config.hardware.numCPU"]
assert cfg["memory"] == vm["config.hardware.memoryMB"]
assert cfg["os_type"] == vm["config.guestId"]
assert cfg["os_name"] == vm["config.guestFullName"]
assert cfg["description"] == ""
assert cfg["tags"] == []
assert cfg["firmware"] == "bios"
assert cfg["machine"] == "vmx-13"
assert cfg["passthrough"] == []
def test_disk(self, vm, index):
assert vm_config(vm, index)["disks"] == [
{
"device": "disk-202-0",
"storage": "LocalDS_0",
"size": 10,
"format": "vmdk",
"bootable": True, # no boot order: the BIOS boots the first disk
}
]
def test_nic(self, vm, index):
assert vm_config(vm, index)["nics"] == [
{
"device": "ethernet-0",
"mac": "00:0C:29:63:65:61",
"model": "e1000",
"bridge": "DC0_DVPG0",
"vlan_id": 0,
}
]
def test_cpu_topology(self, vm, index):
vm["config.hardware.numCPU"] = 8
vm["config.hardware.numCoresPerSocket"] = 4
cfg = vm_config(vm, index)
assert (cfg["sockets"], cfg["cores_per_socket"]) == (2, 4)
def test_explicit_boot_order(self, vm, index):
disk_key = next(
d["key"] for d in vm["config.hardware.device"] if d["_type"] == "VirtualDisk"
)
nic_key = next(
d["key"] for d in vm["config.hardware.device"] if d["_type"] == "VirtualE1000"
)
vm["config.bootOptions.bootOrder"] = [
{"_type": "VirtualMachineBootOptionsBootableCdromDevice"},
{"_type": "VirtualMachineBootOptionsBootableEthernetDevice", "deviceKey": nic_key},
{"_type": "VirtualMachineBootOptionsBootableDiskDevice", "deviceKey": disk_key},
]
cfg = vm_config(vm, index)
assert cfg["boot_order"] == ["cdrom", "ethernet-0", "disk-202-0"]
assert cfg["disks"][0]["bootable"] is True
def test_disk_outside_the_boot_order_is_not_bootable(self, vm, index):
nic_key = next(
d["key"] for d in vm["config.hardware.device"] if d["_type"] == "VirtualE1000"
)
vm["config.bootOptions.bootOrder"] = [
{"_type": "VirtualMachineBootOptionsBootableEthernetDevice", "deviceKey": nic_key},
]
assert vm_config(vm, index)["disks"][0]["bootable"] is False
def test_passthrough_and_annotation(self, vm, index):
vm["config.annotation"] = "Production web server"
vm["config.hardware.device"].append(
{
"_type": "VirtualPCIPassthrough",
"key": 13000,
"deviceInfo": {"label": "PCI device 0", "summary": "NVIDIA T4"},
}
)
cfg = vm_config(vm, index)
assert cfg["description"] == "Production web server"
assert cfg["passthrough"] == [
{"slot": "PCI device 0", "kind": "pci", "config": "NVIDIA T4"}
]
def test_config_less_vm_is_refused(self, vm):
del vm["config.instanceUuid"]
with pytest.raises(ValueError):
vm_config(vm, NetworkIndex())
def _snap(name, moref, created, children=(), state="poweredOff", description=""):
return {
"name": name,
"snapshot": moref,
"createTime": created,
"state": state,
"description": description,
"childSnapshotList": list(children),
}
@pytest.fixture
def tree_vm():
return {
"name": "web01",
"snapshot": {
"currentSnapshot": "snapshot-3",
"rootSnapshotList": [
_snap(
"base",
"snapshot-1",
100.0,
[
_snap("upgrade", "snapshot-2", 200.0, state="poweredOn", description="d"),
_snap("again", "snapshot-3", 300.0),
],
),
_snap("again", "snapshot-4", 400.0),
],
},
}
class TestSnapshots:
def test_flattened_depth_first_with_parents(self, tree_vm):
assert snapshot_list(tree_vm) == [
{
"name": "base",
"vm": "web01",
"created": 100.0,
"description": "",
"has_memory": False,
"parent": "",
},
{
"name": "upgrade",
"vm": "web01",
"created": 200.0,
"description": "d",
"has_memory": True,
"parent": "base",
},
{
"name": "again",
"vm": "web01",
"created": 300.0,
"description": "",
"has_memory": False,
"parent": "base",
},
{
"name": "again",
"vm": "web01",
"created": 400.0,
"description": "",
"has_memory": False,
"parent": "",
},
]
def test_vm_without_snapshots(self):
assert snapshot_list({"name": "x"}) == []
def test_find_by_name(self, tree_vm):
assert find_snapshot(tree_vm, "upgrade") == "snapshot-2"
def test_unknown_name(self, tree_vm):
with pytest.raises(ValueError, match="no snapshot"):
find_snapshot(tree_vm, "nope")
def test_duplicate_names_are_refused_not_guessed(self, tree_vm):
"""vSphere allows two snapshots with one name. Picking one could roll
back or delete the wrong state."""
with pytest.raises(ValueError, match="2 snapshots"):
find_snapshot(tree_vm, "again")