feat(vm_provision_mixin): pin explicit NIC MAC when given
nics[i]['mac'] is set via virtio=<mac>,bridge=... instead of the bare virtio,bridge=... form, so a caller-supplied MAC actually takes effect (needed for DHCP reservations created before the VM exists).
This commit is contained in:
@@ -313,8 +313,9 @@ class ProxmoxVMProvisionMixin:
|
|||||||
if not bridge:
|
if not bridge:
|
||||||
raise ValueError(f"NIC {i}: bridge is required")
|
raise ValueError(f"NIC {i}: bridge is required")
|
||||||
|
|
||||||
# Build base config: model + bridge
|
# Build base config: model[=mac] + bridge
|
||||||
net_config = f"virtio,bridge={bridge}"
|
mac = nic.get("mac")
|
||||||
|
net_config = f"virtio={mac},bridge={bridge}" if mac else f"virtio,bridge={bridge}"
|
||||||
|
|
||||||
# Add VLAN configuration (access vs trunk)
|
# Add VLAN configuration (access vs trunk)
|
||||||
if "trunk_vlan_tags" in nic and nic["trunk_vlan_tags"]:
|
if "trunk_vlan_tags" in nic and nic["trunk_vlan_tags"]:
|
||||||
|
|||||||
@@ -987,3 +987,105 @@ def test_create_vm_writes_snippet_via_ssh_with_correct_content():
|
|||||||
content = base64.b64decode(encoded).decode("utf-8")
|
content = base64.b64decode(encoded).decode("utf-8")
|
||||||
assert content.startswith("#cloud-config\n")
|
assert content.startswith("#cloud-config\n")
|
||||||
assert "hostname: write-shape-vm" in content
|
assert "hostname: write-shape-vm" in content
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Explicit NIC MAC address
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_vm_from_cloud_init_with_explicit_mac():
|
||||||
|
"""When nics[i]['mac'] is set, it's pinned via virtio=<mac>,bridge=...
|
||||||
|
(needed so a caller can create a matching DHCP reservation before the
|
||||||
|
VM even boots)."""
|
||||||
|
mixin = ProxmoxVMProvisionMixin()
|
||||||
|
mixin._node_name = "pve1"
|
||||||
|
|
||||||
|
mock_api = MagicMock()
|
||||||
|
mock_api.cluster.nextid.get.return_value = 107
|
||||||
|
mock_api.storage.return_value.get.return_value = {"path": "/var/lib/vz"}
|
||||||
|
|
||||||
|
mock_node = MagicMock()
|
||||||
|
mock_node.storage.get.return_value = [
|
||||||
|
{"storage": "local-lvm", "type": "lvmthin", "content": "images,rootdir", "enabled": 1},
|
||||||
|
{"storage": "snippets", "type": "dir", "content": "snippets", "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-lvm:vm-107-disk-0",
|
||||||
|
"scsi0": "local-lvm:vm-107-disk-0",
|
||||||
|
}
|
||||||
|
mock_vm.status.start.post.return_value = "UPID:pve1:133: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="pinned-mac-vm",
|
||||||
|
image_url="https://cloud.debian.org/images/cloud/bookworm/latest/debian-12-genericcloud-amd64.qcow2",
|
||||||
|
cpu=2,
|
||||||
|
memory=2048,
|
||||||
|
nics=[{"bridge": "vmbr0", "vlan_tag": 10, "mac": "02:aa:bb:cc:dd:ee"}],
|
||||||
|
cloud_init_config={"hostname": "pinned-mac-vm"},
|
||||||
|
)
|
||||||
|
|
||||||
|
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=02:aa:bb:cc:dd:ee,bridge=vmbr0,tag=10"
|
||||||
|
|
||||||
|
|
||||||
|
def test_create_vm_from_cloud_init_without_mac_uses_bare_virtio():
|
||||||
|
"""Regression: omitting nics[i]['mac'] must still produce the original
|
||||||
|
bare 'virtio,bridge=...' string (auto-generated MAC), not 'virtio=None,...'."""
|
||||||
|
mixin = ProxmoxVMProvisionMixin()
|
||||||
|
mixin._node_name = "pve1"
|
||||||
|
|
||||||
|
mock_api = MagicMock()
|
||||||
|
mock_api.cluster.nextid.get.return_value = 108
|
||||||
|
mock_api.storage.return_value.get.return_value = {"path": "/var/lib/vz"}
|
||||||
|
|
||||||
|
mock_node = MagicMock()
|
||||||
|
mock_node.storage.get.return_value = [
|
||||||
|
{"storage": "local-lvm", "type": "lvmthin", "content": "images,rootdir", "enabled": 1},
|
||||||
|
{"storage": "snippets", "type": "dir", "content": "snippets", "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-lvm:vm-108-disk-0",
|
||||||
|
"scsi0": "local-lvm:vm-108-disk-0",
|
||||||
|
}
|
||||||
|
mock_vm.status.start.post.return_value = "UPID:pve1:134: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="auto-mac-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": "auto-mac-vm"},
|
||||||
|
)
|
||||||
|
|
||||||
|
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"
|
||||||
|
|||||||
Reference in New Issue
Block a user