]> git.proxmox.com Git - pve-container.git/blobdiff - src/PVE/LXC.pm
warn when trying to delete non-existent options
[pve-container.git] / src / PVE / LXC.pm
index fd9a1afd76d268b3d73e55ce4891b68a439f94fb..594a5ce7d71aabd717f32647ea3f764cc70a4438 100644 (file)
@@ -26,6 +26,12 @@ my $nodename = PVE::INotify::nodename();
 
 my $cpuinfo= PVE::ProcFSTools::read_cpuinfo();
 
+our $COMMON_TAR_FLAGS = [ '--sparse', '--numeric-owner', '--acls',
+                          '--xattrs',
+                          '--xattrs-include=user.*',
+                          '--xattrs-include=security.capability',
+                          '--warning=no-xattr-write' ];
+
 cfs_register_file('/lxc/', \&parse_pct_config, \&write_pct_config);
 
 my $rootfs_desc = {
@@ -186,6 +192,12 @@ my $confdesc = {
        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,
     },
+    unprivileged => {
+       optional => 1,
+       type => 'boolean',
+       description => "Makes the container run as unprivileged user. (Should not be modified manually.)",
+       default => 0,
+    },
 };
 
 my $valid_lxc_conf_keys = {
@@ -890,6 +902,15 @@ sub vmstatus {
     return $list;
 }
 
+sub classify_mountpoint {
+    my ($vol) = @_;
+    if ($vol =~ m!^/!) {
+       return 'device' if $vol =~ m!^/dev/!;
+       return 'bind';
+    }
+    return 'volume';
+}
+
 sub parse_ct_mountpoint {
     my ($data, $noerr) = @_;
 
@@ -911,12 +932,15 @@ sub parse_ct_mountpoint {
        $res->{size} = $size;
     }
 
+    $res->{type} = classify_mountpoint($res->{volume});
+
     return $res;
 }
 
 sub print_ct_mountpoint {
     my ($info, $nomp) = @_;
-    my $skip = $nomp ? ['mp'] : [];
+    my $skip = [ 'type' ];
+    push @$skip, 'mp' if $nomp;
     return PVE::JSONSchema::print_property_string($info, $mp_desc, $skip);
 }
 
@@ -1044,13 +1068,27 @@ sub update_lxc_config {
     die "missing 'arch' - internal error" if !$conf->{arch};
     $raw .= "lxc.arch = $conf->{arch}\n";
 
+    my $unprivileged = $conf->{unprivileged};
+    my $custom_idmap = grep { $_->[0] eq 'lxc.id_map' } @{$conf->{lxc}};
+
     my $ostype = $conf->{ostype} || die "missing 'ostype' - internal error";
     if ($ostype =~ /^(?:debian | ubuntu | centos | archlinux)$/x) {
        $raw .= "lxc.include = /usr/share/lxc/config/$ostype.common.conf\n";
+       if ($unprivileged || $custom_idmap) {
+           $raw .= "lxc.include = /usr/share/lxc/config/$ostype.userns.conf\n"
+       }
     } else {
        die "implement me";
     }
 
+    $raw .= "lxc.start.unshare = 1\n";
+
+    # Should we read them from /etc/subuid?
+    if ($unprivileged && !$custom_idmap) {
+       $raw .= "lxc.id_map = u 0 100000 65536\n";
+       $raw .= "lxc.id_map = g 0 100000 65536\n";
+    }
+
     if (!has_dev_console($conf)) {
        $raw .= "lxc.console = none\n";
        $raw .= "lxc.cgroup.devices.deny = c 5:1 rwm\n";
@@ -1070,6 +1108,7 @@ sub update_lxc_config {
 
     my $lxcmem = int($memory*1024*1024);
     $raw .= "lxc.cgroup.memory.limit_in_bytes = $lxcmem\n";
+    $raw .= "lxc.cgroup.memory.kmem.limit_in_bytes = $lxcmem\n";
 
     my $lxcswap = int(($memory + $swap)*1024*1024);
     $raw .= "lxc.cgroup.memory.memsw.limit_in_bytes = $lxcswap\n";
@@ -1085,11 +1124,9 @@ sub update_lxc_config {
 
     my $mountpoint = parse_ct_mountpoint($conf->{rootfs});
     $mountpoint->{mp} = '/';
-    
-    my ($path, $use_loopdev) = mountpoint_mount_path($mountpoint, $storage_cfg);
-    $path = "loop:$path" if $use_loopdev;
 
-    $raw .= "lxc.rootfs = $path\n";
+    $raw .= "lxc.rootfs = $dir/rootfs\n";
+    $raw .= "lxc.hook.stop = /usr/lib/x86_64-linux-gnu/lxc/hooks/unmount-namespace\n";
 
     my $netcount = 0;
     foreach my $k (keys %$conf) {
@@ -1147,9 +1184,6 @@ sub verify_searchdomain_list {
 sub add_unused_volume {
     my ($config, $volid) = @_;
 
-    # skip bind mounts and block devices
-    return if $volid =~ m|^/|;
-
     my $key;
     for (my $ind = $MAX_UNUSED_DISKS - 1; $ind >= 0; $ind--) {
        my $test = "unused$ind";
@@ -1192,6 +1226,11 @@ sub update_pct_config {
 
     if (defined($delete)) {
        foreach my $opt (@$delete) {
+           if (!exists($conf->{$opt})) {
+               warn "no such option: $opt\n";
+               next;
+           }
+
            if ($opt eq 'hostname' || $opt eq 'memory' || $opt eq 'rootfs') {
                die "unable to delete required option '$opt'\n";
            } elsif ($opt eq 'swap') {
@@ -1219,8 +1258,12 @@ sub update_pct_config {
                next if $hotplug_error->($opt);
                check_protection($conf, "can't remove CT $vmid drive '$opt'");
                my $mountpoint = parse_ct_mountpoint($conf->{$opt});
-               add_unused_volume($conf, $mountpoint->{volume});
+               if ($mountpoint->{type} eq 'volume') {
+                   add_unused_volume($conf, $mountpoint->{volume})
+               }
                delete $conf->{$opt};
+           } elsif ($opt eq 'unprivileged') {
+               die "unable to delete read-only option: '$opt'\n";
            } else {
                die "implement me"
            }
@@ -1293,6 +1336,8 @@ sub update_pct_config {
         } elsif ($opt eq 'rootfs') {
            check_protection($conf, "can't update CT $vmid drive '$opt'");
            die "implement me: $opt";
+       } elsif ($opt eq 'unprivileged') {
+           die "unable to modify read-only option: '$opt'\n";
        } else {
            die "implement me: $opt";
        }
@@ -1382,10 +1427,7 @@ sub get_primary_ips {
 sub delete_mountpoint_volume {
     my ($storage_cfg, $vmid, $volume) = @_;
 
-    # skip bind mounts and block devices
-    if ($volume =~ m|^/|) {
-           return;
-    }
+    return if classify_mountpoint($volume) ne 'volume';
 
     my ($vtype, $name, $owner) = PVE::Storage::parse_volname($storage_cfg, $volume);
     PVE::Storage::vdisk_free($storage_cfg, $volume) if $vmid == $owner;
@@ -2080,12 +2122,27 @@ my $check_mount_path = sub {
     }
 };
 
+sub query_loopdev {
+    my ($path) = @_;
+    my $found;
+    my $parser = sub {
+       my $line = shift;
+       if ($line =~ m@^(/dev/loop\d+):@) {
+           $found = $1;
+       }
+    };
+    my $cmd = ['losetup', '--associated', $path];
+    PVE::Tools::run_command($cmd, outfunc => $parser);
+    return $found;
+}
+
 # use $rootdir = undef to just return the corresponding mount path
 sub mountpoint_mount {
     my ($mountpoint, $rootdir, $storage_cfg, $snapname) = @_;
 
     my $volid = $mountpoint->{volume};
     my $mount = $mountpoint->{mp};
+    my $type = $mountpoint->{type};
     
     return if !$volid || !$mount;
 
@@ -2116,16 +2173,11 @@ sub mountpoint_mount {
        if ($format eq 'subvol') {
            if ($mount_path) {
                if ($snapname) {
-                   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]);
-                   } else {
+                   if ($scfg->{type} ne 'zfspool') {
                        die "cannot mount subvol snapshots for storage type '$scfg->{type}'\n";
                    }
-               } else {
-                   PVE::Tools::run_command(['mount', '-o', 'bind', $path, $mount_path]);
                }
+               PVE::Tools::run_command(['mount', '-o', 'bind', $path, $mount_path]);
            }
            return wantarray ? ($path, 0) : $path;
        } elsif ($format eq 'raw' || $format eq 'iso') {
@@ -2152,10 +2204,10 @@ sub mountpoint_mount {
        } else {
            die "unsupported image format '$format'\n";
        }
-    } elsif ($volid =~ m|^/dev/.+|) {
+    } elsif ($type eq 'device') {
        PVE::Tools::run_command(['mount', $volid, $mount_path]) if $mount_path;
        return wantarray ? ($volid, 0) : $volid;
-    } elsif ($volid !~ m|^/dev/.+| && $volid =~ m|^/.+| && -d $volid) {
+    } elsif ($type eq 'bind' && -d $volid) {
        &$check_mount_path($volid);
        PVE::Tools::run_command(['mount', '-o', 'bind', $volid, $mount_path]) if $mount_path;
        return wantarray ? ($volid, 0) : $volid;
@@ -2176,7 +2228,7 @@ sub get_vm_volumes {
 
        my $volid = $mountpoint->{volume};
 
-        return if !$volid || $volid =~ m|^/|;
+        return if !$volid || $mountpoint->{type} ne 'volume';
 
         my ($sid, $volname) = PVE::Storage::parse_volume_id($volid, 1);
         return if !$sid;
@@ -2188,13 +2240,15 @@ sub get_vm_volumes {
 }
 
 sub mkfs {
-    my ($dev) = @_;
+    my ($dev, $rootuid, $rootgid) = @_;
 
-    PVE::Tools::run_command(['mkfs.ext4', '-O', 'mmp', $dev]);
+    PVE::Tools::run_command(['mkfs.ext4', '-O', 'mmp',
+                            '-E', "root_owner=$rootuid:$rootgid",
+                            $dev]);
 }
 
 sub format_disk {
-    my ($storage_cfg, $volid) = @_;
+    my ($storage_cfg, $volid, $rootuid, $rootgid) = @_;
 
     if ($volid =~ m!^/dev/.+!) {
        mkfs($volid);
@@ -2215,7 +2269,7 @@ sub format_disk {
     die "cannot format volume '$volid' (format == $format)\n"
        if $format ne 'raw';
 
-    mkfs($path);
+    mkfs($path, $rootuid, $rootgid);
 }
 
 sub destroy_disks {
@@ -2233,6 +2287,9 @@ sub create_disks {
     my $vollist = [];
 
     eval {
+       my (undef, $rootuid, $rootgid) = PVE::LXC::parse_id_maps($conf);
+       my $chown_vollist = [];
+
        foreach_mountpoint($settings, sub {
            my ($ms, $mountpoint) = @_;
 
@@ -2255,35 +2312,45 @@ sub create_disks {
                    if ($size_kb > 0) {
                        $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'raw',
                                                           undef, $size_kb);
-                       format_disk($storecfg, $volid);
+                       format_disk($storecfg, $volid, $rootuid, $rootgid);
                    } else {
                        $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'subvol',
                                                           undef, 0);
+                       push @$chown_vollist, $volid;
                    }
                } elsif ($scfg->{type} eq 'zfspool') {
 
                    $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'subvol',
                                                       undef, $size_kb);
+                   push @$chown_vollist, $volid;
                } elsif ($scfg->{type} eq 'drbd' || $scfg->{type} eq 'lvm') {
 
                    $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'raw', undef, $size_kb);
-                   format_disk($storecfg, $volid);
+                   format_disk($storecfg, $volid, $rootuid, $rootgid);
 
                } elsif ($scfg->{type} eq 'rbd') {
 
                    die "krbd option must be enabled on storage type '$scfg->{type}'\n" if !$scfg->{krbd};
                    $volid = PVE::Storage::vdisk_alloc($storecfg, $storage, $vmid, 'raw', undef, $size_kb);
-                   format_disk($storecfg, $volid);
+                   format_disk($storecfg, $volid, $rootuid, $rootgid);
                } else {
                    die "unable to create containers on storage type '$scfg->{type}'\n";
                }
                push @$vollist, $volid;
-                my $new_mountpoint = { volume => $volid, size => $size_kb*1024, mp => $mp };
-               $conf->{$ms} = print_ct_mountpoint($new_mountpoint, $ms eq 'rootfs');
+               $mountpoint->{volume} = $volid;
+               $mountpoint->{size} = $size_kb * 1024;
+               $conf->{$ms} = print_ct_mountpoint($mountpoint, $ms eq 'rootfs');
            } else {
                # use specified/existing volid
            }
        });
+
+       PVE::Storage::activate_volumes($storecfg, $chown_vollist, undef);
+       foreach my $volid (@$chown_vollist) {
+           my $path = PVE::Storage::path($storecfg, $volid, undef);
+           chown($rootuid, $rootgid, $path);
+       }
+       PVE::Storage::deactivate_volumes($storecfg, $chown_vollist, undef);
     };
     # free allocated images on error
     if (my $err = $@) {
@@ -2353,4 +2420,45 @@ sub complete_ctid_running {
     return &$complete_ctid_full(1);
 }
 
+sub parse_id_maps {
+    my ($conf) = @_;
+
+    my $id_map = [];
+    my $rootuid = 0;
+    my $rootgid = 0;
+
+    my $lxc = $conf->{lxc};
+    foreach my $entry (@$lxc) {
+       my ($key, $value) = @$entry;
+       next if $key ne 'lxc.id_map';
+       if ($value =~ /^([ug])\s+(\d+)\s+(\d+)\s+(\d+)\s*$/) {
+           my ($type, $ct, $host, $length) = ($1, $2, $3, $4);
+           push @$id_map, [$type, $ct, $host, $length];
+           if ($ct == 0) {
+               $rootuid = $host if $type eq 'u';
+               $rootgid = $host if $type eq 'g';
+           }
+       } else {
+           die "failed to parse id_map: $value\n";
+       }
+    }
+
+    if (!@$id_map && $conf->{unprivileged}) {
+       # Should we read them from /etc/subuid?
+       $id_map = [ ['u', '0', '100000', '65536'],
+                   ['g', '0', '100000', '65536'] ];
+       $rootuid = $rootgid = 100000;
+    }
+
+    return ($id_map, $rootuid, $rootgid);
+}
+
+sub userns_command {
+    my ($id_map) = @_;
+    if (@$id_map) {
+       return ['lxc-usernsexec', (map { ('-m', join(':', @$_)) } @$id_map), '--'];
+    }
+    return [];
+}
+
 1;