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.
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
"""The OVF descriptor an import is built from."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import xml.etree.ElementTree as ET
|
||||
|
||||
import pytest
|
||||
|
||||
from napalm_vmware.provision.ovf import ovf_descriptor
|
||||
|
||||
NS = {
|
||||
"ovf": "http://schemas.dmtf.org/ovf/envelope/1",
|
||||
"rasd": "http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData",
|
||||
"vmw": "http://www.vmware.com/schema/ovf",
|
||||
}
|
||||
OVF = "{http://schemas.dmtf.org/ovf/envelope/1}"
|
||||
RASD = "{" + NS["rasd"] + "}"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def root():
|
||||
return ET.fromstring(
|
||||
ovf_descriptor("web01", cpu=2, memory_mb=4096, capacity_bytes=10 * 1024**3, nic_count=2)
|
||||
)
|
||||
|
||||
|
||||
def _items(root):
|
||||
return root.findall(".//ovf:VirtualHardwareSection/ovf:Item", NS)
|
||||
|
||||
|
||||
def _by_type(root, resource_type):
|
||||
return [i for i in _items(root) if i.find("rasd:ResourceType", NS).text == str(resource_type)]
|
||||
|
||||
|
||||
def test_cpu_and_memory(root):
|
||||
(cpu,) = _by_type(root, 3)
|
||||
(mem,) = _by_type(root, 4)
|
||||
assert cpu.find("rasd:VirtualQuantity", NS).text == "2"
|
||||
assert mem.find("rasd:VirtualQuantity", NS).text == "4096"
|
||||
|
||||
|
||||
def test_disk_references_the_stream_optimized_file(root):
|
||||
disk = root.find(".//ovf:DiskSection/ovf:Disk", NS)
|
||||
assert disk.get(f"{OVF}capacity") == str(10 * 1024**3)
|
||||
assert disk.get(f"{OVF}format").endswith("#streamOptimized")
|
||||
(file_ref,) = root.findall(".//ovf:References/ovf:File", NS)
|
||||
assert disk.get(f"{OVF}fileRef") == file_ref.get(f"{OVF}id")
|
||||
(item,) = _by_type(root, 17)
|
||||
assert item.find("rasd:HostResource", NS).text == f"ovf:/disk/{disk.get(f'{OVF}diskId')}"
|
||||
|
||||
|
||||
def test_one_vmxnet3_nic_per_network(root):
|
||||
nics = _by_type(root, 10)
|
||||
assert [n.find("rasd:Connection", NS).text for n in nics] == ["net0", "net1"]
|
||||
assert {n.find("rasd:ResourceSubType", NS).text for n in nics} == {"VmxNet3"}
|
||||
networks = root.findall(".//ovf:NetworkSection/ovf:Network", NS)
|
||||
assert [n.get(f"{OVF}name") for n in networks] == ["net0", "net1"]
|
||||
|
||||
|
||||
def test_rasd_elements_are_in_schema_order(root):
|
||||
"""vSphere rejects an Item whose CIM elements are out of order."""
|
||||
for item in _items(root):
|
||||
names = [child.tag.removeprefix(RASD) for child in item]
|
||||
assert names == sorted(names), names
|
||||
|
||||
|
||||
def test_name_is_escaped():
|
||||
ET.fromstring(ovf_descriptor("a<b&c", cpu=1, memory_mb=512, capacity_bytes=1, nic_count=1))
|
||||
|
||||
|
||||
def test_efi_firmware():
|
||||
root = ET.fromstring(
|
||||
ovf_descriptor("x", cpu=1, memory_mb=512, capacity_bytes=1, nic_count=1, firmware="efi")
|
||||
)
|
||||
config = root.find(".//vmw:Config", NS)
|
||||
assert (config.get("{" + NS["vmw"] + "}key"), config.get("{" + NS["vmw"] + "}value")) == (
|
||||
"firmware",
|
||||
"efi",
|
||||
)
|
||||
Reference in New Issue
Block a user