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,106 @@
|
||||
"""Port groups: what exists, which VLAN each carries, and which one a NIC uses.
|
||||
|
||||
Two kinds of network can back a VM NIC. A *standard* port group lives on one
|
||||
host's vSwitch and is named in the NIC's backing directly. A *distributed*
|
||||
port group lives on a vCenter-managed vDS, and the NIC names it by key
|
||||
(``dvportgroup-12``) -- so its display name and VLAN have to be looked up.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any
|
||||
|
||||
#: vCenter's default name for a vDS's uplink port group, used when the server
|
||||
#: is too old to report ``config.uplink``.
|
||||
_UPLINK_NAME = re.compile(r"-DVUplinks-\d+$")
|
||||
|
||||
|
||||
@dataclass
|
||||
class NetworkIndex:
|
||||
"""Port group name -> VLAN, and dvPortgroup key -> (name, VLAN)."""
|
||||
|
||||
standard: dict[str, int] = field(default_factory=dict)
|
||||
distributed: dict[str, tuple[str, int]] = field(default_factory=dict)
|
||||
|
||||
|
||||
def dv_vlan(port_config: dict[str, Any] | None) -> int:
|
||||
"""The single VLAN of a dvPortgroup, or 0 for untagged/trunk."""
|
||||
vlan = (port_config or {}).get("vlan") or {}
|
||||
kind = vlan.get("_type", "")
|
||||
if kind.endswith("PvlanSpec"):
|
||||
return int(vlan.get("pvlanId", 0))
|
||||
if kind.endswith("VlanIdSpec"):
|
||||
return int(vlan.get("vlanId", 0))
|
||||
return 0
|
||||
|
||||
|
||||
def _standard_portgroups(hosts: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
return [
|
||||
pg.get("spec") or {} for host in hosts for pg in host.get("config.network.portgroup") or []
|
||||
]
|
||||
|
||||
|
||||
def network_index(hosts: list[dict[str, Any]], dv_portgroups: list[dict[str, Any]]) -> NetworkIndex:
|
||||
index = NetworkIndex()
|
||||
for spec in _standard_portgroups(hosts):
|
||||
index.standard.setdefault(spec.get("name", ""), int(spec.get("vlanId", 0)))
|
||||
for pg in dv_portgroups:
|
||||
vlan = dv_vlan(pg.get("config.defaultPortConfig"))
|
||||
index.distributed[pg["_moref"]] = (pg.get("name", pg["_moref"]), vlan)
|
||||
return index
|
||||
|
||||
|
||||
def nic_network(nic: dict[str, Any], index: NetworkIndex) -> tuple[str, int]:
|
||||
"""``(network name, VLAN)`` a virtual NIC is connected to."""
|
||||
backing = nic.get("backing") or {}
|
||||
kind = backing.get("_type", "")
|
||||
if kind.endswith("DistributedVirtualPortBackingInfo"):
|
||||
key = (backing.get("port") or {}).get("portgroupKey", "")
|
||||
return index.distributed.get(key, (key, 0))
|
||||
if kind.endswith("OpaqueNetworkBackingInfo"):
|
||||
return backing.get("opaqueNetworkId", ""), 0
|
||||
name = backing.get("deviceName", "")
|
||||
return name, index.standard.get(name, 0)
|
||||
|
||||
|
||||
def _network(name: str, kind: str, bridge: str, vlan: int) -> dict[str, Any]:
|
||||
return {
|
||||
"name": name,
|
||||
"type": kind,
|
||||
"bridge": bridge,
|
||||
"vlan_id": vlan,
|
||||
"autostart": True,
|
||||
"active": True,
|
||||
}
|
||||
|
||||
|
||||
def _is_uplink(pg: dict[str, Any]) -> bool:
|
||||
if "config.uplink" in pg:
|
||||
return bool(pg["config.uplink"])
|
||||
return bool(_UPLINK_NAME.search(pg.get("name", "")))
|
||||
|
||||
|
||||
def virtual_networks(
|
||||
hosts: list[dict[str, Any]],
|
||||
dv_portgroups: list[dict[str, Any]],
|
||||
dv_switches: list[dict[str, Any]],
|
||||
) -> dict[str, dict[str, Any]]:
|
||||
"""Every network a VM can attach to, as ``get_virtual_networks`` returns it."""
|
||||
result: dict[str, dict[str, Any]] = {}
|
||||
for spec in _standard_portgroups(hosts):
|
||||
name = spec.get("name", "")
|
||||
result.setdefault(
|
||||
name,
|
||||
_network(name, "portgroup", spec.get("vswitchName", ""), int(spec.get("vlanId", 0))),
|
||||
)
|
||||
switch_names = {s["_moref"]: s.get("name", s["_moref"]) for s in dv_switches}
|
||||
for pg in dv_portgroups:
|
||||
if _is_uplink(pg):
|
||||
continue
|
||||
dvs = pg.get("config.distributedVirtualSwitch", "")
|
||||
vlan = dv_vlan(pg.get("config.defaultPortConfig"))
|
||||
name = pg.get("name", pg["_moref"])
|
||||
result[name] = _network(name, "dvportgroup", switch_names.get(dvs, dvs), vlan)
|
||||
return result
|
||||
Reference in New Issue
Block a user