Files
Christian Manivong c8e472b4c6 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.
2026-09-24 09:07:30 +02:00

95 lines
3.6 KiB
Python

"""NAPALM ``get_facts`` from a host's properties or a vCenter's ``about``."""
from __future__ import annotations
import ipaddress
from typing import Any
#: otherIdentifyingInfo keys that carry a chassis serial, most specific first.
#: Dell reports its service tag as ``ServiceTag``; HPE and Supermicro use
#: ``SerialNumberTag``; blades add ``EnclosureSerialNumberTag`` for the chassis.
_SERIAL_KEYS = ("SerialNumberTag", "EnclosureSerialNumberTag", "ServiceTag")
def host_serial(host: dict[str, Any]) -> str:
"""The host's hardware serial, or ``""`` when it does not report one."""
info = host.get("hardware.systemInfo") or {}
if str(info.get("serialNumber", "")).strip():
return str(info["serialNumber"]).strip()
for source in (info, host.get("summary.hardware") or {}):
by_key: dict[str, str] = {}
for entry in source.get("otherIdentifyingInfo", []):
value = str(entry.get("identifierValue", "")).strip()
key = (entry.get("identifierType") or {}).get("key", "")
if value and key not in by_key:
by_key[key] = value
for key in _SERIAL_KEYS:
if key in by_key:
return by_key[key]
return ""
def _host_names(host: dict[str, Any]) -> tuple[str, str]:
dns = host.get("config.network.dnsConfig") or {}
short = dns.get("hostName") or ""
domain = dns.get("domainName") or ""
if short:
return short, f"{short}.{domain}" if domain else short
name = host.get("name", "")
return name.split(".", 1)[0], name
def host_interface_names(host: dict[str, Any]) -> list[str]:
nics = (host.get("config.network.pnic") or []) + (host.get("config.network.vnic") or [])
return sorted(n["device"] for n in nics if n.get("device"))
def esxi_facts(about: dict[str, Any], host: dict[str, Any]) -> dict[str, Any]:
"""Facts of a standalone ESXi host (or one reached directly)."""
hostname, fqdn = _host_names(host)
# hardware.systemInfo is read straight from SMBIOS; summary.hardware is a
# cached copy that has been seen to disagree with it.
system = host.get("hardware.systemInfo") or {}
summary = host.get("summary.hardware") or {}
product = host.get("config.product") or about
uptime = (host.get("summary.quickStats") or {}).get("uptime")
return {
"hostname": hostname,
"fqdn": fqdn,
"vendor": system.get("vendor") or summary.get("vendor", ""),
"model": system.get("model") or summary.get("model", ""),
"serial_number": host_serial(host),
"os_version": product.get("fullName", ""),
"uptime": float(uptime) if uptime is not None else -1.0,
"interface_list": host_interface_names(host),
}
def _is_ip(value: str) -> bool:
try:
ipaddress.ip_address(value)
except ValueError:
return False
return True
def vcenter_facts(about: dict[str, Any], address: str) -> dict[str, Any]:
"""Facts of a vCenter Server.
The vSphere API does not expose the appliance's own name, uptime or NICs;
those belong to the appliance management API. The address netOrk used to
connect is the best available name, and the instance UUID is the one
identifier that is stable for the lifetime of the installation.
"""
hostname = address if _is_ip(address) else address.split(".", 1)[0]
return {
"hostname": hostname,
"fqdn": address,
"vendor": about.get("vendor", ""),
"model": about.get("name", ""),
"serial_number": about.get("instanceUuid", ""),
"os_version": about.get("fullName", ""),
"uptime": -1.0,
"interface_list": [],
}