]> git.proxmox.com Git - pve-container.git/blobdiff - src/PVE/LXC.pm
fix hardcoded CT uptime in vmstatus
[pve-container.git] / src / PVE / LXC.pm
index 01d031dd9d4c4789ea26327ac5d4be7a39c3ced8..ccd8c27e593adca0266c2fa19a24f34501c1e0e9 100644 (file)
@@ -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<Number>',
+       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=<vmbr<Nummber>>[,hwaddr=<MAC>]\n".
-           "[,mtu=<Number>][,name=<String>][,ip=<IPv4Format/CIDR>]\n".
-           ",ip6=<IPv6Format/CIDR>][,gw=<GatwayIPv4>]\n".
-           ",gw6=<GatwayIPv6>][,firewall=<[1|0]>][,tag=<VlanNo>]",
+       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;
     }