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,121 @@
|
||||
"""``get_vms``: one VMDict per virtual machine, plus the keys netOrk reads.
|
||||
|
||||
Beyond the ``VMDict`` contract, netOrk's VM sync expects ``type``,
|
||||
``interfaces`` (NAPALM shape plus ``ipv4``/``bridge``/``tag``), ``ipv4``,
|
||||
``agent_enabled``/``agent_running`` (here: VMware Tools), ``disks`` and
|
||||
``onboot``. ``moref`` is added so an operator can find the VM in vSphere.
|
||||
|
||||
``vmid`` is the VM's *instance* UUID: stable across vMotion and
|
||||
re-registration, unique within a vCenter, and the key ``SearchIndex.FindByUuid``
|
||||
looks VMs up by. The MoRef is none of those things.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import ipaddress
|
||||
from typing import Any
|
||||
|
||||
from napalm_vmware.parse import vm_devices as dev
|
||||
from napalm_vmware.parse.networks import NetworkIndex, nic_network
|
||||
|
||||
_STATUS = {"poweredOn": "running", "poweredOff": "stopped", "suspended": "suspended"}
|
||||
|
||||
|
||||
def _is_ipv4(address: str) -> bool:
|
||||
try:
|
||||
return isinstance(ipaddress.ip_address(address), ipaddress.IPv4Address)
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def _cpu_usage(vm: dict[str, Any], host_mhz: int) -> float:
|
||||
used = (vm.get("summary.quickStats") or {}).get("overallCpuUsage") or 0
|
||||
capacity = int(vm.get("config.hardware.numCPU") or 0) * host_mhz
|
||||
if not capacity:
|
||||
return 0.0
|
||||
return round(min(used / capacity, 1.0), 4)
|
||||
|
||||
|
||||
def _interfaces(vm: dict[str, Any], index: NetworkIndex) -> dict[str, dict[str, Any]]:
|
||||
guest_nics = {n.get("deviceConfigId"): n for n in vm.get("guest.net") or []}
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for nic in filter(dev.is_nic, dev.devices(vm)):
|
||||
network, vlan = nic_network(nic, index)
|
||||
guest = guest_nics.get(nic.get("key"), {})
|
||||
connected = guest.get("connected", (nic.get("connectable") or {}).get("connected", False))
|
||||
ipv4 = next((a for a in guest.get("ipAddress", []) if _is_ipv4(a)), "")
|
||||
result[dev.label(nic)] = {
|
||||
"is_up": bool(connected),
|
||||
"is_enabled": True,
|
||||
"description": network,
|
||||
"mac_address": nic.get("macAddress", "").upper(),
|
||||
"speed": -1.0,
|
||||
"mtu": 1500,
|
||||
"last_flapped": -1.0,
|
||||
"ipv4": ipv4,
|
||||
"bridge": network,
|
||||
"tag": vlan or None,
|
||||
}
|
||||
return result
|
||||
|
||||
|
||||
def _autostart_vms(hosts: list[dict[str, Any]]) -> set[str]:
|
||||
"""MoRefs of VMs the host's autostart manager will power on at boot."""
|
||||
morefs: set[str] = set()
|
||||
for host in hosts:
|
||||
config = host.get("config.autoStart") or {}
|
||||
if not (config.get("defaults") or {}).get("enabled"):
|
||||
continue
|
||||
for entry in config.get("powerInfo", []):
|
||||
if str(entry.get("startAction", "")).lower() == "poweron":
|
||||
morefs.add(entry.get("key", ""))
|
||||
return morefs
|
||||
|
||||
|
||||
def _vm(
|
||||
vm: dict[str, Any], hosts: dict[str, dict[str, Any]], index: NetworkIndex, onboot: bool
|
||||
) -> dict[str, Any]:
|
||||
host = hosts.get(vm.get("runtime.host", ""), {})
|
||||
stats = vm.get("summary.quickStats") or {}
|
||||
interfaces = _interfaces(vm, index)
|
||||
primary = vm.get("guest.ipAddress") or ""
|
||||
ipv4 = (
|
||||
primary
|
||||
if _is_ipv4(primary)
|
||||
else next((i["ipv4"] for i in interfaces.values() if i["ipv4"]), "")
|
||||
)
|
||||
return {
|
||||
"name": vm.get("name", ""),
|
||||
"vmid": vm["config.instanceUuid"],
|
||||
"moref": vm["_moref"],
|
||||
"type": "vm",
|
||||
"status": _STATUS.get(vm.get("runtime.powerState", ""), "unknown"),
|
||||
"vcpus": int(vm.get("config.hardware.numCPU") or 0),
|
||||
"memory": int(vm.get("config.hardware.memoryMB") or 0),
|
||||
"cpu_usage": _cpu_usage(vm, int((host.get("summary.hardware") or {}).get("cpuMhz") or 0)),
|
||||
"memory_usage": int(stats.get("guestMemoryUsage") or 0),
|
||||
"uptime": int(stats.get("uptimeSeconds") or 0),
|
||||
"node": host.get("name", ""),
|
||||
"interfaces": interfaces,
|
||||
"ipv4": ipv4,
|
||||
"agent_enabled": vm.get("guest.toolsStatus", "toolsNotInstalled") != "toolsNotInstalled",
|
||||
"agent_running": vm.get("guest.toolsRunningStatus") == "guestToolsRunning",
|
||||
"disks": [
|
||||
{"name": dev.label(d), "size_mb": dev.disk_bytes(d) // (1024 * 1024)}
|
||||
for d in filter(dev.is_disk, dev.devices(vm))
|
||||
],
|
||||
"onboot": onboot,
|
||||
}
|
||||
|
||||
|
||||
def vm_list(
|
||||
vms: list[dict[str, Any]], hosts: list[dict[str, Any]], index: NetworkIndex
|
||||
) -> list[dict[str, Any]]:
|
||||
"""All VMs, templates and unidentifiable (config-less) VMs excluded."""
|
||||
by_moref = {h["_moref"]: h for h in hosts}
|
||||
autostart = _autostart_vms(hosts)
|
||||
return [
|
||||
_vm(vm, by_moref, index, vm["_moref"] in autostart)
|
||||
for vm in vms
|
||||
if vm.get("config.instanceUuid") and not vm.get("config.template")
|
||||
]
|
||||
Reference in New Issue
Block a user