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.
81 lines
2.8 KiB
Python
81 lines
2.8 KiB
Python
"""The NoCloud seed: cloud-init's user-data, meta-data and network-config on an ISO."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import io
|
|
|
|
import pycdlib
|
|
import yaml
|
|
|
|
from napalm_vmware.provision.seed import meta_data, network_config, nocloud_iso, user_data
|
|
|
|
|
|
class TestUserData:
|
|
def test_is_a_cloud_config(self):
|
|
text = user_data({"hostname": "web01", "packages": ["open-vm-tools"]}, None)
|
|
assert text.startswith("#cloud-config\n")
|
|
assert yaml.safe_load(text) == {"hostname": "web01", "packages": ["open-vm-tools"]}
|
|
|
|
def test_ssh_keys_are_added_without_duplicates(self):
|
|
config = {"ssh_authorized_keys": ["ssh-ed25519 AAA a"]}
|
|
text = user_data(config, ["ssh-ed25519 AAA a", "ssh-ed25519 BBB b"])
|
|
assert yaml.safe_load(text)["ssh_authorized_keys"] == [
|
|
"ssh-ed25519 AAA a",
|
|
"ssh-ed25519 BBB b",
|
|
]
|
|
assert config == {"ssh_authorized_keys": ["ssh-ed25519 AAA a"]} # caller's dict untouched
|
|
|
|
|
|
def test_meta_data():
|
|
assert yaml.safe_load(meta_data("web01", "iid-1")) == {
|
|
"instance-id": "iid-1",
|
|
"local-hostname": "web01",
|
|
}
|
|
|
|
|
|
class TestNetworkConfig:
|
|
def test_dhcp_nics_matched_by_mac(self):
|
|
cfg = network_config([("00:50:56:aa:bb:cc", True), ("00:50:56:aa:bb:dd", False)])
|
|
assert cfg == {
|
|
"version": 2,
|
|
"ethernets": {
|
|
"nic0": {"match": {"macaddress": "00:50:56:aa:bb:cc"}, "dhcp4": True},
|
|
},
|
|
}
|
|
|
|
def test_no_dhcp_nic_means_no_network_config(self):
|
|
assert network_config([("00:50:56:aa:bb:cc", False)]) is None
|
|
|
|
|
|
def _read(iso: bytes, name: str) -> str:
|
|
reader = pycdlib.PyCdlib()
|
|
reader.open_fp(io.BytesIO(iso))
|
|
out = io.BytesIO()
|
|
reader.get_file_from_iso_fp(out, rr_path=f"/{name}")
|
|
reader.close()
|
|
return out.getvalue().decode()
|
|
|
|
|
|
class TestIso:
|
|
def test_carries_the_three_files_under_their_cloud_init_names(self):
|
|
iso = nocloud_iso("#cloud-config\n{}\n", "instance-id: x\n", {"version": 2})
|
|
assert _read(iso, "user-data") == "#cloud-config\n{}\n"
|
|
assert _read(iso, "meta-data") == "instance-id: x\n"
|
|
assert yaml.safe_load(_read(iso, "network-config")) == {"version": 2}
|
|
|
|
def test_volume_label_is_cidata(self):
|
|
"""cloud-init's NoCloud datasource finds the seed by this label."""
|
|
reader = pycdlib.PyCdlib()
|
|
reader.open_fp(io.BytesIO(nocloud_iso("u", "m", None)))
|
|
assert reader.pvd.volume_identifier.decode().strip() == "cidata"
|
|
reader.close()
|
|
|
|
def test_without_network_config(self):
|
|
reader = pycdlib.PyCdlib()
|
|
reader.open_fp(io.BytesIO(nocloud_iso("u", "m", None)))
|
|
names = [
|
|
c.rock_ridge.name().decode() for c in reader.list_children(rr_path="/") if c.rock_ridge
|
|
]
|
|
reader.close()
|
|
assert "network-config" not in names
|