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.
197 lines
6.5 KiB
Python
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")
|