feat(vm-provision): name a CPU model for every new VM, and list the choices

A VM created without a cpu argument gets Proxmox's API default, kvm64: no
AES-NI and no AVX. MongoDB 5.0 and later exit with "Illegal instruction" on
it, which is how a Graylog provisioned through netOrk failed (netork#494).
Every VM built by hand in the same cluster uses host or x86-64-v2-AES; only
the ones this driver created were left on kvm64.

create_vm_from_cloud_init now always passes cpu=, defaulting to x86-64-v2-AES
as the Proxmox GUI does since PVE 8, and takes cpu_type for a different one.

get_vm_cpu_types() lists x86-64-v2-AES (default), x86-64-v3 and host. The
named models carry the cpuinfo flags they add over qemu64, following
Proxmox's own definitions (CPUConfig.pm), and are available when the node's
CPU has all of them, read from /nodes/{node}/status. host lists the node's
own flags, so on a CPU without AVX it does not pretend to offer any.

A model asked for by name is checked against the node before a VMID is
allocated: one the CPU cannot run would only fail at VM start, after the disk
import, leaving a half-built VM behind. The default is not checked, so a
plain create costs no extra API call. VMCpuTypeDict is imported for type
checking only, so this works with an older napalm_device_types.
This commit is contained in:
Christian Manivong
2026-10-04 12:02:48 +02:00
parent 6464a6c728
commit 77e65ea7bd
2 changed files with 256 additions and 2 deletions
+103 -2
View File
@@ -8,7 +8,7 @@ import logging
import re
import time
import yaml
from typing import Any, Dict, List
from typing import TYPE_CHECKING, Any, Dict, List
from urllib.parse import quote
from napalm_device_types.models import (
@@ -18,8 +18,53 @@ from napalm_device_types.models import (
VMStatusDict,
)
if TYPE_CHECKING:
# Type-only: VMCpuTypeDict is newer than the napalm_device_types floor in
# pyproject.toml, and nothing here needs it at runtime.
from napalm_device_types.models import VMCpuTypeDict
_logger = logging.getLogger(__name__)
# The CPU models a new VM may be given. Each lists the /proc/cpuinfo flags it
# adds on top of QEMU's qemu64 baseline: what the node's CPU must have for the
# model to start at all, and what a guest can count on. The sets follow
# Proxmox's own x86-64-v* definitions (qemu-server, PVE/QemuServer/CPUConfig.pm).
#
# Leaving the model out of qemu.post is not neutral: Proxmox then falls back to
# kvm64, which lacks even AES-NI, let alone the AVX MongoDB 5.0+ needs
# (netOrk#494). The default below is what the Proxmox GUI picks since PVE 8.
_X86_64_V2_AES_FLAGS = ("aes", "popcnt", "pni", "sse4_1", "sse4_2", "ssse3")
_X86_64_V3_FLAGS = _X86_64_V2_AES_FLAGS + (
"avx",
"avx2",
"bmi1",
"bmi2",
"f16c",
"fma",
"abm",
"movbe",
"xsave",
)
_DEFAULT_CPU_TYPE = "x86-64-v2-AES"
_CPU_MODELS = (
(
"x86-64-v2-AES",
_X86_64_V2_AES_FLAGS,
"Proxmox's own default: runs on practically any x86-64 server CPU and "
"can live-migrate between different ones. No AVX.",
),
(
"x86-64-v3",
_X86_64_V3_FLAGS,
"Adds AVX and AVX2 (which MongoDB 5.0 and later need). Every node the VM "
"may run on needs an Intel Haswell or AMD Excavator CPU (2013) or newer.",
),
)
_HOST_CPU_DESCRIPTION = (
"This node's CPU, passed through unchanged: fastest, with every feature it "
"has, but the VM can only live-migrate to nodes with the same CPU."
)
# Downloaded cloud images are cached here on the hypervisor node, keyed by
# filename, so provisioning multiple VMs from the same image only pays the
# download cost once.
@@ -351,6 +396,56 @@ class ProxmoxVMProvisionMixin:
)
return targets
def get_vm_cpu_types(self) -> list[VMCpuTypeDict]:
"""List the CPU models a new VM may be given, judged against this node's CPU."""
return self._cpu_types_for(self._node_cpu_flags())
def _node_cpu_flags(self) -> set[str]:
status = self._node_api().status.get() or {}
return set(str((status.get("cpuinfo") or {}).get("flags", "")).split())
@staticmethod
def _cpu_types_for(node_flags: set[str]) -> list[VMCpuTypeDict]:
types: list[VMCpuTypeDict] = [
{
"name": name,
"description": description,
"features": list(flags),
"available": set(flags) <= node_flags,
"default": name == _DEFAULT_CPU_TYPE,
}
for name, flags, description in _CPU_MODELS
]
types.append(
{
"name": "host",
"description": _HOST_CPU_DESCRIPTION,
"features": sorted(node_flags),
"available": True,
"default": False,
}
)
return types
def _resolve_cpu_type(self, cpu_type: str | None) -> str:
"""The model to create the VM with. A model asked for by name is checked
against this node's CPU first: one it cannot run would fail only at VM
start, after the disk import, leaving a half-built VM behind."""
if cpu_type is None:
return _DEFAULT_CPU_TYPE
node_flags = self._node_cpu_flags()
offered = {t["name"]: t for t in self._cpu_types_for(node_flags)}
entry = offered.get(cpu_type)
if entry is None:
raise ValueError(f"Unknown CPU type {cpu_type!r}; choose one of {', '.join(offered)}")
if not entry["available"]:
missing = sorted(set(entry["features"]) - node_flags)
raise ValueError(
f"CPU type {cpu_type!r} needs {', '.join(missing)}, which the CPU of "
f"node {self._node_name} does not have"
)
return cpu_type
def _wait_for_task(self, upid: str, timeout: int = 120) -> None:
"""
Poll a Proxmox task until completion.
@@ -395,6 +490,7 @@ class ProxmoxVMProvisionMixin:
ssh_public_keys: List[str] | None = None,
disk_resize_gb: int | None = None,
storage: str | None = None,
cpu_type: str | None = None,
download_timeout: int = 300,
timeout: int = 180,
) -> VMProvisionResultDict:
@@ -427,6 +523,7 @@ class ProxmoxVMProvisionMixin:
disk_resize_gb: resize root disk to this size (None = no resize)
storage: storage pool for the root disk (None = auto-detect first
enabled, node-available storage with content='images')
cpu_type: CPU model from get_vm_cpu_types() (None = x86-64-v2-AES)
download_timeout: max seconds for the image download (skipped if cached)
timeout: max seconds for the remaining provisioning steps
@@ -435,10 +532,13 @@ class ProxmoxVMProvisionMixin:
Raises:
RuntimeError: provisioning failure (download, import, config, timeout, etc.)
ValueError: invalid storage or configuration
ValueError: invalid storage or configuration, or a cpu_type that is
unknown or that this node's CPU cannot run (raised before
anything is created)
"""
try:
_logger.info(f"Creating VM '{name}' from image {image_url}")
cpu_model = self._resolve_cpu_type(cpu_type)
# Step 1: Get next VMID
next_vmid = self._api.cluster.nextid.get()
@@ -452,6 +552,7 @@ class ProxmoxVMProvisionMixin:
name=name,
memory=memory,
cores=cpu,
cpu=cpu_model,
ostype="l26",
scsihw="virtio-scsi-pci",
# Without this, Proxmox never attaches the virtio-serial