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:
Christian Manivong
2026-09-24 09:07:30 +02:00
commit c8e472b4c6
47 changed files with 14645 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
"""End-to-end against govmomi's vcsim: real sessions, real tasks, no mocks.
Skipped unless the simulators are running::
docker run -d --rm -p 127.0.0.1:8989:8989 vmware/vcsim -l 0.0.0.0:8989
docker run -d --rm -p 127.0.0.1:8990:8989 vmware/vcsim -esx -l 0.0.0.0:8989
VCSIM_VCENTER_PORT=8989 VCSIM_ESXI_PORT=8990 pytest -m vcsim
vcsim accepts any credentials. It is a simulator: this proves the drivers speak
the vSphere API correctly, not that they understand a particular real host.
"""
from __future__ import annotations
import os
import pytest
from napalm.base.exceptions import ConnectionException
from napalm_vmware import VmwareEsxiDriver, VmwareVcenterDriver
pytestmark = pytest.mark.vcsim
def _driver(cls, env):
port = os.environ.get(env)
if not port:
pytest.skip(f"{env} not set; vcsim not running")
driver = cls("127.0.0.1", "user", "pass", optional_args={"port": port, "verify_ssl": False})
driver.open()
yield driver
driver.close()
@pytest.fixture
def esxi():
yield from _driver(VmwareEsxiDriver, "VCSIM_ESXI_PORT")
@pytest.fixture
def vcenter():
yield from _driver(VmwareVcenterDriver, "VCSIM_VCENTER_PORT")
def test_esxi_reads(esxi):
assert esxi.is_alive() == {"is_alive": True}
assert esxi.get_facts()["os_version"].startswith("VMware ESXi")
assert "vmnic0" in esxi.get_interfaces()
assert esxi.get_vms()
assert esxi.get_vm_storage_pools()
assert esxi.get_virtual_networks()
assert "cpu" in esxi.get_environment()
assert isinstance(esxi.get_device_warnings(), list)
assert isinstance(esxi.get_lldp_neighbors(), dict)
def test_vcenter_reads(vcenter):
assert vcenter.get_facts()["model"] == "VMware vCenter Server"
vms = vcenter.get_vms()
assert len({vm["node"] for vm in vms}) > 1
assert vcenter.get_vm_config(vms[0]["vmid"])["vmid"] == vms[0]["vmid"]
def test_power_cycle(esxi):
vm = esxi.get_vms()[0]
esxi.stop_vm(vm["vmid"], force=True)
assert _status(esxi, vm) == "stopped"
with pytest.raises(RuntimeError, match="not running"):
esxi.suspend_vm(vm["vmid"])
esxi.start_vm(vm["vmid"])
assert _status(esxi, vm) == "running"
esxi.suspend_vm(vm["vmid"])
assert _status(esxi, vm) == "suspended"
esxi.start_vm(vm["vmid"])
esxi.reboot_vm(vm["vmid"], force=True)
assert _status(esxi, vm) == "running"
def test_snapshot_lifecycle(vcenter):
vm = vcenter.get_vms()[0]
vcenter.create_vm_snapshot(vm["vmid"], "netork-test", description="from pytest")
try:
(snap,) = [s for s in vcenter.get_vm_snapshots(vm["vmid"]) if s["name"] == "netork-test"]
assert snap["description"] == "from pytest"
with pytest.raises(ValueError, match="already"):
vcenter.create_vm_snapshot(vm["vmid"], "netork-test")
vcenter.rollback_vm_snapshot(vm["vmid"], "netork-test")
finally:
vcenter.delete_vm_snapshot(vm["vmid"], "netork-test")
assert not [s for s in vcenter.get_vm_snapshots(vm["vmid"]) if s["name"] == "netork-test"]
def test_wrong_driver_is_refused():
port = os.environ.get("VCSIM_VCENTER_PORT")
if not port:
pytest.skip("VCSIM_VCENTER_PORT not set")
driver = VmwareEsxiDriver(
"127.0.0.1", "u", "p", optional_args={"port": port, "verify_ssl": False}
)
with pytest.raises(ConnectionException, match="vmware_vcenter"):
driver.open()
def _status(driver, vm):
return next(v["status"] for v in driver.get_vms() if v["vmid"] == vm["vmid"])