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,134 @@
|
||||
"""Power and snapshot actions: which vSphere call each one makes, and refusals."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from napalm_vmware import VmwareEsxiDriver
|
||||
from tests.conftest import FakeInventory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def vm0(esxi_data):
|
||||
return esxi_data["objects"]["VirtualMachine"][0]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def driver(esxi_data):
|
||||
d = VmwareEsxiDriver("esx01", "root", "secret")
|
||||
d._si = MagicMock()
|
||||
d._inventory = FakeInventory(esxi_data)
|
||||
d.mo = MagicMock(name="vm")
|
||||
d._mo = MagicMock(return_value=d.mo)
|
||||
return d
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def waited():
|
||||
with patch("napalm_vmware.actions.wait_for_task") as wait:
|
||||
yield wait
|
||||
|
||||
|
||||
class TestPower:
|
||||
def test_start(self, driver, vm0, waited):
|
||||
vm0["runtime.powerState"] = "poweredOff"
|
||||
driver.start_vm(vm0["name"])
|
||||
driver.mo.PowerOnVM_Task.assert_called_once_with()
|
||||
assert waited.call_args.args[1] is driver.mo.PowerOnVM_Task.return_value
|
||||
|
||||
def test_forced_stop_powers_off(self, driver, vm0, waited):
|
||||
driver.stop_vm(vm0["name"], force=True)
|
||||
driver.mo.PowerOffVM_Task.assert_called_once_with()
|
||||
|
||||
def test_graceful_stop_asks_tools_and_waits_for_power_off(self, driver, vm0):
|
||||
vm0["guest.toolsRunningStatus"] = "guestToolsRunning"
|
||||
with patch("napalm_vmware.actions.wait_until") as until:
|
||||
driver.stop_vm(vm0["name"])
|
||||
driver.mo.ShutdownGuest.assert_called_once_with()
|
||||
driver._inventory.data["objects"]["VirtualMachine"][0]["runtime.powerState"] = "poweredOff"
|
||||
driver._inventory.properties = lambda obj, paths: {"runtime.powerState": "poweredOff"}
|
||||
check = until.call_args.args[0]
|
||||
assert check() is True
|
||||
|
||||
def test_graceful_stop_without_tools_is_refused(self, driver, vm0):
|
||||
with pytest.raises(RuntimeError, match="VMware Tools"):
|
||||
driver.stop_vm(vm0["name"])
|
||||
driver.mo.ShutdownGuest.assert_not_called()
|
||||
|
||||
def test_stopping_a_stopped_vm_is_refused(self, driver, vm0):
|
||||
vm0["runtime.powerState"] = "poweredOff"
|
||||
with pytest.raises(RuntimeError, match="not running"):
|
||||
driver.stop_vm(vm0["name"], force=True)
|
||||
|
||||
def test_graceful_reboot(self, driver, vm0):
|
||||
vm0["guest.toolsRunningStatus"] = "guestToolsRunning"
|
||||
driver.reboot_vm(vm0["name"])
|
||||
driver.mo.RebootGuest.assert_called_once_with()
|
||||
|
||||
def test_forced_reboot_resets(self, driver, vm0, waited):
|
||||
driver.reboot_vm(vm0["name"], force=True)
|
||||
driver.mo.ResetVM_Task.assert_called_once_with()
|
||||
|
||||
def test_suspend(self, driver, vm0, waited):
|
||||
driver.suspend_vm(vm0["name"])
|
||||
driver.mo.SuspendVM_Task.assert_called_once_with()
|
||||
|
||||
def test_suspend_requires_a_running_vm(self, driver, vm0):
|
||||
vm0["runtime.powerState"] = "suspended"
|
||||
with pytest.raises(RuntimeError, match="not running"):
|
||||
driver.suspend_vm(vm0["name"])
|
||||
|
||||
def test_free_license_refusal_is_explained(self, driver, vm0):
|
||||
from pyVmomi import vim
|
||||
|
||||
vm0["runtime.powerState"] = "poweredOff"
|
||||
driver.mo.PowerOnVM_Task.side_effect = vim.fault.RestrictedVersion(msg="prohibited")
|
||||
with pytest.raises(RuntimeError, match="free vSphere Hypervisor"):
|
||||
driver.start_vm(vm0["name"])
|
||||
|
||||
def test_unknown_vm(self, driver):
|
||||
with pytest.raises(ValueError):
|
||||
driver.start_vm("nope")
|
||||
|
||||
|
||||
def _with_snapshot(vm, name="before-upgrade", moref="snapshot-1"):
|
||||
vm["snapshot"] = {
|
||||
"rootSnapshotList": [
|
||||
{"name": name, "snapshot": moref, "createTime": 1.0, "state": "poweredOff"}
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
class TestSnapshots:
|
||||
def test_create(self, driver, vm0, waited):
|
||||
driver.create_vm_snapshot(vm0["name"], "pre", description="d", include_memory=True)
|
||||
driver.mo.CreateSnapshot_Task.assert_called_once_with(
|
||||
name="pre", description="d", memory=True, quiesce=False
|
||||
)
|
||||
|
||||
def test_memory_is_only_captured_from_a_running_vm(self, driver, vm0, waited):
|
||||
vm0["runtime.powerState"] = "poweredOff"
|
||||
driver.create_vm_snapshot(vm0["name"], "pre", include_memory=True)
|
||||
assert driver.mo.CreateSnapshot_Task.call_args.kwargs["memory"] is False
|
||||
|
||||
def test_create_refuses_a_duplicate_name(self, driver, vm0):
|
||||
_with_snapshot(vm0, "pre")
|
||||
with pytest.raises(ValueError, match="already"):
|
||||
driver.create_vm_snapshot(vm0["name"], "pre")
|
||||
|
||||
def test_delete_keeps_the_children(self, driver, vm0, waited):
|
||||
_with_snapshot(vm0)
|
||||
driver.delete_vm_snapshot(vm0["name"], "before-upgrade")
|
||||
assert driver._mo.call_args.args[1] == "snapshot-1"
|
||||
driver.mo.RemoveSnapshot_Task.assert_called_once_with(removeChildren=False)
|
||||
|
||||
def test_rollback(self, driver, vm0, waited):
|
||||
_with_snapshot(vm0)
|
||||
driver.rollback_vm_snapshot(vm0["name"], "before-upgrade")
|
||||
driver.mo.RevertToSnapshot_Task.assert_called_once_with()
|
||||
|
||||
def test_unknown_snapshot(self, driver, vm0):
|
||||
with pytest.raises(ValueError, match="no snapshot"):
|
||||
driver.rollback_vm_snapshot(vm0["name"], "nope")
|
||||
Reference in New Issue
Block a user