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.
185 lines
7.2 KiB
Python
185 lines
7.2 KiB
Python
"""Datastores, sensors/utilisation, CDP/LLDP neighbours and raw device warnings."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from napalm_vmware.parse.environment import environment
|
|
from napalm_vmware.parse.lldp import lldp_neighbors
|
|
from napalm_vmware.parse.storage import storage_pools
|
|
from napalm_vmware.parse.warnings import host_warnings
|
|
|
|
|
|
class TestStoragePools:
|
|
def test_from_vcsim(self, esxi_data):
|
|
pools = storage_pools(esxi_data["objects"]["Datastore"])
|
|
(name,) = pools
|
|
pool = pools[name]
|
|
assert pool["name"] == name
|
|
assert pool["type"] == "other"
|
|
assert pool["used"] == pool["total"] - pool["available"]
|
|
assert pool["enabled"] is True
|
|
assert pool["shared"] is False
|
|
|
|
def test_shared_vmfs(self):
|
|
ds = {
|
|
"_moref": "datastore-1",
|
|
"name": "san01",
|
|
"summary": {
|
|
"type": "VMFS",
|
|
"capacity": 1000,
|
|
"freeSpace": 250,
|
|
"accessible": False,
|
|
"multipleHostAccess": True,
|
|
},
|
|
}
|
|
assert storage_pools([ds])["san01"] == {
|
|
"name": "san01",
|
|
"type": "vmfs",
|
|
"total": 1000,
|
|
"used": 750,
|
|
"available": 250,
|
|
"enabled": False,
|
|
"shared": True,
|
|
}
|
|
|
|
|
|
def _sensor(name, kind, reading, health="green", modifier=0):
|
|
return {
|
|
"name": name,
|
|
"sensorType": kind,
|
|
"currentReading": reading,
|
|
"unitModifier": modifier,
|
|
"healthState": {"key": health},
|
|
}
|
|
|
|
|
|
class TestEnvironment:
|
|
def test_utilisation_of_one_host(self, esxi_host):
|
|
esxi_host["summary.hardware"].update(cpuMhz=2000, numCpuCores=4, memorySize=8 * 1024**3)
|
|
esxi_host["summary.quickStats"].update(overallCpuUsage=2000, overallMemoryUsage=2048)
|
|
env = environment([esxi_host])
|
|
assert env["cpu"] == {"0": {"%usage": 25.0}}
|
|
assert env["memory"] == {"available_ram": 8 * 1024**3, "used_ram": 2048 * 1024**2}
|
|
|
|
def test_sensors(self, esxi_host):
|
|
esxi_host["runtime.healthSystemRuntime.systemHealthInfo.numericSensorInfo"] = [
|
|
_sensor("CPU1 Temp", "temperature", 4500, modifier=-2),
|
|
_sensor("Inlet Temp", "temperature", 41, health="yellow"),
|
|
_sensor("PCH Temp", "temperature", 90, health="red"),
|
|
_sensor("FAN1", "fan", 5400),
|
|
_sensor("FAN2", "fan", 0, health="red"),
|
|
_sensor("PSU1 Output", "power", 180),
|
|
_sensor("12V", "voltage", 12),
|
|
]
|
|
env = environment([esxi_host])
|
|
assert env["temperature"] == {
|
|
"CPU1 Temp": {"temperature": 45.0, "is_alert": False, "is_critical": False},
|
|
"Inlet Temp": {"temperature": 41.0, "is_alert": True, "is_critical": False},
|
|
"PCH Temp": {"temperature": 90.0, "is_alert": True, "is_critical": True},
|
|
}
|
|
assert env["fans"] == {"FAN1": {"status": True}, "FAN2": {"status": False}}
|
|
assert env["power"] == {"PSU1 Output": {"status": True, "capacity": -1.0, "output": 180.0}}
|
|
|
|
def test_several_hosts_are_keyed_by_host(self, vcenter_data):
|
|
hosts = vcenter_data["objects"]["HostSystem"]
|
|
hosts[0]["runtime.healthSystemRuntime.systemHealthInfo.numericSensorInfo"] = [
|
|
_sensor("Inlet Temp", "temperature", 30)
|
|
]
|
|
env = environment(hosts)
|
|
assert set(env["cpu"]) == {h["name"] for h in hosts}
|
|
assert f"{hosts[0]['name']}/Inlet Temp" in env["temperature"]
|
|
assert env["memory"]["available_ram"] == sum(
|
|
h["summary.hardware"]["memorySize"] for h in hosts
|
|
)
|
|
|
|
def test_host_without_stats(self):
|
|
env = environment([{"_moref": "host-1", "name": "h"}])
|
|
assert env["cpu"] == {"0": {"%usage": 0.0}}
|
|
assert env["memory"] == {"available_ram": 0, "used_ram": 0}
|
|
|
|
|
|
class TestLldpNeighbors:
|
|
def test_lldp_preferred_over_cdp(self):
|
|
hints = [
|
|
{
|
|
"device": "vmnic0",
|
|
"connectedSwitchPort": {"devId": "cdp-sw", "portId": "Gi1/0/1"},
|
|
"lldpInfo": {
|
|
"chassisId": "00:11:22:33:44:55",
|
|
"portId": "1/1/7",
|
|
"parameter": [{"key": "System Name", "value": "core-sw"}],
|
|
},
|
|
},
|
|
{"device": "vmnic1", "connectedSwitchPort": {"devId": "cdp-sw", "portId": "Gi1/0/2"}},
|
|
{"device": "vmnic2", "lldpInfo": {"chassisId": "aa:bb", "portId": "5"}},
|
|
{"device": "vmnic3"},
|
|
]
|
|
assert lldp_neighbors(hints) == {
|
|
"vmnic0": [{"hostname": "core-sw", "port": "1/1/7"}],
|
|
"vmnic1": [{"hostname": "cdp-sw", "port": "Gi1/0/2"}],
|
|
"vmnic2": [{"hostname": "aa:bb", "port": "5"}],
|
|
}
|
|
|
|
def test_nothing_heard(self):
|
|
assert lldp_neighbors([]) == {}
|
|
|
|
|
|
class TestHostWarnings:
|
|
def test_healthy_vcsim_host(self, esxi_host):
|
|
assert host_warnings([esxi_host], [], standalone=True) == []
|
|
|
|
def test_maintenance_mode(self, esxi_host):
|
|
esxi_host["runtime.inMaintenanceMode"] = True
|
|
assert host_warnings([esxi_host], [], standalone=True) == [
|
|
{"code": "vmware_host_maintenance_mode", "meta": {"host": "localhost.localdomain"}}
|
|
]
|
|
|
|
@pytest.mark.parametrize("state", ["disconnected", "notResponding"])
|
|
def test_disconnected_host_under_vcenter(self, esxi_host, state):
|
|
esxi_host["runtime.connectionState"] = state
|
|
assert host_warnings([esxi_host], [], standalone=False) == [
|
|
{
|
|
"code": "vmware_host_disconnected",
|
|
"meta": {"host": "localhost.localdomain", "state": state},
|
|
}
|
|
]
|
|
|
|
def test_config_issues(self, esxi_host):
|
|
esxi_host["configIssue"] = [
|
|
{"fullFormattedMessage": "SSH for the host has been enabled"},
|
|
{"fullFormattedMessage": ""},
|
|
]
|
|
assert host_warnings([esxi_host], [], standalone=True) == [
|
|
{
|
|
"code": "vmware_host_config_issue",
|
|
"meta": {
|
|
"host": "localhost.localdomain",
|
|
"issues": ["SSH for the host has been enabled"],
|
|
},
|
|
}
|
|
]
|
|
|
|
def test_standalone_driver_on_a_vcenter_managed_host(self, esxi_host):
|
|
esxi_host["summary.managementServerIp"] = "192.0.2.5"
|
|
assert host_warnings([esxi_host], [], standalone=True) == [
|
|
{"code": "vmware_host_managed_by_vcenter", "meta": {"vcenter": "192.0.2.5"}}
|
|
]
|
|
|
|
def test_vcenter_driver_does_not_report_its_own_management(self, esxi_host):
|
|
esxi_host["summary.managementServerIp"] = "192.0.2.5"
|
|
assert host_warnings([esxi_host], [], standalone=False) == []
|
|
|
|
def test_free_license_makes_the_api_read_only(self, esxi_host):
|
|
licenses = [{"editionKey": "esxBasic", "name": "VMware vSphere 8 Hypervisor"}]
|
|
assert host_warnings([esxi_host], licenses, standalone=True) == [
|
|
{
|
|
"code": "vmware_api_read_only",
|
|
"meta": {"edition": "VMware vSphere 8 Hypervisor"},
|
|
}
|
|
]
|
|
|
|
def test_evaluation_license_is_not_read_only(self, esxi_host):
|
|
licenses = [{"editionKey": "eval", "name": "Evaluation Mode"}]
|
|
assert host_warnings([esxi_host], licenses, standalone=True) == []
|