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,109 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user