]> git.proxmox.com Git - pve-manager.git/blobdiff - PVE/CLI/pve6to7.pm
Only check deb sources.list entries
[pve-manager.git] / PVE / CLI / pve6to7.pm
index 0b6267d5ad6304dfef7861ec179d11b37bbfc820..2134428d4bb0ed4457211590fa5287cc5295f7a6 100644 (file)
@@ -8,16 +8,25 @@ use PVE::API2::Ceph;
 use PVE::API2::LXC;
 use PVE::API2::Qemu;
 use PVE::API2::Certificates;
+use PVE::API2::Cluster::Ceph;
 
+use PVE::AccessControl;
 use PVE::Ceph::Tools;
 use PVE::Cluster;
 use PVE::Corosync;
 use PVE::INotify;
 use PVE::JSONSchema;
+use PVE::NodeConfig;
 use PVE::RPCEnvironment;
 use PVE::Storage;
-use PVE::Tools qw(run_command);
+use PVE::Storage::Plugin;
+use PVE::Tools qw(run_command split_list);
+use PVE::QemuConfig;
 use PVE::QemuServer;
+use PVE::VZDump::Common;
+use PVE::LXC;
+use PVE::LXC::Config;
+use PVE::LXC::Setup;
 
 use Term::ANSIColor;
 
@@ -35,6 +44,8 @@ my $min_pve_major = 6;
 my $min_pve_minor = 4;
 my $min_pve_pkgrel = 1;
 
