]> git.proxmox.com Git - pve-container.git/blobdiff - src/PVE/LXC/Config.pm
config: parse_volume: don't die when noerr is set
[pve-container.git] / src / PVE / LXC / Config.pm
index a00a804e7fdba63a49d66e4eb650338f91f3ba27..7db023c17447ceba62df75cef8321a1b500fbc8c 100644 (file)
@@ -2,22 +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 PVE::ReplicationTools;
 
 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
@@ -48,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
@@ -70,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;
@@ -93,15 +113,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);
     }
 }
 
@@ -123,11 +188,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');
     }
 }
 
@@ -149,10 +214,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 {
@@ -165,12 +235,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";
 }
@@ -180,7 +250,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';
@@ -188,7 +258,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;
@@ -204,18 +274,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',
@@ -236,6 +308,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',
@@ -246,7 +325,7 @@ my $rootfs_desc = {
        description => 'Enable user quotas inside the container (not supported with zfs subvolumes)',
        optional => 1,
     },
-    replica => {
+    replicate => {
        type => 'boolean',
        description => 'Will include this volume to a storage replica job.',
        optional => 1,
@@ -267,18 +346,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,
@@ -296,14 +443,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 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 => {
@@ -325,14 +472,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 => {
@@ -366,7 +513,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,
@@ -375,9 +524,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,
@@ -391,31 +545,6 @@ my $confdesc = {
        type => 'integer',
        minimum => 0,
     },
-    replica => {
-       optional => 1,
-       description => "Storage replica for local storage.",
-       type => 'boolean',
-       default => 0,
-    },
-    replica_rate_limit => {
-       optional => 1,
-       description => "Storage replica rate limit in KBytes/s.",
-       type => 'integer',
-       minimum => 1,
-    },
-    replica_target => {
-       optional => 1,
-       description => "Storage replica target node.",
-       type => 'string',
-    },
-    replica_interval => {
-       optional => 1,
-       description => "Storage replica sync interval.",
-       type => 'integer',
-       minimum => 1,
-       maximum => 1440,
-       default => 15,
-    },
     cmode => {
        optional => 1,
        description => "Console mode. By default, the console command tries to open a connection to one of the available tty devices. By setting cmode to 'console' it tries to attach to /dev/console instead. If you set cmode to 'shell', it simply invokes a shell inside the container (no login).",
@@ -435,52 +564,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,
@@ -489,15 +634,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',
@@ -518,13 +726,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)',
@@ -534,7 +738,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,
     },
@@ -548,7 +752,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,
     },
@@ -587,7 +791,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,
@@ -596,6 +800,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) = @_;
@@ -635,23 +848,32 @@ 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 {
@@ -662,6 +884,7 @@ sub parse_pct_config {
     my $res = {
        digest => Digest::SHA::sha1_hex($raw),
        snapshots => {},
+       pending => {},
     };
 
     $filename =~ m|/lxc/(\d+).conf$|
@@ -677,7 +900,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 = '';
@@ -693,8 +922,8 @@ 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";
@@ -705,7 +934,14 @@ sub parse_pct_config {
            $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 {
+               warn "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); };
@@ -750,7 +986,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) {
@@ -775,6 +1011,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});
@@ -784,281 +1025,74 @@ sub write_pct_config {
 }
 
 sub update_pct_config {
-    my ($class, $vmid, $conf, $running, $param, $delete) = @_;
-
-    my @nohotplug;
+    my ($class, $vmid, $conf, $running, $param, $delete, $revert) = @_;
 
-    my $new_disks = 0;
-    my @deleted_volumes;
+    my $storage_cfg = PVE::Storage::config();
 
-    my $rootdir;
-    if ($running) {
-       my $pid = PVE::LXC::find_lxc_pid($vmid);
-       $rootdir = "/proc/$pid/root";
+    foreach my $opt (@$revert) {
+       delete $conf->{pending}->{$opt};
+       $class->remove_from_pending_delete($conf, $opt); # also remove from deletion queue
     }
 
-    my $hotplug_error = sub {
-       if ($running) {
-           push @nohotplug, @_;
-           return 1;
-       } else {
-           return 0;
-       }
-    };
+    # write updates to pending section
+    my $modified = {}; # record modified options
 
-    if (defined($delete)) {
-       foreach my $opt (@$delete) {
-           if (!exists($conf->{$opt})) {
-               # silently ignore
-               next;
-           }
-
-           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";
-           }  elsif ($opt eq "replica" || $opt eq "replica_target") {
-               delete $conf->{$opt};
-               delete $conf->{replica} if $opt eq "replica_target";
-
-               # job_remove required updated lxc conf
-               PVE::LXC::Config->write_config($vmid, $conf);
-               PVE::ReplicationTools::job_remove($vmid);
-               next;
-           } elsif ($opt eq "replica_interval" || $opt eq "replica_rate_limit") {
-               delete $conf->{$opt};
-               PVE::LXC::Config->write_config($vmid, $conf);
-               PVE::ReplicationTools::update_conf($vmid, $opt, $param->{$opt});
-           } else {
-               die "implement me (delete: $opt)"
-           }
-           PVE::LXC::Config->write_config($vmid, $conf) if $running;
+    foreach my $opt (@$delete) {
+       if (!defined($conf->{$opt}) && !defined($conf->{pending}->{$opt})) {
+           warn "cannot delete '$opt' - not set in current configuration!\n";
+           next;
        }
-    }
-
-    # There's no separate swap size to configure, there's memory and "total"
-    # memory (iow. memory+swap). This means we have to change them together.
-    my $wanted_memory = PVE::Tools::extract_param($param, 'memory');
-    my $wanted_swap =  PVE::Tools::extract_param($param, 'swap');
-    if (defined($wanted_memory) || defined($wanted_swap)) {
-
-       my $old_memory = ($conf->{memory} || 512);
-       my $old_swap = ($conf->{swap} || 0);
-
-       $wanted_memory //= $old_memory;
-       $wanted_swap //= $old_swap;
-
-       my $total = $wanted_memory + $wanted_swap;
-       if ($running) {
-           my $old_total = $old_memory + $old_swap;
-           if ($total > $old_total) {
-               PVE::LXC::write_cgroup_value("memory", $vmid,
-                                            "memory.memsw.limit_in_bytes",
-                                            int($total*1024*1024));
-               PVE::LXC::write_cgroup_value("memory", $vmid,
-                                            "memory.limit_in_bytes",
-                                            int($wanted_memory*1024*1024));
-           } else {
-               PVE::LXC::write_cgroup_value("memory", $vmid,
-                                            "memory.limit_in_bytes",
-                                            int($wanted_memory*1024*1024));
-               PVE::LXC::write_cgroup_value("memory", $vmid,
-                                            "memory.memsw.limit_in_bytes",
-                                            int($total*1024*1024));
-           }
+       $modified->{$opt} = 1;
+       if ($opt eq 'memory' || $opt eq 'rootfs' || $opt eq 'ostype') {
+           die "unable to delete required option '$opt'\n";
+       } elsif ($opt =~ m/^unused(\d+)$/) {
+           $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
+       } elsif ($opt =~ m/^mp(\d+)$/) {
+           $class->check_protection($conf, "can't remove CT $vmid drive '$opt'");
+       } elsif ($opt eq 'unprivileged') {
+           die "unable to delete read-only option: '$opt'\n";
        }
-       $conf->{memory} = $wanted_memory;
-       $conf->{swap} = $wanted_swap;
-
-       PVE::LXC::Config->write_config($vmid, $conf) if $running;
+       $class->add_to_pending_delete($conf, $opt);
     }
 
-    my $used_volids = {};
     my $check_content_type = sub {
        my ($mp) = @_;
        my $sid = PVE::Storage::parse_volume_id($mp->{volume});
-       my $scfg = PVE::Storage::config();
-       my $storage_config = PVE::Storage::storage_config($scfg, $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'";
-       my $update;
-       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;
-       } elsif ($opt eq "replica") {
-           die "Not all volumes are syncable, please check your config\n"
-               if !PVE::ReplicationTools::check_guest_volumes_syncable($conf, 'lxc');
-           $conf->{$opt} = $param->{$opt};
-           die "replica_target is required\n" if !$conf->{replica_target}
-           && !$param->{replica_target};
-           $update = 1;
-       } elsif ($opt eq "replica_interval" || $opt eq "replica_rate_limit") {
-           $conf->{$opt} = $param->{$opt};
-           PVE::ReplicationTools::update_conf($vmid, $opt, $param->{$opt});
-           $update = 1;
-       } elsif ($opt eq "replica_target") {
-           die "Node: $param->{$opt} does not exists in Cluster.\n"
-               if !PVE::Cluster::check_node_exists($param->{$opt});
-           $update = 1;
-           PVE::ReplicationTools::update_conf($vmid, $opt, $param->{$opt})
-               if defined($conf->{$opt});
-           $conf->{$opt} = $param->{$opt};
-       } else {
-           die "implement me: $opt";
-       }
-       PVE::LXC::Config->write_config($vmid, $conf) if $running || $update;
-    }
-
-    if (defined($param->{replica})) {
-       if ($param->{replica}) {
-           PVE::ReplicationTools::job_enable($vmid);
-       } else {
-           PVE::ReplicationTools::job_disable($vmid);
        }
+       $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 {
@@ -1111,7 +1145,7 @@ sub json_config_properties {
     return $prop;
 }
 
-sub __parse_ct_mountpoint_full {
+my $parse_ct_mountpoint_full = sub {
     my ($class, $desc, $data, $noerr) = @_;
 
     $data //= '';
@@ -1137,27 +1171,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 {
@@ -1183,6 +1231,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) = @_;
 
@@ -1190,6 +1244,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!^/!) {
@@ -1199,24 +1487,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 {
@@ -1250,7 +1551,7 @@ sub get_cmode {
     return $conf->{cmode} // $confdesc->{cmode}->{default};
 }
 
-sub mountpoint_names {
+sub valid_volume_keys {
     my ($class, $reverse) = @_;
 
     my @names = ('rootfs');
@@ -1262,29 +1563,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 {
@@ -1292,7 +1577,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;
@@ -1309,4 +1594,91 @@ sub get_vm_volumes {
     return $vollist;
 }
 
-return 1;
+sub get_replicatable_volumes {
+    my ($class, $storecfg, $vmid, $conf, $cleanup, $noerr) = @_;
+
+    my $volhash = {};
+
+    my $test_volid = sub {
+       my ($volid, $mountpoint) = @_;
+
+       return if !$volid;
+
+       my $mptype = $mountpoint->{type};
+       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;
+
+       my $scfg = PVE::Storage::storage_config($storecfg, $storeid);
+       return if $scfg->{shared};
+
+       my ($path, $owner, $vtype) = PVE::Storage::path($storecfg, $volid);
+       return if !$owner || ($owner != $vmid);
+
+       die "unable to replicate volume '$volid', type '$vtype'\n" if $vtype ne 'images';
+
+       return if !$cleanup && !$replicate;
+
+       if (!PVE::Storage::volume_has_feature($storecfg, 'replicate', $volid)) {
+           return if $cleanup || $noerr;
+           die "missing replicate feature on volume '$volid'\n";
+       }
+
+       $volhash->{$volid} = 1;
+    };
+
+    $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_volume($snap, sub {
+           my ($ms, $mountpoint) = @_;
+           $test_volid->($mountpoint->{volume}, $mountpoint);
+        });
+    }
+
+    # add 'unusedX' volumes to volhash
+    foreach my $key (keys %$conf) {
+       if ($key =~ m/^unused/) {
+           $test_volid->($conf->{$key}, { type => 'volume', replicate => 1 });
+       }
+    }
+
+    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;