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 @@
|
||||
"""Classify and read entries of a VM's ``config.hardware.device`` list."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from typing import Any
|
||||
|
||||
_NIC_TYPES = frozenset(
|
||||
{
|
||||
"VirtualE1000",
|
||||
"VirtualE1000e",
|
||||
"VirtualPCNet32",
|
||||
"VirtualVmxnet",
|
||||
"VirtualVmxnet2",
|
||||
"VirtualVmxnet3",
|
||||
"VirtualVmxnet3Vrdma",
|
||||
"VirtualSriovEthernetCard",
|
||||
}
|
||||
)
|
||||
_PASSTHROUGH_KIND = {"VirtualPCIPassthrough": "pci", "VirtualUSB": "usb"}
|
||||
_DATASTORE_IN_PATH = re.compile(r"^\[([^\]]+)\]")
|
||||
|
||||
|
||||
def devices(vm: dict[str, Any]) -> list[dict[str, Any]]:
|
||||
return vm.get("config.hardware.device") or []
|
||||
|
||||
|
||||
def is_nic(device: dict[str, Any]) -> bool:
|
||||
return device.get("_type") in _NIC_TYPES
|
||||
|
||||
|
||||
def is_disk(device: dict[str, Any]) -> bool:
|
||||
return device.get("_type") == "VirtualDisk"
|
||||
|
||||
|
||||
def passthrough_kind(device: dict[str, Any]) -> str | None:
|
||||
return _PASSTHROUGH_KIND.get(device.get("_type", ""))
|
||||
|
||||
|
||||
def label(device: dict[str, Any]) -> str:
|
||||
"""The name vSphere shows for a device (``"Hard disk 1"``), else its key."""
|
||||
info = device.get("deviceInfo") or {}
|
||||
return info.get("label") or str(device.get("key", ""))
|
||||
|
||||
|
||||
def summary(device: dict[str, Any]) -> str:
|
||||
return (device.get("deviceInfo") or {}).get("summary", "")
|
||||
|
||||
|
||||
def nic_model(device: dict[str, Any]) -> str:
|
||||
"""``"vmxnet3"``, ``"e1000e"`` ... -- the type name without ``Virtual``."""
|
||||
return device.get("_type", "").removeprefix("Virtual").lower()
|
||||
|
||||
|
||||
def disk_bytes(device: dict[str, Any]) -> int:
|
||||
if device.get("capacityInBytes"):
|
||||
return int(device["capacityInBytes"])
|
||||
return int(device.get("capacityInKB", 0)) * 1024
|
||||
|
||||
|
||||
def disk_datastore(device: dict[str, Any]) -> str:
|
||||
"""Datastore name from the backing file path ``"[datastore1] vm/vm.vmdk"``."""
|
||||
path = (device.get("backing") or {}).get("fileName", "")
|
||||
match = _DATASTORE_IN_PATH.match(path)
|
||||
return match.group(1) if match else ""
|
||||
Reference in New Issue
Block a user