]> 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 e76d55888c097afff8051febb8d47fa4a3b50bda..0ed7bd245dd9db32745eb2d6460ffba030807ab9 100644 (file)
@@ -2,21 +2,27 @@ package PVE::LXC::Config;
 
 use strict;
 use warnings;
+use Fcntl qw(O_RDONLY);
 
 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);
 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";
 mkdir $lockdir;
 mkdir "/etc/pve/nodes/$nodename/lxc";
-my $MAX_MOUNT_POINTS = 10;
+my $MAX_MOUNT_POINTS = 256;
 my $MAX_UNUSED_DISKS = $MAX_MOUNT_POINTS;
 
 # BEGIN implemented abstract methods from PVE::AbstractConfig
@@ -47,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
@@ -69,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;
@@ -80,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);
@@ -92,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);
     }
 }
 
@@ -122,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');
     }
 }
 
@@ -148,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 {
@@ -164,12 +254,12 @@ sub __snapshot_rollback_vol_rollback {
 sub __snapshot_rollback_vm_stop {
     my ($class, $vmid) = @_;
 
-    PVE::Tools::run_command(['/usr/bin/lxc-stop', '-n', $vmid, '--kill'])
+    PVE::LXC::vm_stop($vmid, 1)
        if $class->__snapshot_check_running($vmid);
 }
 
 sub __snapshot_rollback_vm_start {
-    my ($class, $vmid, $vmstate, $forcemachine);
+    my ($class, $vmid, $vmstate, $data);
 
     die "implement me - save vmstate\n";
 }
@@ -179,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';
@@ -187,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;
@@ -203,18 +293,20 @@ 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
 
 cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config);
 
+
+my $valid_mount_option_re = qr/(noatime|lazytime|nodev|nosuid|noexec)/;
+
+sub is_valid_mount_option {
+    my ($option) = @_;
+    return $option =~ $valid_mount_option_re;
+}
+
 my $rootfs_desc = {
     volume => {
        type => 'string',
@@ -235,6 +327,13 @@ my $rootfs_desc = {
        description => 'Explicitly enable or disable ACL support.',
        optional => 1,
     },
+    mountoptions => {
+       optional => 1,
+       type => 'string',
+       description => 'Extra mount options for rootfs/mps.',
+       format_description => 'opt[;opt...]',
+       pattern => qr/$valid_mount_option_re(;$valid_mount_option_re)*/,
+    },
     ro => {
        type => 'boolean',
        description => 'Read-only mount point',
@@ -266,18 +365,86 @@ PVE::JSONSchema::register_standard_option('pve-ct-rootfs', {
     optional => 1,
 });
 
-PVE::JSONSchema::register_standard_option('pve-lxc-snapshot-name', {
-    description => "The name of the snapshot.",
-    type => 'string', format => 'pve-configid',
-    maxLength => 40,
-});
+# 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,
+       type => 'string',
+       description => "Allow mounting file systems of specific types."
+           ." This should be a list of file system types as used with the mount command."
+           ." Note that this can have negative effects on the container's security."
+           ." With access to a loop device, mounting a file can circumvent the mknod"
+           ." permission of the devices cgroup, mounting an NFS file system can"
+           ." block the host's I/O completely and prevent it from rebooting, etc.",
+       format_description => 'fstype;fstype;...',
+       pattern => qr/[a-zA-Z0-9_; ]+/,
+    },
+    nesting => {
+       optional => 1,
+       type => 'boolean',
+       default => 0,
+       description => "Allow nesting."
+           ." Best used with unprivileged containers with additional id mapping."
+           ." Note that this will expose procfs and sysfs contents of the host"
+           ." to the guest.",
+    },
+    keyctl => {
+       optional => 1,
+       type => 'boolean',
+       default => 0,
+       description => "For unprivileged containers only: Allow the use of the keyctl() system call."
+           ." This is required to use docker inside a container."
+           ." By default unprivileged containers will see this system call as non-existent."
+           ." This is mostly a workaround for systemd-networkd, as it will treat it as a fatal"
+           ." error when some keyctl() operations are denied by the kernel due to lacking permissions."
+           ." Essentially, you can choose between running systemd-networkd or docker.",
+    },
+    fuse => {
+       optional => 1,
+       type => 'boolean',
+       default => 0,
+       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 = {
     lock => {
        optional => 1,
        type => 'string',
        description => "Lock/unlock the VM.",
-       enum => [qw(migrate backup snapshot rollback)],
+       enum => [qw(backup create destroyed disk fstrim migrate mounted rollback snapshot snapshot-delete)],
     },
     onboot => {
        optional => 1,
@@ -295,14 +462,14 @@ my $confdesc = {
     arch => {
        optional => 1,
        type => 'string',
-       enum => ['amd64', 'i386'],
+       enum => ['amd64', 'i386', 'arm64', 'armhf'],
        description => "OS architecture type.",
        default => 'amd64',
     },
     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 => {
@@ -324,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 => {
@@ -365,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,
@@ -374,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,
@@ -409,52 +583,68 @@ my $confdesc = {
        description => "Makes the container run as unprivileged user. (Should not be modified manually.)",
        default => 0,
     },
+    features => {
+       optional => 1,
+       type => 'string',
+       format => $features_desc,
+       description => "Allow containers access to advanced features.",
+    },
+    hookscript => {
+       optional => 1,
+       type => 'string',
+       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 = {
+    'lxc.apparmor.profile' => 1,
+    'lxc.apparmor.allow_incomplete' => 1,
+    'lxc.apparmor.allow_nesting' => 1,
+    'lxc.apparmor.raw' => 1,
+    'lxc.selinux.context' => 1,
     'lxc.include' => 1,
     'lxc.arch' => 1,
-    'lxc.utsname' => 1,
-    'lxc.haltsignal' => 1,
-    'lxc.rebootsignal' => 1,
-    'lxc.stopsignal' => 1,
-    'lxc.init_cmd' => 1,
-    'lxc.network.type' => 1,
-    'lxc.network.flags' => 1,
-    'lxc.network.link' => 1,
-    'lxc.network.mtu' => 1,
-    'lxc.network.name' => 1,
-    'lxc.network.hwaddr' => 1,
-    'lxc.network.ipv4' => 1,
-    'lxc.network.ipv4.gateway' => 1,
-    'lxc.network.ipv6' => 1,
-    'lxc.network.ipv6.gateway' => 1,
-    'lxc.network.script.up' => 1,
-    'lxc.network.script.down' => 1,
-    'lxc.pts' => 1,
+    'lxc.uts.name' => 1,
+    'lxc.signal.halt' => 1,
+    'lxc.signal.reboot' => 1,
+    'lxc.signal.stop' => 1,
+    'lxc.init.cmd' => 1,
+    'lxc.pty.max' => 1,
     'lxc.console.logfile' => 1,
-    'lxc.console' => 1,
-    'lxc.tty' => 1,
-    'lxc.devttydir' => 1,
+    'lxc.console.path' => 1,
+    'lxc.tty.max' => 1,
+    'lxc.devtty.dir' => 1,
     'lxc.hook.autodev' => 1,
     'lxc.autodev' => 1,
     'lxc.kmsg' => 1,
-    'lxc.mount' => 1,
+    'lxc.mount.fstab' => 1,
     'lxc.mount.entry' => 1,
     'lxc.mount.auto' => 1,
-    'lxc.rootfs' => 'lxc.rootfs is auto generated from rootfs',
+    'lxc.rootfs.path' => 'lxc.rootfs.path is auto generated from rootfs',
     'lxc.rootfs.mount' => 1,
     'lxc.rootfs.options' => 'lxc.rootfs.options is not supported' .
                             ', please use mount point options in the "rootfs" key',
     # lxc.cgroup.*
-    # lxc.limit.*
+    # lxc.prlimit.*
+    # lxc.net.*
     'lxc.cap.drop' => 1,
     'lxc.cap.keep' => 1,
-    'lxc.aa_profile' => 1,
-    'lxc.aa_allow_incomplete' => 1,
-    'lxc.se_context' => 1,
-    'lxc.seccomp' => 1,
-    'lxc.id_map' => 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,
     'lxc.hook.mount' => 1,
@@ -463,15 +653,78 @@ my $valid_lxc_conf_keys = {
     'lxc.hook.post-stop' => 1,
     'lxc.hook.clone' => 1,
     'lxc.hook.destroy' => 1,
-    'lxc.loglevel' => 1,
-    'lxc.logfile' => 1,
+    'lxc.hook.version' => 1,
+    'lxc.log.level' => 1,
+    'lxc.log.file' => 1,
     'lxc.start.auto' => 1,
     'lxc.start.delay' => 1,
     'lxc.start.order' => 1,
     'lxc.group' => 1,
     'lxc.environment' => 1,
+
+    # All these are namespaced via CLONE_NEWIPC (see namespaces(7)).
+    'lxc.sysctl.fs.mqueue' => 1,
+    'lxc.sysctl.kernel.msgmax' => 1,
+    'lxc.sysctl.kernel.msgmnb' => 1,
+    'lxc.sysctl.kernel.msgmni' => 1,
+    'lxc.sysctl.kernel.sem' => 1,
+    'lxc.sysctl.kernel.shmall' => 1,
+    'lxc.sysctl.kernel.shmmax' => 1,
+    'lxc.sysctl.kernel.shmmni' => 1,
+    'lxc.sysctl.kernel.shm_rmid_forced' => 1,
+};
+
+my $deprecated_lxc_conf_keys = {
+    # Deprecated (removed with lxc 3.0):
+    'lxc.aa_profile'           => 'lxc.apparmor.profile',
+    'lxc.aa_allow_incomplete'  => 'lxc.apparmor.allow_incomplete',
+    'lxc.console'              => 'lxc.console.path',
+    'lxc.devttydir'            => 'lxc.tty.dir',
+    'lxc.haltsignal'           => 'lxc.signal.halt',
+    'lxc.rebootsignal'         => 'lxc.signal.reboot',
+    'lxc.stopsignal'           => 'lxc.signal.stop',
+    'lxc.id_map'               => 'lxc.idmap',
+    'lxc.init_cmd'             => 'lxc.init.cmd',
+    'lxc.loglevel'             => 'lxc.log.level',
+    'lxc.logfile'              => 'lxc.log.file',
+    'lxc.mount'                => 'lxc.mount.fstab',
+    'lxc.network.type'         => 'lxc.net.INDEX.type',
+    'lxc.network.flags'        => 'lxc.net.INDEX.flags',
+    'lxc.network.link'         => 'lxc.net.INDEX.link',
+    'lxc.network.mtu'          => 'lxc.net.INDEX.mtu',
+    'lxc.network.name'         => 'lxc.net.INDEX.name',
+    'lxc.network.hwaddr'       => 'lxc.net.INDEX.hwaddr',
+    'lxc.network.ipv4'         => 'lxc.net.INDEX.ipv4.address',
+    'lxc.network.ipv4.gateway' => 'lxc.net.INDEX.ipv4.gateway',
+    'lxc.network.ipv6'         => 'lxc.net.INDEX.ipv6.address',
+    'lxc.network.ipv6.gateway' => 'lxc.net.INDEX.ipv6.gateway',
+    'lxc.network.script.up'    => 'lxc.net.INDEX.script.up',
+    'lxc.network.script.down'  => 'lxc.net.INDEX.script.down',
+    'lxc.pts'                  => 'lxc.pty.max',
+    'lxc.se_context'           => 'lxc.selinux.context',
+    'lxc.seccomp'              => 'lxc.seccomp.profile',
+    'lxc.tty'                  => 'lxc.tty.max',
+    'lxc.utsname'              => 'lxc.uts.name',
 };
 
+sub is_valid_lxc_conf_key {
+    my ($vmid, $key) = @_;
+    if ($key =~ /^lxc\.limit\./) {
+       warn "vm $vmid - $key: lxc.limit.* was renamed to lxc.prlimit.*\n";
+       return 1;
+    }
+    if (defined(my $new_name = $deprecated_lxc_conf_keys->{$key})) {
+       warn "vm $vmid - $key is deprecated and was renamed to $new_name\n";
+       return 1;
+    }
+    my $validity = $valid_lxc_conf_keys->{$key};
+    return $validity if defined($validity);
+    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;
+}
+
 our $netconf_desc = {
     type => {
        type => 'string',
@@ -492,13 +745,9 @@ our $netconf_desc = {
        pattern => '[-_.\w\d]+',
        optional => 1,
     },
-    hwaddr => {
-       type => 'string',
-       format_description => "XX:XX:XX:XX:XX:XX",
+    hwaddr => get_standard_option('mac-addr', {
         description => 'The interface MAC address. This is dynamically allocated by default, but you can set that statically if needed, for example to always have the same link-local IPv6 address. (lxc.network.hwaddr)',
-       pattern => qr/(?:[a-f0-9]{2}:){5}[a-f0-9]{2}/i,
-       optional => 1,
-    },
+       }),
     mtu => {
        type => 'integer',
        description => 'Maximum transfer unit of the interface. (lxc.network.mtu)',
@@ -508,7 +757,7 @@ our $netconf_desc = {
     ip => {
        type => 'string',
        format => 'pve-ipv4-config',
-       format_description => 'IPv4Format/CIDR',
+       format_description => '(IPv4/CIDR|dhcp|manual)',
        description => 'IPv4 address in CIDR format.',
        optional => 1,
     },
@@ -522,7 +771,7 @@ our $netconf_desc = {
     ip6 => {
        type => 'string',
        format => 'pve-ipv6-config',
-       format_description => 'IPv6Format/CIDR',
+       format_description => '(IPv6/CIDR|auto|dhcp|manual)',
        description => 'IPv6 address in CIDR format.',
        optional => 1,
     },
@@ -561,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,
@@ -570,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) = @_;
@@ -609,33 +867,53 @@ 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);
 
     my $res = {
        digest => Digest::SHA::sha1_hex($raw),
        snapshots => {},
+       pending => {},
+    };
+
+    my $handle_error = sub {
+       my ($msg) = @_;
+
+       if ($strict) {
+           die $msg;
+       } else {
+           warn $msg;
+       }
     };
 
     $filename =~ m|/lxc/(\d+).conf$|
@@ -651,7 +929,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 = '';
@@ -667,26 +951,33 @@ sub parse_pct_config {
        if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) {
            my $key = $1;
            my $value = $3;
-           my $validity = $valid_lxc_conf_keys->{$key} || 0;
-           if ($validity eq 1 || $key =~ m/^lxc\.(?:cgroup|limit)\./) {
+           my $validity = is_valid_lxc_conf_key($vmid, $key);
+           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);
        } elsif ($line =~ m/snapstate:\s*(prepare|delete)\s*$/) {
            $conf->{snapstate} = $1;
-       } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
+       } elsif ($line =~ m/^delete:\s*(.*\S)\s*$/) {
+           my $value = $1;
+           if ($section eq 'pending') {
+               $conf->{delete} = $value;
+           } else {
+               $handle_error->("vm $vmid - property 'delete' is only allowed in [pve:pending]\n");
+           }
+       } 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");
        }
     }
 
@@ -724,7 +1015,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) {
@@ -749,6 +1040,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});
@@ -758,244 +1054,74 @@ sub write_pct_config {
 }
 
 sub update_pct_config {
-    my ($class, $vmid, $conf, $running, $param, $delete) = @_;
+    my ($class, $vmid, $conf, $running, $param, $delete, $revert) = @_;
 
-    my @nohotplug;
+    my $storage_cfg = PVE::Storage::config();
 
-    my $new_disks = 0;
-    my @deleted_volumes;
-
-    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') {
-               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";
-           } 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};
     };
 
-    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 = $class->parse_volume($opt, $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} = PVE::Tools::encode_text($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;
-           }
-           $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};
-           $conf->{$opt} = $value;
-           my $mp = PVE::LXC::Config->parse_ct_rootfs($value);
-           if ($mp->{type} eq 'volume') {
-               &$check_content_type($mp);
-               $used_volids->{$mp->{volume}} = 1;
-           }
-           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;
-       } 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 {
@@ -1048,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 //= '';
@@ -1074,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 {
@@ -1120,6 +1260,12 @@ sub parse_lxc_network {
     return $res;
 }
 
+sub parse_features {
+    my ($class, $data) = @_;
+    return {} if !$data;
+    return PVE::JSONSchema::parse_property_string($features_desc, $data);
+}
+
 sub option_exists {
     my ($class, $name) = @_;
 
@@ -1127,6 +1273,240 @@ 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";
+    };
+
+    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};
+       }
+    }
+
+    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) = @_;
+
+       $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;
+    };
+
+    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') {
+               $cgroup->change_cpu_quota(undef, undef); # reset, cgroup module can better decide values
+           } elsif ($opt eq 'cpuunits') {
+               $cgroup->change_cpu_shares(undef, $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') {
+               my $quota = 100000 * $value;
+               $cgroup->change_cpu_quota(int(100000 * $value), 100000);
+           } elsif ($opt eq 'cpuunits') {
+               $cgroup->change_cpu_shares($value, $confdesc->{cpuunits}->{default});
+           } 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";
+               }
+
+               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};
+           } 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};
+       }
+    }
+}
+
+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_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);
+               }
+           } 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};
+       }
+    }
+}
+
+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_volume($opt, $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_volume($opt, $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_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);
+       }
+    }
+}
+
 sub classify_mountpoint {
     my ($class, $vol) = @_;
     if ($vol =~ m!^/!) {
@@ -1136,24 +1516,37 @@ sub classify_mountpoint {
     return 'volume';
 }
 
-sub is_volume_in_use {
-    my ($class, $config, $volid, $include_snapshots) = @_;
+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;
     });
 
-    my $snapshots = $config->{snapshots};
-    if ($include_snapshots && $snapshots) {
+    return $used;
+};
+
+sub is_volume_in_use_by_snapshots {
+    my ($class, $config, $volid) = @_;
+
+    if (my $snapshots = $config->{snapshots}) {
        foreach my $snap (keys %$snapshots) {
-           $used ||= $class->is_volume_in_use($snapshots->{$snap}, $volid);
+           return 1 if $__is_volume_in_use->($class, $snapshots->{$snap}, $volid);
        }
     }
 
-    return $used;
+    return 0;
+}
+
+sub is_volume_in_use {
+    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;
 }
 
 sub has_dev_console {
@@ -1187,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');
@@ -1199,29 +1592,13 @@ sub mountpoint_names {
     return $reverse ? reverse @names : @names;
 }
 
-sub foreach_mountpoint_full {
-    my ($class, $conf, $reverse, $func, @param) = @_;
-
-    foreach my $key ($class->mountpoint_names($reverse)) {
-       my $value = $conf->{$key};
-       next if !defined($value);
-       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 {
@@ -1229,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;
@@ -1257,8 +1634,14 @@ sub get_replicatable_volumes {
        return if !$volid;
 
        my $mptype = $mountpoint->{type};
-       die "unable to replicate mountpoint type '$mptype'\n"
-           if $mptype ne 'volume';
+       my $replicate = $mountpoint->{replicate} // 1;
+
+       if ($mptype ne 'volume') {
+           # skip bindmounts if replicate = 0 even for cleanup,
+           # since bind mounts could not have been replicated ever
+           return if !$replicate;
+           die "unable to replicate mountpoint type '$mptype'\n";
+       }
 
        my ($storeid, $volname) = PVE::Storage::parse_volume_id($volid, $noerr);
        return if !$storeid;
@@ -1271,7 +1654,7 @@ sub get_replicatable_volumes {
 
        die "unable to replicate volume '$volid', type '$vtype'\n" if $vtype ne 'images';
 
-       return if !$cleanup && defined($mountpoint->{replicate}) && !$mountpoint->{replicate};
+       return if !$cleanup && !$replicate;
 
        if (!PVE::Storage::volume_has_feature($storecfg, 'replicate', $volid)) {
            return if $cleanup || $noerr;
@@ -1281,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);
         });
@@ -1304,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;