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.
121 lines
4.4 KiB
Python
121 lines
4.4 KiB
Python
"""A minimal OVF 1.0 descriptor for importing one converted cloud image.
|
|
|
|
vSphere builds the VM from this (``OvfManager.CreateImportSpec``) and then
|
|
takes the disk contents over NFC. The descriptor only has to describe what
|
|
the image does not: CPU, memory, one disk and the NICs.
|
|
|
|
Devices are the ones VMware recommends for a 64-bit Linux guest -- PVSCSI and
|
|
VMXNET3. Both drivers are in the mainline kernel; whether every distribution's
|
|
*cloud* kernel carries them is one of the things #305 checks on real hardware.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from xml.sax.saxutils import escape, quoteattr
|
|
|
|
_HW_VERSION = "vmx-13" # ESXi 6.5 and later
|
|
_GUEST_OS = "otherLinux64Guest"
|
|
_STREAM_OPTIMIZED = "http://www.vmware.com/interfaces/specifications/vmdk.html#streamOptimized"
|
|
|
|
|
|
def _item(**elements: str) -> str:
|
|
# CIM requires the rasd elements in alphabetical order.
|
|
body = "".join(f"<rasd:{k}>{escape(v)}</rasd:{k}>" for k, v in sorted(elements.items()))
|
|
return f"<Item>{body}</Item>"
|
|
|
|
|
|
def _nic_items(nic_count: int, first_instance: int) -> str:
|
|
return "".join(
|
|
_item(
|
|
AutomaticAllocation="true",
|
|
Connection=f"net{i}",
|
|
ElementName=f"Network adapter {i + 1}",
|
|
InstanceID=str(first_instance + i),
|
|
ResourceSubType="VmxNet3",
|
|
ResourceType="10",
|
|
)
|
|
for i in range(nic_count)
|
|
)
|
|
|
|
|
|
def ovf_descriptor(
|
|
name: str,
|
|
*,
|
|
cpu: int,
|
|
memory_mb: int,
|
|
capacity_bytes: int,
|
|
nic_count: int,
|
|
firmware: str = "bios",
|
|
) -> str:
|
|
networks = "".join(
|
|
f'<Network ovf:name="net{i}"><Description>net{i}</Description></Network>'
|
|
for i in range(nic_count)
|
|
)
|
|
hardware = "".join(
|
|
[
|
|
_item(
|
|
AllocationUnits="hertz * 10^6",
|
|
ElementName=f"{cpu} virtual CPU(s)",
|
|
InstanceID="1",
|
|
ResourceType="3",
|
|
VirtualQuantity=str(cpu),
|
|
),
|
|
_item(
|
|
AllocationUnits="byte * 2^20",
|
|
ElementName=f"{memory_mb} MB of memory",
|
|
InstanceID="2",
|
|
ResourceType="4",
|
|
VirtualQuantity=str(memory_mb),
|
|
),
|
|
_item(
|
|
Address="0",
|
|
ElementName="SCSI controller 0",
|
|
InstanceID="3",
|
|
ResourceSubType="VirtualSCSI",
|
|
ResourceType="6",
|
|
),
|
|
_item(
|
|
AddressOnParent="0",
|
|
ElementName="Hard disk 1",
|
|
HostResource="ovf:/disk/vmdisk1",
|
|
InstanceID="4",
|
|
Parent="3",
|
|
ResourceType="17",
|
|
),
|
|
_nic_items(nic_count, first_instance=5),
|
|
]
|
|
)
|
|
firmware_config = (
|
|
'<vmw:Config ovf:required="false" vmw:key="firmware" vmw:value="efi"/>'
|
|
if firmware == "efi"
|
|
else ""
|
|
)
|
|
return (
|
|
'<?xml version="1.0" encoding="UTF-8"?>'
|
|
'<Envelope xmlns="http://schemas.dmtf.org/ovf/envelope/1"'
|
|
' xmlns:ovf="http://schemas.dmtf.org/ovf/envelope/1"'
|
|
' xmlns:rasd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_ResourceAllocationSettingData"'
|
|
' xmlns:vssd="http://schemas.dmtf.org/wbem/wscim/1/cim-schema/2/CIM_VirtualSystemSettingData"'
|
|
' xmlns:vmw="http://www.vmware.com/schema/ovf">'
|
|
'<References><File ovf:href="disk.vmdk" ovf:id="file1"/></References>'
|
|
"<DiskSection><Info>Virtual disks</Info>"
|
|
f'<Disk ovf:capacity="{capacity_bytes}" ovf:capacityAllocationUnits="byte"'
|
|
f' ovf:diskId="vmdisk1" ovf:fileRef="file1" ovf:format="{_STREAM_OPTIMIZED}"/>'
|
|
"</DiskSection>"
|
|
f"<NetworkSection><Info>Networks</Info>{networks}</NetworkSection>"
|
|
f"<VirtualSystem ovf:id={quoteattr(name)}>"
|
|
"<Info>A virtual machine</Info>"
|
|
f"<Name>{escape(name)}</Name>"
|
|
f'<OperatingSystemSection ovf:id="101" vmw:osType="{_GUEST_OS}"><Info>Guest OS</Info>'
|
|
"</OperatingSystemSection>"
|
|
"<VirtualHardwareSection><Info>Virtual hardware</Info>"
|
|
"<System><vssd:ElementName>Virtual Hardware Family</vssd:ElementName>"
|
|
"<vssd:InstanceID>0</vssd:InstanceID>"
|
|
f"<vssd:VirtualSystemIdentifier>{escape(name)}</vssd:VirtualSystemIdentifier>"
|
|
f"<vssd:VirtualSystemType>{_HW_VERSION}</vssd:VirtualSystemType></System>"
|
|
f"{hardware}{firmware_config}"
|
|
"</VirtualHardwareSection>"
|
|
"</VirtualSystem>"
|
|
"</Envelope>"
|
|
)
|