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 my $depreacated_cpu_map = {
35 # there never was such a client CPU, so map it to the server one for backward compat
36 'Icelake-Client' => 'Icelake-Server',
37 'Icelake-Client-noTSX' => 'Icelake-Server-noTSX',
40 my $cpu_vendor_list = {
42 486 => 'GenuineIntel',
43 pentium
=> 'GenuineIntel',
44 pentium2
=> 'GenuineIntel',
45 pentium3
=> 'GenuineIntel',
46 coreduo
=> 'GenuineIntel',
47 core2duo
=> 'GenuineIntel',
48 Conroe
=> 'GenuineIntel',
49 Penryn
=> 'GenuineIntel',
50 Nehalem
=> 'GenuineIntel',
51 'Nehalem-IBRS' => 'GenuineIntel',
52 Westmere
=> 'GenuineIntel',
53 'Westmere-IBRS' => 'GenuineIntel',
54 SandyBridge
=> 'GenuineIntel',
55 'SandyBridge-IBRS' => 'GenuineIntel',
56 IvyBridge
=> 'GenuineIntel',
57 'IvyBridge-IBRS' => 'GenuineIntel',
58 Haswell
=> 'GenuineIntel',
59 'Haswell-IBRS' => 'GenuineIntel',
60 'Haswell-noTSX' => 'GenuineIntel',
61 'Haswell-noTSX-IBRS' => 'GenuineIntel',
62 Broadwell
=> 'GenuineIntel',
63 'Broadwell-IBRS' => 'GenuineIntel',
64 'Broadwell-noTSX' => 'GenuineIntel',
65 'Broadwell-noTSX-IBRS' => 'GenuineIntel',
66 'Skylake-Client' => 'GenuineIntel',
67 'Skylake-Client-IBRS' => 'GenuineIntel',
68 'Skylake-Client-noTSX-IBRS' => 'GenuineIntel',
69 'Skylake-Server' => 'GenuineIntel',
70 'Skylake-Server-IBRS' => 'GenuineIntel',
71 'Skylake-Server-noTSX-IBRS' => 'GenuineIntel',
72 'Cascadelake-Server' => 'GenuineIntel',
73 'Cascadelake-Server-noTSX' => 'GenuineIntel',
74 KnightsMill
=> 'GenuineIntel',
75 'Icelake-Client' => 'GenuineIntel', # depreacated, removed with QEMU 7.1
76 'Icelake-Client-noTSX' => 'GenuineIntel', # depreacated, removed with QEMU 7.1
77 'Icelake-Server' => 'GenuineIntel',
78 'Icelake-Server-noTSX' => 'GenuineIntel',
81 athlon
=> 'AuthenticAMD',
82 phenom
=> 'AuthenticAMD',
83 Opteron_G1
=> 'AuthenticAMD',
84 Opteron_G2
=> 'AuthenticAMD',
85 Opteron_G3
=> 'AuthenticAMD',
86 Opteron_G4
=> 'AuthenticAMD',
87 Opteron_G5
=> 'AuthenticAMD',
88 EPYC
=> 'AuthenticAMD',
89 'EPYC-IBPB' => 'AuthenticAMD',
90 'EPYC-Rome' => 'AuthenticAMD',
91 'EPYC-Milan' => 'AuthenticAMD',
93 # generic types, use vendor from host node
102 my @supported_cpu_flags = (
116 my $cpu_flag_supported_re = qr/([+-])(@{[join('|', @supported_cpu_flags)]})/;
117 my $cpu_flag_any_re = qr/([+-])([a-zA-Z0-9\-_\.]+)/;
119 our $qemu_cmdline_cpu_re = qr/^((?>[+-]?[\w\-\._=]+,?)+)$/;
123 description
=> "Emulated CPU type. Can be default or custom name (custom model names must be prefixed with 'custom-').",
125 format_description
=> 'string',
130 'reported-model' => {
131 description
=> "CPU model and vendor to report to the guest. Must be a QEMU/KVM supported model."
132 ." Only valid for custom CPU model definitions, default models will always report themselves to the guest OS.",
134 enum
=> [ sort { lc("$a") cmp lc("$b") } keys %$cpu_vendor_list ],
139 description
=> "Do not identify as a KVM virtual machine.",
146 pattern
=> qr/[a-zA-Z0-9]{1,12}/,
147 format_description
=> 'vendor-id',
148 description
=> 'The Hyper-V vendor ID. Some drivers or programs inside Windows guests need a specific ID.',
152 description
=> "List of additional CPU flags separated by ';'. Use '+FLAG' to enable,"
153 ." '-FLAG' to disable a flag. Custom CPU models can specify any flag supported by"
154 ." QEMU/KVM, VM-specific flags must be from the following set for security reasons: "
155 . join(', ', @supported_cpu_flags),
156 format_description
=> '+FLAG[;-FLAG...]',
158 pattern
=> qr/$cpu_flag_any_re(;$cpu_flag_any_re)*/,
163 format
=> 'pve-phys-bits',
164 format_description
=> '8-64|host',
165 description
=> "The physical memory address bits that are reported to the guest OS. Should"
166 ." be smaller or equal to the host's. Set to 'host' to use value from host CPU, but"
167 ." note that doing so will break live migration to CPUs with other values.",
172 PVE
::JSONSchema
::register_format
('pve-phys-bits', \
&parse_phys_bits
);
173 sub parse_phys_bits
{
174 my ($str, $noerr) = @_;
176 my $err_msg = "value must be an integer between 8 and 64 or 'host'\n";
178 if ($str !~ m/^(host|\d{1,2})$/) {
179 die $err_msg if !$noerr;
183 if ($str =~ m/^\d+$/ && (int($str) < 8 || int($str) > 64)) {
184 die $err_msg if !$noerr;
191 # $cpu_fmt describes both the CPU config passed as part of a VM config, as well
192 # as the definition of a custom CPU model. There are some slight differences
193 # though, which we catch in the custom validation functions below.
194 PVE
::JSONSchema
::register_format
('pve-cpu-conf', $cpu_fmt, \
&validate_cpu_conf
);
195 sub validate_cpu_conf
{
197 # required, but can't be forced in schema since it's encoded in section header for custom models
198 die "CPU is missing cputype\n" if !$cpu->{cputype
};
201 PVE
::JSONSchema
::register_format
('pve-vm-cpu-conf', $cpu_fmt, \
&validate_vm_cpu_conf
);
202 sub validate_vm_cpu_conf
{
205 validate_cpu_conf
($cpu);
207 my $cputype = $cpu->{cputype
};
209 # a VM-specific config is only valid if the cputype exists
210 if (is_custom_model
($cputype)) {
211 # dies on unknown model
212 get_custom_model
($cputype);
214 die "Built-in cputype '$cputype' is not defined (missing 'custom-' prefix?)\n"
215 if !defined($cpu_vendor_list->{$cputype});
218 # in a VM-specific config, certain properties are limited/forbidden
220 die "VM-specific CPU flags must be a subset of: @{[join(', ', @supported_cpu_flags)]}\n"
221 if ($cpu->{flags
} && $cpu->{flags
} !~ m/^$cpu_flag_supported_re(;$cpu_flag_supported_re)*$/);
223 die "Property 'reported-model' not allowed in VM-specific CPU config.\n"
224 if defined($cpu->{'reported-model'});
229 # Section config settings
231 # shallow copy, since SectionConfig modifies propertyList internally
232 propertyList
=> { %$cpu_fmt },
240 return { %$cpu_fmt };
247 sub parse_section_header
{
248 my ($class, $line) = @_;
250 my ($type, $sectionId, $errmsg, $config) =
251 $class->SUPER::parse_section_header
($line);
254 return ($type, $sectionId, $errmsg, {
255 # name is given by section header, and we can always prepend 'custom-'
256 # since we're reading the custom CPU file
257 cputype
=> "custom-$sectionId",
262 my ($class, $filename, $cfg) = @_;
264 mkdir "/etc/pve/virtual-guest";
266 for my $model (keys %{$cfg->{ids
}}) {
267 my $model_conf = $cfg->{ids
}->{$model};
269 die "internal error: tried saving built-in CPU model (or missing prefix): $model_conf->{cputype}\n"
270 if !is_custom_model
($model_conf->{cputype
});
272 die "internal error: tried saving custom cpumodel with cputype (ignoring prefix: $model_conf->{cputype}) not equal to \$cfg->ids entry ($model)\n"
273 if "custom-$model" ne $model_conf->{cputype
};
275 # saved in section header
276 delete $model_conf->{cputype
};
279 $class->SUPER::write_config
($filename, $cfg);
282 sub add_cpu_json_properties
{
285 foreach my $opt (keys %$cpu_fmt) {
286 $prop->{$opt} = $cpu_fmt->{$opt};
293 my ($include_custom) = @_;
297 for my $default_model (keys %{$cpu_vendor_list}) {
299 name
=> $default_model,
301 vendor
=> $cpu_vendor_list->{$default_model},
305 return $models if !$include_custom;
307 my $conf = load_custom_model_conf
();
308 for my $custom_model (keys %{$conf->{ids
}}) {
309 my $reported_model = $conf->{ids
}->{$custom_model}->{'reported-model'};
310 $reported_model //= $cpu_fmt->{'reported-model'}->{default};
311 my $vendor = $cpu_vendor_list->{$reported_model};
313 name
=> "custom-$custom_model",
322 sub is_custom_model
{
324 return $cputype =~ m/^custom-/;
327 # Use this to get a single model in the format described by $cpu_fmt.
328 # Allows names with and without custom- prefix.
329 sub get_custom_model
{
330 my ($name, $noerr) = @_;
332 $name =~ s/^custom-//;
333 my $conf = load_custom_model_conf
();
335 my $entry = $conf->{ids
}->{$name};
336 if (!defined($entry)) {
337 die "Custom cputype '$name' not found\n" if !$noerr;
342 for my $property (keys %$cpu_fmt) {
343 if (my $value = $entry->{$property}) {
344 $model->{$property} = $value;
351 # Print a QEMU device node for a given VM configuration for hotplugging CPUs
352 sub print_cpu_device
{
353 my ($conf, $id) = @_;
355 my $kvm = $conf->{kvm
} // 1;
356 my $cpu = $kvm ?
"kvm64" : "qemu64";
357 if (my $cputype = $conf->{cpu
}) {
358 my $cpuconf = PVE
::JSONSchema
::parse_property_string
('pve-vm-cpu-conf', $cputype)
359 or die "Cannot parse cpu description: $cputype\n";
360 $cpu = $cpuconf->{cputype
};
362 if (is_custom_model
($cpu)) {
363 my $custom_cpu = get_custom_model
($cpu);
365 $cpu = $custom_cpu->{'reported-model'} // $cpu_fmt->{'reported-model'}->{default};
367 if (my $replacement_type = $depreacated_cpu_map->{$cpu}) {
368 $cpu = $replacement_type;
372 my $cores = $conf->{cores
} || 1;
374 my $current_core = ($id - 1) % $cores;
375 my $current_socket = int(($id - 1 - $current_core)/$cores);
377 # FIXME: hot plugging other architectures like our unofficial arch64 support?
378 return "$cpu-x86_64-cpu,id=cpu$id,socket-id=$current_socket,core-id=$current_core,thread-id=0";
381 # Resolves multiple arrays of hashes representing CPU flags with metadata to a
382 # single string in QEMU "-cpu" compatible format. Later arrays have higher
385 # Hashes take the following format:
388 # op => "+", # defaults to "" if undefined
389 # reason => "to support AES acceleration", # for override warnings
390 # value => "" # needed for kvm=off (value: off) etc...
394 sub resolve_cpu_flags
{
398 for my $flag_name (keys %$hash) {
399 my $flag = $hash->{$flag_name};
400 my $old_flag = $flags->{$flag_name};
403 $flag->{reason
} //= "unknown origin";
406 my $value_changed = (defined($flag->{value
}) != defined($old_flag->{value
})) ||
407 (defined($flag->{value
}) && $flag->{value
} ne $old_flag->{value
});
409 if ($old_flag->{op
} eq $flag->{op
} && !$value_changed) {
410 $flags->{$flag_name}->{reason
} .= " & $flag->{reason}";
414 my $old = print_cpuflag_hash
($flag_name, $flags->{$flag_name});
415 my $new = print_cpuflag_hash
($flag_name, $flag);
416 warn "warning: CPU flag/setting $new overwrites $old\n";
419 $flags->{$flag_name} = $flag;
424 # sort for command line stability
425 for my $flag_name (sort keys %$flags) {
427 $flag_str .= $flags->{$flag_name}->{op
};
428 $flag_str .= $flag_name;
429 $flag_str .= "=$flags->{$flag_name}->{value}"
430 if $flags->{$flag_name}->{value
};
436 sub print_cpuflag_hash
{
437 my ($flag_name, $flag) = @_;
438 my $formatted = "'$flag->{op}$flag_name";
439 $formatted .= "=$flag->{value}" if defined($flag->{value
});
441 $formatted .= " ($flag->{reason})" if defined($flag->{reason
});
445 sub parse_cpuflag_list
{
446 my ($re, $reason, $flaglist) = @_;
449 return $res if !$flaglist;
451 foreach my $flag (split(";", $flaglist)) {
452 if ($flag =~ m/^$re$/) {
453 $res->{$2} = { op
=> $1, reason
=> $reason };
460 # Calculate QEMU's '-cpu' argument from a given VM configuration
461 sub get_cpu_options
{
462 my ($conf, $arch, $kvm, $kvm_off, $machine_version, $winversion, $gpu_passthrough) = @_;
464 my $cputype = $kvm ?
"kvm64" : "qemu64";
465 if ($arch eq 'aarch64') {
466 $cputype = 'cortex-a57';
472 if (my $cpu_prop_str = $conf->{cpu
}) {
473 $cpu = PVE
::JSONSchema
::parse_property_string
('pve-vm-cpu-conf', $cpu_prop_str)
474 or die "Cannot parse cpu description: $cpu_prop_str\n";
476 $cputype = $cpu->{cputype
};
478 if (is_custom_model
($cputype)) {
479 $custom_cpu = get_custom_model
($cputype);
481 $cputype = $custom_cpu->{'reported-model'} // $cpu_fmt->{'reported-model'}->{default};
482 $kvm_off = $custom_cpu->{hidden
} if defined($custom_cpu->{hidden
});
483 $hv_vendor_id = $custom_cpu->{'hv-vendor-id'};
486 if (my $replacement_type = $depreacated_cpu_map->{$cputype}) {
487 $cputype = $replacement_type;
490 # VM-specific settings override custom CPU config
491 $kvm_off = $cpu->{hidden
} if defined($cpu->{hidden
});
492 $hv_vendor_id = $cpu->{'hv-vendor-id'} if defined($cpu->{'hv-vendor-id'});
495 my $pve_flags = get_pve_cpu_flags
($conf, $kvm, $cputype, $arch, $machine_version);
498 ? get_hyperv_enlightenments
(
507 my $custom_cputype_flags = parse_cpuflag_list
(
508 $cpu_flag_any_re, "set by custom CPU model", $custom_cpu->{flags
});
510 my $vm_flags = parse_cpuflag_list
(
511 $cpu_flag_supported_re, "manually set for VM", $cpu->{flags
});
513 my $pve_forced_flags = {};
514 $pve_forced_flags->{'enforce'} = {
515 reason
=> "error if requested CPU settings not available",
516 } if $cputype ne 'host' && $kvm && $arch eq 'x86_64';
517 $pve_forced_flags->{'kvm'} = {
519 reason
=> "hide KVM virtualization from guest",
522 # $cputype is the "reported-model" for custom types, so we can just look up
523 # the vendor in the default list
524 my $cpu_vendor = $cpu_vendor_list->{$cputype};
526 $pve_forced_flags->{'vendor'} = {
527 value
=> $cpu_vendor,
528 } if $cpu_vendor ne 'default';
529 } elsif ($arch ne 'aarch64') {
530 die "internal error"; # should not happen
533 my $cpu_str = $cputype;
535 # will be resolved in parameter order
536 $cpu_str .= resolve_cpu_flags
(
537 $pve_flags, $hv_flags, $custom_cputype_flags, $vm_flags, $pve_forced_flags);
540 foreach my $conf ($custom_cpu, $cpu) {
541 next if !defined($conf);
542 my $conf_val = $conf->{'phys-bits'};
544 if ($conf_val eq 'host') {
545 $phys_bits = ",host-phys-bits=true";
547 $phys_bits = ",phys-bits=$conf_val";
550 $cpu_str .= $phys_bits;
552 return ('-cpu', $cpu_str);
555 # Some hardcoded flags required by certain configurations
556 sub get_pve_cpu_flags
{
557 my ($conf, $kvm, $cputype, $arch, $machine_version) = @_;
560 my $pve_msg = "set by PVE;";
562 $pve_flags->{'lahf_lm'} = {
564 reason
=> "$pve_msg to support Windows 8.1+",
565 } if $cputype eq 'kvm64' && $arch eq 'x86_64';
567 $pve_flags->{'x2apic'} = {
569 reason
=> "$pve_msg incompatible with Solaris",
570 } if $conf->{ostype
} && $conf->{ostype
} eq 'solaris';
572 $pve_flags->{'sep'} = {
574 reason
=> "$pve_msg to support Windows 8+ and improve Windows XP+",
575 } if $cputype eq 'kvm64' || $cputype eq 'kvm32';
577 $pve_flags->{'rdtscp'} = {
579 reason
=> "$pve_msg broken on AMD Opteron",
580 } if $cputype =~ m/^Opteron/;
582 if (min_version
($machine_version, 2, 3) && $kvm && $arch eq 'x86_64') {
583 $pve_flags->{'kvm_pv_unhalt'} = {
585 reason
=> "$pve_msg to improve Linux guest spinlock performance",
587 $pve_flags->{'kvm_pv_eoi'} = {
589 reason
=> "$pve_msg to improve Linux guest interrupt performance",
596 sub get_hyperv_enlightenments
{
597 my ($winversion, $machine_version, $bios, $gpu_passthrough, $hv_vendor_id) = @_;
599 return if $winversion < 6;
600 return if $bios && $bios eq 'ovmf' && $winversion < 8;
603 my $default_reason = "automatic Hyper-V enlightenment for Windows";
605 my ($flag, $value, $reason) = @_;
607 reason
=> $reason // $default_reason,
612 my $hv_vendor_set = defined($hv_vendor_id);
613 if ($gpu_passthrough || $hv_vendor_set) {
614 $hv_vendor_id //= 'proxmox';
615 $flagfn->('hv_vendor_id', $hv_vendor_id, $hv_vendor_set ?
616 "custom hv_vendor_id set" : "NVIDIA workaround for GPU passthrough");
619 if (min_version
($machine_version, 2, 3)) {
620 $flagfn->('hv_spinlocks', '0x1fff');
621 $flagfn->('hv_vapic');
622 $flagfn->('hv_time');
624 $flagfn->('hv_spinlocks', '0xffff');
627 if (min_version
($machine_version, 2, 6)) {
628 $flagfn->('hv_reset');
629 $flagfn->('hv_vpindex');
630 $flagfn->('hv_runtime');
633 if ($winversion >= 7) {
634 my $win7_reason = $default_reason . " 7 and higher";
635 $flagfn->('hv_relaxed', undef, $win7_reason);
637 if (min_version
($machine_version, 2, 12)) {
638 $flagfn->('hv_synic', undef, $win7_reason);
639 $flagfn->('hv_stimer', undef, $win7_reason);
642 if (min_version
($machine_version, 3, 1)) {
643 $flagfn->('hv_ipi', undef, $win7_reason);
650 sub get_cpu_from_running_vm
{
653 my $cmdline = PVE
::QemuServer
::Helpers
::parse_cmdline
($pid);
654 die "could not read commandline of running machine\n"
655 if !$cmdline->{cpu
}->{value
};
657 # sanitize and untaint value
658 $cmdline->{cpu
}->{value
} =~ $qemu_cmdline_cpu_re;
662 __PACKAGE__-
>register();