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.
110 lines
4.0 KiB
Python
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)
|