]> git.proxmox.com Git - pve-container.git/blobdiff - src/PVE/LXC/Config.pm
config: implement method to calculate derived properties from a config
[pve-container.git] / src / PVE / LXC / Config.pm
index d49b67427f1c064616f942f1dceec4d6b8400c58..2dd57e2b630a318514872183d6f3a16da0163b66 100644 (file)
@@ -3,20 +3,31 @@ 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 PVE::LXC;
+
 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,29 +58,42 @@ 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
        return if $backup_only && !$class->mountpoint_backup_enabled($ms, $mountpoint);
 
-       $err = 1
-           if !PVE::Storage::volume_has_feature($storecfg, $feature,
-                                                $mountpoint->{volume},
-                                                $snapname, $running);
+       $err = 1 if !PVE::Storage::volume_has_feature(
+           $storecfg, $feature, $mountpoint->{volume}, $snapname, $running, $opts);
     });
 
     return $err ? 0 : 1;
@@ -80,6 +104,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 +135,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 +210,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 +236,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 {
@@ -169,7 +262,7 @@ sub __snapshot_rollback_vm_stop {
 }
 
 sub __snapshot_rollback_vm_start {
-    my ($class, $vmid, $vmstate, $forcemachine);
+    my ($class, $vmid, $vmstate, $data);
 
     die "implement me - save vmstate\n";
 }
@@ -179,7 +272,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 +280,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 +296,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 +330,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,23 +368,91 @@ 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(mounted migrate backup snapshot rollback)],
+       description => "Lock/unlock the container.",
+       enum => [qw(backup create destroyed disk fstrim migrate mounted rollback snapshot snapshot-delete)],
     },
     onboot => {
        optional => 1,
        type => 'boolean',
-       description => "Specifies whether a VM will be started during system bootup.",
+       description => "Specifies whether a container will be started during system bootup.",
        default => 0,
     },
     startup => get_standard_option('pve-startup-order'),
@@ -295,14 +465,14 @@ my $confdesc = {
     arch => {
        optional => 1,
        type => 'string',
-       enum => ['amd64', 'i386'],
+       enum => ['amd64', 'i386', 'arm64', 'armhf', 'riscv32', 'riscv64'],
        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,35 +494,38 @@ 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 => {
        optional => 1,
        type => 'integer',
-       description => "CPU weight for a VM. Argument is used in the kernel fair scheduler. The larger the number is, the more CPU time this VM gets. Number is relative to the weights of all the other running VMs.\n\nNOTE: You can disable fair-scheduler configuration by setting this to 0.",
+       description => "CPU weight for a container, will be clamped to [1, 10000] in cgroup v2.",
+       verbose_description => "CPU weight for a container. Argument is used in the kernel fair "
+           ."scheduler. The larger the number is, the more CPU time this container gets. Number "
+           ."is relative to the weights of all the other running guests.",
        minimum => 0,
        maximum => 500000,
-       default => 1024,
+       default => 'cgroup v1: 1024, cgroup v2: 100',
     },
     memory => {
        optional => 1,
        type => 'integer',
-       description => "Amount of RAM for the VM in MB.",
+       description => "Amount of RAM for the container in MB.",
        minimum => 16,
        default => 512,
     },
     swap => {
        optional => 1,
        type => 'integer',
-       description => "Amount of SWAP for the VM in MB.",
+       description => "Amount of SWAP for the container in MB.",
        minimum => 0,
        default => 512,
     },
@@ -365,7 +538,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 +549,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,11 +589,36 @@ 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,
@@ -439,9 +644,12 @@ my $valid_lxc_conf_keys = {
                             ', please use mount point options in the "rootfs" key',
     # lxc.cgroup.*
     # lxc.prlimit.*
+    # lxc.net.*
     '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,
@@ -451,6 +659,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,
@@ -458,6 +667,17 @@ my $valid_lxc_conf_keys = {
     '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 = {
@@ -505,7 +725,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;
@@ -531,17 +751,14 @@ 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)',
        minimum => 64, # minimum ethernet frame is 64 bytes
+       maximum => 65535,
        optional => 1,
     },
     ip => {
@@ -597,10 +814,16 @@ our $netconf_desc = {
        description => "Apply rate limiting to the interface",
        optional => 1,
     },
+    # TODO: Rename this option and the qemu-server one to `link-down` for PVE 8.0
+    link_down => {
+       type => 'boolean',
+       description => 'Whether this interface should be disconnected (like pulling the plug).',
+       optional => 1,
+    },
 };
 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,
@@ -609,6 +832,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) = @_;
@@ -648,33 +880,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$|
@@ -690,7 +942,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 = '';
@@ -698,7 +956,7 @@ sub parse_pct_config {
            next;
        }
 
-       if ($line =~ m/^\#(.*)\s*$/) {
+       if ($line =~ m/^\#(.*)$/) {
            $descr .= PVE::Tools::decode_text($1) . "\n";
            next;
        }
@@ -710,22 +968,29 @@ 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);
        } 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");
        }
     }
 
@@ -763,7 +1028,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) {
@@ -788,6 +1053,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});
@@ -797,259 +1067,84 @@ 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') {
-               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};
     };
 
