]> 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 1443f8739d10fe165b8bf5004324a5bca3f15bed..2dd57e2b630a318514872183d6f3a16da0163b66 100644 (file)
@@ -3,6 +3,8 @@ 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;
@@ -11,8 +13,15 @@ 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";
@@ -49,13 +58,23 @@ 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 {
@@ -73,10 +92,8 @@ sub has_feature {
        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, $opts);
+       $err = 1 if !PVE::Storage::volume_has_feature(
+           $storecfg, $feature, $mountpoint->{volume}, $snapname, $running, $opts);
     });
 
     return $err ? 0 : 1;
@@ -87,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);
@@ -99,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);
     }
 }
 
@@ -133,7 +214,7 @@ sub __snapshot_delete_remove_drive {
        delete $snap->{$remove_drive};
 
        $class->add_unused_volume($snap, $mountpoint->{volume})
-           if ($mountpoint->{type} eq 'volume');
+           if $mountpoint && ($mountpoint->{type} eq 'volume');
     }
 }
 
@@ -155,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 {
@@ -217,7 +303,7 @@ sub __snapshot_rollback_get_unused {
 cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config);
 
 
-my $valid_mount_option_re = qr/(noatime|nodev|nosuid|noexec)/;
+my $valid_mount_option_re = qr/(noatime|lazytime|nodev|nosuid|noexec)/;
 
 sub is_valid_mount_option {
     my ($option) = @_;
@@ -282,6 +368,23 @@ PVE::JSONSchema::register_standard_option('pve-ct-rootfs', {
     optional => 1,
 });
 
+# IP address with optional interface suffix for link local ipv6 addresses
+PVE::JSONSchema::register_format('lxc-ip-with-ll-iface', \&verify_ip_with_ll_iface);
+sub verify_ip_with_ll_iface {
+    my ($addr, $noerr) = @_;
+
+    if (my ($addr, $iface) = ($addr =~ /^(fe80:[^%]+)%(.*)$/)) {
+       if (PVE::JSONSchema::pve_verify_ip($addr, 1)
+           && PVE::JSONSchema::pve_verify_iface($iface, 1))
+       {
+           return $addr;
+       }
+    }
+
+    return PVE::JSONSchema::pve_verify_ip($addr, $noerr);
+}
+
+
 my $features_desc = {
     mount => {
        optional => 1,
@@ -343,13 +446,13 @@ my $confdesc = {
     lock => {
        optional => 1,
        type => 'string',
-       description => "Lock/unlock the VM.",
+       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'),
@@ -362,14 +465,14 @@ my $confdesc = {
     arch => {
        optional => 1,
        type => 'string',
-       enum => ['amd64', 'i386', 'arm64', 'armhf'],
+       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 => {
@@ -391,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,
     },
@@ -432,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,
@@ -441,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,
@@ -493,6 +606,12 @@ my $confdesc = {
        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 = {
@@ -639,6 +758,7 @@ our $netconf_desc = {
        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 => {
@@ -694,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,
@@ -706,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) = @_;
@@ -759,7 +894,8 @@ 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,
     };
 }
@@ -773,7 +909,7 @@ for (my $i = 0; $i < $MAX_UNUSED_DISKS; $i++) {
 }
 
 sub parse_pct_config {
-    my ($filename, $raw) = @_;
+    my ($filename, $raw, $strict) = @_;
 
     return undef if !defined($raw);
 
@@ -783,6 +919,16 @@ sub parse_pct_config {
        pending => {},
     };
 
+    my $handle_error = sub {
+       my ($msg) = @_;
+
+       if ($strict) {
+           die $msg;
+       } else {
+           warn $msg;
+       }
+    };
+
     $filename =~ m|/lxc/(\d+).conf$|
        || die "got strange filename '$filename'";
 
@@ -810,7 +956,7 @@ sub parse_pct_config {
            next;
        }
 
-       if ($line =~ m/^\#(.*)\s*$/) {
+       if ($line =~ m/^\#(.*)$/) {
            $descr .= PVE::Tools::decode_text($1) . "\n";
            next;
        }
@@ -822,9 +968,9 @@ sub parse_pct_config {
            if ($validity eq 1) {
                push @{$conf->{lxc}}, [$key, $value];
            } elsif (my $errmsg = $validity) {
-               warn "vm $vmid - $key: $errmsg\n";
+               $handle_error->("vm $vmid - $key: $errmsg\n");
            } else {
-               warn "vm $vmid - unable to parse config: $line\n";
+               $handle_error->("vm $vmid - unable to parse config: $line\n");
            }
        } elsif ($line =~ m/^(description):\s*(.*\S)\s*$/) {
            $descr .= PVE::Tools::decode_text($2);
@@ -835,16 +981,16 @@ sub parse_pct_config {
            if ($section eq 'pending') {
                $conf->{delete} = $value;
            } else {
-               warn "vm $vmid - property 'delete' is only allowed in [pve:pending]\n";
+               $handle_error->("vm $vmid - property 'delete' is only allowed in [pve:pending]\n");
            }
-       } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) {
+       } elsif ($line =~ m/^([a-z][a-z_]*\d*):\s*(.+?)\s*$/) {
            my $key = $1;
            my $value = $2;
            eval { $value = PVE::LXC::Config->check_type($key, $value); };
-           warn "vm $vmid - unable to parse value of '$key' - $@" if $@;
+           $handle_error->("vm $vmid - unable to parse value of '$key' - $@") if $@;
            $conf->{$key} = $value;
        } else {
-           warn "vm $vmid - unable to parse config: $line\n";
+           $handle_error->("vm $vmid - unable to parse config: $line\n");
        }
     }
 
@@ -974,6 +1120,16 @@ sub update_pct_config {
            $value = PVE::LXC::verify_searchdomain_list($value);
        } elsif ($opt eq 'unprivileged') {
            die "unable to modify read-only option: '$opt'\n";
+       } 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);
+
+           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);
@@ -1074,6 +1230,13 @@ sub print_ct_mountpoint {
     return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
 }
 
+sub print_ct_unused {
+    my ($class, $info) = @_;
+
+    my $skip = [ 'type' ];
+    return PVE::JSONSchema::print_property_string($info, $unused_desc, $skip);
+}
+
 sub parse_volume {
     my ($class, $key, $volume_string, $noerr) = @_;
 
@@ -1087,12 +1250,16 @@ sub parse_volume {
        return $parse_ct_mountpoint_full->($class, $unused_desc, $volume_string, $noerr);
     }
 
-    die "parse_volume - unknown type: $key\n";
+    die "parse_volume - unknown type: $key\n" if !$noerr;
+
+    return;
 }
 
 sub print_volume {
     my ($class, $key, $volume) = @_;
 
+    return $class->print_ct_unused($volume) if $key =~ m/^unused(\d+)$/;
+
     return $class->print_ct_mountpoint($volume, $key eq 'rootfs');
 }
 
@@ -1110,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}) {
@@ -1138,6 +1303,22 @@ 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,
@@ -1161,30 +1342,24 @@ sub vmconfig_hotplug_pending {
        $errors->{$opt} = "unable to hotplug $opt: $msg";
     };
 
-    my $changes;
     foreach my $opt (sort keys %{$conf->{pending}}) { # add/change
        next if $selection && !$selection->{$opt};
        if ($LXC_FASTPLUG_OPTIONS->{$opt}) {
            $conf->{$opt} = delete $conf->{pending}->{$opt};
-           $changes = 1;
        }
     }
 
-    if ($changes) {
-       $class->write_config($vmid, $conf);
-    }
-
     my $cgroup = PVE::LXC::CGroup->new($vmid);
 
     # There's no separate swap size to configure, there's memory and "total"
     # memory (iow. memory+swap). This means we have to change them together.
     my $hotplug_memory_done;
     my $hotplug_memory = sub {
-       my ($wanted_memory, $wanted_swap) = @_;
+       my ($new_memory, $new_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);
+       ($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;
     };
@@ -1202,9 +1377,9 @@ sub vmconfig_hotplug_pending {
            } elsif ($opt eq 'swap') {
                $hotplug_memory->(undef, 0);
            } elsif ($opt eq 'cpulimit') {
-               $cgroup->change_cpu_quota(-1, 100000);
+               $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});
+               $cgroup->change_cpu_shares(undef);
            } elsif ($opt =~ m/^net(\d)$/) {
                my $netid = $1;
                PVE::Network::veth_delete("veth${vmid}i$netid");
@@ -1229,7 +1404,7 @@ sub vmconfig_hotplug_pending {
                my $quota = 100000 * $value;
                $cgroup->change_cpu_quota(int(100000 * $value), 100000);
            } elsif ($opt eq 'cpuunits') {
-               $cgroup->change_cpu_shares($value, $confdesc->{cpuunits}->{default});
+               $cgroup->change_cpu_shares($value);
            } elsif ($opt =~ m/^net(\d+)$/) {
                my $netid = $1;
                my $net = $class->parse_lxc_network($value);
@@ -1244,6 +1419,10 @@ sub vmconfig_hotplug_pending {
                    die "skip\n";
                }
 
+               if (exists($conf->{$opt})) {
+                   die "skip\n"; # don't try to hotplug over existing mp
+               }
+
                $class->apply_pending_mountpoint($vmid, $conf, $opt, $storecfg, 1);
                # apply_pending_mountpoint modifies the value if it creates a new disk
                $value = $conf->{pending}->{$opt};
@@ -1258,8 +1437,6 @@ sub vmconfig_hotplug_pending {
            delete $conf->{pending}->{$opt};
        }
     }
-
-    $class->write_config($vmid, $conf);
 }
 
 sub vmconfig_apply_pending {
@@ -1316,8 +1493,6 @@ sub vmconfig_apply_pending {
            $conf->{$opt} = delete $conf->{pending}->{$opt};
        }
     }
-
-    $class->write_config($vmid, $conf);
 }
 
 my $rescan_volume = sub {
@@ -1333,40 +1508,38 @@ sub apply_pending_mountpoint {
 
     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) {
+    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;
            }
-           $conf->{pending}->{$opt} = $class->print_ct_mountpoint($mp);
        }
+    } 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)) {
@@ -1463,6 +1636,15 @@ sub valid_volume_keys {
     return $reverse ? reverse @names : @names;
 }
 
+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";
+    }
+    return $reverse ? reverse @names : @names;
+}
+
 sub get_vm_volumes {
     my ($class, $conf, $excludes) = @_;
 
@@ -1549,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;