Files
napalm-vmware/tests/test_drivers.py
T
Christian Manivong 2b84ceae3a feat: VM provisioning, and reboot_host on ESXi
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.
2026-09-24 10:00:33 +02:00

255 lines
9.2 KiB
Python

"""The two driver classes: identity, connection handling and getters."""
from __future__ import annotations
from importlib.metadata import entry_points
from unittest.mock import MagicMock, patch
import pytest
from napalm.base.exceptions import ConnectionException
from napalm_device_types.roles import role_keys_of
from napalm_vmware import VmwareEsxiDriver, VmwareVcenterDriver
from tests.conftest import FakeInventory
DRIVERS = [VmwareEsxiDriver, VmwareVcenterDriver]
def _driver(cls, data, hostname="esx01.example.lan"):
driver = cls(hostname, "root", "secret")
driver._si = MagicMock()
driver._inventory = FakeInventory(data)
return driver
@pytest.fixture
def esxi(esxi_data):
return _driver(VmwareEsxiDriver, esxi_data)
@pytest.fixture
def vcenter(vcenter_data):
return _driver(VmwareVcenterDriver, vcenter_data, hostname="vc01.example.lan")
class TestIdentity:
@pytest.mark.parametrize("cls", DRIVERS)
def test_entry_point_name_is_the_driver_name(self, cls):
(ep,) = [e for e in entry_points(group="napalm.drivers") if e.name == cls.DRIVER_NAME]
assert ep.load() is cls
@pytest.mark.parametrize("cls", DRIVERS)
def test_is_a_hypervisor_and_nothing_else(self, cls):
assert role_keys_of(cls) == ["hypervisor"]
@pytest.mark.parametrize("cls", DRIVERS)
def test_talks_api_not_ssh(self, cls):
assert cls.USES_SSH is False
@pytest.mark.parametrize("cls", DRIVERS)
def test_never_fingerprints_on_the_vmware_oui(self, cls):
"""00:50:56 is the OUI of *guest* NICs; every VM would score as a host."""
assert not any(p.startswith("00:50:56") for p in cls.OUI_PREFIXES)
def test_fingerprints_exclude_each_other(self):
esxi = {r.pattern: r for r in VmwareEsxiDriver.HTTP_FINGERPRINT}
vc = {r.pattern: r for r in VmwareVcenterDriver.HTTP_FINGERPRINT}
assert esxi["vmware esxi"].mandatory and vc["vmware esxi"].negative
assert vc["vcenter"].mandatory and esxi["vcenter"].negative
@pytest.mark.parametrize(
"method",
[
"get_vms",
"get_vm_config",
"start_vm",
"stop_vm",
"reboot_vm",
"suspend_vm",
"get_vm_snapshots",
"create_vm_snapshot",
"delete_vm_snapshot",
"rollback_vm_snapshot",
"get_vm_storage_pools",
"get_virtual_networks",
"get_device_warnings",
],
)
@pytest.mark.parametrize("cls", DRIVERS)
def test_implements_the_v1_contract(self, cls, method):
assert callable(getattr(cls, method, None))
@pytest.mark.parametrize("cls", DRIVERS)
@pytest.mark.parametrize(
"method",
[
"create_vm_from_cloud_init",
"destroy_vm",
"get_vm_status",
"get_network_targets",
"get_image_storages",
],
)
def test_implements_provisioning(self, cls, method):
assert callable(getattr(cls, method, None))
@pytest.mark.parametrize("cls", DRIVERS)
def test_declares_its_guest_agent(self, cls):
assert cls.GUEST_AGENT_PACKAGES == ("open-vm-tools",)
class TestOptionalArgs:
def test_defaults(self):
d = VmwareEsxiDriver("h", "u", "p")
assert (d._port, d._verify_ssl) == (443, True)
def test_netork_style_args(self):
d = VmwareEsxiDriver("h", "u", "p", optional_args={"port": "8443", "ssl_verify": False})
assert (d._port, d._verify_ssl) == (8443, False)
def test_verify_ssl_wins_over_ssl_verify(self):
d = VmwareEsxiDriver("h", "u", "p", optional_args={"ssl_verify": True, "verify_ssl": False})
assert d._verify_ssl is False
def test_ssh_keys_netork_adds_are_ignored(self):
VmwareEsxiDriver("h", "u", "p", optional_args={"ssh_username": "x", "ssh_port": 22})
class TestConnection:
def _open(self, cls, data, connect_error=None):
driver = cls("host", "u", "p")
si = MagicMock()
with (
patch(
"napalm_vmware.base._session.connect", return_value=si, side_effect=connect_error
) as connect,
patch("napalm_vmware.base._session.disconnect") as disconnect,
patch("napalm_vmware.base.Inventory", return_value=FakeInventory(data)),
):
try:
driver.open()
finally:
self.connect, self.disconnect = connect, disconnect
return driver, si
def test_open_esxi(self, esxi_data):
driver, si = self._open(VmwareEsxiDriver, esxi_data)
assert driver._si is si
self.connect.assert_called_once_with("host", 443, "u", "p", verify_ssl=True, timeout=60)
def test_esxi_driver_refuses_a_vcenter(self, vcenter_data):
with pytest.raises(ConnectionException, match="vmware_vcenter"):
self._open(VmwareEsxiDriver, vcenter_data)
self.disconnect.assert_called_once()
def test_vcenter_driver_refuses_an_esxi_host(self, esxi_data):
with pytest.raises(ConnectionException, match="vmware_esxi"):
self._open(VmwareVcenterDriver, esxi_data)
def test_connect_failure_is_a_connection_exception(self, esxi_data):
with pytest.raises(ConnectionException, match="host"):
self._open(VmwareEsxiDriver, esxi_data, connect_error=OSError("refused"))
def test_close(self, esxi):
si = esxi._si
with patch("napalm_vmware.base._session.disconnect") as disconnect:
esxi.close()
esxi.close() # idempotent
disconnect.assert_called_once_with(si)
assert esxi._inventory is None
def test_is_alive(self, esxi):
with patch("napalm_vmware.base._session.alive", return_value=True):
assert esxi.is_alive() == {"is_alive": True}
esxi._si = None
assert esxi.is_alive() == {"is_alive": False}
class TestEsxiGetters:
def test_facts(self, esxi):
assert esxi.get_facts()["hostname"] == "localhost"
def test_interfaces(self, esxi):
assert set(esxi.get_interfaces()) == {"vmnic0", "vmnic1", "vmk0"}
assert esxi.get_interfaces_ip() == {}
def test_vms(self, esxi):
assert [vm["name"] for vm in esxi.get_vms()] == ["ha-host_VM0", "ha-host_VM1"]
def test_lldp_via_query_network_hint(self, esxi, esxi_host):
esxi_host["configManager.networkSystem"] = "networkSystem"
network_system = MagicMock()
network_system.QueryNetworkHint.return_value = []
esxi._mo = MagicMock(return_value=network_system)
assert esxi.get_lldp_neighbors() == {}
esxi._mo.assert_called_once()
def test_lldp_unsupported_is_empty_not_an_error(self, esxi, esxi_host):
from pyVmomi import vmodl
esxi_host["configManager.networkSystem"] = "networkSystem"
network_system = MagicMock()
network_system.QueryNetworkHint.side_effect = vmodl.fault.NotSupported()
esxi._mo = MagicMock(return_value=network_system)
assert esxi.get_lldp_neighbors() == {}
def test_no_host_visible(self, esxi):
esxi._inventory.objects("HostSystem").clear()
with pytest.raises(RuntimeError, match="no host"):
esxi.get_facts()
def test_warnings(self, esxi, esxi_host):
esxi_host["runtime.inMaintenanceMode"] = True
assert [w["code"] for w in esxi.get_device_warnings()] == ["vmware_host_maintenance_mode"]
def test_storage_and_networks(self, esxi):
assert list(esxi.get_vm_storage_pools())
assert "VM Network" in esxi.get_virtual_networks()
def test_environment(self, esxi):
assert "0" in esxi.get_environment()["cpu"]
class TestVcenterGetters:
def test_facts(self, vcenter):
assert vcenter.get_facts()["hostname"] == "vc01"
def test_host_nics_are_not_the_vcenters(self, vcenter):
"""Reporting every host's vmnics on the vCenter device would attach
their MACs to the wrong device in netOrk's topology."""
assert vcenter.get_interfaces() == {}
assert vcenter.get_interfaces_ip() == {}
assert vcenter.get_lldp_neighbors() == {}
def test_vms_span_all_hosts(self, vcenter):
assert len({vm["node"] for vm in vcenter.get_vms()}) > 1
def test_distributed_portgroups(self, vcenter):
assert vcenter.get_virtual_networks()["DC0_DVPG0"]["type"] == "dvportgroup"
class TestFindVm:
def test_by_name_vmid_or_moref(self, esxi, esxi_data):
vm0 = esxi_data["objects"]["VirtualMachine"][0]
for key in (vm0["name"], vm0["config.instanceUuid"], vm0["_moref"]):
assert esxi.get_vm_config(key)["vmid"] == vm0["config.instanceUuid"]
def test_unknown(self, esxi):
with pytest.raises(ValueError, match="no VM"):
esxi.get_vm_config("nope")
def test_ambiguous_name(self, esxi, esxi_data):
vms = esxi_data["objects"]["VirtualMachine"]
vms[1]["name"] = vms[0]["name"]
with pytest.raises(ValueError, match="2 VMs"):
esxi.get_vm_config(vms[0]["name"])
def test_templates_are_not_found(self, esxi, esxi_data):
vm0 = esxi_data["objects"]["VirtualMachine"][0]
vm0["config.template"] = True
with pytest.raises(ValueError):
esxi.get_vm_config(vm0["name"])
def test_snapshots(self, esxi):
assert esxi.get_vm_snapshots("ha-host_VM0") == []