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.
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
"""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")
|
||||
Reference in New Issue
Block a user