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,97 @@
|
||||
"""tools/sanitize.py: scrub a harvest dump before it becomes a fixture."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
from pathlib import Path
|
||||
|
||||
_spec = importlib.util.spec_from_file_location(
|
||||
"sanitize", Path(__file__).parent.parent / "tools" / "sanitize.py"
|
||||
)
|
||||
sanitize = importlib.util.module_from_spec(_spec)
|
||||
_spec.loader.exec_module(sanitize)
|
||||
|
||||
|
||||
def _dump():
|
||||
return {
|
||||
"about": {"instanceUuid": "35bb7c82-8526-5aa7-a886-cd7f8e6be786"},
|
||||
"licenses": [{"licenseKey": "AAAAA-BBBBB-CCCCC-DDDDD-EEEEE", "editionKey": "esxBasic"}],
|
||||
"objects": {
|
||||
"HostSystem": [
|
||||
{
|
||||
"_moref": "host-21",
|
||||
"name": "esx01.corp.example.com",
|
||||
"hardware.systemInfo": {"serialNumber": "CZJ1234567"},
|
||||
"config.network.dnsConfig": {
|
||||
"hostName": "esx01",
|
||||
"domainName": "corp.example.com",
|
||||
"address": ["10.1.2.3"],
|
||||
},
|
||||
"config.network.pnic": [{"mac": "3c:ec:ef:01:02:03"}],
|
||||
"summary.managementServerIp": "0.0.0.0",
|
||||
}
|
||||
],
|
||||
"VirtualMachine": [
|
||||
{
|
||||
"_moref": "vm-7",
|
||||
"name": "payroll-db",
|
||||
"config.instanceUuid": "5003a1b2-0000-1111-2222-333344445555",
|
||||
"config.hardware.device": [
|
||||
{"backing": {"fileName": "[ds1] payroll-db/payroll-db.vmdk"}},
|
||||
{"macAddress": "00:50:56:aa:bb:cc"},
|
||||
],
|
||||
"guest.net": [{"ipAddress": ["10.1.2.50", "fe80::1"]}],
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _text(data):
|
||||
import json
|
||||
|
||||
return json.dumps(data)
|
||||
|
||||
|
||||
class TestSanitize:
|
||||
def test_sensitive_values_are_gone_everywhere(self):
|
||||
clean = _text(sanitize.sanitize(_dump()))
|
||||
for secret in (
|
||||
"35bb7c82",
|
||||
"AAAAA-BBBBB",
|
||||
"esx01",
|
||||
"corp.example.com",
|
||||
"CZJ1234567",
|
||||
"10.1.2.3",
|
||||
"3c:ec:ef",
|
||||
"payroll-db",
|
||||
"5003a1b2",
|
||||
"00:50:56:aa:bb:cc",
|
||||
"10.1.2.50",
|
||||
):
|
||||
assert secret not in clean, secret
|
||||
|
||||
def test_structure_and_harmless_values_survive(self):
|
||||
clean = sanitize.sanitize(_dump())
|
||||
vm = clean["objects"]["VirtualMachine"][0]
|
||||
assert vm["_moref"] == "vm-7"
|
||||
assert clean["licenses"][0]["editionKey"] == "esxBasic"
|
||||
assert clean["objects"]["HostSystem"][0]["summary.managementServerIp"] == "0.0.0.0"
|
||||
path = vm["config.hardware.device"][0]["backing"]["fileName"]
|
||||
assert path.startswith("[ds1] ") and path.endswith(".vmdk")
|
||||
|
||||
def test_placeholders_are_stable(self):
|
||||
"""The same original value maps to the same placeholder, so references
|
||||
between objects (a VM's name inside its disk path) still line up."""
|
||||
clean = sanitize.sanitize(_dump())
|
||||
vm = clean["objects"]["VirtualMachine"][0]
|
||||
path = vm["config.hardware.device"][0]["backing"]["fileName"]
|
||||
assert path == f"[ds1] {vm['name']}/{vm['name']}.vmdk"
|
||||
|
||||
def test_valid_shapes(self):
|
||||
clean = sanitize.sanitize(_dump())
|
||||
host = clean["objects"]["HostSystem"][0]
|
||||
assert host["config.network.pnic"][0]["mac"].count(":") == 5
|
||||
ip = clean["objects"]["VirtualMachine"][0]["guest.net"][0]["ipAddress"]
|
||||
assert ip[0].startswith("192.0.2.")
|
||||
assert ip[1].startswith("2001:db8::")
|
||||
Reference in New Issue
Block a user