X-Git-Url: https://git.proxmox.com/?a=blobdiff_plain;f=src%2FPVE%2FLXC.pm;h=ccd8c27e593adca0266c2fa19a24f34501c1e0e9;hb=88a8696be455a3f22a8813472d2428dfa62a9c30;hp=01d031dd9d4c4789ea26327ac5d4be7a39c3ced8;hpb=23e3abef768523fee18ed5ba72d1e63c2be9ec8b;p=pve-container.git diff --git a/src/PVE/LXC.pm b/src/PVE/LXC.pm index 01d031d..ccd8c27 100644 --- a/src/PVE/LXC.pm +++ b/src/PVE/LXC.pm @@ -5,6 +5,8 @@ use warnings; use POSIX qw(EINTR); use File::Path; +use File::Spec; +use Cwd qw(); use Fcntl ':flock'; use PVE::Cluster qw(cfs_register_file cfs_read_file); @@ -23,31 +25,30 @@ my $nodename = PVE::INotify::nodename(); cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config); -PVE::JSONSchema::register_format('pve-lxc-network', \&verify_lxc_network); -sub verify_lxc_network { - my ($value, $noerr) = @_; - - return $value if parse_lxc_network($value); - - return undef if $noerr; - - die "unable to parse network setting\n"; -} - -PVE::JSONSchema::register_format('pve-ct-mountpoint', \&verify_ct_mountpoint); -sub verify_ct_mountpoint { - my ($value, $noerr) = @_; - - return $value if parse_ct_mountpoint($value); - - return undef if $noerr; - - die "unable to parse CT mountpoint options\n"; -} +my $rootfs_desc = { + volume => { + type => 'string', + default_key => 1, + format_description => 'volume', + description => 'Volume, device or directory to mount into the container.', + }, + backup => { + type => 'boolean', + format_description => '[1|0]', + description => 'Whether to include the mountpoint in backups.', + optional => 1, + }, + size => { + type => 'string', + format_description => 'DiskSize', + pattern => '\d+[TGMK]?', + description => 'Volume size (read only value).', + optional => 1, + }, +}; PVE::JSONSchema::register_standard_option('pve-ct-rootfs', { - type => 'string', format => 'pve-ct-mountpoint', - typetext => '[volume=]volume,] [,backup=yes|no] [,size=\d+]', + type => 'string', format => $rootfs_desc, description => "Use volume as container root.", optional => 1, }); @@ -138,7 +139,7 @@ my $confdesc = { hostname => { optional => 1, description => "Set a host name for the container.", - type => 'string', + type => 'string', format => 'dns-name', maxLength => 255, }, description => { @@ -148,12 +149,12 @@ my $confdesc = { }, searchdomain => { optional => 1, - type => 'string', + type => 'string', format => 'dns-name-list', description => "Sets DNS search domains for a container. Create will automatically use the setting from the host if you neither set searchdomain or nameserver.", }, nameserver => { optional => 1, - type => 'string', + type => 'string', format => 'address-list', description => "Sets DNS server IP address for a container. Create will automatically use the setting from the host if you neither set searchdomain or nameserver.", }, rootfs => get_standard_option('pve-ct-rootfs'), @@ -179,7 +180,7 @@ my $confdesc = { protection => { optional => 1, type => 'boolean', - description => "Sets the protection flag of the container. This will prevent the remove operation.", + description => "Sets the protection flag of the container. This will prevent the remove operation. This will prevent the CT or CT's disk remove/update operation.", default => 0, }, }; @@ -246,26 +247,108 @@ my $valid_lxc_conf_keys = { 'lxc.' => 1, }; +my $netconf_desc = { + type => { + type => 'string', + optional => 1, + description => "Network interface type.", + enum => [qw(veth)], + }, + name => { + type => 'string', + format_description => 'String', + description => 'Name of the network device as seen from inside the container. (lxc.network.name)', + pattern => '[-_.\w\d]+', + }, + bridge => { + type => 'string', + format_description => 'vmbr', + description => 'Bridge to attach the network device to.', + pattern => '[-_.\w\d]+', + }, + hwaddr => { + type => 'string', + format_description => 'MAC', + description => 'Bridge to attach the network device to. (lxc.network.hwaddr)', + pattern => qr/(?:[a-f0-9]{2}:){5}[a-f0-9]{2}/i, + optional => 1, + }, + mtu => { + type => 'integer', + format_description => 'Number', + description => 'Maximum transfer unit of the interface. (lxc.network.mtu)', + optional => 1, + }, + ip => { + type => 'string', + format => 'pve-ipv4-config', + format_description => 'IPv4Format/CIDR', + description => 'IPv4 address in CIDR format.', + optional => 1, + }, + gw => { + type => 'string', + format => 'ipv4', + format_description => 'GatewayIPv4', + description => 'Default gateway for IPv4 traffic.', + optional => 1, + }, + ip6 => { + type => 'string', + format => 'pve-ipv6-config', + format_description => 'IPv6Format/CIDR', + description => 'IPv6 address in CIDR format.', + optional => 1, + }, + gw6 => { + type => 'string', + format => 'ipv6', + format_description => 'GatewayIPv6', + description => 'Default gateway for IPv6 traffic.', + optional => 1, + }, + firewall => { + type => 'boolean', + format_description => '[1|0]', + description => "Controls whether this interface's firewall rules should be used.", + optional => 1, + }, + tag => { + type => 'integer', + format_description => 'VlanNo', + minimum => '2', + maximum => '4094', + description => "VLAN tag foro this interface.", + optional => 1, + }, +}; +PVE::JSONSchema::register_format('pve-lxc-network', $netconf_desc); + my $MAX_LXC_NETWORKS = 10; for (my $i = 0; $i < $MAX_LXC_NETWORKS; $i++) { $confdesc->{"net$i"} = { optional => 1, - type => 'string', format => 'pve-lxc-network', - description => "Specifies network interfaces for the container.\n\n". - "The string should have the follow format:\n\n". - "-net<[0-9]> bridge=>[,hwaddr=]\n". - "[,mtu=][,name=][,ip=]\n". - ",ip6=][,gw=]\n". - ",gw6=][,firewall=<[1|0]>][,tag=]", + type => 'string', format => $netconf_desc, + description => "Specifies network interfaces for the container.", }; } +my $mp_desc = { + %$rootfs_desc, + mp => { + type => 'string', + format_description => 'Path', + description => 'Path to the mountpoint as seen from inside the container.', + optional => 1, + }, +}; +PVE::JSONSchema::register_format('pve-ct-mountpoint', $mp_desc); + my $MAX_MOUNT_POINTS = 10; for (my $i = 0; $i < $MAX_MOUNT_POINTS; $i++) { $confdesc->{"mp$i"} = { optional => 1, - type => 'string', format => 'pve-ct-mountpoint', - typetext => '[volume=]volume,] [,backup=yes|no] [,size=\d+] [,mp=mountpoint]', + type => 'string', format => $mp_desc, description => "Use volume as container mount point (experimental feature).", optional => 1, }; @@ -290,7 +373,9 @@ sub write_pct_config { foreach my $key (sort keys %$conf) { next if $key eq 'digest' || $key eq 'description' || $key eq 'pending' || $key eq 'snapshots' || $key eq 'snapname' || $key eq 'lxc'; - $raw .= "$key: $conf->{$key}\n"; + my $value = $conf->{$key}; + die "detected invalid newline inside property '$key'\n" if $value =~ m/\n/; + $raw .= "$key: $value\n"; } if (my $lxcconf = $conf->{lxc}) { @@ -385,7 +470,7 @@ sub parse_pct_config { next; } - if ($line =~ m/^(lxc\.[a-z0-9_\.]+)(:|\s*=)\s*(.*?)\s*$/) { + if ($line =~ m/^(lxc\.[a-z0-9_\-\.]+)(:|\s*=)\s*(.*?)\s*$/) { my $key = $1; my $value = $3; if ($valid_lxc_conf_keys->{$key} || $key =~ m/^lxc\.cgroup\./) { @@ -397,7 +482,7 @@ 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/^([a-z][a-z_]*\d*):\s*(\S.*)\s*$/) { my $key = $1; my $value = $2; eval { $value = check_type($key, $value); }; @@ -720,7 +805,9 @@ sub vmstatus { my $d = $list->{$vmid}; next if $d->{status} ne 'running'; - $d->{uptime} = 100; # fixme: + my $pid = find_lxc_pid($vmid); + my $ctime = (stat("/proc/$pid"))[10]; # 10 = ctime + $d->{uptime} = time - $ctime; # the method lxcfs uses $d->{mem} = read_cgroup_value('memory', $vmid, 'memory.usage_in_bytes'); $d->{swap} = read_cgroup_value('memory', $vmid, 'memory.memsw.usage_in_bytes') - $d->{mem}; @@ -777,28 +864,15 @@ sub parse_ct_mountpoint { $data //= ''; - my $res = {}; - - foreach my $p (split (/,/, $data)) { - next if $p =~ m/^\s*$/; - - if ($p =~ m/^(volume|backup|size|mp)=(.+)$/) { - my ($k, $v) = ($1, $2); - return undef if defined($res->{$k}); - $res->{$k} = $v; - } else { - if (!$res->{volume} && $p !~ m/=/) { - $res->{volume} = $p; - } else { - return undef; - } - } + my $res; + eval { $res = PVE::JSONSchema::parse_property_string($mp_desc, $data) }; + if ($@) { + warn $@; + return undef; } return undef if !defined($res->{volume}); - return undef if $res->{backup} && $res->{backup} !~ m/^(yes|no)$/; - if ($res->{size}) { return undef if !defined($res->{size} = &$parse_size($res->{size})); } @@ -848,12 +922,10 @@ sub parse_lxc_network { return $res if !$data; - foreach my $pv (split (/,/, $data)) { - if ($pv =~ m/^(bridge|hwaddr|mtu|name|ip|ip6|gw|gw6|firewall|tag)=(\S+)$/) { - $res->{$1} = $2; - } else { - return undef; - } + eval { $res = PVE::JSONSchema::parse_property_string($netconf_desc, $data) }; + if ($@) { + warn $@; + return undef; } $res->{type} = 'veth'; @@ -977,6 +1049,14 @@ sub check_lock { die "VM is locked ($conf->{'lock'})\n" if $conf->{'lock'}; } +sub check_protection { + my ($vm_conf, $err_msg) = @_; + + if ($vm_conf->{protection}) { + die "$err_msg - protection mode enabled\n"; + } +} + sub update_lxc_config { my ($storage_cfg, $vmid, $conf) = @_; @@ -1009,6 +1089,9 @@ sub update_lxc_config { my $ttycount = get_tty_count($conf); $raw .= "lxc.tty = $ttycount\n"; + # some init scripts expects a linux terminal (turnkey). + $raw .= "lxc.environment = TERM=linux\n"; + my $utsname = $conf->{hostname} || "CT$vmid"; $raw .= "lxc.utsname = $utsname\n"; @@ -1096,7 +1179,7 @@ sub update_pct_config { my @nohotplug; - my $new_disks = []; + my $new_disks = 0; my $rootdir; if ($running) { @@ -1126,11 +1209,10 @@ sub update_pct_config { } elsif ($opt eq 'protection') { delete $conf->{$opt}; } elsif ($opt =~ m/^mp(\d+)$/) { + check_protection($conf, "can't remove CT $vmid drive '$opt'"); delete $conf->{$opt}; push @nohotplug, $opt; next if $running; - } elsif ($opt eq 'rootfs') { - die "implement me" } else { die "implement me" } @@ -1200,11 +1282,13 @@ sub update_pct_config { } elsif ($opt eq 'protection') { $conf->{$opt} = $value ? 1 : 0; } elsif ($opt =~ m/^mp(\d+)$/) { + check_protection($conf, "can't update CT $vmid drive '$opt'"); $conf->{$opt} = $value; - push @$new_disks, $opt; + $new_disks = 1; push @nohotplug, $opt; next; } elsif ($opt eq 'rootfs') { + check_protection($conf, "can't update CT $vmid drive '$opt'"); die "implement me: $opt"; } else { die "implement me: $opt"; @@ -1216,11 +1300,9 @@ sub update_pct_config { die "unable to modify " . join(',', @nohotplug) . " while container is running\n"; } - if (@$new_disks) { + if ($new_disks) { my $storage_cfg = PVE::Storage::config(); create_disks($storage_cfg, $vmid, $conf, $conf); - mount_all($vmid, $storage_cfg, $conf, $new_disks, 1); - umount_all($vmid, $storage_cfg, $conf, 0); } } @@ -1292,6 +1374,12 @@ sub destroy_lxc_container { foreach_mountpoint($conf, sub { my ($ms, $mountpoint) = @_; + + # skip bind mounts and block devices + if ($mountpoint->{volume} =~ m|^/|) { + return; + } + my ($vtype, $name, $owner) = PVE::Storage::parse_volname($storage_cfg, $mountpoint->{volume}); PVE::Storage::vdisk_free($storage_cfg, $mountpoint->{volume}) if $vmid == $owner; }); @@ -1635,20 +1723,19 @@ sub snapshot_create { my $conf = load_config($vmid); - my $cmd = "/usr/bin/lxc-freeze -n $vmid"; my $running = check_running($vmid); eval { if ($running) { - PVE::Tools::run_command($cmd); + PVE::Tools::run_command(['/usr/bin/lxc-freeze', '-n', $vmid]); + PVE::Tools::run_command(['/bin/sync']); }; my $storecfg = PVE::Storage::config(); my $rootinfo = parse_ct_mountpoint($conf->{rootfs}); my $volid = $rootinfo->{volume}; - $cmd = "/usr/bin/lxc-unfreeze -n $vmid"; if ($running) { - PVE::Tools::run_command($cmd); + PVE::Tools::run_command(['/usr/bin/lxc-unfreeze', '-n', $vmid]); }; PVE::Storage::volume_snapshot($storecfg, $volid, $snapname); @@ -1820,6 +1907,19 @@ sub mountpoint_names { return $reverse ? reverse @names : @names; } +# The container might have *different* symlinks than the host. realpath/abs_path +# use the actual filesystem to resolve links. +sub sanitize_mountpoint { + my ($mp) = @_; + $mp = '/' . $mp; # we always start with a slash + $mp =~ s@/{2,}@/@g; # collapse sequences of slashes + $mp =~ s@/\./@@g; # collapse /./ + $mp =~ s@/\.(/)?$@$1@; # collapse a trailing /. or /./ + $mp =~ s@(.*)/[^/]+/\.\./@$1/@g; # collapse /../ without regard for symlinks + $mp =~ s@/\.\.(/)?$@$1@; # collapse trailing /.. or /../ disregarding symlinks + return $mp; +} + sub foreach_mountpoint_full { my ($conf, $reverse, $func) = @_; @@ -1827,7 +1927,14 @@ sub foreach_mountpoint_full { my $value = $conf->{$key}; next if !defined($value); my $mountpoint = parse_ct_mountpoint($value); - $mountpoint->{mp} = '/' if $key eq 'rootfs'; # just to be sure + + # just to be sure: rootfs is / + my $path = $key eq 'rootfs' ? '/' : $mountpoint->{mp}; + $mountpoint->{mp} = sanitize_mountpoint($path); + + $path = $mountpoint->{volume}; + $mountpoint->{volume} = sanitize_mountpoint($path) if $path =~ m|^/|; + &$func($key, $mountpoint); } } @@ -1853,7 +1960,7 @@ sub check_ct_modify_config_perm { if ($opt eq 'cpus' || $opt eq 'cpuunits' || $opt eq 'cpulimit') { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.CPU']); - } elsif ($opt eq 'disk') { + } elsif ($opt eq 'rootfs' || $opt =~ /^mp\d+$/) { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Disk']); } elsif ($opt eq 'memory' || $opt eq 'swap') { $rpcenv->check_vm_perm($authuser, $vmid, $pool, ['VM.Config.Memory']); @@ -1901,7 +2008,7 @@ sub umount_all { } sub mount_all { - my ($vmid, $storage_cfg, $conf, $mkdirs) = @_; + my ($vmid, $storage_cfg, $conf) = @_; my $rootdir = "/var/lib/lxc/$vmid/rootfs"; File::Path::make_path($rootdir); @@ -1924,7 +2031,6 @@ sub mount_all { die "unable to mount base volume - internal error" if $isBase; - File::Path::make_path "$rootdir/$mount" if $mkdirs; mountpoint_mount($mountpoint, $rootdir, $storage_cfg); }); }; @@ -1943,6 +2049,15 @@ sub mountpoint_mount_path { return mountpoint_mount($mountpoint, undef, $storage_cfg, $snapname); } +my $check_mount_path = sub { + my ($path) = @_; + $path = File::Spec->canonpath($path); + my $real = Cwd::realpath($path); + if ($real ne $path) { + die "mount path modified by symlink: $path != $real"; + } +}; + # use $rootdir = undef to just return the corresponding mount path sub mountpoint_mount { my ($mountpoint, $rootdir, $storage_cfg, $snapname) = @_; @@ -1958,6 +2073,7 @@ sub mountpoint_mount { $rootdir =~ s!/+$!!; $mount_path = "$rootdir/$mount"; $mount_path =~ s!/+!/!g; + &$check_mount_path($mount_path); File::Path::mkpath($mount_path); } @@ -1979,7 +2095,7 @@ sub mountpoint_mount { if ($scfg->{type} eq 'zfspool') { my $path_arg = $path; $path_arg =~ s!^/+!!; - PVE::Tools::run_command(['mount', '-o', 'ro', '-t', 'zfs', $path_arg, $mount_path]); + PVE::Tools::run_command(['mount', '-o', 'ro,noload', '-t', 'zfs', $path_arg, $mount_path]); } else { die "cannot mount subvol snapshots for storage type '$scfg->{type}'\n"; } @@ -2001,7 +2117,7 @@ sub mountpoint_mount { } if ($mount_path) { if ($isBase || defined($snapname)) { - PVE::Tools::run_command(['mount', '-o', "ro", @extra_opts, $path, $mount_path]); + PVE::Tools::run_command(['mount', '-o', 'ro,noload', @extra_opts, $path, $mount_path]); } else { PVE::Tools::run_command(['mount', @extra_opts, $path, $mount_path]); } @@ -2014,6 +2130,7 @@ sub mountpoint_mount { PVE::Tools::run_command(['mount', $volid, $mount_path]) if $mount_path; return wantarray ? ($volid, 0) : $volid; } elsif ($volid !~ m|^/dev/.+| && $volid =~ m|^/.+| && -d $volid) { + &$check_mount_path($volid); PVE::Tools::run_command(['mount', '-o', 'bind', $volid, $mount_path]) if $mount_path; return wantarray ? ($volid, 0) : $volid; }