]> git.proxmox.com Git - pve-container.git/blobdiff - src/PVE/LXC/Config.pm
lxc_config: mount /sys as mixed for unprivileged by default
[pve-container.git] / src / PVE / LXC / Config.pm
index 71788bae2cf0e989e1003fae015093b22a37d0f0..0909773f312432526d2f64f5ca42e1f06681b437 100644 (file)
@@ -5,6 +5,7 @@ use warnings;
 
 use PVE::AbstractConfig;
 use PVE::Cluster qw(cfs_register_file);
+use PVE::DataCenterConfig;
 use PVE::GuestHelpers;
 use PVE::INotify;
 use PVE::JSONSchema qw(get_standard_option);
@@ -216,10 +217,12 @@ sub __snapshot_foreach_volume {
 
 cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config);
 
-my $mount_option = qr/(noatime|nodev|nosuid|noexec)/;
 
-sub get_mount_options {
-    return $mount_option;
+my $valid_mount_option_re = qr/(noatime|nodev|nosuid|noexec)/;
+
+sub is_valid_mount_option {
+    my ($option) = @_;
+    return $option =~ $valid_mount_option_re;
 }
 
 my $rootfs_desc = {
@@ -247,7 +250,7 @@ my $rootfs_desc = {
        type => 'string',
        description => 'Extra mount options for rootfs/mps.',
        format_description => 'opt[;opt...]',
-       pattern => qr/$mount_option(;$mount_option)*/,
+       pattern => qr/$valid_mount_option_re(;$valid_mount_option_re)*/,
     },
     ro => {
        type => 'boolean',
@@ -320,6 +323,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 = {
@@ -327,7 +345,7 @@ my $confdesc = {
        optional => 1,
        type => 'string',
        description => "Lock/unlock the VM.",
-       enum => [qw(backup create disk fstrim migrate mounted rollback snapshot snapshot-delete)],
+       enum => [qw(backup create destroyed disk fstrim migrate mounted rollback snapshot snapshot-delete)],
     },
     onboot => {
        optional => 1,
@@ -471,6 +489,11 @@ 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,
+    },
 };
 
 my $valid_lxc_conf_keys = {
@@ -507,6 +530,8 @@ my $valid_lxc_conf_keys = {
     'lxc.cap.drop' => 1,
     'lxc.cap.keep' => 1,
     'lxc.seccomp.profile' => 1,
+    'lxc.seccomp.notify.proxy' => 1,
+    'lxc.seccomp.notify.cookie' => 1,
     'lxc.idmap' => 1,
     'lxc.hook.pre-start' => 1,
     'lxc.hook.pre-mount' => 1,
@@ -516,6 +541,7 @@ my $valid_lxc_conf_keys = {
     'lxc.hook.post-stop' => 1,
     'lxc.hook.clone' => 1,
     'lxc.hook.destroy' => 1,
+    'lxc.hook.version' => 1,
     'lxc.log.level' => 1,
     'lxc.log.file' => 1,
     'lxc.start.auto' => 1,
@@ -747,6 +773,7 @@ sub parse_pct_config {
     my $res = {
        digest => Digest::SHA::sha1_hex($raw),
        snapshots => {},
+       pending => {},
     };
 
     $filename =~ m|/lxc/(\d+).conf$|
@@ -762,7 +789,13 @@ sub parse_pct_config {
     foreach my $line (@lines) {
        next if $line =~ m/^\s*$/;
 
-       if ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
+       if ($line =~ m/^\[pve:pending\]\s*$/i) {
+           $section = 'pending';
+           $conf->{description} = $descr if $descr;
+           $descr = '';
+           $conf = $res->{$section} = {};
+           next;
+       } elsif ($line =~ m/^\[([a-z][a-z0-9_\-]+)\]\s*$/i) {
            $section = $1;
            $conf->{description} = $descr if $descr;
            $descr = '';
@@ -790,6 +823,13 @@ sub parse_pct_config {
            $descr .= PVE::Tools::decode_text($2);
        } elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
            $conf->{snapstate} = $1;
+       } elsif ($line =~ m/^delete:\s*(.*\S)\s*$/) {
+           my $value = $1;
+           if ($section eq 'pending') {
+               $conf->{delete} = $value;
+           } else {
+               warn "vm $vmid - property 'delete' is only allowed in [pve:pending]\n";
+           }
        } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
            my $key = $1;
            my $value = $2;
@@ -835,7 +875,7 @@ sub write_pct_config {
        # add description as comment to top of file
        my $descr = $conf->{description} || '';
        foreach my $cl (split(/\n/, $descr)) {
-           $raw .= '#' .  PVE::Tools::encode_text($cl) . "\n";
+           $raw .= '#' . PVE::Tools::encode_text($cl) . "\n";
        }
 
        foreach my $key (sort keys %$conf) {
@@ -860,6 +900,11 @@ sub write_pct_config {
 
     my $raw = &$generate_raw_config($conf);
 
+    if (scalar(keys %{$conf->{pending}})){
+       $raw .= "\n[pve:pending]\n";
+       $raw .= &$generate_raw_config($conf->{pending});
+    }
+
     foreach my $snapname (sort keys %{$conf->{snapshots}}) {
        $raw .= "\n[$snapname]\n";
        $raw .= &$generate_raw_config($conf->{snapshots}->{$snapname});
@@ -869,268 +914,74 @@ sub write_pct_config {
 }
 
 sub update_pct_config {
-    my ($class, $vmid, $conf, $running, $param, $delete) = @_;
-
-    my @nohotplug;
+    my ($class, $vmid, $conf, $running, $param, $delete, $revert) = @_;
 
-    my $new_disks = 0;
-    my @deleted_volumes;
+    my $storage_cfg = PVE::Storage::config();
 
-    my $rootdir;
-    if ($running) {
-       my $pid = PVE::LXC::find_lxc_pid($vmid);
-       $rootdir = "/proc/$pid/root";
+    foreach my $opt (@$revert) {
+       delete $conf->{pending}->{$opt};
+       $class->remove_from_pending_delete($conf, $opt); # also remove from deletion queue
     }
 
-    my $hotplug_error = sub {
-       if ($running) {
-           push @nohotplug, @_;
-           return 1;
-       } else {
-           return 0;
-       }
-    };
-
-    if (defined($delete)) {
-       foreach my $opt (@$delete) {
-           if (!exists($conf->{$opt})) {
-               # silently ignore
-               next;
-           }
+    # write updates to pending section
+    my $modified = {}; # record modified options
 
-           if ($opt eq 'memory' || $opt eq 'rootfs') {
-               die "unable to delete required option '$opt'\n";
-           } elsif ($opt eq 'hostname') {
-               delete $conf->{$opt};
-           } elsif ($opt eq 'swap') {
-               delete $conf->{$opt};
-               PVE::LXC::write_cgroup_value("memory", $vmid,
-                                            "memory.memsw.limit_in_bytes", -1);
-           } elsif ($opt eq 'description' || $opt eq 'onboot' || $opt eq 'startup' || $opt eq 'hookscript') {
-               delete $conf->{$opt};
-           } elsif ($opt eq 'nameserver' || $opt eq 'searchdomain' ||
-                    $opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
-               next if $hotplug_error->($opt);
-               delete $conf->{$opt};
-           } elsif ($opt eq 'cores') {
-               delete $conf->{$opt}; # rest is handled by pvestatd
-           } elsif ($opt eq 'cpulimit') {
-               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", -1);
-               delete $conf->{$opt};
-           } elsif ($opt eq 'cpuunits') {
-               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $confdesc->{cpuunits}->{default});
-               delete $conf->{$opt};
-           } elsif ($opt =~ m/^net(\d)$/) {
-               delete $conf->{$opt};
-               next if !$running;
-               my $netid = $1;
-               PVE::Network::veth_delete("veth${vmid}i$netid");
-           } elsif ($opt eq 'protection') {
-               delete $conf->{$opt};
-           } elsif ($opt =~ m/^unused(\d+)$/) {
-               next if $hotplug_error->($opt);
-               PVE::LXC::Config->check_protection($conf, "can't remove CT $vmid drive '$opt'");
-               push @deleted_volumes, $conf->{$opt};
-               delete $conf->{$opt};
-           } elsif ($opt =~ m/^mp(\d+)$/) {
-               next if $hotplug_error->($opt);
-               PVE::LXC::Config->check_protection($conf, "can't remove CT $vmid drive '$opt'");
-               my $mp = PVE::LXC::Config->parse_ct_mountpoint($conf->{$opt});
-               delete $conf->{$opt};
-               if ($mp->{type} eq 'volume') {
-                   PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
-               }
-           } elsif ($opt eq 'unprivileged') {
-               die "unable to delete read-only option: '$opt'\n";
-           } elsif ($opt eq 'features') {
-               next if $hotplug_error->($opt);
-               delete $conf->{$opt};
-           } else {
-               die "implement me (delete: $opt)"
-           }
-           PVE::LXC::Config->write_config($vmid, $conf) if $running;
+    foreach my $opt (@$delete) {
+       if (!defined($conf->{$opt}) && !defined($conf->{pending}->{$opt})) {
+           warn "cannot delete '$opt' - not set in current configuration!\n";
+           next;
        }
-    }
-
-    # 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 $wanted_memory = PVE::Tools::extract_param($param, 'memory');
-    my $wanted_swap =  PVE::Tools::extract_param($param, 'swap');
-    if (defined($wanted_memory) || defined($wanted_swap)) {
-
-       my $old_memory = ($conf->{memory} || 512);
-       my $old_swap = ($conf->{swap} || 0);
-
-       $wanted_memory //= $old_memory;
-       $wanted_swap //= $old_swap;
-
-       my $total = $wanted_memory + $wanted_swap;
-       if ($running) {
-           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));
-           }
+       $modified->{$opt} = 1;
+       if ($opt eq 'memory' || $opt eq 'rootfs' || $opt eq 'ostype') {
+           die "unable to delete required option '$opt'\n";
+       } elsif ($opt =~ m/^unused(\d+)$/) {
+           $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
+       } elsif ($opt =~ m/^mp(\d+)$/) {
+           $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
+       } elsif ($opt eq 'unprivileged') {
+           die "unable to delete read-only option: '$opt'\n";
        }
-       $conf->{memory} = $wanted_memory;
-       $conf->{swap} = $wanted_swap;
-
-       PVE::LXC::Config->write_config($vmid, $conf) if $running;
+       $class->add_to_pending_delete($conf, $opt);
     }
 
-    my $storecfg = PVE::Storage::config();
-
-    my $used_volids = {};
     my $check_content_type = sub {
        my ($mp) = @_;
        my $sid = PVE::Storage::parse_volume_id($mp->{volume});
-       my $storage_config = PVE::Storage::storage_config($storecfg, $sid);
+       my $storage_config = PVE::Storage::storage_config($storage_cfg, $sid);
        die "storage '$sid' does not allow content type 'rootdir' (Container)\n"
            if !$storage_config->{content}->{rootdir};
     };
 
-    my $rescan_volume = sub {
-       my ($mp) = @_;
-       eval {
-           $mp->{size} = PVE::Storage::volume_size_info($storecfg, $mp->{volume}, 5)
-               if !defined($mp->{size});
-       };
-       warn "Could not rescan volume size - $@\n" if $@;
-    };
-
-    foreach my $opt (keys %$param) {
+    foreach my $opt (sort keys %$param) { # add/change
+       $modified->{$opt} = 1;
        my $value = $param->{$opt};
-       my $check_protection_msg = "can't update CT $vmid drive '$opt'";
-       if ($opt eq 'hostname' || $opt eq 'arch') {
-           $conf->{$opt} = $value;
-       } elsif ($opt eq 'onboot') {
-           $conf->{$opt} = $value ? 1 : 0;
-       } elsif ($opt eq 'startup') {
-           $conf->{$opt} = $value;
-       } elsif ($opt eq 'tty' || $opt eq 'console' || $opt eq 'cmode') {
-           next if $hotplug_error->($opt);
-           $conf->{$opt} = $value;
+       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);
+           $check_content_type->($mp) if ($mp->{type} eq 'volume');
+       } elsif ($opt eq 'hookscript') {
+           PVE::GuestHelpers::check_hookscript($value);
        } elsif ($opt eq 'nameserver') {
-           next if $hotplug_error->($opt);
-           my $list = PVE::LXC::verify_nameserver_list($value);
-           $conf->{$opt} = $list;
+           $value = PVE::LXC::verify_nameserver_list($value);
        } elsif ($opt eq 'searchdomain') {
-           next if $hotplug_error->($opt);
-           my $list = PVE::LXC::verify_searchdomain_list($value);
-           $conf->{$opt} = $list;
-       } elsif ($opt eq 'cores') {
-           $conf->{$opt} = $value;# rest is handled by pvestatd
-       } elsif ($opt eq 'cpulimit') {
-           if ($value == 0) {
-               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", -1);
-           } else {
-               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.cfs_quota_us", int(100000*$value));
-           }
-           $conf->{$opt} = $value;
-       } elsif ($opt eq 'cpuunits') {
-           $conf->{$opt} = $value;
-           PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $value);
-       } elsif ($opt eq 'description') {
-           $conf->{$opt} = $value;
-       } elsif ($opt =~ m/^net(\d+)$/) {
-           my $netid = $1;
-           my $net = PVE::LXC::Config->parse_lxc_network($value);
-           if (!$running) {
-               $conf->{$opt} = PVE::LXC::Config->print_lxc_network($net);
-           } else {
-               PVE::LXC::update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
-           }
-       } elsif ($opt eq 'protection') {
-           $conf->{$opt} = $value ? 1 : 0;
-       } elsif ($opt =~ m/^mp(\d+)$/) {
-           next if $hotplug_error->($opt);
-           PVE::LXC::Config->check_protection($conf, $check_protection_msg);
-           my $old = $conf->{$opt};
-           my $mp = PVE::LXC::Config->parse_ct_mountpoint($value);
-           if ($mp->{type} eq 'volume') {
-               &$check_content_type($mp);
-               $used_volids->{$mp->{volume}} = 1;
-               &$rescan_volume($mp);
-               $conf->{$opt} = PVE::LXC::Config->print_ct_mountpoint($mp);
-           } else {
-               $conf->{$opt} = $value;
-           }
-           if (defined($old)) {
-               my $mp = PVE::LXC::Config->parse_ct_mountpoint($old);
-               if ($mp->{type} eq 'volume') {
-                   PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
-               }
-           }
-           $new_disks = 1;
-       } elsif ($opt eq 'rootfs') {
-           next if $hotplug_error->($opt);
-           PVE::LXC::Config->check_protection($conf, $check_protection_msg);
-           my $old = $conf->{$opt};
-           my $mp = PVE::LXC::Config->parse_ct_rootfs($value);
-           if ($mp->{type} eq 'volume') {
-               &$check_content_type($mp);
-               $used_volids->{$mp->{volume}} = 1;
-               &$rescan_volume($mp);
-               $conf->{$opt} = PVE::LXC::Config->print_ct_mountpoint($mp, 1);
-           } else {
-               $conf->{$opt} = $value;
-           }
-           if (defined($old)) {
-               my $mp = PVE::LXC::Config->parse_ct_rootfs($old);
-               if ($mp->{type} eq 'volume') {
-                   PVE::LXC::Config->add_unused_volume($conf, $mp->{volume});
-               }
-           }
-           $new_disks = 1;
+           $value = PVE::LXC::verify_searchdomain_list($value);
        } elsif ($opt eq 'unprivileged') {
            die "unable to modify read-only option: '$opt'\n";
-       } elsif ($opt eq 'ostype') {
-           next if $hotplug_error->($opt);
-           $conf->{$opt} = $value;
-       } elsif ($opt eq 'features') {
-           next if $hotplug_error->($opt);
-           $conf->{$opt} = $value;
-       } elsif ($opt eq 'hookscript') {
-           PVE::GuestHelpers::check_hookscript($value);
-           $conf->{$opt} = $value;
-       } else {
-           die "implement me: $opt";
        }
-
-       PVE::LXC::Config->write_config($vmid, $conf) if $running;
+       $conf->{pending}->{$opt} = $value;
+       $class->remove_from_pending_delete($conf, $opt);
     }
 
-    # Apply deletions and creations of new volumes
-    if (@deleted_volumes) {
-       my $storage_cfg = PVE::Storage::config();
-       foreach my $volume (@deleted_volumes) {
-           next if $used_volids->{$volume}; # could have been re-added, too
-           # also check for references in snapshots
-           next if $class->is_volume_in_use($conf, $volume, 1);
-           PVE::LXC::delete_mountpoint_volume($storage_cfg, $vmid, $volume);
-       }
-    }
+    my $changes = $class->cleanup_pending($conf);
 
-    if ($new_disks) {
-       my $storage_cfg = PVE::Storage::config();
-       PVE::LXC::create_disks($storage_cfg, $vmid, $conf, $conf);
+    my $errors = {};
+    if ($running) {
+       $class->vmconfig_hotplug_pending($vmid, $conf, $storage_cfg, $modified, $errors);
+    } else {
+       $class->vmconfig_apply_pending($vmid, $conf, $storage_cfg, $modified, $errors);
     }
 
-    # This should be the last thing we do here
-    if ($running && scalar(@nohotplug)) {
-       die "unable to modify " . join(',', @nohotplug) . " while container is running\n";
-    }
+    return $errors;
 }
 
 sub check_type {
@@ -1268,6 +1119,264 @@ sub option_exists {
 }
 # END JSON config code
 
+my $LXC_FASTPLUG_OPTIONS= {
+    'description' => 1,
+    'onboot' => 1,
+    'startup' => 1,
+    'protection' => 1,
+    'hostname' => 1,
+    'hookscript' => 1,
+    'cores' => 1,
+    'tags' => 1,
+    'lock' => 1,
+};
+
+sub vmconfig_hotplug_pending {
+    my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
+
+    my $pid = PVE::LXC::find_lxc_pid($vmid);
+    my $rootdir = "/proc/$pid/root";
+
+    my $add_hotplug_error = sub {
+       my ($opt, $msg) = @_;
+       $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);
+    }
+
+    # 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));
+       }
+       $hotplug_memory_done = 1;
+    };
+
+    my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
+    # FIXME: $force deletion is not implemented for CTs
+    foreach my $opt (sort keys %$pending_delete_hash) {
+       next if $selection && !$selection->{$opt};
+       eval {
+           if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
+               # pass
+           } elsif ($opt =~ m/^unused(\d+)$/) {
+               PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
+                   if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
+           } 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);
+           } elsif ($opt eq 'cpuunits') {
+               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $confdesc->{cpuunits}->{default});
+           } elsif ($opt =~ m/^net(\d)$/) {
+               my $netid = $1;
+               PVE::Network::veth_delete("veth${vmid}i$netid");
+           } else {
+               die "skip\n"; # skip non-hotpluggable opts
+           }
+       };
+       if (my $err = $@) {
+           $add_hotplug_error->($opt, $err) if $err ne "skip\n";
+       } else {
+           delete $conf->{$opt};
+           $class->remove_from_pending_delete($conf, $opt);
+       }
+    }
+
+    foreach my $opt (sort keys %{$conf->{pending}}) {
+       next if $opt eq 'delete'; # just to be sure
+       next if $selection && !$selection->{$opt};
+       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));
+           } elsif ($opt eq 'cpuunits') {
+               PVE::LXC::write_cgroup_value("cpu", $vmid, "cpu.shares", $value);
+           } elsif ($opt =~ m/^net(\d+)$/) {
+               my $netid = $1;
+               my $net = $class->parse_lxc_network($value);
+               $value = $class->print_lxc_network($net);
+               PVE::LXC::update_net($vmid, $conf, $opt, $net, $netid, $rootdir);
+           } elsif ($opt eq 'memory' || $opt eq 'swap') {
+               if (!$hotplug_memory_done) { # don't call twice if both opts are passed
+                   $hotplug_memory->($conf->{pending}->{memory}, $conf->{pending}->{swap});
+               }
+           } elsif ($opt =~ m/^mp(\d+)$/) {
+               if (!PVE::LXC::Tools::can_use_new_mount_api()) {
+                   die "skip\n";
+               }
+
+               $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};
+           } else {
+               die "skip\n"; # skip non-hotpluggable
+           }
+       };
+       if (my $err = $@) {
+           $add_hotplug_error->($opt, $err) if $err ne "skip\n";
+       } else {
+           $conf->{$opt} = $value;
+           delete $conf->{pending}->{$opt};
+       }
+    }
+
+    $class->write_config($vmid, $conf);
+}
+
+sub vmconfig_apply_pending {
+    my ($class, $vmid, $conf, $storecfg, $selection, $errors) = @_;
+
+    my $add_apply_error = sub {
+       my ($opt, $msg) = @_;
+       my $err_msg = "unable to apply pending change $opt : $msg";
+       $errors->{$opt} = $err_msg;
+       warn $err_msg;
+    };
+
+    my $pending_delete_hash = $class->parse_pending_delete($conf->{pending}->{delete});
+    # FIXME: $force deletion is not implemented for CTs
+    foreach my $opt (sort keys %$pending_delete_hash) {
+       next if $selection && !$selection->{$opt};
+       eval {
+           if ($opt =~ m/^mp(\d+)$/) {
+               my $mp = $class->parse_ct_mountpoint($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);
+               }
+           } elsif ($opt =~ m/^unused(\d+)$/) {
+               PVE::LXC::delete_mountpoint_volume($storecfg, $vmid, $conf->{$opt})
+                   if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
+           }
+       };
+       if (my $err = $@) {
+           $add_apply_error->($opt, $err);
+       } else {
+           delete $conf->{$opt};
+           $class->remove_from_pending_delete($conf, $opt);
+       }
+    }
+
+    $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};
+       eval {
+           if ($opt =~ m/^mp(\d+)$/) {
+               $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 0);
+           } elsif ($opt =~ m/^net(\d+)$/) {
+               my $netid = $1;
+               my $net = $class->parse_lxc_network($conf->{pending}->{$opt});
+               $conf->{pending}->{$opt} = $class->print_lxc_network($net);
+           }
+       };
+       if (my $err = $@) {
+           $add_apply_error->($opt, $err);
+       } else {
+           $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);
+    };
+    warn "Could not rescan volume size - $@\n" if $@;
+};
+
+sub apply_pending_mountpoint {
+    my ($class, $vmid, $conf, $opt, $storecfg, $running) = @_;
+
+    my $mp = $class->parse_ct_mountpoint($conf->{pending}->{$opt});
+    my $old = $conf->{$opt};
+    if ($mp->{type} eq 'volume') {
+       if ($mp->{volume} =~ $PVE::LXC::NEW_DISK_RE) {
+           my $original_value = $conf->{pending}->{$opt};
+           my $vollist = PVE::LXC::create_disks(
+               $storecfg,
+               $vmid,
+               { $opt => $original_value },
+               $conf,
+               1,
+           );
+           if ($running) {
+               # Re-parse mount point:
+               my $mp = $class->parse_ct_mountpoint($conf->{pending}->{$opt});
+               eval {
+                   PVE::LXC::mountpoint_hotplug($vmid, $conf, $opt, $mp, $storecfg);
+               };
+               my $err = $@;
+               if ($err) {
+                   PVE::LXC::destroy_disks($storecfg, $vollist);
+                   # The pending-changes code collects errors but keeps on looping through further
+                   # pending changes, so unroll the change in $conf as well if destroy_disks()
+                   # didn't die().
+                   $conf->{pending}->{$opt} = $original_value;
+                   die $err;
+               }
+           }
+       } else {
+           die "skip\n" if $running && defined($old); # TODO: "changing" mount points?
+           $rescan_volume->($storecfg, $mp);
+           if ($running) {
+               PVE::LXC::mountpoint_hotplug($vmid, $conf, $opt, $mp, $storecfg);
+           }
+           $conf->{pending}->{$opt} = $class->print_ct_mountpoint($mp);
+       }
+    }
+
+    if (defined($old)) {
+       my $mp = $class->parse_ct_mountpoint($old);
+       if ($mp->{type} eq 'volume') {
+           $class->add_unused_volume($conf, $mp->{volume})
+               if !$class->is_volume_in_use($conf, $conf->{$opt}, 1, 1);
+       }
+    }
+}
+
 sub classify_mountpoint {
     my ($class, $vol) = @_;
     if ($vol =~ m!^/!) {
@@ -1277,7 +1386,7 @@ sub classify_mountpoint {
     return 'volume';
 }
 
-my $is_volume_in_use = sub {
+my $__is_volume_in_use = sub {
     my ($class, $config, $volid) = @_;
     my $used = 0;
 
@@ -1295,17 +1404,18 @@ sub is_volume_in_use_by_snapshots {
 
     if (my $snapshots = $config->{snapshots}) {
        foreach my $snap (keys %$snapshots) {
-           return 1 if $is_volume_in_use->($class, $snapshots->{$snap}, $volid);
+           return 1 if $__is_volume_in_use->($class, $snapshots->{$snap}, $volid);
        }
     }
 
     return 0;
-};
+}
 
 sub is_volume_in_use {
-    my ($class, $config, $volid, $include_snapshots) = @_;
-    return 1 if $is_volume_in_use->($class, $config, $volid);
+    my ($class, $config, $volid, $include_snapshots, $include_pending) = @_;
+    return 1 if $__is_volume_in_use->($class, $config, $volid);
     return 1 if $include_snapshots && $class->is_volume_in_use_by_snapshots($config, $volid);
+    return 1 if $include_pending && $__is_volume_in_use->($class, $config->{pending}, $volid);
     return 0;
 }