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
+47
View File
@@ -0,0 +1,47 @@
"""Opening and closing a vSphere API session.
Kept apart from everything else because it is the only code that opens a
socket; the tests replace it wholesale.
"""
from __future__ import annotations
import ssl
from typing import Any
from pyVim.connect import Disconnect, SmartConnect
def _ssl_context(verify: bool) -> ssl.SSLContext:
context = ssl.create_default_context()
if not verify:
# ESXi hosts ship a self-signed certificate; most homelab and many
# production hosts keep it.
context.check_hostname = False
context.verify_mode = ssl.CERT_NONE
return context
def connect( # pragma: no cover - opens a live session
host: str, port: int, user: str, password: str, *, verify_ssl: bool, timeout: int
) -> Any:
return SmartConnect(
host=host,
port=port,
user=user,
pwd=password,
sslContext=_ssl_context(verify_ssl),
httpConnectionTimeout=timeout,
)
def disconnect(si: Any) -> None: # pragma: no cover - talks to a live session
Disconnect(si)
def alive(si: Any) -> bool: # pragma: no cover - talks to a live session
try:
si.CurrentTime()
except Exception:
return False
return True