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,87 @@
|
||||
"""NAPALM interface getters for an ESXi host.
|
||||
|
||||
A host has two kinds of interface worth reporting: physical uplinks
|
||||
(``vmnicN``) and VMkernel adapters (``vmkN``), which carry the host's own IP
|
||||
addresses. Port groups are not interfaces; they are reported by
|
||||
``get_virtual_networks``.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from typing import Any
|
||||
|
||||
_DEFAULT_MTU = 1500
|
||||
_UNASSIGNED = {"", "0.0.0.0"}
|
||||
|
||||
|
||||
def _uplink_mtus(host: dict[str, Any]) -> dict[str, int]:
|
||||
"""pnic key -> MTU of the standard vSwitch that uses it as an uplink."""
|
||||
mtus: dict[str, int] = {}
|
||||
for vswitch in host.get("config.network.vswitch") or []:
|
||||
for pnic_key in vswitch.get("pnic", []):
|
||||
mtus[pnic_key] = int(vswitch.get("mtu") or _DEFAULT_MTU)
|
||||
return mtus
|
||||
|
||||
|
||||
def _iface(up: bool, description: str, speed: float, mtu: int, mac: str) -> dict[str, Any]:
|
||||
return {
|
||||
"is_up": up,
|
||||
"is_enabled": True,
|
||||
"description": description,
|
||||
"last_flapped": -1.0,
|
||||
"speed": speed,
|
||||
"mtu": mtu,
|
||||
"mac_address": mac.upper(),
|
||||
}
|
||||
|
||||
|
||||
def host_interfaces(host: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
"""vmnics and vmks in the shape of NAPALM's ``get_interfaces``."""
|
||||
mtus = _uplink_mtus(host)
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for pnic in host.get("config.network.pnic") or []:
|
||||
link = pnic.get("linkSpeed") or {}
|
||||
description = " ".join(p for p in (pnic.get("driver"), pnic.get("pci")) if p)
|
||||
result[pnic["device"]] = _iface(
|
||||
up=bool(link),
|
||||
description=description,
|
||||
speed=float(link.get("speedMb", 0)),
|
||||
mtu=mtus.get(pnic.get("key", ""), _DEFAULT_MTU),
|
||||
mac=pnic.get("mac", ""),
|
||||
)
|
||||
for vnic in host.get("config.network.vnic") or []:
|
||||
spec = vnic.get("spec") or {}
|
||||
result[vnic["device"]] = _iface(
|
||||
up=True,
|
||||
description=vnic.get("portgroup") or spec.get("portgroup", ""),
|
||||
speed=0.0,
|
||||
mtu=int(spec.get("mtu") or _DEFAULT_MTU),
|
||||
mac=spec.get("mac", ""),
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _ipv4(ip: dict[str, Any]) -> dict[str, dict[str, int]]:
|
||||
address = ip.get("ipAddress", "")
|
||||
if address in _UNASSIGNED:
|
||||
return {}
|
||||
network = ipaddress.IPv4Network(f"0.0.0.0/{ip.get('subnetMask') or '32'}")
|
||||
return {address: {"prefix_length": network.prefixlen}}
|
||||
|
||||
|
||||
def _ipv6(ip: dict[str, Any]) -> dict[str, dict[str, int]]:
|
||||
entries = (ip.get("ipV6Config") or {}).get("ipV6Address", [])
|
||||
return {e["ipAddress"]: {"prefix_length": int(e.get("prefixLength", 128))} for e in entries}
|
||||
|
||||
|
||||
def host_interfaces_ip(host: dict[str, Any]) -> dict[str, dict[str, Any]]:
|
||||
"""Addresses of the VMkernel adapters, as NAPALM's ``get_interfaces_ip``."""
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for vnic in host.get("config.network.vnic") or []:
|
||||
ip = (vnic.get("spec") or {}).get("ip") or {}
|
||||
families = {"ipv4": _ipv4(ip), "ipv6": _ipv6(ip)}
|
||||
families = {k: v for k, v in families.items() if v}
|
||||
if families:
|
||||
result[vnic["device"]] = families
|
||||
return result
|
||||
Reference in New Issue
Block a user