Files
napalm-vmware/tests/test_inventory.py
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

134 lines
5.1 KiB
Python

"""Inventory: the single seam between the drivers and the vSphere API."""
from __future__ import annotations
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
from pyVmomi import vim
from napalm_vmware._inventory import Inventory
def _content(obj, props, missing=()):
return SimpleNamespace(
obj=obj,
propSet=[SimpleNamespace(name=k, val=v) for k, v in props.items()],
missingSet=[SimpleNamespace(path=p) for p in missing],
)
@pytest.fixture
def content():
"""pyVmomi type-checks spec fields, so the objects put into them are real."""
c = MagicMock()
c.about = vim.AboutInfo(apiType="HostAgent", fullName="VMware ESXi 8.0.2")
view = vim.view.ContainerView("session[1]view-1", None)
view.Destroy = MagicMock()
c.viewManager.CreateContainerView.return_value = view
c.licenseManager = vim.LicenseManager("LicenseManager", None)
return c
class TestCollect:
def test_returns_plain_dicts_keyed_by_path_with_moref(self, content):
pc = content.propertyCollector
pc.RetrievePropertiesEx.return_value = SimpleNamespace(
objects=[
_content(
vim.VirtualMachine("vm-1", None),
{"name": "web01", "runtime.powerState": "poweredOn"},
)
],
token=None,
)
rows = Inventory(content).collect("VirtualMachine", ["name", "runtime.powerState"])
assert rows == [{"_moref": "vm-1", "name": "web01", "runtime.powerState": "poweredOn"}]
def test_follows_continuation_tokens(self, content):
pc = content.propertyCollector
pc.RetrievePropertiesEx.return_value = SimpleNamespace(
objects=[_content(vim.VirtualMachine("vm-1", None), {"name": "a"})], token="t1"
)
pc.ContinueRetrievePropertiesEx.return_value = SimpleNamespace(
objects=[_content(vim.VirtualMachine("vm-2", None), {"name": "b"})], token=None
)
rows = Inventory(content).collect("VirtualMachine", ["name"])
assert [r["_moref"] for r in rows] == ["vm-1", "vm-2"]
pc.ContinueRetrievePropertiesEx.assert_called_once_with(token="t1")
def test_empty_result(self, content):
content.propertyCollector.RetrievePropertiesEx.return_value = None
assert Inventory(content).collect("Datastore", ["name"]) == []
def test_missing_properties_are_simply_absent(self, content):
"""An inaccessible VM has no config; the row must still come back."""
content.propertyCollector.RetrievePropertiesEx.return_value = SimpleNamespace(
objects=[
_content(vim.VirtualMachine("vm-9", None), {"name": "ghost"}, ["config.template"])
],
token=None,
)
rows = Inventory(content).collect("VirtualMachine", ["name", "config.template"])
assert rows == [{"_moref": "vm-9", "name": "ghost"}]
def test_the_container_view_is_destroyed(self, content):
content.propertyCollector.RetrievePropertiesEx.return_value = None
Inventory(content).collect("HostSystem", ["name"])
content.viewManager.CreateContainerView.return_value.Destroy.assert_called_once()
def test_unknown_type_name_is_rejected(self, content):
with pytest.raises(ValueError, match="NoSuchThing"):
Inventory(content).collect("NoSuchThing", ["name"])
def test_about_is_plain(content):
assert Inventory(content).about()["apiType"] == "HostAgent"
def test_licenses_are_read_from_the_license_manager(content):
content.propertyCollector.RetrievePropertiesEx.return_value = SimpleNamespace(
objects=[
_content(
vim.LicenseManager("LicenseManager", None),
{"licenses": [vim.LicenseManager.LicenseInfo(editionKey="esxBasic", name="Free")]},
)
],
token=None,
)
assert Inventory(content).licenses()[0]["editionKey"] == "esxBasic"
def test_licenses_empty_without_a_license_manager(content):
content.licenseManager = None
assert Inventory(content).licenses() == []
def test_properties_of_a_single_object(content):
content.propertyCollector.RetrievePropertiesEx.return_value = SimpleNamespace(
objects=[_content(vim.Task("task-7", None), {"info.state": "success"})], token=None
)
task = vim.Task("task-7", None)
assert Inventory(content).properties(task, ["info.state"]) == {
"_moref": "task-7",
"info.state": "success",
}
def test_properties_of_a_vanished_object(content):
content.propertyCollector.RetrievePropertiesEx.return_value = None
assert Inventory(content).properties(vim.Task("task-7", None), ["info.state"]) == {}
def test_raw_properties_keep_the_pyvmomi_objects(content):
disk = vim.vm.device.VirtualDisk(key=2000, capacityInKB=1024)
content.propertyCollector.RetrievePropertiesEx.return_value = SimpleNamespace(
objects=[_content(vim.VirtualMachine("vm-1", None), {"config.hardware.device": [disk]})],
token=None,
)
props = Inventory(content).properties(
vim.VirtualMachine("vm-1", None), ["config.hardware.device"], raw=True
)
assert props["config.hardware.device"][0] is disk