Merge feature/nic-mac-address: pin explicit NIC MAC when given

This commit is contained in:
Christian Manivong
2026-07-08 09:09:31 +02:00
2 changed files with 105 additions and 2 deletions
+3 -2
View File
@@ -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"]:
+102
View File
@@ -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"