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