Flavor ↔ KubeVirt instancetype mapping — Nova — branch stable/2026.2
Nova branch
stable/2026.2@8df1922926dd— KubeVirtv1.9.1@c3d19093f9c7(instancetype.kubevirt.io/v1beta1, the only version of that API in this release; sparse clone ofstaging/src/kubevirt.io/api) — analysed on 2026-10-28 References:nova/…= Nova repository;kubevirt:…= KubeVirt repository at the tag above.
This file says how OpenKeel can expose the Nova flavor API unchanged while running servers as KubeVirt VirtualMachines. It gives the mapping field by field and extra spec by extra spec. Every rule is also data (_rewrite/flavor-kubevirt-mapping_stable-2026.2.json) and code (_rewrite/flavor-to-kubevirt_stable-2026.2.py). The converter was run on 6 reference flavors (_rewrite/flavor-kubevirt-vectors_stable-2026.2.json).
1. Object model
| Nova | KubeVirt | Why it fits |
|---|---|---|
| Public flavor | VirtualMachineClusterInstancetype (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:56-62) | cluster-wide, read by every namespace |
Private flavor + flavor_projects | one VirtualMachineInstancetype per project namespace (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:34-40) | namespaced objects give the same visibility as the access list |
Flavor hardware hints that the image may also set (hw:cpu_sockets, os:secure_boot, devices…) | VirtualMachine(Cluster)Preference with the same name (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:203-316) | preferences are what KubeVirt applies when the VM does not say otherwise |
instance_extra.flavor cur (frozen copy, 01 § 6) | VirtualMachine.spec.instancetype.revisionName, a ControllerRevision (kubevirt:staging/src/kubevirt.io/api/core/v1/types.go:3890-3901) | KubeVirt stores the instancetype as it was when the VM started; deleting or changing the instancetype does not change running VMs |
Resize (old_flavor / new_flavor) | change spec.instancetype.name (and clear revisionName), then restart; keep the previous name and revision to revert | there is no two-phase confirm in KubeVirt: OpenKeel keeps old itself |
Flavor immutability (only description changes) | change of the instancetype spec = new revision for new VMs only | same behaviour if OpenKeel never updates the spec |
| Extra specs not translated | annotation openkeel.io/extra-specs (JSON) | lossless round trip of GET /flavors/{id}/os-extra_specs |
KubeVirt has no disabled flag, no access-list API and no "list sizes visible to a project" query merging both kinds. OpenKeel keeps the flavor catalogue as its source of truth, for example in its own database or as an annotation on the instancetype, and reconciles the KubeVirt objects from it.
2. Base fields
| Nova field (API name) | Nova source | KubeVirt target | Fidelity | Translation and notes |
|---|---|---|---|---|
id (flavorid) | nova/api/openstack/compute/schemas/flavors.py:38-42 | metadata.name + annotation openkeel.io/flavor-id | exact | Nova allows [a-zA-Z0-9. _-], up to 255 characters; Kubernetes requires a lower-case DNS-1123 name. Ids that are already valid are kept; others become fl-<16 hex of sha1(id)>. |
name | nova/db/api/models.py:233 | annotation openkeel.io/flavor-name | exact | unique in Nova (nova/db/api/models.py:229-230): OpenKeel must keep that check. |
description | nova/api/openstack/compute/flavors.py:114-139 | annotation openkeel.io/flavor-description | exact | the only mutable field; an annotation change does not create a new revision. |
vcpus | nova/api/openstack/compute/views/flavors.py:53 | spec.cpu.guest (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:139) | exact | |
ram (MiB) | nova/api/openstack/compute/views/flavors.py:48 | spec.memory.guest: "<ram>Mi" (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:178) | exact | the virt-launcher pod adds its overhead on top, as libvirt does on the hypervisor. |
disk (GiB) | nova/api/openstack/compute/views/flavors.py:49 | the VM's root dataVolumeTemplates[].spec.storage.resources.requests.storage | partial | no disk in an instancetype. 0 = image size, or boot from volume only (02 § 4). |
OS-FLV-EXT-DATA:ephemeral (GiB) | nova/api/openstack/compute/views/flavors.py:51 | VM volume emptyDisk.capacity | partial | one disk; Nova can split it with block device mappings. |
swap (MiB) | nova/api/openstack/compute/views/flavors.py:50 | VM emptyDisk + cloud-init mkswap/swapon | approximate | no swap device in KubeVirt. |
os-flavor-access:is_public | nova/api/openstack/compute/views/flavors.py:54 | kind: Cluster vs namespaced | exact | |
access list (flavor_projects) | nova/objects/flavor.py:72-92 | one namespaced copy per project | exact | addTenantAccess / removeTenantAccess = create or delete the copy. |
OS-FLV-DISABLED:disabled | nova/compute/api.py:1131-1132 | annotation openkeel.io/disabled | none | OpenKeel must refuse new boots and resizes itself (admission webhook or API layer). |
rxtx_factor | nova/api/openstack/compute/views/flavors.py:55 | annotation only | n/a | removed from the API at 2.102. |
vcpu_weight | nova/objects/flavor.py:221 | none | n/a | not exposed by the API. |
extra_specs | nova/api/openstack/compute/views/flavors.py:65-66 | annotation openkeel.io/extra-specs + § 3 | exact (round trip) |
3. Extra specs
Coverage: the 525 validators of Nova's registry (_rewrite/extra-spec-validators_stable-2026.2.json) all match a rule below (checked by script, validator_coverage in the mapping JSON). By fidelity:
| exact | partial | approximate | node-level | none | n/a |
|---|---|---|---|---|---|
| 8 | 458 | 9 | 10 | 18 | 22 |
The 458 "partial" rules are mostly the 378 traits and 45 capabilities: they become node labels, and that only works if an OpenKeel agent puts the same labels on the nodes.
Keys that are not in the registry (allowed by Nova outside its namespaces) are kept in the annotation and reported as unknown by the converter.
| Nova extra spec | Allowed values | Defined at | KubeVirt target | Fidelity | Translation and notes |
|---|---|---|---|---|---|
hw:cpu_policy | str: dedicated, shared, mixed | nova/api/validation/extra_specs/hw.py:65 | spec.cpu.dedicatedCPUPlacement (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:152) | partial | dedicated → dedicatedCPUPlacement: true (exact; needs the kubelet static CPU manager); shared → nothing (default); mixed → none: KubeVirt cannot mix pinned and floating vCPUs |
hw:cpu_dedicated_mask | str (pattern) | nova/api/validation/extra_specs/hw.py:131 | none | none | Only meaningful with hw:cpu_policy=mixed. |
hw:cpu_thread_policy | str: prefer, isolate, require | nova/api/validation/extra_specs/hw.py:89 | kubelet CPU manager option full-pcpus-only | node-level | prefer → default; require → node-level: full-pcpus-only, or nodeSelector on SMT nodes; isolate → none: nodeSelector on nodes without SMT is the closest Nova turns it into the HW_CPU_HYPERTHREADING trait (nova/scheduler/utils.py:419-427). |
hw:emulator_threads_policy | str: isolate, share | nova/api/validation/extra_specs/hw.py:110 | spec.cpu.isolateEmulatorThread (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:161) | partial | isolate → isolateEmulatorThread: true (one extra dedicated CPU, like Nova); share → none: Nova's cpu_shared_set has no KubeVirt equivalent |
hw:cpu_realtime | bool | nova/api/validation/extra_specs/hw.py:23 | spec.cpu.realtime: {} (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:165) | exact | Requires dedicatedCPUPlacement in both. |
hw:cpu_realtime_mask | str (pattern) | nova/api/validation/extra_specs/hw.py:35 | spec.cpu.realtime.mask (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:165) | exact | Nova: exclusion mask such as '^0-1'; KubeVirt: inclusion mask in libvirt syntax. Converted to '0-<vcpus-1>,<mask>'. |
hw:cpu_sockets | int min 1 | nova/api/validation/extra_specs/hw.py:279 | preference spec.cpu.preferredCPUTopology (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:371) | partial | An instancetype only has a vCPU count; the preference chooses sockets, cores or threads ('preferSockets'…) or a spread ratio, not exact counts. |
hw:cpu_cores | int min 1 | nova/api/validation/extra_specs/hw.py:293 | preference spec.cpu.preferredCPUTopology (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:371) | partial | — |
hw:cpu_threads | int min 1 | nova/api/validation/extra_specs/hw.py:307 | preference spec.cpu.preferredCPUTopology (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:371) | partial | — |
hw:cpu_max_sockets | int min 1 | nova/api/validation/extra_specs/hw.py:321 | spec.cpu.maxSockets (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:169) | approximate | Nova: upper bound when choosing the topology. KubeVirt: socket count reachable by CPU hotplug. |
hw:cpu_max_cores | int min 1 | nova/api/validation/extra_specs/hw.py:337 | none | none | — |
hw:cpu_max_threads | int min 1 | nova/api/validation/extra_specs/hw.py:353 | none | none | — |
hw:numa_nodes | int min 1 | nova/api/validation/extra_specs/hw.py:182 | spec.cpu.numa.guestMappingPassthrough: {} (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:156) | partial | KubeVirt copies the host NUMA layout of the pinned CPUs; it cannot build N nodes. Needs dedicatedCPUPlacement and hugepages. |
hw:numa_cpus.{num} | str (pattern) | nova/api/validation/extra_specs/hw.py:197 | none | none | — |
hw:numa_mem.{num} | int min 1 | nova/api/validation/extra_specs/hw.py:223 | none | none | — |
hw:mem_page_size | str (pattern) | nova/api/validation/extra_specs/hw.py:153 | spec.memory.hugepages.pageSize (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:182) | partial | small → nothing (default); any → nothing: KubeVirt does not choose for you; large → approximate: '2Mi'; <size> → exact: '2Mi', '1Gi'… (a bare number is KiB in Nova) |
hw:locked_memory | bool | nova/api/validation/extra_specs/hw.py:167 | none | none | — |
hw:mem_encryption | bool | nova/api/validation/extra_specs/hw.py:399 | spec.launchSecurity (kubevirt:staging/src/kubevirt.io/api/core/v1/schema.go:856-860) | exact | true without a model means AMD SEV in Nova (nova/virt/hardware.py:1488-1489). |
hw:mem_encryption_model | str: amd-sev, amd-sev-es, amd-sev-snp, intel-tdx | nova/api/validation/extra_specs/hw.py:411 | spec.launchSecurity (kubevirt:staging/src/kubevirt.io/api/core/v1/schema.go:856-860) | exact | amd-sev → launchSecurity.sev: {}; amd-sev-es → launchSecurity.sev.policy.encryptedState: true; amd-sev-snp → launchSecurity.snp: {}; intel-tdx → launchSecurity.tdx: {} |
hw:pmem | str (pattern) | nova/api/validation/extra_specs/hw.py:429 | none | none | — |
hw:pmu | bool | nova/api/validation/extra_specs/hw.py:445 | none | none | — |
hw:serial_port_count | int min 0 | nova/api/validation/extra_specs/hw.py:467 | preference devices.preferredAutoattachSerialConsole (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:436) | partial | 0 → false; >=1 → true (KubeVirt attaches one serial console) |
hw:sound_model | str: sb16, es1370, pcspk, ac97, ich6, ich9, usb, virtio | nova/api/validation/extra_specs/hw.py:578 | preference devices.preferredSoundModel (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:456) | partial | ich9 → ich9; ac97 → ac97; others → none: KubeVirt supports ich9 and ac97 |
hw:tpm_model | str: tpm-tis, tpm-crb | nova/api/validation/extra_specs/hw.py:479 | preference devices.preferredTPM: {} (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:531) | partial | KubeVirt emulates a single TPM 2.0 model. |
hw:tpm_version | str: 1.2, 2.0 | nova/api/validation/extra_specs/hw.py:494 | preference devices.preferredTPM: {} (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:531) | partial | 2.0 → preferredTPM: {}; 1.2 → none |
hw:tpm_secret_security | str: user, host, deployment | nova/api/validation/extra_specs/hw.py:511 | preference devices.preferredTPM.persistent (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:531) | approximate | Nova chooses who holds the vTPM secret; KubeVirt only chooses whether the TPM state persists (PVC). |
hw:usb_model | str: none, nec-xhci, qemu-xhci | nova/api/validation/extra_specs/hw.py:591 | none | none | — |
hw:redirected_usb_ports | int min 0, max 15 | nova/api/validation/extra_specs/hw.py:604 | VirtualMachine devices.clientPassthrough: {} (kubevirt:staging/src/kubevirt.io/api/core/v1/schema.go:604) | partial | KubeVirt enables USB redirection with a fixed number of ports. |
hw:vif_multiqueue_enabled | bool | nova/api/validation/extra_specs/hw.py:385 | preference devices.preferredNetworkInterfaceMultiQueue (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:526) | exact | — |
hw:virtio_packed_ring | bool | nova/api/validation/extra_specs/hw.py:566 | none | none | — |
hw:viommu_model | str: intel, smmuv3, virtio, auto | nova/api/validation/extra_specs/hw.py:550 | none | none | — |
hw:watchdog_action | str: none, pause, poweroff, reset, disabled | nova/api/validation/extra_specs/hw.py:528 | VirtualMachine devices.watchdog.i6300esb.action (kubevirt:staging/src/kubevirt.io/api/core/v1/schema.go:1408-1431) | partial | poweroff → poweroff; reset → reset; pause → none; none → watchdog without action: none; disabled → no watchdog |
hw:boot_menu | bool | nova/api/validation/extra_specs/hw.py:374 | none | none | — |
hw:hide_hypervisor_id | bool | nova/api/validation/extra_specs/hw.py:50 | preference features.preferredKvm.hidden (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:570) | exact | — |
hide_hypervisor_id | bool | nova/api/validation/extra_specs/null.py:22 | preference features.preferredKvm.hidden (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:570) | exact | Deprecated alias of hw:hide_hypervisor_id. |
hw:ephemeral_encryption | bool | nova/api/validation/extra_specs/hw.py:621 | StorageClass of the ephemeral volume | node-level | — |
hw:ephemeral_encryption_format | str: plain, luks, luksv2 | nova/api/validation/extra_specs/hw.py:631 | StorageClass of the ephemeral volume | node-level | — |
hw:pci_numa_affinity_policy | str: required, preferred, legacy, socket | nova/api/validation/extra_specs/hw.py:249 | kubelet Topology Manager policy | node-level | — |
hw_rng:allowed | bool | nova/api/validation/extra_specs/hw_rng.py:23 | preference devices.preferredRng: {} (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:516) | partial | Nova only allows the image to ask for an RNG; KubeVirt attaches one. |
hw_rng:rate_bytes | int min 0 | nova/api/validation/extra_specs/hw_rng.py:35 | none | none | — |
hw_rng:rate_period | int min 0 | nova/api/validation/extra_specs/hw_rng.py:46 | none | none | — |
hw_video:ram_max_mb | int min 0 | nova/api/validation/extra_specs/hw_video.py:23 | none | none | — |
os:secure_boot | str: disabled, required | nova/api/validation/extra_specs/os.py:24 | preference firmware.preferredEfi.secureBoot (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:615) | exact | required → preferredEfi: {secureBoot: true} + features.preferredSmm: {}; disabled → preferredEfi: {secureBoot: false} |
quota:cpu_shares | int min 0 | nova/api/validation/extra_specs/quota.py:99 | none (cgroup weight comes from the pod requests) | none | — |
quota:cpu_period | int min 0 | nova/api/validation/extra_specs/quota.py:116 | none | none | — |
quota:cpu_quota | int | nova/api/validation/extra_specs/quota.py:131 | VirtualMachine resources.limits.cpu | approximate | limit = vcpus x cpu_quota / cpu_period. |
quota:disk_*_sec | (family) | nova/api/validation/extra_specs/quota.py:150-168 | StorageClass / CSI QoS | node-level | — |
quota:vif_* | (family) | nova/api/validation/extra_specs/quota.py:170-185 | pod annotations kubernetes.io/ingress-bandwidth / egress-bandwidth (CNI bandwidth plugin) | approximate | Only the pod network; average in KB/s becomes '<8 x n>k'. |
quota:{cpu,memory,disk_io,vif}_{limit,reservation,shares_level,shares_share} | (family) | nova/api/validation/extra_specs/quota.py:24-94 | none | n/a | VMware driver only. |
vmware:* | (family) | nova/api/validation/extra_specs/vmware.py:20-44 | none | n/a | — |
resources{group}:PCPU | int | nova/api/validation/extra_specs/resources.py:24-39 | spec.cpu.dedicatedCPUPlacement: true (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:152) | partial | Exact when PCPU = vcpus; a VCPU/PCPU split is 'mixed', which KubeVirt lacks. |
resources{group}:VGPU | int | nova/api/validation/extra_specs/resources.py:24-39 | spec.gpus[] (deviceName = mediated device resource) (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:102) | partial | Needs the mdev type -> device plugin resource name in the KubeVirt CR permittedHostDevices. |
resources{group}:MEM_ENCRYPTION_CONTEXT | int | nova/api/validation/extra_specs/resources.py:24-39 | implied by spec.launchSecurity (kubevirt:staging/src/kubevirt.io/api/core/v1/schema.go:856-860) | partial | — |
resources{group}:VCPU, MEMORY_MB, DISK_GB | (family) | nova/api/validation/extra_specs/resources.py:24-39 | none (overrides the Placement request only) | n/a | — |
resources{group}:<other standard class> | (family) | nova/api/validation/extra_specs/resources.py:24-62 | spec.hostDevices[] / VM resources.requests (device plugin resource) (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:108) | partial | PGPU, SRIOV_NET_VF, FPGA, PMEM_NAMESPACE_*, NET_BW_*…: each needs a device plugin or an extended resource; CUSTOM_* likewise. |
trait{group}:<TRAIT> | (family) | nova/api/validation/extra_specs/traits.py:24-70 | required: spec.nodeSelector['openkeel.io/trait.<TRAIT>'] = 'true'; forbidden: VM nodeAffinity NotIn (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:83) | partial | Traits become node labels set by an OpenKeel node agent; an instancetype has nodeSelector but no affinity, so 'forbidden' goes to the VirtualMachine. |
group_policy | str: isolate, none | nova/api/validation/extra_specs/null.py:38 | none | n/a | Placement granular request policy. |
aggregate_instance_extra_specs:{key} | str | nova/api/validation/extra_specs/aggregate_instance_extra_specs.py:53 | spec.nodeSelector['openkeel.io/aggregate.<key>'] = <value> (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:83) | partial | Plain values only; the filter's operators (s==, <in>, <or>…) need node affinity. |
capabilities:* | (family) | nova/api/validation/extra_specs/capabilities.py:58-108 | spec.nodeSelector on node labels (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:83) | partial | ComputeCapabilitiesFilter operators need node affinity. |
pci_passthrough:alias | str (pattern) | nova/api/validation/extra_specs/pci_passthrough.py:22 | spec.hostDevices[] (one per device) (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:108) | partial | '<alias>:<count>'; the [pci] alias must map to a permittedHostDevices resource. |
accel:device_profile | str | nova/api/validation/extra_specs/accel.py:22 | none (Cyborg) | none | — |
Notes on the important translations
- CPU pinning.
hw:cpu_policy=dedicatedmaps exactly todedicatedCPUPlacement, but KubeVirt needs the kubeletstaticCPU manager and Guaranteed QoS on the node.mixedhas no equivalent. Nova also converts dedicated vCPUs intoPCPUresources (nova/scheduler/utils.py:363-417); in KubeVirt, the kubelet CPU manager does that accounting. - Realtime mask. Nova's mask excludes vCPUs (
^0-1), KubeVirt's mask lists them in libvirt syntax (kubevirt:staging/src/kubevirt.io/api/core/v1/schema.go:393-398). The converter writes0-<vcpus-1>,<nova mask>. - Huge pages. A bare number is KiB in Nova (
nova/virt/hardware.py:1572-1573).1GB→1Gi,2048→2Mi.largehas no "best available" equivalent: the converter picks2Mi. - NUMA. KubeVirt can only pass through the host layout of the pinned CPUs (
kubevirt:staging/src/kubevirt.io/api/core/v1/schema.go:403-410).hw:numa_nodes,hw:numa_cpus.Nandhw:numa_mem.Nlose their exact layout. - Memory encryption is the best fit: the four Nova models map to
sev,sev+encryptedState,snpandtdx(kubevirt:staging/src/kubevirt.io/api/core/v1/schema.go:856-893). Nova's default model, when onlyhw:mem_encryption=trueis set, is AMD SEV (nova/virt/hardware.py:1488-1489). - Traits and aggregates. An instancetype has a
nodeSelector(kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:83) but no affinity. So:requiredtraits and plain aggregate values go there;forbiddentraits and aggregate operators (<in>,s>=…) must go into the VirtualMachine's affinity.
- Devices. PCI aliases and vGPUs need an OpenKeel configuration mapping each Nova
[pci] aliasand mdev type to apermittedHostDevicesresource name. - QoS. Disk and network QoS move to the StorageClass and the CNI. CPU shares follow the pod requests, which KubeVirt derives from
vcpus / cpuAllocationRatio[inferred: KubeVirt cluster configuration, not part of the sparse clone].
4. Image properties that end in the preference
Most hw:<x> extra specs have an image twin hw_<x>, and Nova refuses conflicts (01 § 5). In KubeVirt, a single preference holds both:
- OpenKeel builds the preference from the flavor's extra specs, then the image properties;
- it refuses the boot, as Nova does, when the two disagree, applying Nova's rules:
nova/virt/hardware.py:1361-1390for strict equality, andnova/virt/hardware.py:1776-1823for an image that cannot raise the flavor's CPU policy.
Image-only properties (not flavors) with a direct preference target [inferred: not part of this flavor scope, listed to complete the preference]:
| Image property | Preference field |
|---|---|
hw_disk_bus | devices.preferredDiskBus |
hw_cdrom_bus | devices.preferredCdromBus |
hw_vif_model | devices.preferredInterfaceModel |
hw_video_model | devices.preferredVideoType |
hw_input_bus / hw_pointer_model | devices.preferredInputBus / preferredInputType |
hw_machine_type | machine.preferredMachineType |
hw_firmware_type=uefi | firmware.preferredEfi |
hw_rng_model | devices.preferredRng |
os_secure_boot | firmware.preferredEfi.secureBoot |
hw_architecture | preferredArchitecture / requirements.architecture |
min_ram | requirements.memory.guest |
requirements.cpu.guest and requirements.memory.guest (kubevirt:staging/src/kubevirt.io/api/instancetype/v1beta1/types.go:641-669) make KubeVirt refuse an instancetype that is too small for the preference. That matches Nova's FlavorMemoryTooSmall check against the image min_ram.
5. What OpenKeel puts in each VirtualMachine
The converter also returns a VM fragment:
spec.instancetype/spec.preference→{kind, name};- the root disk size (
disk, or the image size whendisk = 0); - the ephemeral and swap
emptyDisks; - what has no place in an instancetype: watchdog, USB redirection, CPU limit from
quota:cpu_quota, bandwidth annotations,NotInaffinity for forbidden traits.
The server API must keep showing the Nova flavor:
- from 2.47,
server.flavor={original_name, vcpus, ram, disk, ephemeral, swap, extra_specs}taken from the revision and the annotations, not from the current instancetype (nova/api/openstack/compute/views/servers.py:652-684); - before 2.47,
{id, links}.
6. Reference flavors (vectors)
_rewrite/flavor-kubevirt-vectors_stable-2026.2.json, produced by flavor-to-kubevirt_stable-2026.2.py --vectors:
| Vector | Instancetype result | Not translated |
|---|---|---|
m1.small, public, no extra specs | cluster instancetype 2: cpu.guest 1, memory.guest 2048Mi; root disk 20Gi | — |
| private, UUID id, 100 GiB ephemeral, 4 GiB swap, multiqueue | 2 namespaced instancetypes + preferences (proj-a, proj-b); root disk = image size; ephemeral and swap emptyDisks | swap is an approximation |
NFV: dedicated, require, isolate, 1GB pages, 1 NUMA node, realtime ^0-1 | dedicatedCPUPlacement, isolateEmulatorThread, realtime.mask 0-7,^0-1, hugepages 1Gi, numa.guestMappingPassthrough | hw:cpu_thread_policy, hw:pci_numa_affinity_policy (node-level) |
| confidential: SEV-SNP, secure boot, vTPM 2.0, hidden hypervisor | launchSecurity.snp; preference preferredEfi.secureBoot, preferredSmm, preferredTPM, preferredKvm.hidden | — |
| GPU + PCI + traits + aggregates | gpus[1], hostDevices[2] (resource names to configure), nodeSelector for the required trait and ssd=true; VM NotIn affinity for the forbidden trait | aggregate operator <in> r1 |
| QoS and devices | preference preferSockets, serial console; VM watchdog reset, CPU limit 1000m, ingress-bandwidth 81920k | quota:disk_read_iops_sec (StorageClass), hw:sound_model=virtio, hw:locked_memory, my-own-key (kept) |
7. Gaps and recommendations
- No exact equivalent:
hw:cpu_policy=mixed;- an explicit NUMA layout;
hw:cpu_thread_policyper VM;- swap;
- per-flavor disk and VIF QoS;
hw:locked_memory,hw:pmem,hw:pmu,hw:viommu_model,hw:usb_model,hw:boot_menu,hw:virtio_packed_ring,hw_rngrates,hw_video:ram_max_mb;accel:device_profile(Cyborg). Announce them as compatibility gaps of OpenKeel. Keep the keys, so that the API round trip stays exact.
- Validate like Nova ≥ 2.86 with the registry file: same types, enumerations, patterns and namespaces. Refuse what Nova refuses, and accept what Nova accepts even if KubeVirt ignores it.
- Keep Placement semantics where KubeVirt has none. Quotas count
vcpus,ramand disk of the flavor. They are not KubernetesResourceQuotarequests, which are divided by the CPU allocation ratio. - Node labels for traits, aggregates and capabilities must be published by an OpenKeel node agent with a fixed prefix (
openkeel.io/trait.<TRAIT>,openkeel.io/aggregate.<key>). - Names: keep the Nova name uniqueness check and the id → Kubernetes name encoding stable. The encoding is part of the contract between OpenKeel components.
8. Checklist for the OpenKeel flavor service
- [ ] Flavor API of 01, up to 2.104, with the same status codes (200 on create, 202 on delete, 200 on extra-spec delete) and messages of 02.
- [ ] Visibility rules (public + access list; admin
is_publicfilter; disabled hidden). - [ ] Extra-spec validation from the registry file (≥ 2.86 only).
- [ ] Reconciler: flavor → (Cluster)Instancetype / Preference per the rules; delete leaves VMs untouched.
- [ ] Server view of the flavor from the VM's revision.
- [ ] Resize: switch the instancetype, keep
oldfor revert, refuse the cases of 01 § 7. - [ ] Versioned notifications
flavor.create/update/deletewithFlavorPayload1.4, if consumers rely on them.
Source: reverse-spec-nova/07-flavors-kubevirt-mapping_stable-2026.2.md. The _rewrite/ and _inventory/ files are not published on the site.