1 package PVE
::QemuServer
::CPUConfig
;
7 use PVE
::Cluster
qw(cfs_register_file cfs_read_file);
8 use PVE
::QemuServer
::Helpers
qw(min_version);
10 use base
qw(PVE::SectionConfig Exporter);
17 # under certain race-conditions, this module might be loaded before pve-cluster
18 # has started completely, so ensure we don't prevent the FUSE mount with our dir
19 if (PVE
::Cluster
::check_cfs_is_mounted
(1)) {
20 mkdir "/etc/pve/virtual-guest";
23 my $default_filename = "virtual-guest/cpu-models.conf";
26 sub { PVE
::QemuServer
::CPUConfig-
>parse_config(@_); },
27 sub { PVE
::QemuServer
::CPUConfig-
>write_config(@_); },
30 sub load_custom_model_conf
{
31 return cfs_read_file
($default_filename);
34 #builtin models : reported-model is mandatory
35 my $builtin_models = {
37 'reported-model' => 'qemu64',
38 flags
=> "+popcnt;+pni;+sse4.1;+sse4.2;+ssse3",
41 'reported-model' => 'qemu64',
42 flags
=> "+aes;+popcnt;+pni;+sse4.1;+sse4.2;+ssse3",
45 'reported-model' => 'qemu64',
46 flags
=> "+aes;+popcnt;+pni;+sse4.1;+sse4.2;+ssse3;+avx;+avx2;+bmi1;+bmi2;+f16c;+fma;+abm;+movbe;+xsave",
49 'reported-model' => 'qemu64',
50 flags
=> "+aes;+popcnt;+pni;+sse4.1;+sse4.2;+ssse3;+avx;+avx2;+bmi1;+bmi2;+f16c;+fma;+abm;+movbe;+xsave;+avx512f;+avx512bw;+avx512cd;+avx512dq;+avx512vl",
54 my $depreacated_cpu_map = {
55 # there never was such a client CPU, so map it to the server one for backward compat
56 'Icelake-Client' => 'Icelake-Server',
57 'Icelake-Client-noTSX' => 'Icelake-Server-noTSX',
60 my $cpu_vendor_list = {
62 486 => 'GenuineIntel',
63 pentium
=> 'GenuineIntel',
64 pentium2
=> 'GenuineIntel',
65 pentium3
=> 'GenuineIntel',
66 coreduo
=> 'GenuineIntel',
67 core2duo
=> 'GenuineIntel',
68 Conroe
=> 'GenuineIntel',
69 Penryn
=> 'GenuineIntel',
70 Nehalem
=> 'GenuineIntel',
71 'Nehalem-IBRS' => 'GenuineIntel',
72 Westmere
=> 'GenuineIntel',
73 'Westmere-IBRS' => 'GenuineIntel',
74 SandyBridge
=> 'GenuineIntel',
75 'SandyBridge-IBRS' => 'GenuineIntel',
76 IvyBridge
=> 'GenuineIntel',
77 'IvyBridge-IBRS' => 'GenuineIntel',
78 Haswell
=> 'GenuineIntel',
79 'Haswell-IBRS' => 'GenuineIntel',
80 'Haswell-noTSX' => 'GenuineIntel',
81 'Haswell-noTSX-IBRS' => 'GenuineIntel',
82 Broadwell
=> 'GenuineIntel',
83 'Broadwell-IBRS' => 'GenuineIntel',
84 'Broadwell-noTSX' => 'GenuineIntel',
85 'Broadwell-noTSX-IBRS' => 'GenuineIntel',
86 'Skylake-Client' => 'GenuineIntel',
87 'Skylake-Client-IBRS' => 'GenuineIntel',
88 'Skylake-Client-noTSX-IBRS' => 'GenuineIntel',
89 'Skylake-Client-v4' => 'GenuineIntel',
90 'Skylake-Server' => 'GenuineIntel',
91 'Skylake-Server-IBRS' => 'GenuineIntel',
92 'Skylake-Server-noTSX-IBRS' => 'GenuineIntel',
93 'Skylake-Server-v4' => 'GenuineIntel',
94 'Skylake-Server-v5' => 'GenuineIntel',
95 'Cascadelake-Server' => 'GenuineIntel',
96 'Cascadelake-Server-v2' => 'GenuineIntel',
97 'Cascadelake-Server-noTSX' => 'GenuineIntel',
98 'Cascadelake-Server-v4' => 'GenuineIntel',
99 'Cascadelake-Server-v5' => 'GenuineIntel',
100 'Cooperlake' => 'GenuineIntel',
101 'Cooperlake-v2' => 'GenuineIntel',
102 KnightsMill
=> 'GenuineIntel',
103 'Icelake-Client' => 'GenuineIntel', # depreacated, removed with QEMU 7.1
104 'Icelake-Client-noTSX' => 'GenuineIntel', # depreacated, removed with QEMU 7.1
105 'Icelake-Server' => 'GenuineIntel',
106 'Icelake-Server-noTSX' => 'GenuineIntel',
107 'Icelake-Server-v3' => 'GenuineIntel',
108 'Icelake-Server-v4' => 'GenuineIntel',
109 'Icelake-Server-v5' => 'GenuineIntel',
110 'Icelake-Server-v6' => 'GenuineIntel',
111 'SapphireRapids' => 'GenuineIntel',
114 athlon
=> 'AuthenticAMD',
115 phenom
=> 'AuthenticAMD',
116 Opteron_G1
=> 'AuthenticAMD',
117 Opteron_G2
=> 'AuthenticAMD',
118 Opteron_G3
=> 'AuthenticAMD',
119 Opteron_G4
=> 'AuthenticAMD',
120 Opteron_G5
=> 'AuthenticAMD',
121 EPYC
=> 'AuthenticAMD',
122 'EPYC-IBPB' => 'AuthenticAMD',
123 'EPYC-v3' => 'AuthenticAMD',
124 'EPYC-Rome' => 'AuthenticAMD',
125 'EPYC-Rome-v2' => 'AuthenticAMD',
126 'EPYC-Milan' => 'AuthenticAMD',
128 # generic types, use vendor from host node
137 my @supported_cpu_flags = (
151 my $cpu_flag_supported_re = qr/([+-])(@{[join('|', @supported_cpu_flags)]})/;
152 my $cpu_flag_any_re = qr/([+-])([a-zA-Z0-9\-_\.]+)/;
154 our $qemu_cmdline_cpu_re = qr/^((?>[+-]?[\w\-\._=]+,?)+)$/;
158 description
=> "Emulated CPU type. Can be default or custom name (custom model names must be prefixed with 'custom-').",
160 format_description
=> 'string',
165 'reported-model' => {
166 description
=> "CPU model and vendor to report to the guest. Must be a QEMU/KVM supported model."
167 ." Only valid for custom CPU model definitions, default models will always report themselves to the guest OS.",
169 enum
=> [ sort { lc("$a") cmp lc("$b") } keys %$cpu_vendor_list ],
174 description
=> "Do not identify as a KVM virtual machine.",
181 pattern
=> qr/[a-zA-Z0-9]{1,12}/,
182 format_description
=> 'vendor-id',
183 description
=> 'The Hyper-V vendor ID. Some drivers or programs inside Windows guests need a specific ID.',
187 description
=> "List of additional CPU flags separated by ';'. Use '+FLAG' to enable,"
188 ." '-FLAG' to disable a flag. Custom CPU models can specify any flag supported by"
189 ." QEMU/KVM, VM-specific flags must be from the following set for security reasons: "
190 . join(', ', @supported_cpu_flags),
191 format_description
=> '+FLAG[;-FLAG...]',
193 pattern
=> qr/$cpu_flag_any_re(;$cpu_flag_any_re)*/,
198 format
=> 'pve-phys-bits',
199 format_description
=> '8-64|host',
200 description
=> "The physical memory address bits that are reported to the guest OS. Should"
201 ." be smaller or equal to the host's. Set to 'host' to use value from host CPU, but"
202 ." note that doing so will break live migration to CPUs with other values.",
207 PVE
::JSONSchema
::register_format
('pve-phys-bits', \
&parse_phys_bits
);
208 sub parse_phys_bits
{
209 my ($str, $noerr) = @_;
211 my $err_msg = "value must be an integer between 8 and 64 or 'host'\n";
213 if ($str !~ m/^(host|\d{1,2})$/) {
214 die $err_msg if !$noerr;
218 if ($str =~ m/^\d+$/ && (int($str) < 8 || int($str) > 64)) {
219 die $err_msg if !$noerr;
226 # $cpu_fmt describes both the CPU config passed as part of a VM config, as well
227 # as the definition of a custom CPU model. There are some slight differences
228 # though, which we catch in the custom validation functions below.
229 PVE
::JSONSchema
::register_format
('pve-cpu-conf', $cpu_fmt, \
&validate_cpu_conf
);
230 sub validate_cpu_conf
{
232 # required, but can't be forced in schema since it's encoded in section header for custom models
233 die "CPU is missing cputype\n" if !$cpu->{cputype
};
236 PVE
::JSONSchema
::register_format
('pve-vm-cpu-conf', $cpu_fmt, \
&validate_vm_cpu_conf
);
237 sub validate_vm_cpu_conf
{
240 validate_cpu_conf
($cpu);
242 my $cputype = $cpu->{cputype
};
244 # a VM-specific config is only valid if the cputype exists
245 if (is_custom_model
($cputype)) {
246 # dies on unknown model
247 get_custom_model
($cputype);
249 die "Built-in cputype '$cputype' is not defined (missing 'custom-' prefix?)\n"
250 if !defined($cpu_vendor_list->{$cputype}) && !defined($builtin_models->{$cputype});
253 # in a VM-specific config, certain properties are limited/forbidden
255 die "VM-specific CPU flags must be a subset of: @{[join(', ', @supported_cpu_flags)]}\n"
256 if ($cpu->{flags
} && $cpu->{flags
} !~ m/^$cpu_flag_supported_re(;$cpu_flag_supported_re)*$/);
258 die "Property 'reported-model' not allowed in VM-specific CPU config.\n"
259 if defined($cpu->{'reported-model'});
264 # Section config settings
266 # shallow copy, since SectionConfig modifies propertyList internally
267 propertyList
=> { %$cpu_fmt },
275 return { %$cpu_fmt };
282 sub parse_section_header
{
283 my ($class, $line) = @_;
285 my ($type, $sectionId, $errmsg, $config) =
286 $class->SUPER::parse_section_header
($line);
289 return ($type, $sectionId, $errmsg, {
290 # name is given by section header, and we can always prepend 'custom-'
291 # since we're reading the custom CPU file
292 cputype
=> "custom-$sectionId",
297 my ($class, $filename, $cfg) = @_;
299 mkdir "/etc/pve/virtual-guest";
301 for my $model (keys %{$cfg->{ids
}}) {
302 my $model_conf = $cfg->{ids
}->{$model};
304 die "internal error: tried saving built-in CPU model (or missing prefix): $model_conf->{cputype}\n"
305 if !is_custom_model
($model_conf->{cputype
});
307 die "internal error: tried saving custom cpumodel with cputype (ignoring prefix: $model_conf->{cputype}) not equal to \$cfg->ids entry ($model)\n"
308 if "custom-$model" ne $model_conf->{cputype
};
310 # saved in section header
311 delete $model_conf->{cputype
};
314 $class->SUPER::write_config
($filename, $cfg);
317 sub add_cpu_json_properties
{
320 foreach my $opt (keys %$cpu_fmt) {
321 $prop->{$opt} = $cpu_fmt->{$opt};
328 my ($include_custom) = @_;
332 for my $default_model (keys %{$cpu_vendor_list}) {
334 name
=> $default_model,
336 vendor
=> $cpu_vendor_list->{$default_model},
340 for my $model (keys %{$builtin_models}) {
341 my $reported_model = $builtin_models->{$model}->{'reported-model'};
342 my $vendor = $cpu_vendor_list->{$reported_model};
350 return $models if !$include_custom;
352 my $conf = load_custom_model_conf
();
353 for my $custom_model (keys %{$conf->{ids
}}) {
354 my $reported_model = $conf->{ids
}->{$custom_model}->{'reported-model'};
355 $reported_model //= $cpu_fmt->{'reported-model'}->{default};
356 my $vendor = $cpu_vendor_list->{$reported_model};
358 name
=> "custom-$custom_model",
367 sub is_custom_model
{
369 return $cputype =~ m/^custom-/;
372 # Use this to get a single model in the format described by $cpu_fmt.
373 # Allows names with and without custom- prefix.
374 sub get_custom_model
{
375 my ($name, $noerr) = @_;
377 $name =~ s/^custom-//;
378 my $conf = load_custom_model_conf
();
380 my $entry = $conf->{ids
}->{$name};
381 if (!defined($entry)) {
382 die "Custom cputype '$name' not found\n" if !$noerr;
387 for my $property (keys %$cpu_fmt) {
388 if (my $value = $entry->{$property}) {
389 $model->{$property} = $value;
396 # Print a QEMU device node for a given VM configuration for hotplugging CPUs
397 sub print_cpu_device
{
398 my ($conf, $id) = @_;
400 my $kvm = $conf->{kvm
} // 1;
401 my $cpu = $kvm ?
"kvm64" : "qemu64";
402 if (my $cputype = $conf->{cpu
}) {
403 my $cpuconf = PVE
::JSONSchema
::parse_property_string
('pve-vm-cpu-conf', $cputype)
404 or die "Cannot parse cpu description: $cputype\n";
405 $cpu = $cpuconf->{cputype
};
407 if (my $model = $builtin_models->{$cpu}) {
408 $cpu = $model->{'reported-model'};
409 } elsif (is_custom_model
($cputype)) {
410 my $custom_cpu = get_custom_model
($cpu);
412 $cpu = $custom_cpu->{'reported-model'} // $cpu_fmt->{'reported-model'}->{default};
414 if (my $replacement_type = $depreacated_cpu_map->{$cpu}) {
415 $cpu = $replacement_type;
419 my $cores = $conf->{cores
} || 1;
421 my $current_core = ($id - 1) % $cores;
422 my $current_socket = int(($id - 1 - $current_core)/$cores);
424 # FIXME: hot plugging other architectures like our unofficial arch64 support?
425 return "$cpu-x86_64-cpu,id=cpu$id,socket-id=$current_socket,core-id=$current_core,thread-id=0";
428 # Resolves multiple arrays of hashes representing CPU flags with metadata to a
429 # single string in QEMU "-cpu" compatible format. Later arrays have higher
432 # Hashes take the following format:
435 # op => "+", # defaults to "" if undefined
436 # reason => "to support AES acceleration", # for override warnings
437 # value => "" # needed for kvm=off (value: off) etc...
441 sub resolve_cpu_flags
{
445 for my $flag_name (keys %$hash) {
446 my $flag = $hash->{$flag_name};
447 my $old_flag = $flags->{$flag_name};
450 $flag->{reason
} //= "unknown origin";
453 my $value_changed = (defined($flag->{value
}) != defined($old_flag->{value
})) ||
454 (defined($flag->{value
}) && $flag->{value
} ne $old_flag->{value
});
456 if ($old_flag->{op
} eq $flag->{op
} && !$value_changed) {
457 $flags->{$flag_name}->{reason
} .= " & $flag->{reason}";
461 my $old = print_cpuflag_hash
($flag_name, $flags->{$flag_name});
462 my $new = print_cpuflag_hash
($flag_name, $flag);
463 warn "warning: CPU flag/setting $new overwrites $old\n";
466 $flags->{$flag_name} = $flag;
471 # sort for command line stability
472 for my $flag_name (sort keys %$flags) {
474 $flag_str .= $flags->{$flag_name}->{op
};
475 $flag_str .= $flag_name;
476 $flag_str .= "=$flags->{$flag_name}->{value}"
477 if $flags->{$flag_name}->{value
};
483 sub print_cpuflag_hash
{
484 my ($flag_name, $flag) = @_;
485 my $formatted = "'$flag->{op}$flag_name";
486 $formatted .= "=$flag->{value}" if defined($flag->{value
});
488 $formatted .= " ($flag->{reason})" if defined($flag->{reason
});
492 sub parse_cpuflag_list
{
493 my ($re, $reason, $flaglist) = @_;
496 return $res if !$flaglist;
498 foreach my $flag (split(";", $flaglist)) {
499 if ($flag =~ m/^$re$/) {
500 $res->{$2} = { op
=> $1, reason
=> $reason };
507 # Calculate QEMU's '-cpu' argument from a given VM configuration
508 sub get_cpu_options
{
509 my ($conf, $arch, $kvm, $kvm_off, $machine_version, $winversion, $gpu_passthrough) = @_;
511 my $cputype = $kvm ?
"kvm64" : "qemu64";
512 if ($arch eq 'aarch64') {
513 $cputype = 'cortex-a57';
520 if (my $cpu_prop_str = $conf->{cpu
}) {
521 $cpu = PVE
::JSONSchema
::parse_property_string
('pve-vm-cpu-conf', $cpu_prop_str)
522 or die "Cannot parse cpu description: $cpu_prop_str\n";
524 $cputype = $cpu->{cputype
};
525 if (my $model = $builtin_models->{$cputype}) {
526 $cputype = $model->{'reported-model'};
527 $builtin_cpu->{flags
} = $model->{'flags'};
528 } elsif (is_custom_model
($cputype)) {
529 $custom_cpu = get_custom_model
($cputype);
531 $cputype = $custom_cpu->{'reported-model'} // $cpu_fmt->{'reported-model'}->{default};
532 $kvm_off = $custom_cpu->{hidden
} if defined($custom_cpu->{hidden
});
533 $hv_vendor_id = $custom_cpu->{'hv-vendor-id'};
536 if (my $replacement_type = $depreacated_cpu_map->{$cputype}) {
537 $cputype = $replacement_type;
540 # VM-specific settings override custom CPU config
541 $kvm_off = $cpu->{hidden
} if defined($cpu->{hidden
});
542 $hv_vendor_id = $cpu->{'hv-vendor-id'} if defined($cpu->{'hv-vendor-id'});
545 my $pve_flags = get_pve_cpu_flags
($conf, $kvm, $cputype, $arch, $machine_version);
548 ? get_hyperv_enlightenments
(
557 my $builtin_cputype_flags = parse_cpuflag_list
(
558 $cpu_flag_any_re, "set by builtin CPU model", $builtin_cpu->{flags
});
560 my $custom_cputype_flags = parse_cpuflag_list
(
561 $cpu_flag_any_re, "set by custom CPU model", $custom_cpu->{flags
});
563 my $vm_flags = parse_cpuflag_list
(
564 $cpu_flag_supported_re, "manually set for VM", $cpu->{flags
});
566 my $pve_forced_flags = {};
567 $pve_forced_flags->{'enforce'} = {
568 reason
=> "error if requested CPU settings not available",
569 } if $cputype ne 'host' && $kvm && $arch eq 'x86_64';
570 $pve_forced_flags->{'kvm'} = {
572 reason
=> "hide KVM virtualization from guest",
575 # $cputype is the "reported-model" for custom types, so we can just look up
576 # the vendor in the default list
577 my $cpu_vendor = $cpu_vendor_list->{$cputype};
579 $pve_forced_flags->{'vendor'} = {
580 value
=> $cpu_vendor,
581 } if $cpu_vendor ne 'default';
582 } elsif ($arch ne 'aarch64') {
583 die "internal error"; # should not happen
586 my $cpu_str = $cputype;
588 # will be resolved in parameter order
589 $cpu_str .= resolve_cpu_flags
(
590 $pve_flags, $hv_flags, $builtin_cputype_flags, $custom_cputype_flags, $vm_flags, $pve_forced_flags);
593 foreach my $conf ($custom_cpu, $cpu) {
594 next if !defined($conf);
595 my $conf_val = $conf->{'phys-bits'};
597 if ($conf_val eq 'host') {
598 $phys_bits = ",host-phys-bits=true";
600 $phys_bits = ",phys-bits=$conf_val";
603 $cpu_str .= $phys_bits;
605 return ('-cpu', $cpu_str);
608 # Some hardcoded flags required by certain configurations
609 sub get_pve_cpu_flags
{
610 my ($conf, $kvm, $cputype, $arch, $machine_version) = @_;
613 my $pve_msg = "set by PVE;";
615 $pve_flags->{'lahf_lm'} = {
617 reason
=> "$pve_msg to support Windows 8.1+",
618 } if $cputype eq 'kvm64' && $arch eq 'x86_64';
620 $pve_flags->{'x2apic'} = {
622 reason
=> "$pve_msg incompatible with Solaris",
623 } if $conf->{ostype
} && $conf->{ostype
} eq 'solaris';
625 $pve_flags->{'sep'} = {
627 reason
=> "$pve_msg to support Windows 8+ and improve Windows XP+",
628 } if $cputype eq 'kvm64' || $cputype eq 'kvm32';
630 $pve_flags->{'rdtscp'} = {
632 reason
=> "$pve_msg broken on AMD Opteron",
633 } if $cputype =~ m/^Opteron/;
635 if (min_version
($machine_version, 2, 3) && $kvm && $arch eq 'x86_64') {
636 $pve_flags->{'kvm_pv_unhalt'} = {
638 reason
=> "$pve_msg to improve Linux guest spinlock performance",
640 $pve_flags->{'kvm_pv_eoi'} = {
642 reason
=> "$pve_msg to improve Linux guest interrupt performance",
649 sub get_hyperv_enlightenments
{
650 my ($winversion, $machine_version, $bios, $gpu_passthrough, $hv_vendor_id) = @_;
652 return if $winversion < 6;
653 return if $bios && $bios eq 'ovmf' && $winversion < 8;
656 my $default_reason = "automatic Hyper-V enlightenment for Windows";
658 my ($flag, $value, $reason) = @_;
660 reason
=> $reason // $default_reason,
665 my $hv_vendor_set = defined($hv_vendor_id);
666 if ($gpu_passthrough || $hv_vendor_set) {
667 $hv_vendor_id //= 'proxmox';
668 $flagfn->('hv_vendor_id', $hv_vendor_id, $hv_vendor_set ?
669 "custom hv_vendor_id set" : "NVIDIA workaround for GPU passthrough");
672 if (min_version
($machine_version, 2, 3)) {
673 $flagfn->('hv_spinlocks', '0x1fff');
674 $flagfn->('hv_vapic');
675 $flagfn->('hv_time');
677 $flagfn->('hv_spinlocks', '0xffff');
680 if (min_version
($machine_version, 2, 6)) {
681 $flagfn->('hv_reset');
682 $flagfn->('hv_vpindex');
683 $flagfn->('hv_runtime');
686 if ($winversion >= 7) {
687 my $win7_reason = $default_reason . " 7 and higher";
688 $flagfn->('hv_relaxed', undef, $win7_reason);
690 if (min_version
($machine_version, 2, 12)) {
691 $flagfn->('hv_synic', undef, $win7_reason);
692 $flagfn->('hv_stimer', undef, $win7_reason);
695 if (min_version
($machine_version, 3, 1)) {
696 $flagfn->('hv_ipi', undef, $win7_reason);
703 sub get_cpu_from_running_vm
{
706 my $cmdline = PVE
::QemuServer
::Helpers
::parse_cmdline
($pid);
707 die "could not read commandline of running machine\n"
708 if !$cmdline->{cpu
}->{value
};
710 # sanitize and untaint value
711 $cmdline->{cpu
}->{value
} =~ $qemu_cmdline_cpu_re;
715 __PACKAGE__-
>register();