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,94 @@
|
||||
"""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": [],
|
||||
}
|
||||
Reference in New Issue
Block a user