"""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)