+my $forced_legacy_cgroup = 0;
+
 my $counters = {
     pass => 0,
     skip => 0,
@@ -199,7 +210,7 @@ sub check_storage_health {
 
     my $info = PVE::Storage::storage_info($cfg);
 
-    foreach my $storeid (keys %$info) {
+    foreach my $storeid (sort keys %$info) {
        my $d = $info->{$storeid};
        if ($d->{enabled}) {
            if ($d->{type} eq 'sheepdog') {
@@ -297,17 +308,25 @@ sub check_cluster_corosync {
        if $conf_nodelist_count != $cfs_nodelist_count;
 
     print "\nChecking nodelist entries..\n";
+    my $nodelist_pass = 1;
     for my $cs_node (sort keys %$conf_nodelist) {
        my $entry = $conf_nodelist->{$cs_node};
-       log_fail("$cs_node: no name entry in corosync.conf.")
-           if !defined($entry->{name});
-       log_fail("$cs_node: no nodeid configured in corosync.conf.")
-           if !defined($entry->{nodeid});
+       if (!defined($entry->{name})) {
+           $nodelist_pass = 0;
+           log_fail("$cs_node: no name entry in corosync.conf.");
+       }
+       if (!defined($entry->{nodeid})) {
+           $nodelist_pass = 0;
+           log_fail("$cs_node: no nodeid configured in corosync.conf.");
+       }
        my $gotLinks = 0;
        for my $link (0..7) {
            $gotLinks++ if defined($entry->{"ring${link}_addr"});
        }
-       log_fail("$cs_node: no ringX_addr (0 <= X <= 7) link defined in corosync.conf.") if $gotLinks <= 0;
+       if ($gotLinks <= 0) {
+           $nodelist_pass = 0;
+           log_fail("$cs_node: no ringX_addr (0 <= X <= 7) link defined in corosync.conf.");
+       }
 
        my $verify_ring_ip = sub {
            my $key = shift;
@@ -315,11 +334,11 @@ sub check_cluster_corosync {
                my ($resolved_ip, undef) = PVE::Corosync::resolve_hostname_like_corosync($ring, $conf);
                if (defined($resolved_ip)) {
                    if ($resolved_ip ne $ring) {
+                       $nodelist_pass = 0;
                        log_warn("$cs_node: $key '$ring' resolves to '$resolved_ip'.\n Consider replacing it with the currently resolved IP address.");
-                   } else {
-                       log_pass("$cs_node: $key is configured to use IP address '$ring'");
                    }
                } else {
+                   $nodelist_pass = 0;
                    log_fail("$cs_node: unable to resolve $key '$ring' to an IP address according to Corosync's resolve strategy - cluster will potentially fail with Corosync 3.x/kronosnet!");
                }
            }
@@ -328,42 +347,38 @@ sub check_cluster_corosync {
            $verify_ring_ip->("ring${link}_addr");
        }
     }
+    log_pass("nodelist settings OK") if $nodelist_pass;
 
     print "\nChecking totem settings..\n";
     my $totem = $conf->{main}->{totem};
+    my $totem_pass = 1;
+
     my $transport = $totem->{transport};
     if (defined($transport)) {
        if ($transport ne 'knet') {
+           $totem_pass = 0;
            log_fail("Corosync transport explicitly set to '$transport' instead of implicit default!");
-       } else {
-           log_pass("Corosync transport set to '$transport'.");
        }
-    } else {
-       log_pass("Corosync transport set to implicit default.");
     }
 
     # TODO: are those values still up-to-date?
     if ((!defined($totem->{secauth}) || $totem->{secauth} ne 'on') && (!defined($totem->{crypto_cipher}) || $totem->{crypto_cipher} eq 'none')) {
+       $totem_pass = 0;
        log_fail("Corosync authentication/encryption is not explicitly enabled (secauth / crypto_cipher / crypto_hash)!");
-    } else {
-       if (defined($totem->{crypto_cipher}) && $totem->{crypto_cipher} eq '3des') {
-           log_fail("Corosync encryption cipher set to '3des', no longer supported in Corosync 3.x!"); # FIXME: can be removed?
-       } else {
-           log_pass("Corosync encryption and authentication enabled.");
-       }
+    } elsif (defined($totem->{crypto_cipher}) && $totem->{crypto_cipher} eq '3des') {
+       $totem_pass = 0;
+       log_fail("Corosync encryption cipher set to '3des', no longer supported in Corosync 3.x!"); # FIXME: can be removed?
     }
 
+    log_pass("totem settings OK") if $totem_pass;
     print "\n";
     log_info("run 'pvecm status' to get detailed cluster status..");
 
-    print_header("CHECKING INSTALLED COROSYNC VERSION");
     if (defined(my $corosync = $get_pkg->('corosync'))) {
        if ($corosync->{OldVersion} =~ m/^2\./) {
-           log_fail("corosync 2.x installed, cluster-wide upgrade to 3.x needed!");
-       } elsif ($corosync->{OldVersion} =~ m/^3\./) {
-           log_pass("corosync 3.x installed.");
-       } else {
-           log_fail("unexpected corosync version installed: $corosync->{OldVersion}!");
+           log_fail("\ncorosync 2.x installed, cluster-wide upgrade to 3.x needed!");
+       } elsif ($corosync->{OldVersion} !~ m/^3\./) {
+           log_fail("\nunexpected corosync version installed: $corosync->{OldVersion}!");
        }
     }
 }
@@ -380,9 +395,12 @@ sub check_ceph {
 
     log_info("getting Ceph status/health information..");
     my $ceph_status = eval { PVE::API2::Ceph->status({ node => $nodename }); };
-    my $osd_flags = eval { PVE::API2::Ceph->get_flags({ node => $nodename }); };
+    my $noout = eval { PVE::API2::Cluster::Ceph->get_flag({ flag => "noout" }); };
+    if ($@) {
+       log_fail("failed to get 'noout' flag status - $@");
+    }
+
     my $noout_wanted = 1;
-    my $noout = $osd_flags && $osd_flags =~ m/noout/;
 
     if (!$ceph_status || !$ceph_status->{health}) {
        log_fail("unable to determine Ceph status!");
@@ -400,19 +418,6 @@ sub check_ceph {
        }
     }
 
-    log_info("getting Ceph OSD flags..");
-    eval {
-       if (!$osd_flags) {
-           log_fail("unable to get Ceph OSD flags!");
-       } else {
-           if ($osd_flags =~ m/recovery_deletes/ && $osd_flags =~ m/purged_snapdirs/) {
-               log_pass("all PGs have been scrubbed at least once while running Ceph Luminous."); # FIXME: remove?
-           } else {
-               log_fail("missing 'recovery_deletes' and/or 'purged_snapdirs' flag, scrub of all PGs required before upgrading to Nautilus!");
-           }
-       }
-    };
-
     # TODO: check OSD min-required version, if to low it breaks stuff!
 
     log_info("getting Ceph daemon versions..");
@@ -447,9 +452,7 @@ sub check_ceph {
            log_warn("unable to determine overall Ceph daemon versions!");
        } elsif (keys %$overall_versions == 1) {
            log_pass("single running overall version detected for all Ceph daemon types.");
-           if ((keys %$overall_versions)[0] =~ /^ceph version 15\./) {
-               $noout_wanted = 0;
-           }
+           $noout_wanted = 1; # off post-upgrade, on pre-upgrade
        } else {
            log_warn("overall version mismatch detected, check 'ceph versions' output for details!");
        }
@@ -462,7 +465,7 @@ sub check_ceph {
            log_warn("'noout' flag set, Ceph cluster upgrade seems finished.");
        }
     } elsif ($noout_wanted) {
-       log_warn("'noout' flag not set - recommended to prevent rebalancing during upgrades.");
+       log_warn("'noout' flag not set - recommended to prevent rebalancing during cluster-wide upgrades.");
     }
 
     log_info("checking Ceph config..");
@@ -473,8 +476,6 @@ sub check_ceph {
        my $global_monhost = $global->{mon_host} // $global->{"mon host"} // $global->{"mon-host"};
        if (!defined($global_monhost)) {
            log_warn("No 'mon_host' entry found in ceph config.\n  It's recommended to add mon_host with all monitor addresses (without ports) to the global section.");
-       } else {
-           log_pass("Found 'mon_host' entry.");
        }
 
        my $ipv6 = $global->{ms_bind_ipv6} // $global->{"ms bind ipv6"} // $global->{"ms-bind-ipv6"};
@@ -482,17 +483,11 @@ sub check_ceph {
            my $ipv4 = $global->{ms_bind_ipv4} // $global->{"ms bind ipv4"} // $global->{"ms-bind-ipv4"};
            if ($ipv6 eq 'true' && (!defined($ipv4) || $ipv4 ne 'false')) {
                log_warn("'ms_bind_ipv6' is enabled but 'ms_bind_ipv4' is not disabled.\n  Make sure to disable 'ms_bind_ipv4' for ipv6 only clusters, or add an ipv4 network to public/cluster network.");
-           } else {
-               log_pass("'ms_bind_ipv6' is enabled and 'ms_bind_ipv4' disabled");
            }
-       } else {
-           log_pass("'ms_bind_ipv6' not enabled");
        }
 
        if (defined($global->{keyring})) {
            log_warn("[global] config section contains 'keyring' option, which will prevent services from starting with Nautilus.\n Move 'keyring' option to [client] section instead.");
-       } else {
-           log_pass("no 'keyring' option in [global] section found.");
        }
 
     } else {
@@ -501,18 +496,565 @@ sub check_ceph {
 
     my $local_ceph_ver = PVE::Ceph::Tools::get_local_version(1);
     if (defined($local_ceph_ver)) {
-       if ($local_ceph_ver == 14) {
-           my $ceph_volume_osds = PVE::Ceph::Tools::ceph_volume_list();
-           my $scanned_osds = PVE::Tools::dir_glob_regex('/etc/ceph/osd', '^.*\.json$');
-           if (-e '/var/lib/ceph/osd/' && !defined($scanned_osds) && !(keys %$ceph_volume_osds)) {
-               log_warn("local Ceph version is Nautilus, local OSDs detected, but no conversion from ceph-disk to ceph-volume done (yet).");
-           }
+       if ($local_ceph_ver <= 14) {
+           log_fail("local Ceph version too low, at least Octopus required..");
        }
     } else {
        log_fail("unable to determine local Ceph version.");
     }
 }
 
+sub check_backup_retention_settings {
+    log_info("Checking backup retention settings..");
+
+    my $pass = 1;
+
+    my $node_has_retention;
+
+    my $maxfiles_msg = "parameter 'maxfiles' is deprecated with PVE 7.x and will be removed in a " .
+       "future version, use 'prune-backups' instead.";
+
+    eval {
+       my $confdesc = PVE::VZDump::Common::get_confdesc();
+
+       my $fn = "/etc/vzdump.conf";
+       my $raw = PVE::Tools::file_get_contents($fn);
+
+       my $conf_schema = { type => 'object', properties => $confdesc, };
+       my $param = PVE::JSONSchema::parse_config($conf_schema, $fn, $raw);
+
+       if (defined($param->{maxfiles})) {
+           $pass = 0;
+           log_warn("$fn - $maxfiles_msg");
+       }
+
+       $node_has_retention = defined($param->{maxfiles}) || defined($param->{'prune-backups'});
+    };
+    if (my $err = $@) {
+       $pass = 0;
+       log_warn("unable to parse node's VZDump configuration - $err");
+    }
+
+    my $storage_cfg = PVE::Storage::config();
+
+    for my $storeid (keys $storage_cfg->{ids}->%*) {
+       my $scfg = $storage_cfg->{ids}->{$storeid};
+
+       if (defined($scfg->{maxfiles})) {
+           $pass = 0;
+           log_warn("storage '$storeid' - $maxfiles_msg");
+       }
+
+       next if !$scfg->{content}->{backup};
+       next if defined($scfg->{maxfiles}) || defined($scfg->{'prune-backups'});
+       next if $node_has_retention;
+
+       log_info("storage '$storeid' - no backup retention settings defined - by default, PVE " .
+           "7.x will no longer keep only the last backup, but all backups");
+    }
+
+    eval {
+       my $vzdump_cron = PVE::Cluster::cfs_read_file('vzdump.cron');
+
+       # only warn once, there might be many jobs...
+       if (scalar(grep { defined($_->{maxfiles}) } $vzdump_cron->{jobs}->@*)) {
+           $pass = 0;
+           log_warn("/etc/pve/vzdump.cron - $maxfiles_msg");
+       }
+    };
+    if (my $err = $@) {
+       $pass = 0;
+       log_warn("unable to parse node's VZDump configuration - $err");
+    }
+
+    log_pass("no problems found.") if $pass;
+}
+
+sub check_cifs_credential_location {
+    log_info("checking CIFS credential location..");
+
+    my $regex = qr/^(.*)\.cred$/;
+
+    my $found;
+
+    PVE::Tools::dir_glob_foreach('/etc/pve/priv/', $regex, sub {
+       my ($filename) = @_;
+
+       my ($basename) = $filename =~ $regex;
+
+       log_warn("CIFS credentials '/etc/pve/priv/$filename' will be moved to " .
+           "'/etc/pve/priv/storage/$basename.pw' during the update");
+
+       $found = 1;
+    });
+
+    log_pass("no CIFS credentials at outdated location found.") if !$found;
+}
+
+sub check_custom_pool_roles {
+    log_info("Checking custom roles for pool permissions..");
+
+    if (! -f "/etc/pve/user.cfg") {
+       log_skip("user.cfg does not exist");
+       return;
+    }
+
+    my $raw = eval { PVE::Tools::file_get_contents('/etc/pve/user.cfg'); };
+    if ($@) {
+       log_fail("Failed to read '/etc/pve/user.cfg' - $@");
+       return;
+    }
+
+    my $roles = {};
+    while ($raw =~ /^\s*(.+?)\s*$/gm) {
+       my $line = $1;
+       my @data;
+
+       foreach my $d (split (/:/, $line)) {
+           $d =~ s/^\s+//;
+           $d =~ s/\s+$//;
+           push @data, $d
+       }
+
+       my $et = shift @data;
+       next if $et ne 'role';
+
+       my ($role, $privlist) = @data;
+       if (!PVE::AccessControl::verify_rolename($role, 1)) {
+           warn "user config - ignore role '$role' - invalid characters in role name\n";
+           next;
+       }
+
+       $roles->{$role} = {} if !$roles->{$role};
+       foreach my $priv (split_list($privlist)) {
+           $roles->{$role}->{$priv} = 1;
+       }
+    }
+
+    foreach my $role (sort keys %{$roles}) {
+       if (PVE::AccessControl::role_is_special($role)) {
+           next;
+       }
+
+       if ($role eq "PVEPoolUser") {
+           # the user created a custom role named PVEPoolUser
+           log_fail("Custom role '$role' has a restricted name - a built-in role 'PVEPoolUser' will be available with the upgrade");
+       } else {
+           log_pass("Custom role '$role' has no restricted name");
+       }
+
+       my $perms = $roles->{$role};
+       if ($perms->{'Pool.Allocate'} && $perms->{'Pool.Audit'}) {
+           log_pass("Custom role '$role' contains updated pool permissions");
+       } elsif ($perms->{'Pool.Allocate'}) {
+           log_warn("Custom role '$role' contains permission 'Pool.Allocate' - to ensure same behavior add 'Pool.Audit' to this role");
+       } else {
+           log_pass("Custom role '$role' contains no permissions that need to be updated");
+       }
+    }
+}
+
+my sub check_max_length {
+    my ($raw, $max_length, $warning) = @_;
+    log_warn($warning) if defined($raw) && length($raw) > $max_length; 
+}
+
+sub check_node_and_guest_configurations {
+    log_info("Checking node and guest description/note legnth..");
+
+    my @affected_nodes = grep {
+       my $desc = PVE::NodeConfig::load_config($_)->{desc};
+       defined($desc) && length($desc) > 64 * 1024
+    } PVE::Cluster::get_nodelist();
+
+    if (scalar(@affected_nodes) > 0) {
+       log_warn("Node config description of the following nodes too long for new limit of 64 KiB:\n    "
+           . join(', ', @affected_nodes));
+    } else {
+       log_pass("All node config descriptions fit in the new limit of 64 KiB");
+    }
+
+    my $affected_guests_long_desc = [];
+    my $affected_cts_cgroup_keys = [];
+
+    my $cts = PVE::LXC::config_list();
+    for my $vmid (sort { $a <=> $b } keys %$cts) {
+       my $conf = PVE::LXC::Config->load_config($vmid);
+
+       my $desc = $conf->{description};
+       push @$affected_guests_long_desc, "CT $vmid" if defined($desc) && length($desc) > 8 * 1024;
+
+       my $lxc_raw_conf = $conf->{lxc};
+       push @$affected_cts_cgroup_keys, "CT $vmid"  if (grep (@$_[0] =~ /^lxc\.cgroup\./, @$lxc_raw_conf));
+    }
+    my $vms = PVE::QemuServer::config_list();
+    for my $vmid (sort { $a <=> $b } keys %$vms) {
+       my $desc = PVE::QemuConfig->load_config($vmid)->{description};
+       push @$affected_guests_long_desc, "VM $vmid" if defined($desc) && length($desc) > 8 * 1024;
+    }
+    if (scalar($affected_guests_long_desc->@*) > 0) {
+       log_warn("Guest config description of the following virtual-guests too long for new limit of 64 KiB:\n"
+           ."    " . join(", ", $affected_guests_long_desc->@*));
+    } else {
+       log_pass("All guest config descriptions fit in the new limit of 8 KiB");
+    }
+
+    log_info("Checking container configs for deprecated lxc.cgroup entries");
+
+    if (scalar($affected_cts_cgroup_keys->@*) > 0) {
+       if ($forced_legacy_cgroup) {
+           log_pass("Found legacy 'lxc.cgroup' keys, but system explicitly configured for legacy hybrid cgroup hierarchy.");
+       }  else {
+           log_warn("The following CTs have 'lxc.cgroup' keys configured, which will be ignored in the new default unified cgroupv2:\n"
+               ."    " . join(", ", $affected_cts_cgroup_keys->@*) ."\n"
+               ."    Often it can be enough to change to the new 'lxc.cgroup2' prefix after the upgrade to Proxmox VE 7.x");
+       }
+    } else {
+       log_pass("No legacy 'lxc.cgroup' keys found.");
+    }
+}
+
+sub check_storage_content {
+    log_info("Checking storage content type configuration..");
+
+    my $found;
+    my $pass = 1;
+
+    my $storage_cfg = PVE::Storage::config();
+
+    for my $storeid (sort keys $storage_cfg->{ids}->%*) {
+       my $scfg = $storage_cfg->{ids}->{$storeid};
+
+       next if $scfg->{shared};
+       next if !PVE::Storage::storage_check_enabled($storage_cfg, $storeid, undef, 1);
+
+       my $valid_content = PVE::Storage::Plugin::valid_content_types($scfg->{type});
+
+       if (scalar(keys $scfg->{content}->%*) == 0 && !$valid_content->{none}) {
+           $pass = 0;
+           log_fail("storage '$storeid' does not support configured content type 'none'");
+           delete $scfg->{content}->{none}; # scan for guest images below
+       }
+
+       next if $scfg->{content}->{images};
+       next if $scfg->{content}->{rootdir};
+
+       # Skip 'iscsi(direct)' (and foreign plugins with potentially similiar behavior) with 'none',
+       # because that means "use LUNs directly" and vdisk_list() in PVE 6.x still lists those.
+       # It's enough to *not* skip 'dir', because it is the only other storage that supports 'none'
+       # and 'images' or 'rootdir', hence being potentially misconfigured.
+       next if $scfg->{type} ne 'dir' && $scfg->{content}->{none};
+
+       eval { PVE::Storage::activate_storage($storage_cfg, $storeid) };
+       if (my $err = $@) {
+           log_warn("activating '$storeid' failed - $err");
+           next;
+       }
+
+       my $res = eval { PVE::Storage::vdisk_list($storage_cfg, $storeid); };
+       if (my $err = $@) {
+           log_warn("listing images on '$storeid' failed - $err");
+           next;
+       }
+       my @volids = map { $_->{volid} } $res->{$storeid}->@*;
+
+       my $number = scalar(@volids);
+       if ($number > 0) {
+           log_info("storage '$storeid' - neither content type 'images' nor 'rootdir' configured"
+               .", but found $number guest volume(s)");
+       }
+    }
+
+    my $check_volid = sub {
+       my ($volid, $vmid, $vmtype, $reference) = @_;
+
+       my $guesttext = $vmtype eq 'qemu' ? 'VM' : 'CT';
+       my $prefix = "$guesttext $vmid - volume '$volid' ($reference)";
+
+       my ($storeid) = PVE::Storage::parse_volume_id($volid, 1);
+       return if !defined($storeid);
+
+       my $scfg = $storage_cfg->{ids}->{$storeid};
+       if (!$scfg) {
+           $pass = 0;
+           log_warn("$prefix - storage does not exist!");
+           return;
+       }
+
+       # cannot use parse_volname for containers, as it can return 'images'
+       # but containers cannot have ISO images attached, so assume 'rootdir'
+       my $vtype = 'rootdir';
+       if ($vmtype eq 'qemu') {
+           ($vtype) = eval { PVE::Storage::parse_volname($storage_cfg, $volid); };
+           return if $@;
+       }
+
+       if (!$scfg->{content}->{$vtype}) {
+           $found = 1;
+           $pass = 0;
+           log_warn("$prefix - storage does not have content type '$vtype' configured.");
+       }
+    };
+
+    my $cts = PVE::LXC::config_list();
+    for my $vmid (sort { $a <=> $b } keys %$cts) {
+       my $conf = PVE::LXC::Config->load_config($vmid);
+
+       my $volhash = {};
+
+       my $check = sub {
+           my ($ms, $mountpoint, $reference) = @_;
+
+           my $volid = $mountpoint->{volume};
+           return if !$volid || $mountpoint->{type} ne 'volume';
+
+           return if $volhash->{$volid}; # volume might be referenced multiple times
+
+           $volhash->{$volid} = 1;
+
+           $check_volid->($volid, $vmid, 'lxc', $reference);
+       };
+
+       my $opts = { include_unused => 1 };
+       PVE::LXC::Config->foreach_volume_full($conf, $opts, $check, 'in config');
+       for my $snapname (keys $conf->{snapshots}->%*) {
+           my $snap = $conf->{snapshots}->{$snapname};
+           PVE::LXC::Config->foreach_volume_full($snap, $opts, $check, "in snapshot '$snapname'");
+       }
+    }
+
+    my $vms = PVE::QemuServer::config_list();
+    for my $vmid (sort { $a <=> $b } keys %$vms) {
+       my $conf = PVE::QemuConfig->load_config($vmid);
+
+       my $volhash = {};
+
+       my $check = sub {
+           my ($key, $drive, $reference) = @_;
+
+           my $volid = $drive->{file};
+           return if $volid =~ m|^/|;
+
+           return if $volhash->{$volid}; # volume might be referenced multiple times
+
+           $volhash->{$volid} = 1;
+
+           $check_volid->($volid, $vmid, 'qemu', $reference);
+       };
+
+       my $opts = {
+           extra_keys => ['vmstate'],
+           include_unused => 1,
+       };
+       # startup from a suspended state works even without 'images' content type on the
+       # state storage, so do not check 'vmstate' for $conf
+       PVE::QemuConfig->foreach_volume_full($conf, { include_unused => 1 }, $check, 'in config');
+       for my $snapname (keys $conf->{snapshots}->%*) {
+           my $snap = $conf->{snapshots}->{$snapname};
+           PVE::QemuConfig->foreach_volume_full($snap, $opts, $check, "in snapshot '$snapname'");
+       }
+    }
+
+    if ($found) {
+       log_warn("Proxmox VE 7.0 enforces stricter content type checks. The guests above " .
+           "might not work until the storage configuration is fixed.");
+    }
+
+    if ($pass) {
+       log_pass("no problems found");
+    }
+}
+
+sub check_containers_cgroup_compat {
+    if ($forced_legacy_cgroup) {
+       log_skip("System explicitly configured for legacy hybrid cgroup hierarchy.");
+       return;
+    }
+
+    my $supports_cgroupv2 = sub {
+       my ($conf, $rootdir, $ctid) = @_;
+
+       my $get_systemd_version = sub {
+           my ($self) = @_;
+
+           my $sd_lib_dir = -d "/lib/systemd" ? "/lib/systemd" : "/usr/lib/systemd";
+           my $libsd = PVE::Tools::dir_glob_regex($sd_lib_dir, "libsystemd-shared-.+\.so");
+           if (defined($libsd) && $libsd =~ /libsystemd-shared-(\d+)\.so/) {
+               return $1;
+           }
+
+           return undef;
+       };
+
+       my  $unified_cgroupv2_support = sub {
+           my ($self) = @_;
+
+           # https://www.freedesktop.org/software/systemd/man/systemd.html
+           # systemd is installed as symlink to /sbin/init
+           my $systemd = CORE::readlink('/sbin/init');
+
+           # assume non-systemd init will run with unified cgroupv2
+           if (!defined($systemd) || $systemd !~ m@/systemd$@) {
+               return 1;
+           }
+
+           # systemd version 232 (e.g. debian stretch) supports the unified hierarchy
+           my $sdver = $get_systemd_version->();
+           if (!defined($sdver) || $sdver < 232) {
+               return 0;
+           }
+
+           return 1;
+       };
+
+       my $ostype = $conf->{ostype};
+       if (!defined($ostype)) {
+           log_warn("Found CT ($ctid) without 'ostype' set!");
+       } elsif ($ostype eq 'devuan' || $ostype eq 'alpine') {
+           return 1; # no systemd, no cgroup problems
+       }
+
+       my $lxc_setup = PVE::LXC::Setup->new($conf, $rootdir);
+       return $lxc_setup->protected_call($unified_cgroupv2_support);
+    };
+
+    my $log_problem = sub {
+       my ($ctid) = @_;
+       log_warn("Found at least one CT ($ctid) which does not support running in a unified cgroup v2" .
+           " layout.\n    Either upgrade the Container distro or set systemd.unified_cgroup_hierarchy=0 " .
+           "in the Proxmox VE hosts' kernel cmdline! Skipping further CT compat checks."
+       );
+    };
+
+    my $cts = eval { PVE::API2::LXC->vmlist({ node => $nodename }) };
+    if ($@) {
+       log_warn("Failed to retrieve information about this node's CTs - $@");
+       return;
+    }
+
+    if (!defined($cts) || !scalar(@$cts)) {
+       log_skip("No containers on node detected.");
+       return;
+    }
+
+    my @running_cts = sort { $a <=> $b } grep { $_->{status} eq 'running' } @$cts;
+    my @offline_cts = sort { $a <=> $b } grep { $_->{status} ne 'running' } @$cts;
+
+    for my $ct (@running_cts) {
+       my $ctid = $ct->{vmid};
+       my $pid = eval { PVE::LXC::find_lxc_pid($ctid) };
+       if (my $err = $@) {
+           log_warn("Failed to get PID for running CT $ctid - $err");
+           next;
+       }
+       my $rootdir = "/proc/$pid/root";
+       my $conf = PVE::LXC::Config->load_config($ctid);
+
+       my $ret = eval { $supports_cgroupv2->($conf, $rootdir, $ctid) };
+       if (my $err = $@) {
+           log_warn("Failed to get cgroup support status for CT $ctid - $err");
+           next;
+       }
+       if (!$ret) {
+           $log_problem->($ctid);
+           return;
+       }
+    }
+
+    my $storage_cfg = PVE::Storage::config();
+    for my $ct (@offline_cts) {
+       my $ctid = $ct->{vmid};
+       my ($conf, $rootdir, $ret);
+       eval {
+           $conf = PVE::LXC::Config->load_config($ctid);
+           $rootdir = PVE::LXC::mount_all($ctid, $storage_cfg, $conf);
+           $ret = $supports_cgroupv2->($conf, $rootdir, $ctid);
+       };
+       if (my $err = $@) {
+           log_warn("Failed to load config and mount CT $ctid - $err");
+           eval { PVE::LXC::umount_all($ctid, $storage_cfg, $conf) };
+           next;
+       }
+       if (!$ret) {
+           $log_problem->($ctid);
+           eval { PVE::LXC::umount_all($ctid, $storage_cfg, $conf) };
+           last;
+       }
+
+       eval { PVE::LXC::umount_all($ctid, $storage_cfg, $conf) };
+    }
+};
+
+sub check_security_repo {
+    log_info("Checking if the suite for the Debian security repository is correct..");
+
+    my $found = 0;
+
+    my $dir = '/etc/apt/sources.list.d';
+    my $in_dir = 0;
+
+    my $check_file = sub {
+       my ($file) = @_;
+
+       $file = "${dir}/${file}" if $in_dir;
+
+       my $raw = eval { PVE::Tools::file_get_contents($file) };
+       return if !defined($raw);
+       my @lines = split(/\n/, $raw);
+
+       my $number = 0;
+       for my $line (@lines) {
+           $number++;
+
+           next if length($line) == 0; # split would result in undef then...
+
+           ($line) = split(/#/, $line);
+
+           next if $line !~ m/^deb[[:space:]]/; # is case sensitive
+
+           my $suite;
+
+           # catch any of
+           # https://deb.debian.org/debian-security
+           # http://security.debian.org/debian-security
+           # http://security.debian.org/
+           if ($line =~ m|https?://deb\.debian\.org/debian-security/?\s+(\S*)|i) {
+               $suite = $1;
+           } elsif ($line =~ m|https?://security\.debian\.org(?:.*?)\s+(\S*)|i) {
+               $suite = $1;
+           } else {
+               next;
+           }
+
+           $found = 1;
+
+           my $where = "in ${file}:${number}";
+
+           if ($suite eq 'buster/updates') {
+               log_info("Make sure to change the suite of the Debian security repository " .
+                   "from 'buster/updates' to 'bullseye-security' - $where");
+           } elsif ($suite eq 'bullseye-security') {
+               log_pass("already using 'bullseye-security'");
+           } else {
+               log_fail("The new suite of the Debian security repository should be " .
+                   "'bullseye-security' - $where");
+           }
+       }
+    };
+
+    $check_file->("/etc/apt/sources.list");
+
+    $in_dir = 1;
+
+    PVE::Tools::dir_glob_foreach($dir, '^.*\.list$', $check_file);
+
+    if (!$found) {
+       # only warn, it might be defined in a .sources file or in a way not catched above
+       log_warn("No Debian security repository detected in /etc/apt/sources.list and " .
+           "/etc/apt/sources.list.d/*.list");
+    }
+}
+
 sub check_misc {
     print_header("MISCELLANEOUS CHECKS");
     my $ssh_config = eval { PVE::Tools::file_get_contents('/root/.ssh/config') };
@@ -529,8 +1071,8 @@ sub check_misc {
     $log_systemd_unit_state->('pvestatd.service');
 
     my $root_free = PVE::Tools::df('/', 10);
-    log_warn("Less than 2G free space on root file system.")
-       if defined($root_free) && $root_free->{avail} < 2*1024*1024*1024;
+    log_warn("Less than 4 GiB free space on root file system.")
+       if defined($root_free) && $root_free->{avail} < 4*1024*1024*1024;
 
     log_info("Checking for running guests..");
     my $running_guests = 0;
@@ -568,7 +1110,6 @@ sub check_misc {
        }
     }
 
-    log_info("Check node certificate's RSA key size");
     my $certs = PVE::API2::Certificates->info({ node => $nodename });
     my $certs_check = {
        'rsaEncryption' => {
@@ -581,27 +1122,42 @@ sub check_misc {
        },
     };
 
+    my $log_cert_heading_called;
+    my $log_cert_heading_once = sub {
+       return if $log_cert_heading_called;
+       log_info("Check node certificate's RSA key size");
+       $log_cert_heading_called = 1;
+    };
+
     my $certs_check_failed = 0;
     foreach my $cert (@$certs) {
        my ($type, $size, $fn) = $cert->@{qw(public-key-type public-key-bits filename)};
 
        if (!defined($type) || !defined($size)) {
+           $log_cert_heading_once->();
            log_warn("'$fn': cannot check certificate, failed to get it's type or size!");
        }
 
        my $check = $certs_check->{$type};
        if (!defined($check)) {
+           $log_cert_heading_once->();
            log_warn("'$fn': certificate's public key type '$type' unknown, check Debian Busters release notes");
            next;
        }
 
        if ($size < $check->{minsize}) {
+           $log_cert_heading_once->();
            log_fail("'$fn', certificate's $check->{name} public key size is less than 2048 bit");
            $certs_check_failed = 1;
-       } else {
-           log_pass("Certificate '$fn' passed Debian Busters security level for TLS connections ($size >= 2048)");
        }
     }
+
+    check_backup_retention_settings();
+    check_cifs_credential_location();
+    check_custom_pool_roles();
+    check_node_and_guest_configurations();
+    check_storage_content();
+    check_security_repo();
 }
 
 __PACKAGE__->register_method ({
@@ -612,18 +1168,35 @@ __PACKAGE__->register_method ({
     parameters => {
        additionalProperties => 0,
        properties => {
+           full => {
+               description => 'perform additional, expensive checks.',
+               type => 'boolean',
+               optional => 1,
+               default => 0,
+           },
        },
     },
     returns => { type => 'null' },
     code => sub {
        my ($param) = @_;
 
+       my $kernel_cli = PVE::Tools::file_get_contents('/proc/cmdline');
+       if ($kernel_cli =~ /systemd.unified_cgroup_hierarchy=0/){
+           $forced_legacy_cgroup = 1;
+       }
+
        check_pve_packages();
        check_cluster_corosync();
        check_ceph();
        check_storage_health();
        check_misc();
 
+       if ($param->{full}) {
+           check_containers_cgroup_compat();
+       } else {
+           log_skip("NOTE: Expensive checks, like CT cgroupv2 compat, not performed without '--full' parameter");
+       }
+
        print_header("SUMMARY");
 
        my $total = 0;
@@ -646,7 +1219,4 @@ __PACKAGE__->register_method ({
 
 our $cmddef = [ __PACKAGE__, 'checklist', [], {}];
 
-# for now drop all unknown params and just check
-@ARGV = ();
-
 1;