feat(vm_provision_mixin): generalize create_vm_from_cloud_init for arbitrary NIC configs

- Replace fixed mgmt/capture dual-NIC parameters with generic nics list
- Loop over NICs to build net0, net1, ... config strings (access VLAN or trunk)
- Remove hardcoded 'ip link set eth1 up' Cloud-Init hack (caller responsibility)
- Add disk_resize_gb parameter for post-clone disk expansion (scsi0/virtio0/ide0/sata0)
- Make DHCP configuration per-NIC with sensible defaults (primary NIC only)
- Update docstrings and logging to reflect generic NIC architecture
This commit is contained in:
Christian Manivong
2026-07-06 23:29:33 +02:00
parent 7bdac4c496
commit d2c361937e
+58 -36
View File
@@ -54,13 +54,11 @@ class ProxmoxVMProvisionMixin:
template: str, template: str,
cpu: int, cpu: int,
memory: int, memory: int,
mgmt_bridge: str, nics: List[Dict[str, Any]],
mgmt_vlan_tag: int | None,
capture_bridge: str,
capture_vlan_tags: List[int],
cloud_init_config: Dict[str, Any], cloud_init_config: Dict[str, Any],
ssh_public_keys: List[str] | None = None, ssh_public_keys: List[str] | None = None,
timeout: int = 120, disk_resize_gb: int | None = None,
timeout: int = 180,
) -> VMProvisionResultDict: ) -> VMProvisionResultDict:
""" """
Create a new VM from a Cloud-Init template via Proxmox API. Create a new VM from a Cloud-Init template via Proxmox API.
@@ -68,24 +66,23 @@ class ProxmoxVMProvisionMixin:
Steps: Steps:
1. Get next available VMID from cluster 1. Get next available VMID from cluster
2. Clone template VM (full clone, new VMID) 2. Clone template VM (full clone, new VMID)
3. Configure CPU, memory, and dual NICs (mgmt + capture) 3. Configure CPU, memory, and network interfaces
4. Verify snippet storage exists 4. Verify snippet storage exists
5. Render cloud-init config to YAML and upload 5. Render cloud-init config to YAML and upload
6. Set Cloud-Init config references and SSH keys 6. Set Cloud-Init config references and SSH keys
7. Start the VM 7. Optionally resize root disk
8. Return VMID, name, node 8. Start the VM
9. Return VMID, name, node
Args: Args:
name: new VM display name name: new VM display name
template: template VMID/name to clone from template: template VMID/name to clone from
cpu: number of vCPUs cpu: number of vCPUs
memory: RAM in MB memory: RAM in MB
mgmt_bridge: management bridge name nics: list of NIC config dicts (bridge, vlan_tag/trunk_vlan_tags, dhcp flag)
mgmt_vlan_tag: VLAN tag for mgmt NIC (None = untagged)
capture_bridge: packet capture bridge name (must be VLAN-aware)
capture_vlan_tags: list of VLAN IDs for capture NIC (trunk)
cloud_init_config: user-data dict (will be YAML-rendered) cloud_init_config: user-data dict (will be YAML-rendered)
ssh_public_keys: SSH public keys to inject ssh_public_keys: SSH public keys to inject
disk_resize_gb: resize root disk to this size (None = no resize)
timeout: max seconds for provisioning timeout: max seconds for provisioning
Returns: Returns:
@@ -112,24 +109,30 @@ class ProxmoxVMProvisionMixin:
) )
self._wait_for_task(clone_upid, timeout=timeout) self._wait_for_task(clone_upid, timeout=timeout)
# Step 3: Configure CPU, memory, and dual NICs # Step 3: Configure CPU, memory, and NICs
_logger.info(f"Configuring VM {vmid}: {cpu} CPU, {memory}MB RAM") _logger.info(f"Configuring VM {vmid}: {cpu} CPU, {memory}MB RAM, {len(nics)} NIC(s)")
# Build net0 (mgmt) config config_args = {"cores": cpu, "memory": memory}
net0_config = f"virtio,bridge={mgmt_bridge}"
if mgmt_vlan_tag is not None:
net0_config += f",tag={mgmt_vlan_tag}"
# Build net1 (capture trunk) config # Build NIC config strings generically
vlan_list = ";".join(str(v) for v in capture_vlan_tags) for i, nic in enumerate(nics):
net1_config = f"virtio,bridge={capture_bridge},trunks={vlan_list}" bridge = nic.get("bridge")
if not bridge:
raise ValueError(f"NIC {i}: bridge is required")
self._node_api().qemu(vmid).config.post( # Build base config: model + bridge
cores=cpu, net_config = f"virtio,bridge={bridge}"
memory=memory,
net0=net0_config, # Add VLAN configuration (access vs trunk)
net1=net1_config, if "trunk_vlan_tags" in nic and nic["trunk_vlan_tags"]:
) vlan_list = ";".join(str(v) for v in nic["trunk_vlan_tags"])
net_config += f",trunks={vlan_list}"
elif "vlan_tag" in nic and nic["vlan_tag"] is not None:
net_config += f",tag={nic['vlan_tag']}"
config_args[f"net{i}"] = net_config
self._node_api().qemu(vmid).config.post(**config_args)
# Step 4: Verify snippet storage exists # Step 4: Verify snippet storage exists
_logger.info("Checking for snippet storage...") _logger.info("Checking for snippet storage...")
@@ -152,12 +155,6 @@ class ProxmoxVMProvisionMixin:
# Step 5: Render and upload Cloud-Init config # Step 5: Render and upload Cloud-Init config
_logger.info(f"Rendering Cloud-Init config for VMID {vmid}") _logger.info(f"Rendering Cloud-Init config for VMID {vmid}")
# Ensure cloud_init_config includes runcmd to bring up capture NIC
if "runcmd" not in cloud_init_config:
cloud_init_config["runcmd"] = []
if not any("eth1" in cmd if isinstance(cmd, str) else False for cmd in cloud_init_config.get("runcmd", [])):
cloud_init_config["runcmd"].insert(0, "ip link set eth1 up")
user_data_yaml = "#cloud-config\n" + yaml.dump( user_data_yaml = "#cloud-config\n" + yaml.dump(
cloud_init_config, default_flow_style=False cloud_init_config, default_flow_style=False
) )
@@ -179,10 +176,14 @@ class ProxmoxVMProvisionMixin:
"ide2": f"{snippet_storage}:cloudinit", "ide2": f"{snippet_storage}:cloudinit",
"citype": "nocloud", "citype": "nocloud",
"cicustom": f"user={snippet_storage}:snippets/{filename}", "cicustom": f"user={snippet_storage}:snippets/{filename}",
"ipconfig0": "ip=dhcp", # net0 gets DHCP (mgmt)
# NO ipconfig1 for net1 (capture NIC stays unnummeriert)
} }
# Configure DHCP for NICs where enabled (default True for index 0, False otherwise)
for i, nic in enumerate(nics):
dhcp_enabled = nic.get("dhcp", i == 0) # Default DHCP for first NIC only
if dhcp_enabled:
cloud_init_args[f"ipconfig{i}"] = "ip=dhcp"
if ssh_public_keys: if ssh_public_keys:
# URL-encode SSH keys for Proxmox API # URL-encode SSH keys for Proxmox API
sshkeys = ";".join(ssh_public_keys) sshkeys = ";".join(ssh_public_keys)
@@ -190,7 +191,28 @@ class ProxmoxVMProvisionMixin:
self._node_api().qemu(vmid).config.post(**cloud_init_args) self._node_api().qemu(vmid).config.post(**cloud_init_args)
# Step 7: Start the VM # Step 7: Optionally resize root disk
if disk_resize_gb is not None:
_logger.info(f"Resizing root disk to {disk_resize_gb}GB")
# Find root disk (scsi0, virtio0, ide0, sata0 — whichever is first)
try:
config = self._node_api().qemu(vmid).config.get()
root_disk = None
for prefix in ("scsi", "virtio", "ide", "sata"):
if f"{prefix}0" in config:
root_disk = f"{prefix}0"
break
if root_disk:
self._node_api().qemu(vmid).resize.put(
disk=root_disk,
size=f"{disk_resize_gb}G",
)
else:
_logger.warning(f"Could not find root disk for VM {vmid}, skipping resize")
except Exception as e:
_logger.warning(f"Failed to resize disk: {e}, continuing anyway")
# Step 8: Start the VM
_logger.info(f"Starting VM {vmid}") _logger.info(f"Starting VM {vmid}")
start_upid = self._node_api().qemu(vmid).status.start.post() start_upid = self._node_api().qemu(vmid).status.start.post()
self._wait_for_task(start_upid, timeout=timeout) self._wait_for_task(start_upid, timeout=timeout)