Merge fix/cloudinit-drive-image-storage: ide2 needs images storage
This commit is contained in:
@@ -375,7 +375,13 @@ class ProxmoxVMProvisionMixin:
|
||||
_logger.info(f"Setting Cloud-Init config for VM {vmid}")
|
||||
|
||||
cloud_init_args = {
|
||||
"ide2": f"{snippet_storage}:cloudinit",
|
||||
# The cloud-init drive is a disk image — it needs a storage
|
||||
# with content='images' (same requirement as the root disk),
|
||||
# NOT the snippet storage (content='snippets'). These are
|
||||
# often different storages; Proxmox fails at VM start with
|
||||
# "storage 'X' does not support content-type 'images'" if
|
||||
# this points at a snippets-only storage.
|
||||
"ide2": f"{image_storage}:cloudinit",
|
||||
"citype": "nocloud",
|
||||
"cicustom": f"user={snippet_storage}:snippets/{filename}",
|
||||
}
|
||||
@@ -546,9 +552,7 @@ class ProxmoxVMProvisionMixin:
|
||||
while True:
|
||||
elapsed = time.time() - start_time
|
||||
if wait_for_ip and elapsed > timeout:
|
||||
raise RuntimeError(
|
||||
f"VM {vmid} failed to acquire IP within {timeout}s"
|
||||
)
|
||||
raise RuntimeError(f"VM {vmid} failed to acquire IP within {timeout}s")
|
||||
|
||||
try:
|
||||
# Query guest-agent network interfaces
|
||||
|
||||
@@ -85,7 +85,9 @@ def test_create_vm_from_cloud_init_single_nic():
|
||||
]
|
||||
mixin._api = mock_api
|
||||
mixin._node_api = MagicMock(return_value=mock_node)
|
||||
mixin._download_cloud_image = MagicMock(return_value="/var/lib/vz/template/netork-images/debian-12.qcow2")
|
||||
mixin._download_cloud_image = MagicMock(
|
||||
return_value="/var/lib/vz/template/netork-images/debian-12.qcow2"
|
||||
)
|
||||
mixin._run_node_command = MagicMock(return_value="")
|
||||
|
||||
# Mock VM operations
|
||||
@@ -122,9 +124,7 @@ def test_create_vm_from_cloud_init_single_nic():
|
||||
mixin._download_cloud_image.assert_called_once()
|
||||
|
||||
# Verify NIC config was set correctly: net0 with tag=10, DHCP enabled
|
||||
net_call_args = next(
|
||||
c for c in mock_vm.config.post.call_args_list if "net0" in c[1]
|
||||
)
|
||||
net_call_args = next(c for c in mock_vm.config.post.call_args_list if "net0" in c[1])
|
||||
assert "tag=10" in net_call_args[1]["net0"]
|
||||
assert "vmbr0" in net_call_args[1]["net0"]
|
||||
|
||||
@@ -134,7 +134,9 @@ def test_create_vm_from_cloud_init_single_nic():
|
||||
# test_create_vm_writes_snippet_via_ssh_with_correct_content for the
|
||||
# dedicated check).
|
||||
write_cmd = next(
|
||||
c[0][0] for c in mixin._run_node_command.call_args_list if "snippets/101-user-data.yaml" in c[0][0]
|
||||
c[0][0]
|
||||
for c in mixin._run_node_command.call_args_list
|
||||
if "snippets/101-user-data.yaml" in c[0][0]
|
||||
)
|
||||
assert "/var/lib/vz/snippets" in write_cmd
|
||||
|
||||
@@ -156,7 +158,9 @@ def test_create_vm_from_cloud_init_dual_nic_trunk():
|
||||
]
|
||||
mixin._api = mock_api
|
||||
mixin._node_api = MagicMock(return_value=mock_node)
|
||||
mixin._download_cloud_image = MagicMock(return_value="/var/lib/vz/template/netork-images/debian-12.qcow2")
|
||||
mixin._download_cloud_image = MagicMock(
|
||||
return_value="/var/lib/vz/template/netork-images/debian-12.qcow2"
|
||||
)
|
||||
mixin._run_node_command = MagicMock(return_value="")
|
||||
|
||||
# Mock VM operations
|
||||
@@ -183,7 +187,11 @@ def test_create_vm_from_cloud_init_dual_nic_trunk():
|
||||
memory=4096,
|
||||
nics=[
|
||||
{"bridge": "vmbr0", "vlan_tag": 10}, # net0: mgmt with DHCP
|
||||
{"bridge": "vmbr1", "trunk_vlan_tags": [20, 30], "dhcp": False}, # net1: trunk, no DHCP
|
||||
{
|
||||
"bridge": "vmbr1",
|
||||
"trunk_vlan_tags": [20, 30],
|
||||
"dhcp": False,
|
||||
}, # net1: trunk, no DHCP
|
||||
],
|
||||
cloud_init_config={"hostname": "sat-1", "runcmd": ["custom cmd"]},
|
||||
timeout=120,
|
||||
@@ -193,9 +201,7 @@ def test_create_vm_from_cloud_init_dual_nic_trunk():
|
||||
assert result["name"] == "wireshark-sat-1"
|
||||
|
||||
# Verify both NICs configured
|
||||
net_call_args = next(
|
||||
c for c in mock_vm.config.post.call_args_list if "net0" in c[1]
|
||||
)
|
||||
net_call_args = next(c for c in mock_vm.config.post.call_args_list if "net0" in c[1])
|
||||
assert "net1" in net_call_args[1]
|
||||
assert "tag=10" in net_call_args[1]["net0"]
|
||||
assert "trunks=20;30" in net_call_args[1]["net1"]
|
||||
@@ -225,7 +231,9 @@ def test_create_vm_missing_snippet_storage():
|
||||
{"storage": "local-lvm", "type": "lvmthin", "content": "images,rootdir", "enabled": 1}
|
||||
]
|
||||
mixin._node_api = MagicMock(return_value=mock_node)
|
||||
mixin._download_cloud_image = MagicMock(return_value="/var/lib/vz/template/netork-images/debian-12.qcow2")
|
||||
mixin._download_cloud_image = MagicMock(
|
||||
return_value="/var/lib/vz/template/netork-images/debian-12.qcow2"
|
||||
)
|
||||
mixin._run_node_command = MagicMock(return_value="")
|
||||
mock_vm = MagicMock()
|
||||
mock_node.qemu.return_value = mock_vm
|
||||
@@ -267,7 +275,9 @@ def test_create_vm_with_disk_resize():
|
||||
]
|
||||
mixin._api = mock_api
|
||||
mixin._node_api = MagicMock(return_value=mock_node)
|
||||
mixin._download_cloud_image = MagicMock(return_value="/var/lib/vz/template/netork-images/debian-12.qcow2")
|
||||
mixin._download_cloud_image = MagicMock(
|
||||
return_value="/var/lib/vz/template/netork-images/debian-12.qcow2"
|
||||
)
|
||||
mixin._run_node_command = MagicMock(return_value="")
|
||||
|
||||
# Mock VM operations
|
||||
@@ -469,9 +479,7 @@ def test_get_network_targets_linux_bridge_vlan_aware_flag():
|
||||
def test_get_network_targets_ovs_bridge_always_vlan_aware():
|
||||
"""An OVS bridge is always vlan_aware, regardless of bridge_vlan_aware."""
|
||||
mixin = ProxmoxVMProvisionMixin()
|
||||
mixin._get_node_network = MagicMock(
|
||||
return_value=[{"iface": "vmbr1", "type": "OVSBridge"}]
|
||||
)
|
||||
mixin._get_node_network = MagicMock(return_value=[{"iface": "vmbr1", "type": "OVSBridge"}])
|
||||
mixin._get_sdn_vnets = MagicMock(return_value=[])
|
||||
|
||||
targets = mixin.get_network_targets()
|
||||
@@ -515,9 +523,7 @@ def test_get_network_targets_vnet_without_tag_has_none_fixed_vlan_tag():
|
||||
"""A vnet with no tag (e.g. VXLAN/EVPN zone) reports fixed_vlan_tag=None."""
|
||||
mixin = ProxmoxVMProvisionMixin()
|
||||
mixin._get_node_network = MagicMock(return_value=[])
|
||||
mixin._get_sdn_vnets = MagicMock(
|
||||
return_value=[{"vnet": "vnet2", "zone": "zone-vxlan"}]
|
||||
)
|
||||
mixin._get_sdn_vnets = MagicMock(return_value=[{"vnet": "vnet2", "zone": "zone-vxlan"}])
|
||||
|
||||
targets = mixin.get_network_targets()
|
||||
|
||||
@@ -769,7 +775,9 @@ def test_create_vm_from_cloud_init_with_real_world_storage_shape():
|
||||
]
|
||||
mixin._api = mock_api
|
||||
mixin._node_api = MagicMock(return_value=mock_node)
|
||||
mixin._download_cloud_image = MagicMock(return_value="/var/lib/vz/template/netork-images/debian-12.qcow2")
|
||||
mixin._download_cloud_image = MagicMock(
|
||||
return_value="/var/lib/vz/template/netork-images/debian-12.qcow2"
|
||||
)
|
||||
mixin._run_node_command = MagicMock(return_value="")
|
||||
|
||||
mock_vm = MagicMock()
|
||||
@@ -818,8 +826,12 @@ def test_create_vm_from_cloud_init_explicit_storage_skips_auto_detect():
|
||||
]
|
||||
mixin._api = mock_api
|
||||
mixin._node_api = MagicMock(return_value=mock_node)
|
||||
mixin._download_cloud_image = MagicMock(return_value="/var/lib/vz/template/netork-images/debian-12.qcow2")
|
||||
mixin._find_default_image_storage = MagicMock(side_effect=AssertionError("should not be called"))
|
||||
mixin._download_cloud_image = MagicMock(
|
||||
return_value="/var/lib/vz/template/netork-images/debian-12.qcow2"
|
||||
)
|
||||
mixin._find_default_image_storage = MagicMock(
|
||||
side_effect=AssertionError("should not be called")
|
||||
)
|
||||
mixin._run_node_command = MagicMock(return_value="")
|
||||
|
||||
mock_vm = MagicMock()
|
||||
@@ -944,7 +956,9 @@ def test_create_vm_writes_snippet_via_ssh_with_correct_content():
|
||||
]
|
||||
mixin._api = mock_api
|
||||
mixin._node_api = MagicMock(return_value=mock_node)
|
||||
mixin._download_cloud_image = MagicMock(return_value="/var/lib/vz/template/netork-images/debian-12.qcow2")
|
||||
mixin._download_cloud_image = MagicMock(
|
||||
return_value="/var/lib/vz/template/netork-images/debian-12.qcow2"
|
||||
)
|
||||
mixin._run_node_command = MagicMock(return_value="")
|
||||
|
||||
mock_vm = MagicMock()
|
||||
@@ -1012,7 +1026,9 @@ def test_create_vm_from_cloud_init_with_explicit_mac():
|
||||
]
|
||||
mixin._api = mock_api
|
||||
mixin._node_api = MagicMock(return_value=mock_node)
|
||||
mixin._download_cloud_image = MagicMock(return_value="/var/lib/vz/template/netork-images/debian-12.qcow2")
|
||||
mixin._download_cloud_image = MagicMock(
|
||||
return_value="/var/lib/vz/template/netork-images/debian-12.qcow2"
|
||||
)
|
||||
mixin._run_node_command = MagicMock(return_value="")
|
||||
|
||||
mock_vm = MagicMock()
|
||||
@@ -1060,7 +1076,9 @@ def test_create_vm_from_cloud_init_without_mac_uses_bare_virtio():
|
||||
]
|
||||
mixin._api = mock_api
|
||||
mixin._node_api = MagicMock(return_value=mock_node)
|
||||
mixin._download_cloud_image = MagicMock(return_value="/var/lib/vz/template/netork-images/debian-12.qcow2")
|
||||
mixin._download_cloud_image = MagicMock(
|
||||
return_value="/var/lib/vz/template/netork-images/debian-12.qcow2"
|
||||
)
|
||||
mixin._run_node_command = MagicMock(return_value="")
|
||||
|
||||
mock_vm = MagicMock()
|
||||
@@ -1089,3 +1107,59 @@ def test_create_vm_from_cloud_init_without_mac_uses_bare_virtio():
|
||||
|
||||
net_call_args = next(c for c in mock_vm.config.post.call_args_list if "net0" in c[1])
|
||||
assert net_call_args[1]["net0"] == "virtio,bridge=vmbr0"
|
||||
|
||||
|
||||
def test_create_vm_cloudinit_drive_uses_image_storage_not_snippet_storage():
|
||||
"""Regression: real Proxmox fails at VM *start* time with "storage 'X'
|
||||
does not support content-type 'images'" if ide2 (the cloud-init drive)
|
||||
points at the snippets storage — a cloud-init drive is a disk image and
|
||||
needs content='images', same as the root disk. Only cicustom (the
|
||||
snippet file reference) should use the snippets storage. Found live: a
|
||||
real deployment had 'local' (snippets-only) and 'local-zfs' (images) as
|
||||
two distinct storages, and ide2 was wrongly set to the former."""
|
||||
mixin = ProxmoxVMProvisionMixin()
|
||||
mixin._node_name = "pve1"
|
||||
|
||||
mock_api = MagicMock()
|
||||
mock_api.cluster.nextid.get.return_value = 109
|
||||
mock_api.storage.return_value.get.return_value = {"path": "/var/lib/vz"}
|
||||
|
||||
mock_node = MagicMock()
|
||||
mock_node.storage.get.return_value = [
|
||||
{"storage": "local-zfs", "type": "zfspool", "content": "images,rootdir", "enabled": 1},
|
||||
{"storage": "local", "type": "dir", "content": "iso,snippets,backup,vztmpl", "enabled": 1},
|
||||
]
|
||||
mixin._api = mock_api
|
||||
mixin._node_api = MagicMock(return_value=mock_node)
|
||||
mixin._download_cloud_image = MagicMock(
|
||||
return_value="/var/lib/vz/template/netork-images/debian-12.qcow2"
|
||||
)
|
||||
mixin._run_node_command = MagicMock(return_value="")
|
||||
|
||||
mock_vm = MagicMock()
|
||||
mock_node.qemu.return_value = mock_vm
|
||||
mock_node.qemu.post.return_value = None
|
||||
mock_vm.config.post.return_value = None
|
||||
mock_vm.config.get.return_value = {
|
||||
"unused0": "local-zfs:vm-109-disk-0",
|
||||
"scsi0": "local-zfs:vm-109-disk-0",
|
||||
}
|
||||
mock_vm.status.start.post.return_value = "UPID:pve1:135:start"
|
||||
|
||||
mock_task = MagicMock()
|
||||
mock_task.status.get.return_value = {"status": "stopped", "exitstatus": "OK"}
|
||||
mock_node.tasks.return_value = mock_task
|
||||
|
||||
with patch("time.sleep"):
|
||||
mixin.create_vm_from_cloud_init(
|
||||
name="cloudinit-storage-vm",
|
||||
image_url="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2",
|
||||
cpu=2,
|
||||
memory=2048,
|
||||
nics=[{"bridge": "vmbr0"}],
|
||||
cloud_init_config={"hostname": "cloudinit-storage-vm"},
|
||||
)
|
||||
|
||||
cloud_init_call = next(c for c in mock_vm.config.post.call_args_list if "ide2" in c[1])
|
||||
assert cloud_init_call[1]["ide2"] == "local-zfs:cloudinit"
|
||||
assert cloud_init_call[1]["cicustom"].startswith("user=local:snippets/")
|
||||
|
||||
Reference in New Issue
Block a user