-    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 = $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} = $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;
-       } else {
-           die "implement me: $opt";
-       }
-
-       PVE::LXC::Config->write_config($vmid, $conf) if $running;
-    }
+       } elsif ($opt eq 'tags') {
+           $value = PVE::GuestHelpers::get_unique_tags($value);
+       } elsif ($opt =~ m/^net(\d+)$/) {
+           my $res = PVE::JSONSchema::parse_property_string($netconf_desc, $value);
 
-    # 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);
+           if (my $mtu = $res->{mtu}) {
+               my $bridge_mtu = PVE::Network::read_bridge_mtu($res->{bridge});
+               die "$opt: MTU size '$mtu' is bigger than bridge MTU '$bridge_mtu'\n"
+                   if ($mtu > $bridge_mtu);
+           }
        }
+       $conf->{pending}->{$opt} = $value;
+       $class->remove_from_pending_delete($conf, $opt);
     }
 
-    if ($new_disks) {
-       my $storage_cfg = PVE::Storage::config();
-       PVE::LXC::create_disks($storage_cfg, $vmid, $conf, $conf);
-    }
+    my $changes = $class->cleanup_pending($conf);
 
-    # This should be the last thing we do here
-    if ($running && scalar(@nohotplug)) {
-       die "unable to modify " . join(',', @nohotplug) . " while container is running\n";
+    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);
     }
+
+    return $errors;
 }
 
 sub check_type {
@@ -1102,7 +1197,7 @@ sub json_config_properties {
     return $prop;
 }
 
-sub __parse_ct_mountpoint_full {
+my $parse_ct_mountpoint_full = sub {
     my ($class, $desc, $data, $noerr) = @_;
 
     $data //= '';
@@ -1128,27 +1223,50 @@ 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);
+}
+
+sub print_ct_unused {
+    my ($class, $info) = @_;
 
-    my $res =  $class->__parse_ct_mountpoint_full($rootfs_desc, $data, $noerr);
+    my $skip = [ 'type' ];
+    return PVE::JSONSchema::print_property_string($info, $unused_desc, $skip);
+}
 
-    $res->{mp} = '/' if defined($res);
+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);
+    }
 
-    return $res;
+    die "parse_volume - unknown type: $key\n" if !$noerr;
+
+    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_unused($volume) if $key =~ m/^unused(\d+)$/;
+
+    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 {
@@ -1159,11 +1277,9 @@ sub print_lxc_network {
 sub parse_lxc_network {
     my ($class, $data) = @_;
 
-    my $res = {};
-
-    return $res if !$data;
+    return {} if !$data;
 
-    $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data);
+    my $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data);
 
     $res->{type} = 'veth';
     if (!$res->{hwaddr}) {
@@ -1174,6 +1290,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) = @_;
 
@@ -1181,6 +1303,254 @@ sub option_exists {
 }
 # END JSON config code
 
+# takes a max memory value as KiB and returns an tuple with max and high values
+sub calculate_memory_constraints {
+    my ($memory) = @_;
+
+    return if !defined($memory);
+
+    # cgroup memory usage is limited by the hard 'max' limit (OOM-killer enforced) and the soft
+    # 'high' limit (cgroup processes get throttled and put under heavy reclaim pressure).
+    my $memory_max = int($memory * 1024 * 1024);
+    # Set the high to 1016/1024 (~99.2%) of the 'max' hard limit clamped to 128 MiB max, to scale
+    # it for the lower range while having a decent 2^x based rest for 2^y memory configs.
+    my $memory_high = $memory >= 16 * 1024 ? int(($memory - 128) * 1024 * 1024) : int($memory * 1024 * 1016);
+
+    return ($memory_max, $memory_high);
+}
+
+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 ($new_memory, $new_swap) = @_;
+
+       ($new_memory, my $new_memory_high) = calculate_memory_constraints($new_memory);
+       $new_swap = int($new_swap * 1024 * 1024) if defined($new_swap);
+       $cgroup->change_memory_limit($new_memory, $new_swap, $new_memory_high);
+
+       $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);
+           } 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);
+           } 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' && $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 $mp->{type} eq 'volume';
+       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!^/!) {
@@ -1190,11 +1560,11 @@ sub classify_mountpoint {
     return 'volume';
 }
 
-my $is_volume_in_use = sub {
+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;
@@ -1208,17 +1578,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;
 }
 
@@ -1253,7 +1624,7 @@ sub get_cmode {
     return $conf->{cmode} // $confdesc->{cmode}->{default};
 }
 
-sub mountpoint_names {
+sub valid_volume_keys {
     my ($class, $reverse) = @_;
 
     my @names = ('rootfs');
@@ -1265,29 +1636,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 {
@@ -1295,7 +1650,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;
@@ -1353,14 +1708,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);
         });
@@ -1376,4 +1731,39 @@ 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;
+}
+
+sub get_derived_property {
+    my ($class, $conf, $name) = @_;
+
+    if ($name eq 'max-cpu') {
+       return $conf->{cpulimit} || $conf->{cores} || 0;
+    } elsif ($name eq 'max-memory') {
+       return ($conf->{memory} || 512) * 1024 * 1024;
+    } else {
+       die "unknown derived property - $name\n";
+    }
+}
+
 1;