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,65 @@
|
||||
"""NAPALM ``get_environment`` from host quick stats and hardware sensors."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
_SENSORS = "runtime.healthSystemRuntime.systemHealthInfo.numericSensorInfo"
|
||||
_MB = 1024 * 1024
|
||||
|
||||
|
||||
def _reading(sensor: dict[str, Any]) -> float:
|
||||
return float(sensor.get("currentReading", 0)) * 10 ** int(sensor.get("unitModifier", 0))
|
||||
|
||||
|
||||
def _health(sensor: dict[str, Any]) -> str:
|
||||
return (sensor.get("healthState") or {}).get("key", "unknown")
|
||||
|
||||
|
||||
def _cpu_percent(host: dict[str, Any]) -> float:
|
||||
hardware = host.get("summary.hardware") or {}
|
||||
capacity = int(hardware.get("cpuMhz") or 0) * int(hardware.get("numCpuCores") or 0)
|
||||
used = (host.get("summary.quickStats") or {}).get("overallCpuUsage") or 0
|
||||
return round(used / capacity * 100, 2) if capacity else 0.0
|
||||
|
||||
|
||||
def _add_sensors(env: dict[str, Any], host: dict[str, Any], prefix: str) -> None:
|
||||
for sensor in host.get(_SENSORS) or []:
|
||||
name = f"{prefix}{sensor.get('name', '')}"
|
||||
kind = sensor.get("sensorType")
|
||||
health = _health(sensor)
|
||||
if kind == "temperature":
|
||||
env["temperature"][name] = {
|
||||
"temperature": _reading(sensor),
|
||||
"is_alert": health in ("yellow", "red"),
|
||||
"is_critical": health == "red",
|
||||
}
|
||||
elif kind == "fan":
|
||||
env["fans"][name] = {"status": health == "green"}
|
||||
elif kind == "power":
|
||||
env["power"][name] = {
|
||||
"status": health == "green",
|
||||
"capacity": -1.0,
|
||||
"output": _reading(sensor),
|
||||
}
|
||||
|
||||
|
||||
def environment(hosts: list[dict[str, Any]]) -> dict[str, Any]:
|
||||
"""One host: keyed like a single device. Several (vCenter): keyed by host."""
|
||||
env: dict[str, Any] = {
|
||||
"fans": {},
|
||||
"temperature": {},
|
||||
"power": {},
|
||||
"cpu": {},
|
||||
"memory": {"available_ram": 0, "used_ram": 0},
|
||||
}
|
||||
single = len(hosts) == 1
|
||||
for host in hosts:
|
||||
key = "0" if single else host.get("name", host["_moref"])
|
||||
env["cpu"][key] = {"%usage": _cpu_percent(host)}
|
||||
hardware = host.get("summary.hardware") or {}
|
||||
stats = host.get("summary.quickStats") or {}
|
||||
env["memory"]["available_ram"] += int(hardware.get("memorySize") or 0)
|
||||
env["memory"]["used_ram"] += int(stats.get("overallMemoryUsage") or 0) * _MB
|
||||
_add_sensors(env, host, "" if single else f"{key}/")
|
||||
return env
|
||||
Reference in New Issue
Block a user