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,131 @@
|
||||
"""get_vms: VMDict plus the extra keys netOrk's VM sync reads."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from napalm_vmware.parse.networks import network_index
|
||||
from napalm_vmware.parse.vms import vm_list
|
||||
|
||||
|
||||
def _vms(data):
|
||||
objs = data["objects"]
|
||||
index = network_index(objs["HostSystem"], objs.get("DistributedVirtualPortgroup", []))
|
||||
return vm_list(objs["VirtualMachine"], objs["HostSystem"], index)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vm0(esxi_data):
|
||||
return esxi_data["objects"]["VirtualMachine"][0]
|
||||
|
||||
|
||||
class TestVmListFromVcsim:
|
||||
def test_one_entry_per_vm(self, esxi_data):
|
||||
assert [vm["name"] for vm in _vms(esxi_data)] == ["ha-host_VM0", "ha-host_VM1"]
|
||||
|
||||
def test_contract_fields(self, esxi_data, vm0):
|
||||
vm = _vms(esxi_data)[0]
|
||||
assert vm["vmid"] == vm0["config.instanceUuid"]
|
||||
assert vm["moref"] == vm0["_moref"]
|
||||
assert vm["status"] == "running"
|
||||
assert vm["vcpus"] == vm0["config.hardware.numCPU"]
|
||||
assert vm["memory"] == vm0["config.hardware.memoryMB"]
|
||||
assert vm["node"] == "localhost.localdomain"
|
||||
assert vm["type"] == "vm"
|
||||
|
||||
def test_vcenter_node_is_the_esxi_host(self, vcenter_data):
|
||||
nodes = {vm["name"]: vm["node"] for vm in _vms(vcenter_data)}
|
||||
assert nodes["DC0_H0_VM0"] == "DC0_H0"
|
||||
|
||||
def test_nic_on_a_distributed_portgroup(self, vcenter_data):
|
||||
vm = _vms(vcenter_data)[0]
|
||||
(iface,) = vm["interfaces"].values()
|
||||
assert iface["bridge"] == "DC0_DVPG0"
|
||||
assert iface["tag"] is None
|
||||
assert iface["mac_address"] == "00:0C:29:63:65:61"
|
||||
|
||||
def test_disks_in_megabytes(self, vcenter_data):
|
||||
vm = _vms(vcenter_data)[0]
|
||||
assert vm["disks"] == [{"name": "disk-202-0", "size_mb": 10240}]
|
||||
|
||||
def test_tools_not_installed(self, esxi_data):
|
||||
vm = _vms(esxi_data)[0]
|
||||
assert vm["agent_enabled"] is False
|
||||
assert vm["agent_running"] is False
|
||||
|
||||
|
||||
class TestVmListBranches:
|
||||
def test_templates_are_not_vms(self, esxi_data, vm0):
|
||||
vm0["config.template"] = True
|
||||
assert [vm["name"] for vm in _vms(esxi_data)] == ["ha-host_VM1"]
|
||||
|
||||
def test_vm_without_config_is_skipped(self, esxi_data, vm0):
|
||||
"""An orphaned or inaccessible VM has no instanceUuid to identify it by."""
|
||||
del vm0["config.instanceUuid"]
|
||||
assert len(_vms(esxi_data)) == 1
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("power", "status"),
|
||||
[("poweredOn", "running"), ("poweredOff", "stopped"), ("suspended", "suspended")],
|
||||
)
|
||||
def test_power_state(self, esxi_data, vm0, power, status):
|
||||
vm0["runtime.powerState"] = power
|
||||
assert _vms(esxi_data)[0]["status"] == status
|
||||
|
||||
def test_usage_from_quickstats(self, esxi_data, esxi_host, vm0):
|
||||
esxi_host["summary.hardware"]["cpuMhz"] = 2000
|
||||
vm0["config.hardware.numCPU"] = 2
|
||||
vm0["summary.quickStats"].update(
|
||||
overallCpuUsage=1000, guestMemoryUsage=512, uptimeSeconds=3600
|
||||
)
|
||||
vm = _vms(esxi_data)[0]
|
||||
assert vm["cpu_usage"] == 0.25
|
||||
assert vm["memory_usage"] == 512
|
||||
assert vm["uptime"] == 3600
|
||||
|
||||
def test_cpu_usage_is_capped_at_one(self, esxi_data, esxi_host, vm0):
|
||||
esxi_host["summary.hardware"]["cpuMhz"] = 1000
|
||||
vm0["config.hardware.numCPU"] = 1
|
||||
vm0["summary.quickStats"]["overallCpuUsage"] = 1500
|
||||
assert _vms(esxi_data)[0]["cpu_usage"] == 1.0
|
||||
|
||||
def test_tools_running_and_guest_ip(self, esxi_data, vm0):
|
||||
vm0["guest.toolsStatus"] = "toolsOk"
|
||||
vm0["guest.toolsRunningStatus"] = "guestToolsRunning"
|
||||
vm0["guest.net"][0]["ipAddress"] = ["fe80::1", "192.0.2.50"]
|
||||
vm = _vms(esxi_data)[0]
|
||||
assert vm["agent_enabled"] is True
|
||||
assert vm["agent_running"] is True
|
||||
assert vm["ipv4"] == "192.0.2.50"
|
||||
(iface,) = vm["interfaces"].values()
|
||||
assert iface["ipv4"] == "192.0.2.50"
|
||||
assert iface["is_up"] is True
|
||||
|
||||
def test_guest_primary_ip_wins(self, esxi_data, vm0):
|
||||
vm0["guest.ipAddress"] = "192.0.2.99"
|
||||
vm0["guest.net"][0]["ipAddress"] = ["192.0.2.50"]
|
||||
assert _vms(esxi_data)[0]["ipv4"] == "192.0.2.99"
|
||||
|
||||
def test_ipv6_primary_ip_is_not_an_ipv4(self, esxi_data, vm0):
|
||||
vm0["guest.ipAddress"] = "2001:db8::5"
|
||||
assert _vms(esxi_data)[0]["ipv4"] == ""
|
||||
|
||||
def test_vlan_of_a_standard_portgroup_becomes_the_tag(self, esxi_data, esxi_host):
|
||||
esxi_host["config.network.portgroup"][0]["spec"]["vlanId"] = 30
|
||||
vm = _vms(esxi_data)[0]
|
||||
(iface,) = vm["interfaces"].values()
|
||||
assert iface["bridge"] == "VM Network"
|
||||
assert iface["tag"] == 30
|
||||
|
||||
def test_autostart(self, esxi_data, esxi_host, vm0):
|
||||
esxi_host["config.autoStart"]["defaults"]["enabled"] = True
|
||||
esxi_host["config.autoStart"]["powerInfo"] = [
|
||||
{"key": vm0["_moref"], "startAction": "powerOn", "startOrder": 1}
|
||||
]
|
||||
assert [vm["onboot"] for vm in _vms(esxi_data)] == [True, False]
|
||||
|
||||
def test_autostart_entry_ignored_while_the_manager_is_off(self, esxi_data, esxi_host, vm0):
|
||||
esxi_host["config.autoStart"]["powerInfo"] = [
|
||||
{"key": vm0["_moref"], "startAction": "powerOn", "startOrder": 1}
|
||||
]
|
||||
assert _vms(esxi_data)[0]["onboot"] is False
|
||||
Reference in New Issue
Block a user