Files
Christian Manivong c8e472b4c6 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.
2026-09-24 09:07:30 +02:00

110 lines
4.0 KiB
Python

"""Power and snapshot operations (the write half of the HypervisorDriver contract)."""
from __future__ import annotations
from typing import Any
from pyVmomi import vim
from napalm_vmware._tasks import invoke, wait_for_task, wait_until
from napalm_vmware.parse.snapshots import find_snapshot, snapshot_list
_TASK_TIMEOUT = 300
_GUEST_SHUTDOWN_TIMEOUT = 300
_RUNNING = "poweredOn"
_TOOLS_RUNNING = "guestToolsRunning"
class VmwareActionsMixin:
"""Mixed into both drivers ahead of :class:`VmwareBaseDriver`."""
_inventory: Any
_find_vm: Any
_mo: Any
def _vm_mo(self, vm: dict[str, Any]) -> Any:
return self._mo(vim.VirtualMachine, vm["_moref"])
def _run(self, call: Any, *args: Any, **kwargs: Any) -> None:
wait_for_task(self._inventory, invoke(call, *args, **kwargs), _TASK_TIMEOUT)
@staticmethod
def _require_running(vm: dict[str, Any], action: str) -> None:
if vm.get("runtime.powerState") != _RUNNING:
raise RuntimeError(f"Cannot {action} VM {vm.get('name')!r}: it is not running")
@staticmethod
def _require_tools(vm: dict[str, Any], action: str) -> None:
if vm.get("guest.toolsRunningStatus") != _TOOLS_RUNNING:
raise RuntimeError(
f"A graceful {action} of {vm.get('name')!r} needs VMware Tools running "
"in the guest; use force to cut power instead"
)
# -- power ---------------------------------------------------------------
def start_vm(self, name: str) -> None:
self._run(self._vm_mo(self._find_vm(name)).PowerOnVM_Task)
def stop_vm(self, name: str, force: bool = False) -> None:
vm = self._find_vm(name)
self._require_running(vm, "stop")
mo = self._vm_mo(vm)
if force:
self._run(mo.PowerOffVM_Task)
return
self._require_tools(vm, "shutdown")
invoke(mo.ShutdownGuest)
wait_until(
lambda: (
self._inventory.properties(mo, ["runtime.powerState"]).get("runtime.powerState")
== "poweredOff"
),
_GUEST_SHUTDOWN_TIMEOUT,
f"guest shutdown of {vm.get('name')!r}",
)
def reboot_vm(self, name: str, force: bool = False) -> None:
vm = self._find_vm(name)
self._require_running(vm, "reboot")
mo = self._vm_mo(vm)
if force:
self._run(mo.ResetVM_Task)
return
# RebootGuest returns once Tools accepted the request; the guest gives
# no signal when it is back, so there is nothing further to wait for.
self._require_tools(vm, "reboot")
invoke(mo.RebootGuest)
def suspend_vm(self, name: str) -> None:
vm = self._find_vm(name)
self._require_running(vm, "suspend")
self._run(self._vm_mo(vm).SuspendVM_Task)
# -- snapshots -------------------------------------------------------------
def create_vm_snapshot(
self, name: str, snapshot: str, description: str = "", include_memory: bool = False
) -> None:
vm = self._find_vm(name)
if any(s["name"] == snapshot for s in snapshot_list(vm)):
raise ValueError(f"VM {vm.get('name')!r} already has a snapshot named {snapshot!r}")
self._run(
self._vm_mo(vm).CreateSnapshot_Task,
name=snapshot,
description=description,
memory=include_memory and vm.get("runtime.powerState") == _RUNNING,
quiesce=False,
)
def _snapshot_mo(self, name: str, snapshot: str) -> Any:
return self._mo(vim.vm.Snapshot, find_snapshot(self._find_vm(name), snapshot))
def delete_vm_snapshot(self, name: str, snapshot: str) -> None:
# removeChildren=False consolidates the snapshot into its children
# instead of discarding them.
self._run(self._snapshot_mo(name, snapshot).RemoveSnapshot_Task, removeChildren=False)
def rollback_vm_snapshot(self, name: str, snapshot: str) -> None:
self._run(self._snapshot_mo(name, snapshot).RevertToSnapshot_Task)