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.
107 lines
3.8 KiB
Python
107 lines
3.8 KiB
Python
"""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
|