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,83 @@
|
||||
"""vSphere tasks and faults, in the terms of the HypervisorDriver contract.
|
||||
|
||||
Contract methods raise ``RuntimeError`` when the platform refuses an
|
||||
operation. vSphere reports refusals two ways: a ``MethodFault`` raised by the
|
||||
call itself, or a task that ends in state ``error``. Both end up here.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from pyVmomi import vmodl
|
||||
|
||||
from napalm_vmware._plain import to_plain
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
_POLL_SECONDS = 0.5
|
||||
_FREE_LICENSE = (
|
||||
"the host runs the free vSphere Hypervisor license, which makes the "
|
||||
"vSphere API read-only; assign a paid license to change VMs from netOrk"
|
||||
)
|
||||
|
||||
|
||||
def fault_message(fault: dict[str, Any]) -> str:
|
||||
"""A readable sentence for a (plain) vSphere fault."""
|
||||
if fault.get("_type") == "RestrictedVersion":
|
||||
return _FREE_LICENSE
|
||||
return fault.get("localizedMessage") or fault.get("msg") or fault.get("_type", "unknown fault")
|
||||
|
||||
|
||||
def invoke(call: Callable[..., T], *args: Any, **kwargs: Any) -> T:
|
||||
"""Run a vSphere call, re-raising any ``MethodFault`` as ``RuntimeError``."""
|
||||
try:
|
||||
return call(*args, **kwargs)
|
||||
except vmodl.MethodFault as fault:
|
||||
raise RuntimeError(fault_message(to_plain(fault))) from fault
|
||||
|
||||
|
||||
def wait_for_task(
|
||||
inventory: Any,
|
||||
task: Any,
|
||||
timeout: float,
|
||||
*,
|
||||
clock: Callable[[], float] = time.monotonic,
|
||||
sleep: Callable[[float], None] = time.sleep,
|
||||
) -> None:
|
||||
"""Block until ``task`` succeeds; raise ``RuntimeError`` otherwise."""
|
||||
deadline = clock() + timeout
|
||||
while True:
|
||||
info = inventory.properties(task, ["info.state", "info.error"])
|
||||
if not info:
|
||||
raise RuntimeError("the vSphere task disappeared before it finished")
|
||||
state = info.get("info.state")
|
||||
if state == "success":
|
||||
return
|
||||
if state == "error":
|
||||
raise RuntimeError(fault_message(info.get("info.error") or {}))
|
||||
if clock() >= deadline:
|
||||
raise RuntimeError(f"the vSphere task did not finish within {timeout:g}s")
|
||||
sleep(_POLL_SECONDS)
|
||||
|
||||
|
||||
def wait_until(
|
||||
check: Callable[[], bool],
|
||||
timeout: float,
|
||||
what: str,
|
||||
*,
|
||||
clock: Callable[[], float] = time.monotonic,
|
||||
sleep: Callable[[float], None] = time.sleep,
|
||||
) -> None:
|
||||
"""Poll ``check`` until it is true; for operations vSphere runs without a task.
|
||||
|
||||
``ShutdownGuest`` only *asks* VMware Tools to shut the guest down and
|
||||
returns at once, so completion has to be observed from the power state.
|
||||
"""
|
||||
deadline = clock() + timeout
|
||||
while not check():
|
||||
if clock() >= deadline:
|
||||
raise RuntimeError(f"{what} did not finish within {timeout:g}s")
|
||||
sleep(_POLL_SECONDS)
|
||||
Reference in New Issue
Block a user