]> git.proxmox.com Git - pve-manager.git/blame - PVE/CLI/pve7to8.pm
bump version to 7.4-7
[pve-manager.git] / PVE / CLI / pve7to8.pm
CommitLineData
855e0adb
TL
1package PVE::CLI::pve7to8;
2
3use strict;
4use warnings;
5
6use PVE::API2::APT;
7use PVE::API2::Ceph;
8use PVE::API2::LXC;
9use PVE::API2::Qemu;
10use PVE::API2::Certificates;
11use PVE::API2::Cluster::Ceph;
12
13use PVE::AccessControl;
14use PVE::Ceph::Tools;
15use PVE::Cluster;
16use PVE::Corosync;
17use PVE::INotify;
18use PVE::JSONSchema;
19use PVE::NodeConfig;
20use PVE::RPCEnvironment;
21use PVE::Storage;
22use PVE::Storage::Plugin;
23use PVE::Tools qw(run_command split_list);
24use PVE::QemuConfig;
25use PVE::QemuServer;
26use PVE::VZDump::Common;
27use PVE::LXC;
28use PVE::LXC::Config;
29use PVE::LXC::Setup;
30
31use Term::ANSIColor;
32
33use PVE::CLIHandler;
34
35use base qw(PVE::CLIHandler);
36
37my $nodename = PVE::INotify::nodename();
38
39sub setup_environment {
40 PVE::RPCEnvironment->setup_default_cli_env();
41}
42
82bbcfad 43my ($min_pve_major, $min_pve_minor, $min_pve_pkgrel) = (7, 4, 1);
855e0adb 44
bda81173
TL
45my $ceph_release2code = {
46 '12' => 'Luminous',
47 '13' => 'Mimic',
48 '14' => 'Nautilus',
49 '15' => 'Octopus',
50 '16' => 'Pacific',
51 '17' => 'Quincy',
52 '18' => 'Reef',
53};
54my $ceph_supported_release = 17; # the version we support for upgrading (i.e., available on both)
55my $ceph_supported_code_name = $ceph_release2code->{"$ceph_supported_release"}
56 or die "inconsistent source code, could not map expected ceph version to code name!";
57
855e0adb
TL
58my $forced_legacy_cgroup = 0;
59
60my $counters = {
61 pass => 0,
62 skip => 0,
63 warn => 0,
64 fail => 0,
65};
66
67my $log_line = sub {
68 my ($level, $line) = @_;
69
70 $counters->{$level}++ if defined($level) && defined($counters->{$level});
71
72 print uc($level), ': ' if defined($level);
73 print "$line\n";
74};
75
76sub log_pass {
77 print color('green');
78 $log_line->('pass', @_);
79 print color('reset');
80}
81
82sub log_info {
83 $log_line->('info', @_);
84}
85sub log_skip {
86 $log_line->('skip', @_);
87}
88sub log_warn {
89 print color('yellow');
90 $log_line->('warn', @_);
91 print color('reset');
92}
93sub log_fail {
6bafdd65 94 print color('bold red');
855e0adb
TL
95 $log_line->('fail', @_);
96 print color('reset');
97}
98
99my $print_header_first = 1;
100sub print_header {
101 my ($h) = @_;
102 print "\n" if !$print_header_first;
103 print "= $h =\n\n";
104 $print_header_first = 0;
105}
106
107my $get_systemd_unit_state = sub {
ea9dcd0b 108 my ($unit, $surpress_stderr) = @_;
855e0adb
TL
109
110 my $state;
111 my $filter_output = sub {
112 $state = shift;
113 chomp $state;
114 };
ea9dcd0b
TL
115
116 my %extra = (outfunc => $filter_output, noerr => 1);
117 $extra{errfunc} = sub { } if $surpress_stderr;
118
855e0adb 119 eval {
ea9dcd0b 120 run_command(['systemctl', 'is-enabled', "$unit"], %extra);
855e0adb 121 return if !defined($state);
ea9dcd0b 122 run_command(['systemctl', 'is-active', "$unit"], %extra);
855e0adb
TL
123 };
124
125 return $state // 'unknown';
126};
127my $log_systemd_unit_state = sub {
128 my ($unit, $no_fail_on_inactive) = @_;
129
130 my $log_method = \&log_warn;
131
132 my $state = $get_systemd_unit_state->($unit);
133 if ($state eq 'active') {
134 $log_method = \&log_pass;
135 } elsif ($state eq 'inactive') {
136 $log_method = $no_fail_on_inactive ? \&log_warn : \&log_fail;
137 } elsif ($state eq 'failed') {
138 $log_method = \&log_fail;
139 }
140
141 $log_method->("systemd unit '$unit' is in state '$state'");
142};
143
144my $versions;
145my $get_pkg = sub {
146 my ($pkg) = @_;
147
148 $versions = eval { PVE::API2::APT->versions({ node => $nodename }) } if !defined($versions);
149
150 if (!defined($versions)) {
151 my $msg = "unable to retrieve package version information";
152 $msg .= "- $@" if $@;
153 log_fail("$msg");
154 return undef;
155 }
156
157 my $pkgs = [ grep { $_->{Package} eq $pkg } @$versions ];
158 if (!defined $pkgs || $pkgs == 0) {
159 log_fail("unable to determine installed $pkg version.");
160 return undef;
161 } else {
162 return $pkgs->[0];
163 }
164};
165
166sub check_pve_packages {
167 print_header("CHECKING VERSION INFORMATION FOR PVE PACKAGES");
168
169 print "Checking for package updates..\n";
170 my $updates = eval { PVE::API2::APT->list_updates({ node => $nodename }); };
171 if (!defined($updates)) {
172 log_warn("$@") if $@;
173 log_fail("unable to retrieve list of package updates!");
174 } elsif (@$updates > 0) {
175 my $pkgs = join(', ', map { $_->{Package} } @$updates);
176 log_warn("updates for the following packages are available:\n $pkgs");
177 } else {
178 log_pass("all packages uptodate");
179 }
180
181 print "\nChecking proxmox-ve package version..\n";
182 if (defined(my $proxmox_ve = $get_pkg->('proxmox-ve'))) {
183 my $min_pve_ver = "$min_pve_major.$min_pve_minor-$min_pve_pkgrel";
184
185 my ($maj, $min, $pkgrel) = $proxmox_ve->{OldVersion} =~ m/^(\d+)\.(\d+)-(\d+)/;
186
187 my $upgraded = 0;
188
189 if ($maj > $min_pve_major) {
190 log_pass("already upgraded to Proxmox VE " . ($min_pve_major + 1));
191 $upgraded = 1;
192 } elsif ($maj >= $min_pve_major && $min >= $min_pve_minor && $pkgrel >= $min_pve_pkgrel) {
193 log_pass("proxmox-ve package has version >= $min_pve_ver");
194 } else {
195 log_fail("proxmox-ve package is too old, please upgrade to >= $min_pve_ver!");
196 }
197
82bbcfad 198 my ($krunning, $kinstalled) = (qr/6\.(?:2|5)/, 'pve-kernel-6.2');
855e0adb 199 if (!$upgraded) {
82bbcfad
TL
200 # we got a few that avoided 5.15 in cluster with mixed CPUs, so allow older too
201 ($krunning, $kinstalled) = (qr/(?:5\.(?:13|15)|6\.2)/, 'pve-kernel-5.15');
855e0adb
TL
202 }
203
204 print "\nChecking running kernel version..\n";
205 my $kernel_ver = $proxmox_ve->{RunningKernel};
206 if (!defined($kernel_ver)) {
207 log_fail("unable to determine running kernel version.");
208 } elsif ($kernel_ver =~ /^$krunning/) {
3831dc0a
TL
209 if ($upgraded) {
210 log_pass("running new kernel '$kernel_ver' after upgrade.");
211 } else {
212 log_pass("running kernel '$kernel_ver' is considered suitable for upgrade.");
213 }
855e0adb 214 } elsif ($get_pkg->($kinstalled)) {
eb12dccb
TL
215 # with 6.2 kernel being available in both we might want to fine-tune the check?
216 log_warn("a suitable kernel ($kinstalled) is intalled, but an unsuitable ($kernel_ver) is booted, missing reboot?!");
855e0adb
TL
217 } else {
218 log_warn("unexpected running and installed kernel '$kernel_ver'.");
219 }
18bf77f5
TL
220
221 if ($upgraded && $kernel_ver =~ /^$krunning/) {
222 my $outdated_kernel_meta_pkgs = [];
223 for my $kernel_meta_version ('5.4', '5.11', '5.13', '5.15') {
224 my $pkg = "pve-kernel-${kernel_meta_version}";
225 if ($get_pkg->($pkg)) {
226 push @$outdated_kernel_meta_pkgs, $pkg;
227 }
228 }
229 if (scalar(@$outdated_kernel_meta_pkgs) > 0) {
230 log_info(
231 "Found outdated kernel meta-packages, taking up extra space on boot partitions.\n"
232 ." After a successful upgrade, you can remove them using this command:\n"
233 ." apt remove " . join(' ', $outdated_kernel_meta_pkgs->@*)
234 );
235 }
236 }
855e0adb
TL
237 } else {
238 log_fail("proxmox-ve package not found!");
239 }
240}
241
242
243sub check_storage_health {
244 print_header("CHECKING CONFIGURED STORAGES");
245 my $cfg = PVE::Storage::config();
246
247 my $ctime = time();
248
249 my $info = PVE::Storage::storage_info($cfg);
250
251 foreach my $storeid (sort keys %$info) {
252 my $d = $info->{$storeid};
253 if ($d->{enabled}) {
82bbcfad 254 if ($d->{active}) {
855e0adb
TL
255 log_pass("storage '$storeid' enabled and active.");
256 } else {
257 log_warn("storage '$storeid' enabled but not active!");
258 }
259 } else {
260 log_skip("storage '$storeid' disabled.");
261 }
262 }
3019f374
TL
263
264 check_storage_content();
855e0adb
TL
265}
266
267sub check_cluster_corosync {
268 print_header("CHECKING CLUSTER HEALTH/SETTINGS");
269
270 if (!PVE::Corosync::check_conf_exists(1)) {
271 log_skip("standalone node.");
272 return;
273 }
274
275 $log_systemd_unit_state->('pve-cluster.service');
276 $log_systemd_unit_state->('corosync.service');
277
278 if (PVE::Cluster::check_cfs_quorum(1)) {
279 log_pass("Cluster Filesystem is quorate.");
280 } else {
281 log_fail("Cluster Filesystem readonly, lost quorum?!");
282 }
283
284 my $conf = PVE::Cluster::cfs_read_file('corosync.conf');
285 my $conf_nodelist = PVE::Corosync::nodelist($conf);
286 my $node_votes = 0;
287
288 print "\nAnalzying quorum settings and state..\n";
289 if (!defined($conf_nodelist)) {
290 log_fail("unable to retrieve nodelist from corosync.conf");
291 } else {
292 if (grep { $conf_nodelist->{$_}->{quorum_votes} != 1 } keys %$conf_nodelist) {
293 log_warn("non-default quorum_votes distribution detected!");
294 }
295 map { $node_votes += $conf_nodelist->{$_}->{quorum_votes} // 0 } keys %$conf_nodelist;
296 }
297
298 my ($expected_votes, $total_votes);
299 my $filter_output = sub {
300 my $line = shift;
301 ($expected_votes) = $line =~ /^Expected votes:\s*(\d+)\s*$/
302 if !defined($expected_votes);
303 ($total_votes) = $line =~ /^Total votes:\s*(\d+)\s*$/
304 if !defined($total_votes);
305 };
306 eval {
307 run_command(['corosync-quorumtool', '-s'], outfunc => $filter_output, noerr => 1);
308 };
309
310 if (!defined($expected_votes)) {
82bbcfad 311 log_fail("unable to get expected number of votes, assuming 0.");
855e0adb
TL
312 $expected_votes = 0;
313 }
314 if (!defined($total_votes)) {
82bbcfad 315 log_fail("unable to get expected number of votes, assuming 0.");
855e0adb
TL
316 $total_votes = 0;
317 }
318
319 my $cfs_nodelist = PVE::Cluster::get_clinfo()->{nodelist};
320 my $offline_nodes = grep { $cfs_nodelist->{$_}->{online} != 1 } keys %$cfs_nodelist;
321 if ($offline_nodes > 0) {
322 log_fail("$offline_nodes nodes are offline!");
323 }
324
325 my $qdevice_votes = 0;
326 if (my $qdevice_setup = $conf->{main}->{quorum}->{device}) {
327 $qdevice_votes = $qdevice_setup->{votes} // 1;
328 }
329
330 log_info("configured votes - nodes: $node_votes");
331 log_info("configured votes - qdevice: $qdevice_votes");
332 log_info("current expected votes: $expected_votes");
333 log_info("current total votes: $total_votes");
334
335 log_warn("expected votes set to non-standard value '$expected_votes'.")
336 if $expected_votes != $node_votes + $qdevice_votes;
337 log_warn("total votes < expected votes: $total_votes/$expected_votes!")
338 if $total_votes < $expected_votes;
339
340 my $conf_nodelist_count = scalar(keys %$conf_nodelist);
341 my $cfs_nodelist_count = scalar(keys %$cfs_nodelist);
342 log_warn("cluster consists of less than three quorum-providing nodes!")
343 if $conf_nodelist_count < 3 && $conf_nodelist_count + $qdevice_votes < 3;
344
345 log_fail("corosync.conf ($conf_nodelist_count) and pmxcfs ($cfs_nodelist_count) don't agree about size of nodelist.")
346 if $conf_nodelist_count != $cfs_nodelist_count;
347
348 print "\nChecking nodelist entries..\n";
349 my $nodelist_pass = 1;
350 for my $cs_node (sort keys %$conf_nodelist) {
351 my $entry = $conf_nodelist->{$cs_node};
352 if (!defined($entry->{name})) {
353 $nodelist_pass = 0;
354 log_fail("$cs_node: no name entry in corosync.conf.");
355 }
356 if (!defined($entry->{nodeid})) {
357 $nodelist_pass = 0;
358 log_fail("$cs_node: no nodeid configured in corosync.conf.");
359 }
360 my $gotLinks = 0;
361 for my $link (0..7) {
362 $gotLinks++ if defined($entry->{"ring${link}_addr"});
363 }
364 if ($gotLinks <= 0) {
365 $nodelist_pass = 0;
366 log_fail("$cs_node: no ringX_addr (0 <= X <= 7) link defined in corosync.conf.");
367 }
368
369 my $verify_ring_ip = sub {
370 my $key = shift;
371 if (defined(my $ring = $entry->{$key})) {
372 my ($resolved_ip, undef) = PVE::Corosync::resolve_hostname_like_corosync($ring, $conf);
373 if (defined($resolved_ip)) {
374 if ($resolved_ip ne $ring) {
375 $nodelist_pass = 0;
82bbcfad
TL
376 log_warn(
377 "$cs_node: $key '$ring' resolves to '$resolved_ip'.\n"
378 ." Consider replacing it with the currently resolved IP address."
379 );
855e0adb
TL
380 }
381 } else {
382 $nodelist_pass = 0;
82bbcfad
TL
383 log_fail(
384 "$cs_node: unable to resolve $key '$ring' to an IP address according to Corosync's"
385 ." resolve strategy - cluster will potentially fail with Corosync 3.x/kronosnet!"
386 );
855e0adb
TL
387 }
388 }
389 };
390 for my $link (0..7) {
391 $verify_ring_ip->("ring${link}_addr");
392 }
393 }
394 log_pass("nodelist settings OK") if $nodelist_pass;
395
396 print "\nChecking totem settings..\n";
397 my $totem = $conf->{main}->{totem};
398 my $totem_pass = 1;
399
400 my $transport = $totem->{transport};
401 if (defined($transport)) {
402 if ($transport ne 'knet') {
403 $totem_pass = 0;
404 log_fail("Corosync transport explicitly set to '$transport' instead of implicit default!");
405 }
406 }
407
408 # TODO: are those values still up-to-date?
409 if ((!defined($totem->{secauth}) || $totem->{secauth} ne 'on') && (!defined($totem->{crypto_cipher}) || $totem->{crypto_cipher} eq 'none')) {
410 $totem_pass = 0;
411 log_fail("Corosync authentication/encryption is not explicitly enabled (secauth / crypto_cipher / crypto_hash)!");
412 } elsif (defined($totem->{crypto_cipher}) && $totem->{crypto_cipher} eq '3des') {
413 $totem_pass = 0;
414 log_fail("Corosync encryption cipher set to '3des', no longer supported in Corosync 3.x!"); # FIXME: can be removed?
415 }
416
417 log_pass("totem settings OK") if $totem_pass;
418 print "\n";
419 log_info("run 'pvecm status' to get detailed cluster status..");
420
421 if (defined(my $corosync = $get_pkg->('corosync'))) {
422 if ($corosync->{OldVersion} =~ m/^2\./) {
423 log_fail("\ncorosync 2.x installed, cluster-wide upgrade to 3.x needed!");
424 } elsif ($corosync->{OldVersion} !~ m/^3\./) {
425 log_fail("\nunexpected corosync version installed: $corosync->{OldVersion}!");
426 }
427 }
428}
429
430sub check_ceph {
431 print_header("CHECKING HYPER-CONVERGED CEPH STATUS");
432
433 if (PVE::Ceph::Tools::check_ceph_inited(1)) {
434 log_info("hyper-converged ceph setup detected!");
435 } else {
436 log_skip("no hyper-converged ceph setup detected!");
437 return;
438 }
439
440 log_info("getting Ceph status/health information..");
441 my $ceph_status = eval { PVE::API2::Ceph->status({ node => $nodename }); };
442 my $noout = eval { PVE::API2::Cluster::Ceph->get_flag({ flag => "noout" }); };
443 if ($@) {
444 log_fail("failed to get 'noout' flag status - $@");
445 }
446
447 my $noout_wanted = 1;
448
449 if (!$ceph_status || !$ceph_status->{health}) {
450 log_fail("unable to determine Ceph status!");
451 } else {
452 my $ceph_health = $ceph_status->{health}->{status};
453 if (!$ceph_health) {
454 log_fail("unable to determine Ceph health!");
455 } elsif ($ceph_health eq 'HEALTH_OK') {
456 log_pass("Ceph health reported as 'HEALTH_OK'.");
457 } elsif ($ceph_health eq 'HEALTH_WARN' && $noout && (keys %{$ceph_status->{health}->{checks}} == 1)) {
458 log_pass("Ceph health reported as 'HEALTH_WARN' with a single failing check and 'noout' flag set.");
459 } else {
bda81173
TL
460 log_warn(
461 "Ceph health reported as '$ceph_health'.\n Use the PVE dashboard or 'ceph -s'"
462 ." to determine the specific issues and try to resolve them."
463 );
855e0adb
TL
464 }
465 }
466
467 # TODO: check OSD min-required version, if to low it breaks stuff!
468
bda81173
TL
469 log_info("cehcking local Ceph version..");
470 if (my $release = eval { PVE::Ceph::Tools::get_local_version(1) }) {
471 my $code_name = $ceph_release2code->{"$release"} || 'unknown';
472 if ($release == $ceph_supported_release) {
473 log_pass("found expected Ceph $ceph_supported_release $ceph_supported_code_name release.")
474 } elsif ($release > $ceph_supported_release) {
475 log_warn(
476 "found newer Ceph release $release $code_name as the expected $ceph_supported_release"
477 ." $ceph_supported_code_name, installed third party repos?!"
478 )
479 } else {
480 log_fail(
481 "Hyper-converged Ceph $release $code_name is to old for upgrade!\n"
482 ." Upgrade Ceph first to $ceph_supported_code_name following our how-to:\n"
483 ." <https://pve.proxmox.com/wiki/Category:Ceph_Upgrade>"
484 );
485 }
486 } else {
487 log_fail("unable to determine local Ceph version!");
488 }
489
855e0adb
TL
490 log_info("getting Ceph daemon versions..");
491 my $ceph_versions = eval { PVE::Ceph::Tools::get_cluster_versions(undef, 1); };
492 if (!$ceph_versions) {
493 log_fail("unable to determine Ceph daemon versions!");
494 } else {
495 my $services = [
496 { 'key' => 'mon', 'name' => 'monitor' },
497 { 'key' => 'mgr', 'name' => 'manager' },
498 { 'key' => 'mds', 'name' => 'MDS' },
499 { 'key' => 'osd', 'name' => 'OSD' },
500 ];
501
502 foreach my $service (@$services) {
bda81173
TL
503 my ($name, $key) = $service->@{'name', 'key'};
504 if (my $service_versions = $ceph_versions->{$key}) {
855e0adb
TL
505 if (keys %$service_versions == 0) {
506 log_skip("no running instances detected for daemon type $name.");
507 } elsif (keys %$service_versions == 1) {
508 log_pass("single running version detected for daemon type $name.");
509 } else {
510 log_warn("multiple running versions detected for daemon type $name!");
511 }
512 } else {
513 log_skip("unable to determine versions of running Ceph $name instances.");
514 }
515 }
516
517 my $overall_versions = $ceph_versions->{overall};
518 if (!$overall_versions) {
519 log_warn("unable to determine overall Ceph daemon versions!");
520 } elsif (keys %$overall_versions == 1) {
521 log_pass("single running overall version detected for all Ceph daemon types.");
522 $noout_wanted = 0; # off post-upgrade, on pre-upgrade
523 } else {
524 log_warn("overall version mismatch detected, check 'ceph versions' output for details!");
525 }
526 }
527
528 if ($noout) {
529 if ($noout_wanted) {
530 log_pass("'noout' flag set to prevent rebalancing during cluster-wide upgrades.");
531 } else {
532 log_warn("'noout' flag set, Ceph cluster upgrade seems finished.");
533 }
534 } elsif ($noout_wanted) {
535 log_warn("'noout' flag not set - recommended to prevent rebalancing during upgrades.");
536 }
537
538 log_info("checking Ceph config..");
539 my $conf = PVE::Cluster::cfs_read_file('ceph.conf');
540 if (%$conf) {
541 my $global = $conf->{global};
542
543 my $global_monhost = $global->{mon_host} // $global->{"mon host"} // $global->{"mon-host"};
544 if (!defined($global_monhost)) {
82bbcfad
TL
545 log_warn(
546 "No 'mon_host' entry found in ceph config.\n It's recommended to add mon_host with"
547 ." all monitor addresses (without ports) to the global section."
548 );
855e0adb
TL
549 }
550
551 my $ipv6 = $global->{ms_bind_ipv6} // $global->{"ms bind ipv6"} // $global->{"ms-bind-ipv6"};
552 if ($ipv6) {
553 my $ipv4 = $global->{ms_bind_ipv4} // $global->{"ms bind ipv4"} // $global->{"ms-bind-ipv4"};
554 if ($ipv6 eq 'true' && (!defined($ipv4) || $ipv4 ne 'false')) {
82bbcfad
TL
555 log_warn(
556 "'ms_bind_ipv6' is enabled but 'ms_bind_ipv4' is not disabled.\n Make sure to"
557 ." disable 'ms_bind_ipv4' for ipv6 only clusters, or add an ipv4 network to public/cluster network."
558 );
855e0adb
TL
559 }
560 }
561
562 if (defined($global->{keyring})) {
82bbcfad
TL
563 log_warn(
564 "[global] config section contains 'keyring' option, which will prevent services from"
565 ." starting with Nautilus.\n Move 'keyring' option to [client] section instead."
566 );
855e0adb
TL
567 }
568
569 } else {
570 log_warn("Empty ceph config found");
571 }
572
573 my $local_ceph_ver = PVE::Ceph::Tools::get_local_version(1);
574 if (defined($local_ceph_ver)) {
575 if ($local_ceph_ver <= 14) {
576 log_fail("local Ceph version too low, at least Octopus required..");
577 }
578 } else {
579 log_fail("unable to determine local Ceph version.");
580 }
581}
582
583sub check_backup_retention_settings {
584 log_info("Checking backup retention settings..");
585
586 my $pass = 1;
587
588 my $node_has_retention;
589
590 my $maxfiles_msg = "parameter 'maxfiles' is deprecated with PVE 7.x and will be removed in a " .
591 "future version, use 'prune-backups' instead.";
592
593 eval {
594 my $confdesc = PVE::VZDump::Common::get_confdesc();
595
596 my $fn = "/etc/vzdump.conf";
597 my $raw = PVE::Tools::file_get_contents($fn);
598
599 my $conf_schema = { type => 'object', properties => $confdesc, };
600 my $param = PVE::JSONSchema::parse_config($conf_schema, $fn, $raw);
601
602 if (defined($param->{maxfiles})) {
603 $pass = 0;
604 log_warn("$fn - $maxfiles_msg");
605 }
606
607 $node_has_retention = defined($param->{maxfiles}) || defined($param->{'prune-backups'});
608 };
609 if (my $err = $@) {
610 $pass = 0;
611 log_warn("unable to parse node's VZDump configuration - $err");
612 }
613
614 my $storage_cfg = PVE::Storage::config();
615
616 for my $storeid (keys $storage_cfg->{ids}->%*) {
617 my $scfg = $storage_cfg->{ids}->{$storeid};
618
619 if (defined($scfg->{maxfiles})) {
620 $pass = 0;
621 log_warn("storage '$storeid' - $maxfiles_msg");
622 }
623
624 next if !$scfg->{content}->{backup};
625 next if defined($scfg->{maxfiles}) || defined($scfg->{'prune-backups'});
626 next if $node_has_retention;
627
82bbcfad
TL
628 log_info(
629 "storage '$storeid' - no backup retention settings defined - by default, since PVE 7.0"
630 ." it will no longer keep only the last backup, but all backups"
631 );
855e0adb
TL
632 }
633
634 eval {
635 my $vzdump_cron = PVE::Cluster::cfs_read_file('vzdump.cron');
636
637 # only warn once, there might be many jobs...
638 if (scalar(grep { defined($_->{maxfiles}) } $vzdump_cron->{jobs}->@*)) {
639 $pass = 0;
640 log_warn("/etc/pve/vzdump.cron - $maxfiles_msg");
641 }
642 };
643 if (my $err = $@) {
644 $pass = 0;
645 log_warn("unable to parse node's VZDump configuration - $err");
646 }
647
648 log_pass("no problems found.") if $pass;
649}
650
651sub check_cifs_credential_location {
652 log_info("checking CIFS credential location..");
653
654 my $regex = qr/^(.*)\.cred$/;
655
656 my $found;
657
658 PVE::Tools::dir_glob_foreach('/etc/pve/priv/', $regex, sub {
659 my ($filename) = @_;
660
661 my ($basename) = $filename =~ $regex;
662
82bbcfad
TL
663 log_warn(
664 "CIFS credentials '/etc/pve/priv/$filename' will be moved to"
665 ." '/etc/pve/priv/storage/$basename.pw' during the update"
666 );
855e0adb
TL
667
668 $found = 1;
669 });
670
671 log_pass("no CIFS credentials at outdated location found.") if !$found;
672}
673
674sub check_custom_pool_roles {
675 log_info("Checking custom roles for pool permissions..");
676
677 if (! -f "/etc/pve/user.cfg") {
678 log_skip("user.cfg does not exist");
679 return;
680 }
681
682 my $raw = eval { PVE::Tools::file_get_contents('/etc/pve/user.cfg'); };
683 if ($@) {
684 log_fail("Failed to read '/etc/pve/user.cfg' - $@");
685 return;
686 }
687
688 my $roles = {};
689 while ($raw =~ /^\s*(.+?)\s*$/gm) {
690 my $line = $1;
691 my @data;
692
693 foreach my $d (split (/:/, $line)) {
694 $d =~ s/^\s+//;
695 $d =~ s/\s+$//;
696 push @data, $d
697 }
698
699 my $et = shift @data;
700 next if $et ne 'role';
701
702 my ($role, $privlist) = @data;
703 if (!PVE::AccessControl::verify_rolename($role, 1)) {
704 warn "user config - ignore role '$role' - invalid characters in role name\n";
705 next;
706 }
707
708 $roles->{$role} = {} if !$roles->{$role};
709 foreach my $priv (split_list($privlist)) {
710 $roles->{$role}->{$priv} = 1;
711 }
712 }
713
714 foreach my $role (sort keys %{$roles}) {
82bbcfad 715 next if PVE::AccessControl::role_is_special($role);
855e0adb 716
82bbcfad 717 # TODO: any role updates?
855e0adb
TL
718 }
719}
720
721my sub check_max_length {
722 my ($raw, $max_length, $warning) = @_;
723 log_warn($warning) if defined($raw) && length($raw) > $max_length;
724}
725
726sub check_node_and_guest_configurations {
727 log_info("Checking node and guest description/note legnth..");
728
729 my @affected_nodes = grep {
730 my $desc = PVE::NodeConfig::load_config($_)->{desc};
731 defined($desc) && length($desc) > 64 * 1024
732 } PVE::Cluster::get_nodelist();
733
734 if (scalar(@affected_nodes) > 0) {
735 log_warn("Node config description of the following nodes too long for new limit of 64 KiB:\n "
736 . join(', ', @affected_nodes));
737 } else {
738 log_pass("All node config descriptions fit in the new limit of 64 KiB");
739 }
740
741 my $affected_guests_long_desc = [];
742 my $affected_cts_cgroup_keys = [];
743
744 my $cts = PVE::LXC::config_list();
745 for my $vmid (sort { $a <=> $b } keys %$cts) {
746 my $conf = PVE::LXC::Config->load_config($vmid);
747
748 my $desc = $conf->{description};
749 push @$affected_guests_long_desc, "CT $vmid" if defined($desc) && length($desc) > 8 * 1024;
750
751 my $lxc_raw_conf = $conf->{lxc};
752 push @$affected_cts_cgroup_keys, "CT $vmid" if (grep (@$_[0] =~ /^lxc\.cgroup\./, @$lxc_raw_conf));
753 }
754 my $vms = PVE::QemuServer::config_list();
755 for my $vmid (sort { $a <=> $b } keys %$vms) {
756 my $desc = PVE::QemuConfig->load_config($vmid)->{description};
757 push @$affected_guests_long_desc, "VM $vmid" if defined($desc) && length($desc) > 8 * 1024;
758 }
759 if (scalar($affected_guests_long_desc->@*) > 0) {
760 log_warn("Guest config description of the following virtual-guests too long for new limit of 64 KiB:\n"
761 ." " . join(", ", $affected_guests_long_desc->@*));
762 } else {
763 log_pass("All guest config descriptions fit in the new limit of 8 KiB");
764 }
765
766 log_info("Checking container configs for deprecated lxc.cgroup entries");
767
768 if (scalar($affected_cts_cgroup_keys->@*) > 0) {
769 if ($forced_legacy_cgroup) {
770 log_pass("Found legacy 'lxc.cgroup' keys, but system explicitly configured for legacy hybrid cgroup hierarchy.");
771 } else {
772 log_warn("The following CTs have 'lxc.cgroup' keys configured, which will be ignored in the new default unified cgroupv2:\n"
773 ." " . join(", ", $affected_cts_cgroup_keys->@*) ."\n"
774 ." Often it can be enough to change to the new 'lxc.cgroup2' prefix after the upgrade to Proxmox VE 7.x");
775 }
776 } else {
777 log_pass("No legacy 'lxc.cgroup' keys found.");
778 }
779}
780
781sub check_storage_content {
782 log_info("Checking storage content type configuration..");
783
784 my $found;
785 my $pass = 1;
786
787 my $storage_cfg = PVE::Storage::config();
788
789 for my $storeid (sort keys $storage_cfg->{ids}->%*) {
790 my $scfg = $storage_cfg->{ids}->{$storeid};
791
792 next if $scfg->{shared};
793 next if !PVE::Storage::storage_check_enabled($storage_cfg, $storeid, undef, 1);
794
795 my $valid_content = PVE::Storage::Plugin::valid_content_types($scfg->{type});
796
797 if (scalar(keys $scfg->{content}->%*) == 0 && !$valid_content->{none}) {
798 $pass = 0;
799 log_fail("storage '$storeid' does not support configured content type 'none'");
800 delete $scfg->{content}->{none}; # scan for guest images below
801 }
802
803 next if $scfg->{content}->{images};
804 next if $scfg->{content}->{rootdir};
805
806 # Skip 'iscsi(direct)' (and foreign plugins with potentially similiar behavior) with 'none',
807 # because that means "use LUNs directly" and vdisk_list() in PVE 6.x still lists those.
808 # It's enough to *not* skip 'dir', because it is the only other storage that supports 'none'
809 # and 'images' or 'rootdir', hence being potentially misconfigured.
810 next if $scfg->{type} ne 'dir' && $scfg->{content}->{none};
811
812 eval { PVE::Storage::activate_storage($storage_cfg, $storeid) };
813 if (my $err = $@) {
814 log_warn("activating '$storeid' failed - $err");
815 next;
816 }
817
818 my $res = eval { PVE::Storage::vdisk_list($storage_cfg, $storeid); };
819 if (my $err = $@) {
820 log_warn("listing images on '$storeid' failed - $err");
821 next;
822 }
823 my @volids = map { $_->{volid} } $res->{$storeid}->@*;
824
825 my $number = scalar(@volids);
826 if ($number > 0) {
82bbcfad
TL
827 log_info(
828 "storage '$storeid' - neither content type 'images' nor 'rootdir' configured, but"
829 ."found $number guest volume(s)"
830 );
855e0adb
TL
831 }
832 }
833
834 my $check_volid = sub {
835 my ($volid, $vmid, $vmtype, $reference) = @_;
836
837 my $guesttext = $vmtype eq 'qemu' ? 'VM' : 'CT';
838 my $prefix = "$guesttext $vmid - volume '$volid' ($reference)";
839
840 my ($storeid) = PVE::Storage::parse_volume_id($volid, 1);
841 return if !defined($storeid);
842
843 my $scfg = $storage_cfg->{ids}->{$storeid};
844 if (!$scfg) {
845 $pass = 0;
846 log_warn("$prefix - storage does not exist!");
847 return;
848 }
849
850 # cannot use parse_volname for containers, as it can return 'images'
851 # but containers cannot have ISO images attached, so assume 'rootdir'
852 my $vtype = 'rootdir';
853 if ($vmtype eq 'qemu') {
854 ($vtype) = eval { PVE::Storage::parse_volname($storage_cfg, $volid); };
855 return if $@;
856 }
857
858 if (!$scfg->{content}->{$vtype}) {
859 $found = 1;
860 $pass = 0;
861 log_warn("$prefix - storage does not have content type '$vtype' configured.");
862 }
863 };
864
865 my $cts = PVE::LXC::config_list();
866 for my $vmid (sort { $a <=> $b } keys %$cts) {
867 my $conf = PVE::LXC::Config->load_config($vmid);
868
869 my $volhash = {};
870
871 my $check = sub {
872 my ($ms, $mountpoint, $reference) = @_;
873
874 my $volid = $mountpoint->{volume};
875 return if !$volid || $mountpoint->{type} ne 'volume';
876
877 return if $volhash->{$volid}; # volume might be referenced multiple times
878
879 $volhash->{$volid} = 1;
880
881 $check_volid->($volid, $vmid, 'lxc', $reference);
882 };
883
884 my $opts = { include_unused => 1 };
885 PVE::LXC::Config->foreach_volume_full($conf, $opts, $check, 'in config');
886 for my $snapname (keys $conf->{snapshots}->%*) {
887 my $snap = $conf->{snapshots}->{$snapname};
888 PVE::LXC::Config->foreach_volume_full($snap, $opts, $check, "in snapshot '$snapname'");
889 }
890 }
891
892 my $vms = PVE::QemuServer::config_list();
893 for my $vmid (sort { $a <=> $b } keys %$vms) {
894 my $conf = PVE::QemuConfig->load_config($vmid);
895
896 my $volhash = {};
897
898 my $check = sub {
899 my ($key, $drive, $reference) = @_;
900
901 my $volid = $drive->{file};
902 return if $volid =~ m|^/|;
855e0adb
TL
903 return if $volhash->{$volid}; # volume might be referenced multiple times
904
905 $volhash->{$volid} = 1;
855e0adb
TL
906 $check_volid->($volid, $vmid, 'qemu', $reference);
907 };
908
909 my $opts = {
910 extra_keys => ['vmstate'],
911 include_unused => 1,
912 };
913 # startup from a suspended state works even without 'images' content type on the
914 # state storage, so do not check 'vmstate' for $conf
915 PVE::QemuConfig->foreach_volume_full($conf, { include_unused => 1 }, $check, 'in config');
916 for my $snapname (keys $conf->{snapshots}->%*) {
917 my $snap = $conf->{snapshots}->{$snapname};
918 PVE::QemuConfig->foreach_volume_full($snap, $opts, $check, "in snapshot '$snapname'");
919 }
920 }
921
922 if ($found) {
82bbcfad 923 log_warn("Proxmox VE enforces stricter content type checks since 7.0. The guests above " .
855e0adb
TL
924 "might not work until the storage configuration is fixed.");
925 }
926
927 if ($pass) {
928 log_pass("no problems found");
929 }
930}
931
932sub check_containers_cgroup_compat {
933 if ($forced_legacy_cgroup) {
82bbcfad
TL
934 log_warn("System explicitly configured for legacy hybrid cgroup hierarchy.\n"
935 ." NOTE: support for the hybrid cgroup hierachy will be removed in future Proxmox VE 9 (~ 2025)."
936 );
855e0adb
TL
937 }
938
939 my $supports_cgroupv2 = sub {
940 my ($conf, $rootdir, $ctid) = @_;
941
942 my $get_systemd_version = sub {
943 my ($self) = @_;
944
945 my $sd_lib_dir = -d "/lib/systemd" ? "/lib/systemd" : "/usr/lib/systemd";
946 my $libsd = PVE::Tools::dir_glob_regex($sd_lib_dir, "libsystemd-shared-.+\.so");
947 if (defined($libsd) && $libsd =~ /libsystemd-shared-(\d+)\.so/) {
948 return $1;
949 }
950
951 return undef;
952 };
953
954 my $unified_cgroupv2_support = sub {
955 my ($self) = @_;
956
957 # https://www.freedesktop.org/software/systemd/man/systemd.html
958 # systemd is installed as symlink to /sbin/init
959 my $systemd = CORE::readlink('/sbin/init');
960
961 # assume non-systemd init will run with unified cgroupv2
962 if (!defined($systemd) || $systemd !~ m@/systemd$@) {
963 return 1;
964 }
965
966 # systemd version 232 (e.g. debian stretch) supports the unified hierarchy
967 my $sdver = $get_systemd_version->();
968 if (!defined($sdver) || $sdver < 232) {
969 return 0;
970 }
971
972 return 1;
973 };
974
975 my $ostype = $conf->{ostype};
976 if (!defined($ostype)) {
977 log_warn("Found CT ($ctid) without 'ostype' set!");
978 } elsif ($ostype eq 'devuan' || $ostype eq 'alpine') {
979 return 1; # no systemd, no cgroup problems
980 }
981
982 my $lxc_setup = PVE::LXC::Setup->new($conf, $rootdir);
983 return $lxc_setup->protected_call($unified_cgroupv2_support);
984 };
985
986 my $log_problem = sub {
987 my ($ctid) = @_;
82bbcfad
TL
988 my $extra = $forced_legacy_cgroup ? '' : " or set systemd.unified_cgroup_hierarchy=0 in the Proxmox VE hosts' kernel cmdline";
989 log_warn(
990 "Found at least one CT ($ctid) which does not support running in a unified cgroup v2 layout\n"
991 ." Consider upgrading the Containers distro${extra}! Skipping further CT compat checks."
855e0adb
TL
992 );
993 };
994
995 my $cts = eval { PVE::API2::LXC->vmlist({ node => $nodename }) };
996 if ($@) {
997 log_warn("Failed to retrieve information about this node's CTs - $@");
998 return;
999 }
1000
1001 if (!defined($cts) || !scalar(@$cts)) {
1002 log_skip("No containers on node detected.");
1003 return;
1004 }
1005
1006 my @running_cts = sort { $a <=> $b } grep { $_->{status} eq 'running' } @$cts;
1007 my @offline_cts = sort { $a <=> $b } grep { $_->{status} ne 'running' } @$cts;
1008
1009 for my $ct (@running_cts) {
1010 my $ctid = $ct->{vmid};
1011 my $pid = eval { PVE::LXC::find_lxc_pid($ctid) };
1012 if (my $err = $@) {
1013 log_warn("Failed to get PID for running CT $ctid - $err");
1014 next;
1015 }
1016 my $rootdir = "/proc/$pid/root";
1017 my $conf = PVE::LXC::Config->load_config($ctid);
1018
1019 my $ret = eval { $supports_cgroupv2->($conf, $rootdir, $ctid) };
1020 if (my $err = $@) {
1021 log_warn("Failed to get cgroup support status for CT $ctid - $err");
1022 next;
1023 }
1024 if (!$ret) {
1025 $log_problem->($ctid);
1026 return;
1027 }
1028 }
1029
1030 my $storage_cfg = PVE::Storage::config();
1031 for my $ct (@offline_cts) {
1032 my $ctid = $ct->{vmid};
1033 my ($conf, $rootdir, $ret);
1034 eval {
1035 $conf = PVE::LXC::Config->load_config($ctid);
1036 $rootdir = PVE::LXC::mount_all($ctid, $storage_cfg, $conf);
1037 $ret = $supports_cgroupv2->($conf, $rootdir, $ctid);
1038 };
1039 if (my $err = $@) {
1040 log_warn("Failed to load config and mount CT $ctid - $err");
1041 eval { PVE::LXC::umount_all($ctid, $storage_cfg, $conf) };
1042 next;
1043 }
1044 if (!$ret) {
1045 $log_problem->($ctid);
1046 eval { PVE::LXC::umount_all($ctid, $storage_cfg, $conf) };
1047 last;
1048 }
1049
1050 eval { PVE::LXC::umount_all($ctid, $storage_cfg, $conf) };
1051 }
1052};
1053
64fa63b7 1054sub check_apt_repos {
855e0adb
TL
1055 log_info("Checking if the suite for the Debian security repository is correct..");
1056
1057 my $found = 0;
1058
1059 my $dir = '/etc/apt/sources.list.d';
1060 my $in_dir = 0;
1061
64fa63b7
TL
1062 # TODO: check that (original) debian and Proxmox VE mirrors are present.
1063
855e0adb
TL
1064 my $check_file = sub {
1065 my ($file) = @_;
1066
1067 $file = "${dir}/${file}" if $in_dir;
1068
1069 my $raw = eval { PVE::Tools::file_get_contents($file) };
1070 return if !defined($raw);
1071 my @lines = split(/\n/, $raw);
1072
1073 my $number = 0;
1074 for my $line (@lines) {
1075 $number++;
1076
1077 next if length($line) == 0; # split would result in undef then...
1078
1079 ($line) = split(/#/, $line);
1080
1081 next if $line !~ m/^deb[[:space:]]/; # is case sensitive
1082
1083 my $suite;
1084
1085 # catch any of
1086 # https://deb.debian.org/debian-security
1087 # http://security.debian.org/debian-security
1088 # http://security.debian.org/
1089 if ($line =~ m|https?://deb\.debian\.org/debian-security/?\s+(\S*)|i) {
1090 $suite = $1;
1091 } elsif ($line =~ m|https?://security\.debian\.org(?:.*?)\s+(\S*)|i) {
1092 $suite = $1;
1093 } else {
1094 next;
1095 }
1096
1097 $found = 1;
1098
1099 my $where = "in ${file}:${number}";
1100
1101 if ($suite eq 'buster/updates') {
1102 log_info("Make sure to change the suite of the Debian security repository " .
1103 "from 'buster/updates' to 'bullseye-security' - $where");
1104 } elsif ($suite eq 'bullseye-security') {
1105 log_pass("already using 'bullseye-security'");
1106 } else {
1107 log_fail("The new suite of the Debian security repository should be " .
1108 "'bullseye-security' - $where");
1109 }
1110 }
1111 };
1112
1113 $check_file->("/etc/apt/sources.list");
1114
1115 $in_dir = 1;
1116
1117 PVE::Tools::dir_glob_foreach($dir, '^.*\.list$', $check_file);
1118
1119 if (!$found) {
1120 # only warn, it might be defined in a .sources file or in a way not catched above
1121 log_warn("No Debian security repository detected in /etc/apt/sources.list and " .
1122 "/etc/apt/sources.list.d/*.list");
1123 }
1124}
1125
81e10d49
TL
1126sub check_time_sync {
1127 my $unit_active = sub { return $get_systemd_unit_state->($_[0], 1) eq 'active' ? $_[0] : undef };
1128
1129 log_info("Checking for supported & active NTP service..");
1130 if ($unit_active->('systemd-timesyncd.service')) {
1131 log_warn(
1132 "systemd-timesyncd is not the best choice for time-keeping on servers, due to only applying"
1133 ." updates on boot.\n While not necesarry for the upgrade it's recommended to use one of:\n"
1134 ." * chrony (Default in new Proxmox VE installations)\n * ntpsec\n * openntpd\n"
1135 );
1136 } elsif ($unit_active->('ntp.service')) {
1137 log_info("Debian deprecated and removed the ntp package for Bookworm, but the system"
1138 ." will automatically migrate to the 'ntpsec' replacement package on upgrade.");
1139 } elsif (my $active_ntp = ($unit_active->('chrony.service') || $unit_active->('openntpd.service') || $unit_active->('ntpsec.service'))) {
1140 log_pass("Detected active time synchronisation unit '$active_ntp'");
1141 } else {
1142 log_warn(
1143 "No (active) time synchronisation daemon (NTP) detected, but synchronized systems are important,"
1144 ." especially for cluster and/or ceph!"
1145 );
1146 }
1147}
1148
855e0adb
TL
1149sub check_misc {
1150 print_header("MISCELLANEOUS CHECKS");
1151 my $ssh_config = eval { PVE::Tools::file_get_contents('/root/.ssh/config') };
1152 if (defined($ssh_config)) {
1153 log_fail("Unsupported SSH Cipher configured for root in /root/.ssh/config: $1")
1154 if $ssh_config =~ /^Ciphers .*(blowfish|arcfour|3des).*$/m;
1155 } else {
1156 log_skip("No SSH config file found.");
1157 }
1158
1159 log_info("Checking common daemon services..");
1160 $log_systemd_unit_state->('pveproxy.service');
1161 $log_systemd_unit_state->('pvedaemon.service');
69eaceb3 1162 $log_systemd_unit_state->('pvescheduler.service');
855e0adb
TL
1163 $log_systemd_unit_state->('pvestatd.service');
1164
81e10d49
TL
1165 check_time_sync();
1166
855e0adb 1167 my $root_free = PVE::Tools::df('/', 10);
5af851c5
TL
1168 log_warn("Less than 5 GB free space on root file system.")
1169 if defined($root_free) && $root_free->{avail} < 5 * 1000*1000*1000;
855e0adb
TL
1170
1171 log_info("Checking for running guests..");
1172 my $running_guests = 0;
1173
1174 my $vms = eval { PVE::API2::Qemu->vmlist({ node => $nodename }) };
1175 log_warn("Failed to retrieve information about this node's VMs - $@") if $@;
1176 $running_guests += grep { $_->{status} eq 'running' } @$vms if defined($vms);
1177
1178 my $cts = eval { PVE::API2::LXC->vmlist({ node => $nodename }) };
1179 log_warn("Failed to retrieve information about this node's CTs - $@") if $@;
1180 $running_guests += grep { $_->{status} eq 'running' } @$cts if defined($cts);
1181
1182 if ($running_guests > 0) {
1183 log_warn("$running_guests running guest(s) detected - consider migrating or stopping them.")
1184 } else {
1185 log_pass("no running guest detected.")
1186 }
1187
1188 log_info("Checking if the local node's hostname '$nodename' is resolvable..");
1189 my $local_ip = eval { PVE::Network::get_ip_from_hostname($nodename) };
1190 if ($@) {
1191 log_warn("Failed to resolve hostname '$nodename' to IP - $@");
1192 } else {
1193 log_info("Checking if resolved IP is configured on local node..");
1194 my $cidr = Net::IP::ip_is_ipv6($local_ip) ? "$local_ip/128" : "$local_ip/32";
1195 my $configured_ips = PVE::Network::get_local_ip_from_cidr($cidr);
1196 my $ip_count = scalar(@$configured_ips);
1197
1198 if ($ip_count <= 0) {
1199 log_fail("Resolved node IP '$local_ip' not configured or active for '$nodename'");
1200 } elsif ($ip_count > 1) {
1201 log_warn("Resolved node IP '$local_ip' active on multiple ($ip_count) interfaces!");
1202 } else {
1203 log_pass("Resolved node IP '$local_ip' configured and active on single interface.");
1204 }
1205 }
1206
1207 log_info("Check node certificate's RSA key size");
1208 my $certs = PVE::API2::Certificates->info({ node => $nodename });
1209 my $certs_check = {
1210 'rsaEncryption' => {
1211 minsize => 2048,
1212 name => 'RSA',
1213 },
1214 'id-ecPublicKey' => {
1215 minsize => 224,
1216 name => 'ECC',
1217 },
1218 };
1219
1220 my $certs_check_failed = 0;
1221 foreach my $cert (@$certs) {
1222 my ($type, $size, $fn) = $cert->@{qw(public-key-type public-key-bits filename)};
1223
1224 if (!defined($type) || !defined($size)) {
1225 log_warn("'$fn': cannot check certificate, failed to get it's type or size!");
1226 }
1227
1228 my $check = $certs_check->{$type};
1229 if (!defined($check)) {
82bbcfad 1230 log_warn("'$fn': certificate's public key type '$type' unknown!");
855e0adb
TL
1231 next;
1232 }
1233
1234 if ($size < $check->{minsize}) {
1235 log_fail("'$fn', certificate's $check->{name} public key size is less than 2048 bit");
1236 $certs_check_failed = 1;
1237 } else {
82bbcfad 1238 log_pass("Certificate '$fn' passed Debian Busters (and newer) security level for TLS connections ($size >= 2048)");
855e0adb
TL
1239 }
1240 }
1241
1242 check_backup_retention_settings();
1243 check_cifs_credential_location();
1244 check_custom_pool_roles();
1245 check_node_and_guest_configurations();
64fa63b7 1246 check_apt_repos();
855e0adb
TL
1247}
1248
1f4a4dbe
TL
1249my sub colored_if {
1250 my ($str, $color, $condition) = @_;
1251 return "". ($condition ? colored($str, $color) : $str);
1252}
1253
855e0adb
TL
1254__PACKAGE__->register_method ({
1255 name => 'checklist',
1256 path => 'checklist',
1257 method => 'GET',
1258 description => 'Check (pre-/post-)upgrade conditions.',
1259 parameters => {
1260 additionalProperties => 0,
1261 properties => {
1262 full => {
1263 description => 'perform additional, expensive checks.',
1264 type => 'boolean',
1265 optional => 1,
1266 default => 0,
1267 },
1268 },
1269 },
1270 returns => { type => 'null' },
1271 code => sub {
1272 my ($param) = @_;
1273
1274 my $kernel_cli = PVE::Tools::file_get_contents('/proc/cmdline');
1275 if ($kernel_cli =~ /systemd.unified_cgroup_hierarchy=0/){
1276 $forced_legacy_cgroup = 1;
1277 }
1278
1279 check_pve_packages();
1280 check_cluster_corosync();
1281 check_ceph();
1282 check_storage_health();
1283 check_misc();
1284
1285 if ($param->{full}) {
1286 check_containers_cgroup_compat();
1287 } else {
1288 log_skip("NOTE: Expensive checks, like CT cgroupv2 compat, not performed without '--full' parameter");
1289 }
1290
1291 print_header("SUMMARY");
1292
1293 my $total = 0;
1294 $total += $_ for values %$counters;
1295
1296 print "TOTAL: $total\n";
1297 print colored("PASSED: $counters->{pass}\n", 'green');
1298 print "SKIPPED: $counters->{skip}\n";
1f4a4dbe 1299 print colored_if("WARNINGS: $counters->{warn}\n", 'yellow', $counters->{warn} > 0);
6bafdd65 1300 print colored_if("FAILURES: $counters->{fail}\n", 'bold red', $counters->{fail} > 0);
855e0adb
TL
1301
1302 if ($counters->{warn} > 0 || $counters->{fail} > 0) {
6bafdd65 1303 my $color = $counters->{fail} > 0 ? 'bold red' : 'yellow';
855e0adb
TL
1304 print colored("\nATTENTION: Please check the output for detailed information!\n", $color);
1305 print colored("Try to solve the problems one at a time and then run this checklist tool again.\n", $color) if $counters->{fail} > 0;
1306 }
1307
1308 return undef;
1309 }});
1310
1311our $cmddef = [ __PACKAGE__, 'checklist', [], {}];
1312
13131;