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.
84 lines
2.7 KiB
Python
84 lines
2.7 KiB
Python
"""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)
|