]> git.proxmox.com Git - pve-container.git/blobdiff - src/PVE/LXC/Config.pm
migrate: also set targetsid for unreferenced disks
[pve-container.git] / src / PVE / LXC / Config.pm
index c34a8922b9725e724522425b7573fdbc2c70fb8a..0ed7bd245dd9db32745eb2d6460ffba030807ab9 100644 (file)
@@ -2,6 +2,7 @@ package PVE::LXC::Config;
 
 use strict;
 use warnings;
+use Fcntl qw(O_RDONLY);
 
 use PVE::AbstractConfig;
 use PVE::Cluster qw(cfs_register_file);
@@ -13,6 +14,9 @@ use PVE::Tools;
 
 use base qw(PVE::AbstractConfig);
 
+use constant {FIFREEZE => 0xc0045877,
+              FITHAW   => 0xc0045878};
+
 my $nodename = PVE::INotify::nodename();
 my $lock_handles =  {};
 my $lockdir = "/run/lock/lxc";
@@ -49,20 +53,35 @@ sub cfs_config_path {
 sub mountpoint_backup_enabled {
     my ($class, $mp_key, $mountpoint) = @_;
 
-    return 1 if $mp_key eq 'rootfs';
-
-    return 0 if $mountpoint->{type} ne 'volume';
-
-    return 1 if $mountpoint->{backup};
-
-    return 0;
+    my $enabled;
+    my $reason;
+
+    if ($mp_key eq 'rootfs') {
+       $enabled = 1;
+       $reason = 'rootfs';
+    } elsif ($mountpoint->{type} ne 'volume') {
+       $enabled = 0;
+       $reason = 'not a volume';
+    } elsif ($mountpoint->{backup}) {
+       $enabled = 1;
+       $reason = 'enabled';
+    } else {
+       $enabled = 0;
+       $reason = 'disabled';
+    }
+    return wantarray ? ($enabled, $reason) : $enabled;
 }
 
 sub has_feature {
     my ($class, $feature, $conf, $storecfg, $snapname, $running, $backup_only) = @_;
     my $err;
 
-    $class->foreach_mountpoint($conf, sub {
+    my $opts;
+    if ($feature eq 'copy' || $feature eq 'clone') {
+       $opts = {'valid_target_formats' => ['raw', 'subvol']};
+    }
+
+    $class->foreach_volume($conf, sub {
        my ($ms, $mountpoint) = @_;
 
        return if $err; # skip further test
@@ -71,7 +90,7 @@ sub has_feature {
        $err = 1
            if !PVE::Storage::volume_has_feature($storecfg, $feature,
                                                 $mountpoint->{volume},
-                                                $snapname, $running);
+                                                $snapname, $running, $opts);
     });
 
     return $err ? 0 : 1;
@@ -82,6 +101,25 @@ sub __snapshot_save_vmstate {
     die "implement me - snapshot_save_vmstate\n";
 }
 
+sub __snapshot_activate_storages {
+    my ($class, $conf, $include_vmstate) = @_;
+
+    my $storecfg = PVE::Storage::config();
+    my $opts = $include_vmstate ? { 'extra_keys' => ['vmstate'] } : {};
+    my $storage_hash = {};
+
+    $class->foreach_volume_full($conf, $opts, sub {
+       my ($vs, $mountpoint) = @_;
+
+       return if $mountpoint->{type} ne 'volume';
+
+       my ($storeid) = PVE::Storage::parse_volume_id($mountpoint->{volume});
+       $storage_hash->{$storeid} = 1;
+    });
+
+    PVE::Storage::activate_storage_list($storecfg, [ sort keys $storage_hash->%* ]);
+}
+
 sub __snapshot_check_running {
     my ($class, $vmid) = @_;
     return PVE::LXC::check_running($vmid);
@@ -94,15 +132,60 @@ sub __snapshot_check_freeze_needed {
     return ($ret, $ret);
 }
 
+# implements similar functionality to fsfreeze(8)
+sub fsfreeze_mountpoint {
+    my ($path, $thaw) = @_;
+
+    my $op = $thaw ? 'thaw' : 'freeze';
+    my $ioctl = $thaw ? FITHAW : FIFREEZE;
+
+    sysopen my $fd, $path, O_RDONLY or die "failed to open $path: $!\n";
+    my $ioctl_err;
+    if (!ioctl($fd, $ioctl, 0)) {
+       $ioctl_err = "$!";
+    }
+    close($fd);
+    die "fs$op '$path' failed - $ioctl_err\n" if defined $ioctl_err;
+}
+
 sub __snapshot_freeze {
     my ($class, $vmid, $unfreeze) = @_;
 
+    my $conf = $class->load_config($vmid);
+    my $storagecfg = PVE::Storage::config();
+
+    my $freeze_mps = [];
+    $class->foreach_volume($conf, sub {
+       my ($ms, $mountpoint) = @_;
+
+       return if $mountpoint->{type} ne 'volume';
+
+       if (PVE::Storage::volume_snapshot_needs_fsfreeze($storagecfg, $mountpoint->{volume})) {
+           push @$freeze_mps, $mountpoint->{mp};
+       }
+    });
+
+    my $freeze_mountpoints = sub {
+       my ($thaw) = @_;
+
+       return if scalar(@$freeze_mps) == 0;
+
+       my $pid = PVE::LXC::find_lxc_pid($vmid);
+
+       for my $mp (@$freeze_mps) {
+           eval{ fsfreeze_mountpoint("/proc/${pid}/root/${mp}", $thaw); };
+           warn $@ if $@;
+       }
+    };
+
     if ($unfreeze) {
-       eval { PVE::Tools::run_command(['/usr/bin/lxc-unfreeze', '-n', $vmid]); };
+       eval { PVE::LXC::thaw($vmid); };
        warn $@ if $@;
+       $freeze_mountpoints->(1);
     } else {
-       PVE::Tools::run_command(['/usr/bin/lxc-freeze', '-n', $vmid]);
+       PVE::LXC::freeze($vmid);
        PVE::LXC::sync_container_namespace($vmid);
+       $freeze_mountpoints->(0);
     }
 }
 
@@ -124,11 +207,11 @@ sub __snapshot_delete_remove_drive {
        die "implement me - saving vmstate\n";
     } else {
        my $value = $snap->{$remove_drive};
-       my $mountpoint = $remove_drive eq 'rootfs' ? $class->parse_ct_rootfs($value, 1) : $class->parse_ct_mountpoint($value, 1);
+       my $mountpoint = $class->parse_volume($remove_drive, $value, 1);
        delete $snap->{$remove_drive};
 
        $class->add_unused_volume($snap, $mountpoint->{volume})
-           if ($mountpoint->{type} eq 'volume');
+           if $mountpoint && ($mountpoint->{type} eq 'volume');
     }
 }
 
@@ -150,10 +233,15 @@ sub __snapshot_delete_vol_snapshot {
 }
 
 sub __snapshot_rollback_vol_possible {
-    my ($class, $mountpoint, $snapname) = @_;
+    my ($class, $mountpoint, $snapname, $blockers) = @_;
 
     my $storecfg = PVE::Storage::config();
-    PVE::Storage::volume_rollback_is_possible($storecfg, $mountpoint->{volume}, $snapname);
+    PVE::Storage::volume_rollback_is_possible(
+       $storecfg,
+       $mountpoint->{volume},
+       $snapname,
+       $blockers,
+    );
 }
 
 sub __snapshot_rollback_vol_rollback {
@@ -181,7 +269,7 @@ sub __snapshot_rollback_get_unused {
 
     my $unused = [];
 
-    $class->__snapshot_foreach_volume($conf, sub {
+    $class->foreach_volume($conf, sub {
        my ($vs, $volume) = @_;
 
        return if $volume->{type} ne 'volume';
@@ -189,7 +277,7 @@ sub __snapshot_rollback_get_unused {
        my $found = 0;
        my $volid = $volume->{volume};
 
-       $class->__snapshot_foreach_volume($snap, sub {
+       $class->foreach_volume($snap, sub {
            my ($ms, $mountpoint) = @_;
 
            return if $found;
@@ -205,12 +293,6 @@ sub __snapshot_rollback_get_unused {
     return $unused;
 }
 
-sub __snapshot_foreach_volume {
-    my ($class, $conf, $func) = @_;
-
-    $class->foreach_mountpoint($conf, $func);
-}
-
 # END implemented abstract methods from PVE::AbstractConfig
 
 # BEGIN JSON config code
@@ -218,7 +300,7 @@ sub __snapshot_foreach_volume {
 cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config);
 
 
-my $valid_mount_option_re = qr/(noatime|nodev|nosuid|noexec)/;
+my $valid_mount_option_re = qr/(noatime|lazytime|nodev|nosuid|noexec)/;
 
 sub is_valid_mount_option {
     my ($option) = @_;
@@ -283,6 +365,23 @@ PVE::JSONSchema::register_standard_option('pve-ct-rootfs', {
     optional => 1,
 });
 
+# IP address with optional interface suffix for link local ipv6 addresses
+PVE::JSONSchema::register_format('lxc-ip-with-ll-iface', \&verify_ip_with_ll_iface);
+sub verify_ip_with_ll_iface {
+    my ($addr, $noerr) = @_;
+
+    if (my ($addr, $iface) = ($addr =~ /^(fe80:[^%]+)%(.*)$/)) {
+       if (PVE::JSONSchema::pve_verify_ip($addr, 1)
+           && PVE::JSONSchema::pve_verify_iface($iface, 1))
+       {
+           return $addr;
+       }
+    }
+
+    return PVE::JSONSchema::pve_verify_ip($addr, $noerr);
+}
+
+
 my $features_desc = {
     mount => {
        optional => 1,
@@ -323,6 +422,21 @@ my $features_desc = {
        description => "Allow using 'fuse' file systems in a container."
            ." Note that interactions between fuse and the freezer cgroup can potentially cause I/O deadlocks.",
     },
+    mknod => {
+       optional => 1,
+       type => 'boolean',
+       default => 0,
+       description => "Allow unprivileged containers to use mknod() to add certain device nodes."
+           ." This requires a kernel with seccomp trap to user space support (5.3 or newer)."
+           ." This is experimental.",
+    },
+    force_rw_sys => {
+       optional => 1,
+       type => 'boolean',
+       default => 0,
+       description => "Mount /sys in unprivileged containers as `rw` instead of `mixed`."
+           ." This can break networking under newer (>= v245) systemd-network use."
+    },
 };
 
 my $confdesc = {
@@ -355,7 +469,7 @@ my $confdesc = {
     ostype => {
        optional => 1,
        type => 'string',
-       enum => [qw(debian ubuntu centos fedora opensuse archlinux alpine gentoo unmanaged)],
+       enum => [qw(debian devuan ubuntu centos fedora opensuse archlinux alpine gentoo nixos unmanaged)],
        description => "OS type. This is used to setup configuration inside the container, and corresponds to lxc setup scripts in /usr/share/lxc/config/<ostype>.common.conf. Value 'unmanaged' can be used to skip and OS specific setup.",
     },
     console => {
@@ -377,14 +491,14 @@ my $confdesc = {
        type => 'integer',
        description => "The number of cores assigned to the container. A container can use all available cores by default.",
        minimum => 1,
-       maximum => 128,
+       maximum => 8192,
     },
     cpulimit => {
        optional => 1,
        type => 'number',
        description => "Limit of CPU usage.\n\nNOTE: If the computer has 2 CPUs, it has a total of '2' CPU time. Value '0' indicates no CPU limit.",
        minimum => 0,
-       maximum => 128,
+       maximum => 8192,
        default => 0,
     },
     cpuunits => {
@@ -418,7 +532,9 @@ my $confdesc = {
     description => {
        optional => 1,
        type => 'string',
-        description => "Container description. Only used on the configuration web interface.",
+       description => "Description for the Container. Shown in the web-interface CT's summary."
+           ." This is saved as comment inside the configuration file.",
+       maxLength => 1024 * 8,
     },
     searchdomain => {
        optional => 1,
@@ -427,9 +543,14 @@ my $confdesc = {
     },
     nameserver => {
        optional => 1,
-       type => 'string', format => 'address-list',
+       type => 'string', format => 'lxc-ip-with-ll-iface-list',
        description => "Sets DNS server IP address for a container. Create will automatically use the setting from the host if you neither set searchdomain nor nameserver.",
     },
+    timezone => {
+       optional => 1,
+       type => 'string', format => 'pve-ct-timezone',
+       description => "Time zone to use in the container. If option isn't set, then nothing will be done. Can be set to 'host' to match the host time zone, or an arbitrary time zone option from /usr/share/zoneinfo/zone.tab",
+    },
     rootfs => get_standard_option('pve-ct-rootfs'),
     parent => {
        optional => 1,
@@ -474,6 +595,17 @@ my $confdesc = {
        format => 'pve-volume-id',
        description => 'Script that will be exectued during various steps in the containers lifetime.',
     },
+    tags => {
+       type => 'string', format => 'pve-tag-list',
+       description => 'Tags of the Container. This is only meta information.',
+       optional => 1,
+    },
+    debug => {
+       optional => 1,
+       type => 'boolean',
+       description => "Try to be more verbose. For now this only enables debug log-level on start.",
+       default => 0,
+    },
 };
 
 my $valid_lxc_conf_keys = {
@@ -587,7 +719,7 @@ sub is_valid_lxc_conf_key {
     }
     my $validity = $valid_lxc_conf_keys->{$key};
     return $validity if defined($validity);
-    return 1 if $key =~ /^lxc\.cgroup\./  # allow all cgroup values
+    return 1 if $key =~ /^lxc\.cgroup2?\./  # allow all cgroup values
              || $key =~ /^lxc\.prlimit\./ # allow all prlimits
              || $key =~ /^lxc\.net\./;    # allow custom network definitions
     return 0;
@@ -678,7 +810,7 @@ our $netconf_desc = {
 };
 PVE::JSONSchema::register_format('pve-lxc-network', $netconf_desc);
 
-my $MAX_LXC_NETWORKS = 10;
+my $MAX_LXC_NETWORKS = 32;
 for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
     $confdesc->{"net$i"} = {
        optional => 1,
@@ -687,6 +819,15 @@ for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) {
     };
 }
 
+PVE::JSONSchema::register_format('pve-ct-timezone', \&verify_ct_timezone);
+sub verify_ct_timezone {
+    my ($timezone, $noerr) = @_;
+
+    return if $timezone eq 'host'; # using host settings
+
+    PVE::JSONSchema::pve_verify_timezone($timezone);
+}
+
 PVE::JSONSchema::register_format('pve-lxc-mp-string', \&verify_lxc_mp_string);
 sub verify_lxc_mp_string {
     my ($mp, $noerr) = @_;
@@ -726,27 +867,36 @@ my $mp_desc = {
 };
 PVE::JSONSchema::register_format('pve-ct-mountpoint', $mp_desc);
 
-my $unuseddesc = {
-    optional => 1,
-    type => 'string', format => 'pve-volume-id',
-    description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
+my $unused_desc = {
+    volume => {
+       type => 'string',
+       default_key => 1,
+       format => 'pve-volume-id',
+       format_description => 'volume',
+       description => 'The volume that is not used currently.',
+    }
 };
 
 for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
     $confdesc->{"mp$i"} = {
        optional => 1,
        type => 'string', format => $mp_desc,
-       description => "Use volume as container mount point.",
+       description => "Use volume as container mount point. Use the special " .
+           "syntax STORAGE_ID:SIZE_IN_GiB to allocate a new volume.",
        optional => 1,
     };
 }
 
-for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) {
-    $confdesc->{"unused$i"} = $unuseddesc;
+for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) {
+    $confdesc->{"unused$i"} = {
+       optional => 1,
+       type => 'string', format => $unused_desc,
+       description => "Reference to unused volumes. This is used internally, and should not be modified manually.",
+    }
 }
 
 sub parse_pct_config {
-    my ($filename, $raw) = @_;
+    my ($filename, $raw, $strict) = @_;
 
     return undef if !defined($raw);
 
@@ -756,6 +906,16 @@ sub parse_pct_config {
        pending => {},
     };
 
+    my $handle_error = sub {
+       my ($msg) = @_;
+
+       if ($strict) {
+           die $msg;
+       } else {
+           warn $msg;
+       }
+    };
+
     $filename =~ m|/lxc/(\d+).conf$|
        || die "got strange filename '$filename'";
 
@@ -795,9 +955,9 @@ sub parse_pct_config {
            if ($validity eq 1) {
                push @{$conf->{lxc}}, [$key, $value];
            } elsif (my $errmsg = $validity) {
-               warn "vm $vmid - $key: $errmsg\n";
+               $handle_error->("vm $vmid - $key: $errmsg\n");
            } else {
-               warn "vm $vmid - unable to parse config: $line\n";
+               $handle_error->("vm $vmid - unable to parse config: $line\n");
            }
        } elsif ($line =~ m/^(description):\s*(.*\S)\s*$/) {
            $descr .= PVE::Tools::decode_text($2);
@@ -808,16 +968,16 @@ sub parse_pct_config {
            if ($section eq 'pending') {
                $conf->{delete} = $value;
            } else {
-               warn "vm $vmid - property 'delete' is only allowed in [pve:pending]\n";
+               $handle_error->("vm $vmid - property 'delete' is only allowed in [pve:pending]\n");
            }
-       } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
+       } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) {
            my $key = $1;
            my $value = $2;
            eval { $value = PVE::LXC::Config->check_type($key, $value); };
-           warn "vm $vmid - unable to parse value of '$key' - $@" if $@;
+           $handle_error->("vm $vmid - unable to parse value of '$key' - $@") if $@;
            $conf->{$key} = $value;
        } else {
-           warn "vm $vmid - unable to parse config: $line\n";
+           $handle_error->("vm $vmid - unable to parse config: $line\n");
        }
     }
 
@@ -937,7 +1097,7 @@ sub update_pct_config {
        my $value = $param->{$opt};
        if ($opt =~ m/^mp(\d+)$/ || $opt eq 'rootfs') {
            $class->check_protection($conf, "can't update CT $vmid drive '$opt'");
-           my $mp = $opt eq 'rootfs' ? $class->parse_ct_rootfs($value) : $class->parse_ct_mountpoint($value);
+           my $mp = $class->parse_volume($opt, $value);
            $check_content_type->($mp) if ($mp->{type} eq 'volume');
        } elsif ($opt eq 'hookscript') {
            PVE::GuestHelpers::check_hookscript($value);
@@ -1014,7 +1174,7 @@ sub json_config_properties {
     return $prop;
 }
 
-sub __parse_ct_mountpoint_full {
+my $parse_ct_mountpoint_full = sub {
     my ($class, $desc, $data, $noerr) = @_;
 
     $data //= '';
@@ -1040,27 +1200,41 @@ sub __parse_ct_mountpoint_full {
     return $res;
 };
 
-sub parse_ct_rootfs {
-    my ($class, $data, $noerr) = @_;
+sub print_ct_mountpoint {
+    my ($class, $info, $nomp) = @_;
+    my $skip = [ 'type' ];
+    push @$skip, 'mp' if $nomp;
+    return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
+}
 
-    my $res =  $class->__parse_ct_mountpoint_full($rootfs_desc, $data, $noerr);
+sub parse_volume {
+    my ($class, $key, $volume_string, $noerr) = @_;
+
+    if ($key eq 'rootfs') {
+       my $res =  $parse_ct_mountpoint_full->($class, $rootfs_desc, $volume_string, $noerr);
+       $res->{mp} = '/' if defined($res);
+       return $res;
+    } elsif ($key =~ m/^mp\d+$/) {
+       return $parse_ct_mountpoint_full->($class, $mp_desc, $volume_string, $noerr);
+    } elsif ($key =~ m/^unused\d+$/) {
+       return $parse_ct_mountpoint_full->($class, $unused_desc, $volume_string, $noerr);
+    }
 
-    $res->{mp} = '/' if defined($res);
+    die "parse_volume - unknown type: $key\n" if !$noerr;
 
-    return $res;
+    return;
 }
 
-sub parse_ct_mountpoint {
-    my ($class, $data, $noerr) = @_;
+sub print_volume {
+    my ($class, $key, $volume) = @_;
 
-    return $class->__parse_ct_mountpoint_full($mp_desc, $data, $noerr);
+    return $class->print_ct_mountpoint($volume, $key eq 'rootfs');
 }
 
-sub print_ct_mountpoint {
-    my ($class, $info, $nomp) = @_;
-    my $skip = [ 'type' ];
-    push @$skip, 'mp' if $nomp;
-    return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
+sub volid_key {
+    my ($class) = @_;
+
+    return 'volume';
 }
 
 sub print_lxc_network {
@@ -1122,48 +1296,25 @@ sub vmconfig_hotplug_pending {
        $errors->{$opt} = "unable to hotplug $opt: $msg";
     };
 
-    my $changes;
     foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
        next if $selection && !$selection->{$opt};
        if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
            $conf->{$opt} = delete $conf->{pending}->{$opt};
-           $changes = 1;
        }
     }
 
-    if ($changes) {
-       $class->write_config($vmid, $conf);
-    }
+    my $cgroup = PVE::LXC::CGroup->new($vmid);
 
     # There's no separate swap size to configure, there's memory and "total"
     # memory (iow. memory+swap). This means we have to change them together.
     my $hotplug_memory_done;
     my $hotplug_memory = sub {
        my ($wanted_memory, $wanted_swap) = @_;
-       my $old_memory = ($conf->{memory} || $confdesc->{memory}->{default});
-       my $old_swap = ($conf->{swap} || $confdesc->{swap}->{default});
-
-       $wanted_memory //= $old_memory;
-       $wanted_swap //= $old_swap;
-
-       my $total = $wanted_memory + $wanted_swap;
-       my $old_total = $old_memory + $old_swap;
-
-       if ($total > $old_total) {
-           PVE::LXC::write_cgroup_value("memory", $vmid,
-                                        "memory.memsw.limit_in_bytes",
-                                        int($total*1024*1024));
-           PVE::LXC::write_cgroup_value("memory", $vmid,
-                                        "memory.limit_in_bytes",
-                                        int($wanted_memory*1024*1024));
-       } else {
-           PVE::LXC::write_cgroup_value("memory", $vmid,
-                                        "memory.limit_in_bytes",
-                                        int($wanted_memory*1024*1024));
-           PVE::LXC::write_cgroup_value("memory", $vmid,
-                                        "memory.memsw.limit_in_bytes",
-                                        int($total*1024*1024));
-       }
+
+       $wanted_memory = int($wanted_memory * 1024 * 1024) if defined($wanted_memory);
+       $wanted_swap = int($wanted_swap * 1024 * 1024) if defined($wanted_swap);
+       $cgroup->change_memory_limit($wanted_memory, $wanted_swap);
+
        $hotplug_memory_done = 1;
     };
 
@@ -1180,10 +1331,9 @@ sub vmconfig_hotplug_pending {
            } elsif ($opt eq 'swap') {
                $hotplug_memory->(undef, 0);
            } elsif ($opt eq 'cpulimit') {
-               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_period_us", -1);
-               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", -1);
+               $cgroup->change_cpu_quota(undef, undef); # reset, cgroup module can better decide values
            } elsif ($opt eq 'cpuunits') {
-               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shared", $confdesc->{cpuunits}->{default});
+               $cgroup->change_cpu_shares(undef, $confdesc->{cpuunits}->{default});
            } elsif ($opt =~ m/^net(\d)$/) {
                my $netid = $1;
                PVE::Network::veth_delete("veth${vmid}i$netid");
@@ -1205,10 +1355,10 @@ sub vmconfig_hotplug_pending {
        my $value = $conf->{pending}->{$opt};
        eval {
            if ($opt eq 'cpulimit') {
-               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_period_us", 100000);
-               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", int(100000*$value));
+               my $quota = 100000 * $value;
+               $cgroup->change_cpu_quota(int(100000 * $value), 100000);
            } elsif ($opt eq 'cpuunits') {
-               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $value);
+               $cgroup->change_cpu_shares($value, $confdesc->{cpuunits}->{default});
            } elsif ($opt =~ m/^net(\d+)$/) {
                my $netid = $1;
                my $net = $class->parse_lxc_network($value);
@@ -1223,6 +1373,10 @@ sub vmconfig_hotplug_pending {
                    die "skip\n";
                }
 
+               if (exists($conf->{$opt})) {
+                   die "skip\n"; # don't try to hotplug over existing mp
+               }
+
                $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 1);
                # apply_pending_mountpoint modifies the value if it creates a new disk
                $value = $conf->{pending}->{$opt};
@@ -1237,8 +1391,6 @@ sub vmconfig_hotplug_pending {
            delete $conf->{pending}->{$opt};
        }
     }
-
-    $class->write_config($vmid, $conf);
 }
 
 sub vmconfig_apply_pending {
@@ -1255,10 +1407,9 @@ sub vmconfig_apply_pending {
     # FIXME: $force deletion is not implemented for CTs
     foreach my $opt (sort keys %$pending_delete_hash) {
        next if $selection && !$selection->{$opt};
-       $class->cleanup_pending($conf);
        eval {
            if ($opt =~ m/^mp(\d+)$/) {
-               my $mp = $class->parse_ct_mountpoint($conf->{$opt});
+               my $mp = $class->parse_volume($opt, $conf->{$opt});
                if ($mp->{type} eq 'volume') {
                    $class->add_unused_volume($conf, $mp->{volume})
                        if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
@@ -1276,6 +1427,8 @@ sub vmconfig_apply_pending {
        }
     }
 
+    $class->cleanup_pending($conf);
+
     foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
        next if $opt eq 'delete'; # just to be sure
        next if $selection && !$selection->{$opt};
@@ -1291,19 +1444,15 @@ sub vmconfig_apply_pending {
        if (my $err = $@) {
            $add_apply_error->($opt, $err);
        } else {
-           $class->cleanup_pending($conf);
            $conf->{$opt} = delete $conf->{pending}->{$opt};
        }
     }
-
-    $class->write_config($vmid, $conf);
 }
 
 my $rescan_volume = sub {
     my ($storecfg, $mp) = @_;
     eval {
-       $mp->{size} = PVE::Storage::volume_size_info($storecfg, $mp->{volume}, 5)
-           if !defined($mp->{size});
+       $mp->{size} = PVE::Storage::volume_size_info($storecfg, $mp->{volume}, 5);
     };
     warn "Could not rescan volume size - $@\n" if $@;
 };
@@ -1311,7 +1460,7 @@ my $rescan_volume = sub {
 sub apply_pending_mountpoint {
     my ($class, $vmid, $conf, $opt, $storecfg, $running) = @_;
 
-    my $mp = $class->parse_ct_mountpoint($conf->{pending}->{$opt});
+    my $mp = $class->parse_volume($opt, $conf->{pending}->{$opt});
     my $old = $conf->{$opt};
     if ($mp->{type} eq 'volume') {
        if ($mp->{volume} =~ $PVE::LXC::NEW_DISK_RE) {
@@ -1325,7 +1474,7 @@ sub apply_pending_mountpoint {
            );
            if ($running) {
                # Re-parse mount point:
-               my $mp = $class->parse_ct_mountpoint($conf->{pending}->{$opt});
+               my $mp = $class->parse_volume($opt, $conf->{pending}->{$opt});
                eval {
                    PVE::LXC::mountpoint_hotplug($vmid, $conf, $opt, $mp, $storecfg);
                };
@@ -1350,7 +1499,7 @@ sub apply_pending_mountpoint {
     }
 
     if (defined($old)) {
-       my $mp = $class->parse_ct_mountpoint($old);
+       my $mp = $class->parse_volume($opt, $old);
        if ($mp->{type} eq 'volume') {
            $class->add_unused_volume($conf, $mp->{volume})
                if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
@@ -1371,7 +1520,7 @@ my $__is_volume_in_use = sub {
     my ($class, $config, $volid) = @_;
     my $used = 0;
 
-    $class->foreach_mountpoint($config, sub {
+    $class->foreach_volume($config, sub {
        my ($ms, $mountpoint) = @_;
        return if $used;
        $used = $mountpoint->{type} eq 'volume' && $mountpoint->{volume} eq $volid;
@@ -1431,7 +1580,7 @@ sub get_cmode {
     return $conf->{cmode} // $confdesc->{cmode}->{default};
 }
 
-sub mountpoint_names {
+sub valid_volume_keys {
     my ($class, $reverse) = @_;
 
     my @names = ('rootfs');
@@ -1443,29 +1592,13 @@ sub mountpoint_names {
     return $reverse ? reverse @names : @names;
 }
 
-sub foreach_mountpoint_full {
-    my ($class, $conf, $reverse, $func, @param) = @_;
-
-    my $mps = [ grep { defined($conf->{$_}) } $class->mountpoint_names($reverse) ];
-    foreach my $key (@$mps) {
-       my $value = $conf->{$key};
-       my $mountpoint = $key eq 'rootfs' ? $class->parse_ct_rootfs($value, 1) : $class->parse_ct_mountpoint($value, 1);
-       next if !defined($mountpoint);
-
-       &$func($key, $mountpoint, @param);
+sub valid_volume_keys_with_unused {
+    my ($class, $reverse) = @_;
+    my @names = $class->valid_volume_keys();
+    for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) {
+       push @names, "unused$i";
     }
-}
-
-sub foreach_mountpoint {
-    my ($class, $conf, $func, @param) = @_;
-
-    $class->foreach_mountpoint_full($conf, 0, $func, @param);
-}
-
-sub foreach_mountpoint_reverse {
-    my ($class, $conf, $func, @param) = @_;
-
-    $class->foreach_mountpoint_full($conf, 1, $func, @param);
+    return $reverse ? reverse @names : @names;
 }
 
 sub get_vm_volumes {
@@ -1473,7 +1606,7 @@ sub get_vm_volumes {
 
     my $vollist = [];
 
-    $class->foreach_mountpoint($conf, sub {
+    $class->foreach_volume($conf, sub {
        my ($ms, $mountpoint) = @_;
 
        return if $excludes && $ms eq $excludes;
@@ -1531,14 +1664,14 @@ sub get_replicatable_volumes {
        $volhash->{$volid} = 1;
     };
 
-    $class->foreach_mountpoint($conf, sub {
+    $class->foreach_volume($conf, sub {
        my ($ms, $mountpoint) = @_;
        $test_volid->($mountpoint->{volume}, $mountpoint);
     });
 
     foreach my $snapname (keys %{$conf->{snapshots}}) {
        my $snap = $conf->{snapshots}->{$snapname};
-       $class->foreach_mountpoint($snap, sub {
+       $class->foreach_volume($snap, sub {
            my ($ms, $mountpoint) = @_;
            $test_volid->($mountpoint->{volume}, $mountpoint);
         });
@@ -1554,4 +1687,27 @@ sub get_replicatable_volumes {
     return $volhash;
 }
 
+sub get_backup_volumes {
+    my ($class, $conf) = @_;
+
+    my $return_volumes = [];
+
+    my $test_mountpoint = sub {
+       my ($key, $volume) = @_;
+
+       my ($included, $reason) = $class->mountpoint_backup_enabled($key, $volume);
+
+       push @$return_volumes, {
+           key => $key,
+           included => $included,
+           reason => $reason,
+           volume_config => $volume,
+       };
+    };
+
+    PVE::LXC::Config->foreach_volume($conf, $test_mountpoint);
+
+    return $return_volumes;
+}
+
 1;