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,96 @@
|
||||
"""``get_vm_config``: a VM's virtual hardware as a VMConfigDict."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from napalm_device_types.models import VMConfigDict, VMDiskDict, VMNICDict
|
||||
|
||||
from napalm_vmware.parse import vm_devices as dev
|
||||
from napalm_vmware.parse.networks import NetworkIndex, nic_network
|
||||
|
||||
_GB = 1024**3
|
||||
_BOOT_KIND = {
|
||||
"VirtualMachineBootOptionsBootableCdromDevice": "cdrom",
|
||||
"VirtualMachineBootOptionsBootableFloppyDevice": "floppy",
|
||||
}
|
||||
|
||||
|
||||
def _boot_order(vm: dict[str, Any], labels: dict[int, str]) -> list[str]:
|
||||
order: list[str] = []
|
||||
for entry in vm.get("config.bootOptions.bootOrder") or []:
|
||||
kind = entry.get("_type", "")
|
||||
if kind in _BOOT_KIND:
|
||||
order.append(_BOOT_KIND[kind])
|
||||
elif "deviceKey" in entry:
|
||||
order.append(labels.get(entry["deviceKey"], str(entry["deviceKey"])))
|
||||
return order
|
||||
|
||||
|
||||
def _disks(vm: dict[str, Any], boot_order: list[str]) -> list[VMDiskDict]:
|
||||
disks = [d for d in dev.devices(vm) if dev.is_disk(d)]
|
||||
result: list[VMDiskDict] = []
|
||||
for position, disk in enumerate(disks):
|
||||
name = dev.label(disk)
|
||||
# Without an explicit boot order the firmware boots the first disk.
|
||||
bootable = name in boot_order if boot_order else position == 0
|
||||
result.append(
|
||||
{
|
||||
"device": name,
|
||||
"storage": dev.disk_datastore(disk),
|
||||
"size": dev.disk_bytes(disk) // _GB,
|
||||
"format": "vmdk",
|
||||
"bootable": bootable,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def _nics(vm: dict[str, Any], index: NetworkIndex) -> list[VMNICDict]:
|
||||
result: list[VMNICDict] = []
|
||||
for nic in filter(dev.is_nic, dev.devices(vm)):
|
||||
network, vlan = nic_network(nic, index)
|
||||
result.append(
|
||||
{
|
||||
"device": dev.label(nic),
|
||||
"mac": nic.get("macAddress", "").upper(),
|
||||
"model": dev.nic_model(nic),
|
||||
"bridge": network,
|
||||
"vlan_id": vlan,
|
||||
}
|
||||
)
|
||||
return result
|
||||
|
||||
|
||||
def vm_config(vm: dict[str, Any], index: NetworkIndex) -> VMConfigDict:
|
||||
"""The VM's hardware. Raises ``ValueError`` for a VM without a config."""
|
||||
if not vm.get("config.instanceUuid"):
|
||||
raise ValueError(f"VM {vm.get('name', vm.get('_moref'))!r} has no readable configuration")
|
||||
labels = {d["key"]: dev.label(d) for d in dev.devices(vm) if "key" in d}
|
||||
boot_order = _boot_order(vm, labels)
|
||||
vcpus = int(vm.get("config.hardware.numCPU") or 0)
|
||||
cores = int(vm.get("config.hardware.numCoresPerSocket") or 1)
|
||||
result: VMConfigDict = {
|
||||
"name": vm.get("name", ""),
|
||||
"vmid": vm["config.instanceUuid"],
|
||||
"vcpus": vcpus,
|
||||
"memory": int(vm.get("config.hardware.memoryMB") or 0),
|
||||
"os_type": vm.get("config.guestId", ""),
|
||||
"boot_order": boot_order,
|
||||
"disks": _disks(vm, boot_order),
|
||||
"nics": _nics(vm, index),
|
||||
"description": vm.get("config.annotation", ""),
|
||||
"tags": [],
|
||||
"os_name": vm.get("config.guestFullName", ""),
|
||||
"sockets": max(vcpus // cores, 1),
|
||||
"cores_per_socket": cores,
|
||||
"firmware": vm.get("config.firmware", "bios"),
|
||||
"passthrough": [
|
||||
{"slot": dev.label(d), "kind": kind, "config": dev.summary(d)}
|
||||
for d in dev.devices(vm)
|
||||
if (kind := dev.passthrough_kind(d))
|
||||
],
|
||||
}
|
||||
if vm.get("config.version"):
|
||||
result["machine"] = vm["config.version"]
|
||||
return result
|
||||
Reference in New Issue
Block a user