create_vm_from_cloud_init takes the same qcow2/raw cloud images netOrk offers for Proxmox. ESXi can neither boot nor download them, so the driver does both: download with checksum check and one retry, convert with qemu-img to a streamOptimized VMDK (cached by URL), import through a minimal OVF descriptor over NFC, pin requested MACs, grow the disk, and attach a NoCloud seed ISO placed next to the VM's files. NoCloud rather than guestinfo because it needs nothing in the guest; the user-data installs open-vm-tools, which the driver declares as its guest agent. A failure after the import removes the VM again. Placement is a pure decision over inventory rows: a connected host outside maintenance mode that sees the datastore and every port group, with the resource pool and VM folder found by walking up to the datacenter -- one path for a standalone host and for a vCenter. Two faults vcsim surfaced and the tests now pin: a chunked upload body next to a Content-Length is refused with 500, so the disk goes up as a sized file object that also reports lease progress; and a device edit replaces the device as sent, so disk and NIC edits start from the live objects, backing included (vcsim panicked on a disk without one). destroy_vm, get_vm_status, get_network_targets (port groups with their fixed VLAN) and get_image_storages complete the contract. reboot_host on ESXi uses RebootHost_Task and refuses outside maintenance mode: force=True would cut power to running VMs. Tested against vcsim in ESXi and vCenter mode, end to end.
153 lines
5.7 KiB
Python
153 lines
5.7 KiB
Python
"""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")
|
|
|
|
|
|
class TestRebootHost:
|
|
def test_esxi_in_maintenance_mode_is_rebooted(self, driver, esxi_host):
|
|
esxi_host["runtime.inMaintenanceMode"] = True
|
|
driver.reboot_host()
|
|
driver.mo.RebootHost_Task.assert_called_once_with(force=False)
|
|
|
|
def test_refused_outside_maintenance_mode(self, driver):
|
|
"""force=True would power off running VMs without asking them."""
|
|
with pytest.raises(RuntimeError, match="maintenance mode"):
|
|
driver.reboot_host()
|
|
driver.mo.RebootHost_Task.assert_not_called()
|
|
|
|
def test_a_vcenter_cannot_reboot_itself(self):
|
|
from napalm_vmware import VmwareVcenterDriver
|
|
|
|
assert not hasattr(VmwareVcenterDriver, "reboot_host")
|