]> 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 98348666b206226631c789a0f364b51f251a6088..2dd57e2b630a318514872183d6f3a16da0163b66 100644 (file)
@@ -2,6 +2,7 @@ package PVE::LXC::Config;
 
 use strict;
 use warnings;
+
 use Fcntl qw(O_RDONLY);
 
 use PVE::AbstractConfig;
@@ -12,10 +13,14 @@ 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};
+use constant {
+    FIFREEZE => 0xc0045877,
+    FITHAW   => 0xc0045878,
+};
 
 my $nodename = PVE::INotify::nodename();
 my $lock_handles =  {};
@@ -87,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;
@@ -101,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);
@@ -139,6 +161,8 @@ sub __snapshot_freeze {
     $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};
        }
@@ -190,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');
     }
 }
 
@@ -212,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 {
@@ -274,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) = @_;
@@ -339,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,
@@ -400,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'),
@@ -419,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 => {
@@ -461,22 +507,25 @@ my $confdesc = {
     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,
     },
@@ -489,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,
@@ -498,7 +549,7 @@ 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 => {
@@ -707,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 => {
@@ -762,6 +814,12 @@ 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);
 
@@ -836,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,
     };
 }
@@ -850,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);
 
@@ -860,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'";
 
@@ -887,7 +956,7 @@ sub parse_pct_config {
            next;
        }
 
-       if ($line =~ m/^\#(.*)\s*$/) {
+       if ($line =~ m/^\#(.*)$/) {
            $descr .= PVE::Tools::decode_text($1) . "\n";
            next;
        }
@@ -899,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);
@@ -912,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");
        }
     }
 
@@ -1051,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);
@@ -1151,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) = @_;
 
@@ -1164,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');
 }
 
@@ -1187,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}) {
@@ -1215,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,
@@ -1251,11 +1355,11 @@ sub vmconfig_hotplug_pending {
     # 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;
     };
@@ -1273,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");
@@ -1300,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);
@@ -1404,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)) {
@@ -1534,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) = @_;
 
@@ -1643,4 +1754,16 @@ sub get_backup_volumes {
     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;