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.
134 lines
5.1 KiB
Python
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
|