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