]> git.proxmox.com Git - qemu-server.git/blob - PVE/QemuServer/CPUConfig.pm
cpuconfig: add amd epyc milan model
[qemu-server.git] / PVE / QemuServer / CPUConfig.pm
1 package PVE::QemuServer::CPUConfig;
2
3 use strict;
4 use warnings;
5
6 use PVE::JSONSchema;
7 use PVE::Cluster qw(cfs_register_file cfs_read_file);
8 use PVE::QemuServer::Helpers qw(min_version);
9
10 use base qw(PVE::SectionConfig Exporter);
11
12 our @EXPORT_OK = qw(
13 print_cpu_device
14 get_cpu_options
15 );
16
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";
21 }
22
23 my $default_filename = "virtual-guest/cpu-models.conf";
24 cfs_register_file($default_filename,
25 sub { PVE::QemuServer::CPUConfig->parse_config(@_); },
26 sub { PVE::QemuServer::CPUConfig->write_config(@_); });
27
28 sub load_custom_model_conf {
29 return cfs_read_file($default_filename);
30 }
31
32 my $cpu_vendor_list = {
33 # Intel CPUs
34 486 => 'GenuineIntel',
35 pentium => 'GenuineIntel',
36 pentium2 => 'GenuineIntel',
37 pentium3 => 'GenuineIntel',
38 coreduo => 'GenuineIntel',
39 core2duo => 'GenuineIntel',
40 Conroe => 'GenuineIntel',
41 Penryn => 'GenuineIntel',
42 Nehalem => 'GenuineIntel',
43 'Nehalem-IBRS' => 'GenuineIntel',
44 Westmere => 'GenuineIntel',
45 'Westmere-IBRS' => 'GenuineIntel',
46 SandyBridge => 'GenuineIntel',
47 'SandyBridge-IBRS' => 'GenuineIntel',
48 IvyBridge => 'GenuineIntel',
49 'IvyBridge-IBRS' => 'GenuineIntel',
50 Haswell => 'GenuineIntel',
51 'Haswell-IBRS' => 'GenuineIntel',
52 'Haswell-noTSX' => 'GenuineIntel',
53 'Haswell-noTSX-IBRS' => 'GenuineIntel',
54 Broadwell => 'GenuineIntel',
55 'Broadwell-IBRS' => 'GenuineIntel',
56 'Broadwell-noTSX' => 'GenuineIntel',
57 'Broadwell-noTSX-IBRS' => 'GenuineIntel',
58 'Skylake-Client' => 'GenuineIntel',
59 'Skylake-Client-IBRS' => 'GenuineIntel',
60 'Skylake-Client-noTSX-IBRS' => 'GenuineIntel',
61 'Skylake-Server' => 'GenuineIntel',
62 'Skylake-Server-IBRS' => 'GenuineIntel',
63 'Skylake-Server-noTSX-IBRS' => 'GenuineIntel',
64 'Cascadelake-Server' => 'GenuineIntel',
65 'Cascadelake-Server-noTSX' => 'GenuineIntel',
66 KnightsMill => 'GenuineIntel',
67 'Icelake-Client' => 'GenuineIntel',
68 'Icelake-Client-noTSX' => 'GenuineIntel',
69 'Icelake-Server' => 'GenuineIntel',
70 'Icelake-Server-noTSX' => 'GenuineIntel',
71
72 # AMD CPUs
73 athlon => 'AuthenticAMD',
74 phenom => 'AuthenticAMD',
75 Opteron_G1 => 'AuthenticAMD',
76 Opteron_G2 => 'AuthenticAMD',
77 Opteron_G3 => 'AuthenticAMD',
78 Opteron_G4 => 'AuthenticAMD',
79 Opteron_G5 => 'AuthenticAMD',
80 EPYC => 'AuthenticAMD',
81 'EPYC-IBPB' => 'AuthenticAMD',
82 'EPYC-Rome' => 'AuthenticAMD',
83 'EPYC-Milan' => 'AuthenticAMD',
84
85 # generic types, use vendor from host node
86 host => 'default',
87 kvm32 => 'default',
88 kvm64 => 'default',
89 qemu32 => 'default',
90 qemu64 => 'default',
91 max => 'default',
92 };
93
94 my @supported_cpu_flags = (
95 'pcid',
96 'spec-ctrl',
97 'ibpb',
98 'ssbd',
99 'virt-ssbd',
100 'amd-ssbd',
101 'amd-no-ssb',
102 'pdpe1gb',
103 'md-clear',
104 'hv-tlbflush',
105 'hv-evmcs',
106 'aes'
107 );
108 my $cpu_flag_supported_re = qr/([+-])(@{[join('|', @supported_cpu_flags)]})/;
109 my $cpu_flag_any_re = qr/([+-])([a-zA-Z0-9\-_\.]+)/;
110
111 our $qemu_cmdline_cpu_re = qr/^((?>[+-]?[\w\-\._=]+,?)+)$/;
112
113 my $cpu_fmt = {
114 cputype => {
115 description => "Emulated CPU type. Can be default or custom name (custom model names must be prefixed with 'custom-').",
116 type => 'string',
117 format_description => 'string',
118 default => 'kvm64',
119 default_key => 1,
120 optional => 1,
121 },
122 'reported-model' => {
123 description => "CPU model and vendor to report to the guest. Must be a QEMU/KVM supported model."
124 ." Only valid for custom CPU model definitions, default models will always report themselves to the guest OS.",
125 type => 'string',
126 enum => [ sort { lc("$a") cmp lc("$b") } keys %$cpu_vendor_list ],
127 default => 'kvm64',
128 optional => 1,
129 },
130 hidden => {
131 description => "Do not identify as a KVM virtual machine.",
132 type => 'boolean',
133 optional => 1,
134 default => 0
135 },
136 'hv-vendor-id' => {
137 type => 'string',
138 pattern => qr/[a-zA-Z0-9]{1,12}/,
139 format_description => 'vendor-id',
140 description => 'The Hyper-V vendor ID. Some drivers or programs inside Windows guests need a specific ID.',
141 optional => 1,
142 },
143 flags => {
144 description => "List of additional CPU flags separated by ';'. Use '+FLAG' to enable,"
145 ." '-FLAG' to disable a flag. Custom CPU models can specify any flag supported by"
146 ." QEMU/KVM, VM-specific flags must be from the following set for security reasons: "
147 . join(', ', @supported_cpu_flags),
148 format_description => '+FLAG[;-FLAG...]',
149 type => 'string',
150 pattern => qr/$cpu_flag_any_re(;$cpu_flag_any_re)*/,
151 optional => 1,
152 },
153 'phys-bits' => {
154 type => 'string',
155 format => 'pve-phys-bits',
156 format_description => '8-64|host',
157 description => "The physical memory address bits that are reported to the guest OS. Should"
158 ." be smaller or equal to the host's. Set to 'host' to use value from host CPU, but"
159 ." note that doing so will break live migration to CPUs with other values.",
160 optional => 1,
161 },
162 };
163
164 PVE::JSONSchema::register_format('pve-phys-bits', \&parse_phys_bits);
165 sub parse_phys_bits {
166 my ($str, $noerr) = @_;
167
168 my $err_msg = "value must be an integer between 8 and 64 or 'host'\n";
169
170 if ($str !~ m/^(host|\d{1,2})$/) {
171 die $err_msg if !$noerr;
172 return;
173 }
174
175 if ($str =~ m/^\d+$/ && (int($str) < 8 || int($str) > 64)) {
176 die $err_msg if !$noerr;
177 return;
178 }
179
180 return $str;
181 }
182
183 # $cpu_fmt describes both the CPU config passed as part of a VM config, as well
184 # as the definition of a custom CPU model. There are some slight differences
185 # though, which we catch in the custom validation functions below.
186 PVE::JSONSchema::register_format('pve-cpu-conf', $cpu_fmt, \&validate_cpu_conf);
187 sub validate_cpu_conf {
188 my ($cpu) = @_;
189
190 # required, but can't be forced in schema since it's encoded in section
191 # header for custom models
192 die "CPU is missing cputype\n" if !$cpu->{cputype};
193
194 return $cpu;
195 }
196 PVE::JSONSchema::register_format('pve-vm-cpu-conf', $cpu_fmt, \&validate_vm_cpu_conf);
197 sub validate_vm_cpu_conf {
198 my ($cpu) = @_;
199
200 validate_cpu_conf($cpu);
201
202 my $cputype = $cpu->{cputype};
203
204 # a VM-specific config is only valid if the cputype exists
205 if (is_custom_model($cputype)) {
206 # dies on unknown model
207 get_custom_model($cputype);
208 } else {
209 die "Built-in cputype '$cputype' is not defined (missing 'custom-' prefix?)\n"
210 if !defined($cpu_vendor_list->{$cputype});
211 }
212
213 # in a VM-specific config, certain properties are limited/forbidden
214
215 die "VM-specific CPU flags must be a subset of: @{[join(', ', @supported_cpu_flags)]}\n"
216 if ($cpu->{flags} && $cpu->{flags} !~ m/^$cpu_flag_supported_re(;$cpu_flag_supported_re)*$/);
217
218 die "Property 'reported-model' not allowed in VM-specific CPU config.\n"
219 if defined($cpu->{'reported-model'});
220
221 return $cpu;
222 }
223
224 # Section config settings
225 my $defaultData = {
226 # shallow copy, since SectionConfig modifies propertyList internally
227 propertyList => { %$cpu_fmt },
228 };
229
230 sub private {
231 return $defaultData;
232 }
233
234 sub options {
235 return { %$cpu_fmt };
236 }
237
238 sub type {
239 return 'cpu-model';
240 }
241
242 sub parse_section_header {
243 my ($class, $line) = @_;
244
245 my ($type, $sectionId, $errmsg, $config) =
246 $class->SUPER::parse_section_header($line);
247
248 return if !$type;
249 return ($type, $sectionId, $errmsg, {
250 # name is given by section header, and we can always prepend 'custom-'
251 # since we're reading the custom CPU file
252 cputype => "custom-$sectionId",
253 });
254 }
255
256 sub write_config {
257 my ($class, $filename, $cfg) = @_;
258
259 mkdir "/etc/pve/virtual-guest";
260
261 for my $model (keys %{$cfg->{ids}}) {
262 my $model_conf = $cfg->{ids}->{$model};
263
264 die "internal error: tried saving built-in CPU model (or missing prefix): $model_conf->{cputype}\n"
265 if !is_custom_model($model_conf->{cputype});
266
267 die "internal error: tried saving custom cpumodel with cputype (ignoring prefix: $model_conf->{cputype}) not equal to \$cfg->ids entry ($model)\n"
268 if "custom-$model" ne $model_conf->{cputype};
269
270 # saved in section header
271 delete $model_conf->{cputype};
272 }
273
274 $class->SUPER::write_config($filename, $cfg);
275 }
276
277 sub add_cpu_json_properties {
278 my ($prop) = @_;
279
280 foreach my $opt (keys %$cpu_fmt) {
281 $prop->{$opt} = $cpu_fmt->{$opt};
282 }
283
284 return $prop;
285 }
286
287 sub get_cpu_models {
288 my ($include_custom) = @_;
289
290 my $models = [];
291
292 for my $default_model (keys %{$cpu_vendor_list}) {
293 push @$models, {
294 name => $default_model,
295 custom => 0,
296 vendor => $cpu_vendor_list->{$default_model},
297 };
298 }
299
300 return $models if !$include_custom;
301
302 my $conf = load_custom_model_conf();
303 for my $custom_model (keys %{$conf->{ids}}) {
304 my $reported_model = $conf->{ids}->{$custom_model}->{'reported-model'};
305 $reported_model //= $cpu_fmt->{'reported-model'}->{default};
306 my $vendor = $cpu_vendor_list->{$reported_model};
307 push @$models, {
308 name => "custom-$custom_model",
309 custom => 1,
310 vendor => $vendor,
311 };
312 }
313
314 return $models;
315 }
316
317 sub is_custom_model {
318 my ($cputype) = @_;
319 return $cputype =~ m/^custom-/;
320 }
321
322 # Use this to get a single model in the format described by $cpu_fmt.
323 # Allows names with and without custom- prefix.
324 sub get_custom_model {
325 my ($name, $noerr) = @_;
326
327 $name =~ s/^custom-//;
328 my $conf = load_custom_model_conf();
329
330 my $entry = $conf->{ids}->{$name};
331 if (!defined($entry)) {
332 die "Custom cputype '$name' not found\n" if !$noerr;
333 return;
334 }
335
336 my $model = {};
337 for my $property (keys %$cpu_fmt) {
338 if (my $value = $entry->{$property}) {
339 $model->{$property} = $value;
340 }
341 }
342
343 return $model;
344 }
345
346 # Print a QEMU device node for a given VM configuration for hotplugging CPUs
347 sub print_cpu_device {
348 my ($conf, $id) = @_;
349
350 my $kvm = $conf->{kvm} // 1;
351 my $cpu = $kvm ? "kvm64" : "qemu64";
352 if (my $cputype = $conf->{cpu}) {
353 my $cpuconf = PVE::JSONSchema::parse_property_string('pve-vm-cpu-conf', $cputype)
354 or die "Cannot parse cpu description: $cputype\n";
355 $cpu = $cpuconf->{cputype};
356
357 if (is_custom_model($cpu)) {
358 my $custom_cpu = get_custom_model($cpu);
359
360 $cpu = $custom_cpu->{'reported-model'} //
361 $cpu_fmt->{'reported-model'}->{default};
362 }
363 }
364
365 my $cores = $conf->{cores} || 1;
366
367 my $current_core = ($id - 1) % $cores;
368 my $current_socket = int(($id - 1 - $current_core)/$cores);
369
370 return "$cpu-x86_64-cpu,id=cpu$id,socket-id=$current_socket,core-id=$current_core,thread-id=0";
371 }
372
373 # Resolves multiple arrays of hashes representing CPU flags with metadata to a
374 # single string in QEMU "-cpu" compatible format. Later arrays have higher
375 # priority.
376 #
377 # Hashes take the following format:
378 # {
379 # aes => {
380 # op => "+", # defaults to "" if undefined
381 # reason => "to support AES acceleration", # for override warnings
382 # value => "" # needed for kvm=off (value: off) etc...
383 # },
384 # ...
385 # }
386 sub resolve_cpu_flags {
387 my $flags = {};
388
389 for my $hash (@_) {
390 for my $flag_name (keys %$hash) {
391 my $flag = $hash->{$flag_name};
392 my $old_flag = $flags->{$flag_name};
393
394 $flag->{op} //= "";
395 $flag->{reason} //= "unknown origin";
396
397 if ($old_flag) {
398 my $value_changed = (defined($flag->{value}) != defined($old_flag->{value})) ||
399 (defined($flag->{value}) && $flag->{value} ne $old_flag->{value});
400
401 if ($old_flag->{op} eq $flag->{op} && !$value_changed) {
402 $flags->{$flag_name}->{reason} .= " & $flag->{reason}";
403 next;
404 }
405
406 my $old = print_cpuflag_hash($flag_name, $flags->{$flag_name});
407 my $new = print_cpuflag_hash($flag_name, $flag);
408 warn "warning: CPU flag/setting $new overwrites $old\n";
409 }
410
411 $flags->{$flag_name} = $flag;
412 }
413 }
414
415 my $flag_str = '';
416 # sort for command line stability
417 for my $flag_name (sort keys %$flags) {
418 $flag_str .= ',';
419 $flag_str .= $flags->{$flag_name}->{op};
420 $flag_str .= $flag_name;
421 $flag_str .= "=$flags->{$flag_name}->{value}"
422 if $flags->{$flag_name}->{value};
423 }
424
425 return $flag_str;
426 }
427
428 sub print_cpuflag_hash {
429 my ($flag_name, $flag) = @_;
430 my $formatted = "'$flag->{op}$flag_name";
431 $formatted .= "=$flag->{value}" if defined($flag->{value});
432 $formatted .= "'";
433 $formatted .= " ($flag->{reason})" if defined($flag->{reason});
434 return $formatted;
435 }
436
437 sub parse_cpuflag_list {
438 my ($re, $reason, $flaglist) = @_;
439
440 my $res = {};
441 return $res if !$flaglist;
442
443 foreach my $flag (split(";", $flaglist)) {
444 if ($flag =~ m/^$re$/) {
445 $res->{$2} = { op => $1, reason => $reason };
446 }
447 }
448
449 return $res;
450 }
451
452 # Calculate QEMU's '-cpu' argument from a given VM configuration
453 sub get_cpu_options {
454 my ($conf, $arch, $kvm, $kvm_off, $machine_version, $winversion, $gpu_passthrough) = @_;
455
456 my $cputype = $kvm ? "kvm64" : "qemu64";
457 if ($arch eq 'aarch64') {
458 $cputype = 'cortex-a57';
459 }
460
461 my $cpu = {};
462 my $custom_cpu;
463 my $hv_vendor_id;
464 if (my $cpu_prop_str = $conf->{cpu}) {
465 $cpu = PVE::JSONSchema::parse_property_string('pve-vm-cpu-conf', $cpu_prop_str)
466 or die "Cannot parse cpu description: $cpu_prop_str\n";
467
468 $cputype = $cpu->{cputype};
469
470 if (is_custom_model($cputype)) {
471 $custom_cpu = get_custom_model($cputype);
472
473 $cputype = $custom_cpu->{'reported-model'} //
474 $cpu_fmt->{'reported-model'}->{default};
475 $kvm_off = $custom_cpu->{hidden}
476 if defined($custom_cpu->{hidden});
477 $hv_vendor_id = $custom_cpu->{'hv-vendor-id'};
478 }
479
480 # VM-specific settings override custom CPU config
481 $kvm_off = $cpu->{hidden}
482 if defined($cpu->{hidden});
483 $hv_vendor_id = $cpu->{'hv-vendor-id'}
484 if defined($cpu->{'hv-vendor-id'});
485 }
486
487 my $pve_flags = get_pve_cpu_flags($conf, $kvm, $cputype, $arch, $machine_version);
488
489 my $hv_flags = $kvm
490 ? get_hyperv_enlightenments(
491 $winversion,
492 $machine_version,
493 $conf->{bios},
494 $gpu_passthrough,
495 $hv_vendor_id,
496 )
497 : undef;
498
499 my $custom_cputype_flags = parse_cpuflag_list(
500 $cpu_flag_any_re, "set by custom CPU model", $custom_cpu->{flags});
501
502 my $vm_flags = parse_cpuflag_list(
503 $cpu_flag_supported_re, "manually set for VM", $cpu->{flags});
504
505 my $pve_forced_flags = {};
506 $pve_forced_flags->{'enforce'} = {
507 reason => "error if requested CPU settings not available",
508 } if $cputype ne 'host' && $kvm && $arch eq 'x86_64';
509 $pve_forced_flags->{'kvm'} = {
510 value => "off",
511 reason => "hide KVM virtualization from guest",
512 } if $kvm_off;
513
514 # $cputype is the "reported-model" for custom types, so we can just look up
515 # the vendor in the default list
516 my $cpu_vendor = $cpu_vendor_list->{$cputype};
517 if ($cpu_vendor) {
518 $pve_forced_flags->{'vendor'} = {
519 value => $cpu_vendor,
520 } if $cpu_vendor ne 'default';
521 } elsif ($arch ne 'aarch64') {
522 die "internal error"; # should not happen
523 }
524
525 my $cpu_str = $cputype;
526
527 # will be resolved in parameter order
528 $cpu_str .= resolve_cpu_flags(
529 $pve_flags, $hv_flags, $custom_cputype_flags, $vm_flags, $pve_forced_flags);
530
531 my $phys_bits = '';
532 foreach my $conf ($custom_cpu, $cpu) {
533 next if !defined($conf);
534 my $conf_val = $conf->{'phys-bits'};
535 next if !$conf_val;
536 if ($conf_val eq 'host') {
537 $phys_bits = ",host-phys-bits=true";
538 } else {
539 $phys_bits = ",phys-bits=$conf_val";
540 }
541 }
542 $cpu_str .= $phys_bits;
543
544 return ('-cpu', $cpu_str);
545 }
546
547 # Some hardcoded flags required by certain configurations
548 sub get_pve_cpu_flags {
549 my ($conf, $kvm, $cputype, $arch, $machine_version) = @_;
550
551 my $pve_flags = {};
552 my $pve_msg = "set by PVE;";
553
554 $pve_flags->{'lahf_lm'} = {
555 op => '+',
556 reason => "$pve_msg to support Windows 8.1+",
557 } if $cputype eq 'kvm64' && $arch eq 'x86_64';
558
559 $pve_flags->{'x2apic'} = {
560 op => '-',
561 reason => "$pve_msg incompatible with Solaris",
562 } if $conf->{ostype} && $conf->{ostype} eq 'solaris';
563
564 $pve_flags->{'sep'} = {
565 op => '+',
566 reason => "$pve_msg to support Windows 8+ and improve Windows XP+",
567 } if $cputype eq 'kvm64' || $cputype eq 'kvm32';
568
569 $pve_flags->{'rdtscp'} = {
570 op => '-',
571 reason => "$pve_msg broken on AMD Opteron",
572 } if $cputype =~ m/^Opteron/;
573
574 if (min_version($machine_version, 2, 3) && $kvm && $arch eq 'x86_64') {
575 $pve_flags->{'kvm_pv_unhalt'} = {
576 op => '+',
577 reason => "$pve_msg to improve Linux guest spinlock performance",
578 };
579 $pve_flags->{'kvm_pv_eoi'} = {
580 op => '+',
581 reason => "$pve_msg to improve Linux guest interrupt performance",
582 };
583 }
584
585 return $pve_flags;
586 }
587
588 sub get_hyperv_enlightenments {
589 my ($winversion, $machine_version, $bios, $gpu_passthrough, $hv_vendor_id) = @_;
590
591 return if $winversion < 6;
592 return if $bios && $bios eq 'ovmf' && $winversion < 8;
593
594 my $flags = {};
595 my $default_reason = "automatic Hyper-V enlightenment for Windows";
596 my $flagfn = sub {
597 my ($flag, $value, $reason) = @_;
598 $flags->{$flag} = {
599 reason => $reason // $default_reason,
600 value => $value,
601 }
602 };
603
604 my $hv_vendor_set = defined($hv_vendor_id);
605 if ($gpu_passthrough || $hv_vendor_set) {
606 $hv_vendor_id //= 'proxmox';
607 $flagfn->('hv_vendor_id', $hv_vendor_id, $hv_vendor_set ?
608 "custom hv_vendor_id set" : "NVIDIA workaround for GPU passthrough");
609 }
610
611 if (min_version($machine_version, 2, 3)) {
612 $flagfn->('hv_spinlocks', '0x1fff');
613 $flagfn->('hv_vapic');
614 $flagfn->('hv_time');
615 } else {
616 $flagfn->('hv_spinlocks', '0xffff');
617 }
618
619 if (min_version($machine_version, 2, 6)) {
620 $flagfn->('hv_reset');
621 $flagfn->('hv_vpindex');
622 $flagfn->('hv_runtime');
623 }
624
625 if ($winversion >= 7) {
626 my $win7_reason = $default_reason . " 7 and higher";
627 $flagfn->('hv_relaxed', undef, $win7_reason);
628
629 if (min_version($machine_version, 2, 12)) {
630 $flagfn->('hv_synic', undef, $win7_reason);
631 $flagfn->('hv_stimer', undef, $win7_reason);
632 }
633
634 if (min_version($machine_version, 3, 1)) {
635 $flagfn->('hv_ipi', undef, $win7_reason);
636 }
637 }
638
639 return $flags;
640 }
641
642 sub get_cpu_from_running_vm {
643 my ($pid) = @_;
644
645 my $cmdline = PVE::QemuServer::Helpers::parse_cmdline($pid);
646 die "could not read commandline of running machine\n"
647 if !$cmdline->{cpu}->{value};
648
649 # sanitize and untaint value
650 $cmdline->{cpu}->{value} =~ $qemu_cmdline_cpu_re;
651 return $1;
652 }
653
654 __PACKAGE__->register();
655 __PACKAGE__->init();
656
657 1;