[pve-devel] [PATCH lxc 4/4] LXC::Setup: chroot into the container

Wolfgang Bumiller w.bumiller at proxmox.com
Thu Oct 29 11:04:02 CET 2015


In order to better deal with paths and symlinks inside
containers we now chroot() into the container's rootdir in
LXC::Setup.
---
 src/PVE/LXC/Setup.pm        |  87 ++++++++++++++++---
 src/PVE/LXC/Setup/Base.pm   | 206 +++++++++++++++++++++-----------------------
 src/PVE/LXC/Setup/Debian.pm |   2 +-
 src/PVE/LXC/Setup/Redhat.pm |   4 +-
 4 files changed, 175 insertions(+), 124 deletions(-)

diff --git a/src/PVE/LXC/Setup.pm b/src/PVE/LXC/Setup.pm
index deab108..abf696d 100644
--- a/src/PVE/LXC/Setup.pm
+++ b/src/PVE/LXC/Setup.pm
@@ -2,6 +2,7 @@ package PVE::LXC::Setup;
 
 use strict;
 use warnings;
+use POSIX;
 use PVE::Tools;
 
 use PVE::LXC::Setup::Debian;
@@ -51,65 +52,129 @@ sub new {
 	"no such OS type '$type'\n";
 
     $self->{plugin} = $plugin_class->new($conf, $rootdir);
+    $self->{in_chroot} = 0;
     
     return $self;
 }
 
+sub protected_call {
+    my ($self, $sub) = @_;
+
+    # avoid recursion:
+    return $sub->() if $self->{in_chroot};
+
+    my $rootdir = $self->{rootdir};
+    if (!-d "$rootdir/dev" && !mkdir("$rootdir/dev")) {
+	die "failed to create temporary /dev directory: $!\n";
+    }
+
+    my $child = fork();
+    die "fork failed: $!\n" if !defined($child);
+
+    if (!$child) {
+	# avoid recursive forks
+	$self->{in_chroot} = 1;
+	$self->{plugin}->{in_chroot} = 1;
+	eval {
+	    PVE::Tools::run_command(['mount', '--bind', '/dev', "$rootdir/dev"]);
+	    chroot($rootdir) or die "failed to change root to: $rootdir: $!\n";
+	    chdir('/') or die "failed to change to root directory\n";
+	    $sub->();
+	};
+	if (my $err = $@) {
+	    print STDERR "$err\n";
+	    POSIX::_exit(1);
+	}
+	POSIX::_exit(0);
+    }
+    while (waitpid($child, 0) != $child) {}
+    eval { PVE::Tools::run_command(['umount', "$rootdir/dev"]); };
+    warn $@ if $@;
+    return $? == 0;
+}
+
 sub template_fixup {
     my ($self) = @_;
 
-    $self->{plugin}->template_fixup($self->{conf});
+    my $code = sub {
+	$self->{plugin}->template_fixup($self->{conf});
+    };
+    $self->protected_call($code);
 }
  
 sub setup_network {
     my ($self) = @_;
 
-    $self->{plugin}->setup_network($self->{conf});
+    my $code = sub {
+	$self->{plugin}->setup_network($self->{conf});
+    };
+    $self->protected_call($code);
 }
 
 sub set_hostname {
     my ($self) = @_;
 
-    $self->{plugin}->set_hostname($self->{conf});
+    my $code = sub {
+	$self->{plugin}->set_hostname($self->{conf});
+    };
+    $self->protected_call($code);
 }
 
 sub set_dns {
     my ($self) = @_;
 
-    $self->{plugin}->set_dns($self->{conf});
+    my $code = sub {
+	$self->{plugin}->set_dns($self->{conf});
+    };
+    $self->protected_call($code);
 }
 
 sub setup_init {
     my ($self) = @_;
 
-    $self->{plugin}->setup_init($self->{conf});
+    my $code = sub {
+	$self->{plugin}->setup_init($self->{conf});
+    };
+    $self->protected_call($code);
 }
 
 sub set_user_password {
     my ($self, $user, $pw) = @_;
     
-    $self->{plugin}->set_user_password($self->{conf}, $user, $pw);
+    my $code = sub {
+	$self->{plugin}->set_user_password($self->{conf}, $user, $pw);
+    };
+    $self->protected_call($code);
 }
 
 sub rewrite_ssh_host_keys {
     my ($self) = @_;
 
-    $self->{plugin}->rewrite_ssh_host_keys($self->{conf});
+    my $code = sub {
+	$self->{plugin}->rewrite_ssh_host_keys($self->{conf});
+    };
+    $self->protected_call($code);
 }    
 
 sub pre_start_hook {
     my ($self) = @_;
 
-    # Create /fastboot to skip run fsck
-    $self->{plugin}->ct_file_set_contents('/fastboot', '');
+    my $code = sub {
+	# Create /fastboot to skip run fsck
+	$self->{plugin}->ct_file_set_contents('/fastboot', '');
 
-    $self->{plugin}->pre_start_hook($self->{conf});
+	$self->{plugin}->pre_start_hook($self->{conf});
+    };
+    $self->protected_call($code);
 }
 
 sub post_create_hook {
     my ($self, $root_password) = @_;
 
-    $self->{plugin}->post_create_hook($self->{conf}, $root_password);
+    my $code = sub {
+	$self->{plugin}->post_create_hook($self->{conf}, $root_password);
+    };
+    $self->protected_call($code);
 }
 
 1;
diff --git a/src/PVE/LXC/Setup/Base.pm b/src/PVE/LXC/Setup/Base.pm
index cbb23a1..da651b3 100644
--- a/src/PVE/LXC/Setup/Base.pm
+++ b/src/PVE/LXC/Setup/Base.pm
@@ -7,7 +7,9 @@ use File::stat;
 use Digest::SHA;
 use IO::File;
 use Encode;
+use Fcntl;
 use File::Path;
+use File::Spec;
 
 use PVE::INotify;
 use PVE::Tools;
@@ -137,10 +139,6 @@ sub set_dns {
 
     my ($searchdomains, $nameserver) = lookup_dns_conf($conf);
     
-    my $rootdir = $self->{rootdir};
-    
-    my $filename = "$rootdir/etc/resolv.conf";
-
     my $data = '';
 
     $data .= "search " . join(' ', PVE::Tools::split_list($searchdomains)) . "\n"
@@ -150,7 +148,7 @@ sub set_dns {
 	$data .= "nameserver $ns\n";
     }
 
-    PVE::Tools::file_set_contents($filename, $data);
+    $self->ct_file_set_contents("/etc/resolv.conf", $data);
 }
 
 sub set_hostname {
@@ -160,17 +158,15 @@ sub set_hostname {
 
     $hostname =~ s/\..*$//;
 
-    my $rootdir = $self->{rootdir};
-
-    my $hostname_fn = "$rootdir/etc/hostname";
+    my $hostname_fn = "/etc/hostname";
     
-    my $oldname = PVE::Tools::file_read_firstline($hostname_fn) || 'localhost';
+    my $oldname = $self->ct_file_read_firstline($hostname_fn) || 'localhost';
 
-    my $hosts_fn = "$rootdir/etc/hosts";
+    my $hosts_fn = "/etc/hosts";
     my $etc_hosts_data = '';
     
-    if (-f $hosts_fn) {
-	$etc_hosts_data =  PVE::Tools::file_get_contents($hosts_fn);
+    if ($self->ct_file_exists($hosts_fn)) {
+	$etc_hosts_data =  $self->ct_file_get_contents($hosts_fn);
     }
 
     my ($ipv4, $ipv6) = PVE::LXC::get_primary_ips($conf);
@@ -181,8 +177,8 @@ sub set_hostname {
     $etc_hosts_data = update_etc_hosts($etc_hosts_data, $hostip, $oldname, 
 				       $hostname, $searchdomains);
     
-    PVE::Tools::file_set_contents($hostname_fn, "$hostname\n");
-    PVE::Tools::file_set_contents($hosts_fn, $etc_hosts_data);
+    $self->ct_file_set_contents($hostname_fn, "$hostname\n");
+    $self->ct_file_set_contents($hosts_fn, $etc_hosts_data);
 }
 
 sub setup_network {
@@ -200,49 +196,40 @@ sub setup_init {
 sub setup_systemd_console {
     my ($self, $conf) = @_;
 
-    my $rootdir = $self->{rootdir};
-
-    my $systemd_dir_rel = -x "$rootdir/lib/systemd/systemd" ?
+    my $systemd_dir_rel = -x "/lib/systemd/systemd" ?
 	"/lib/systemd/system" : "/usr/lib/systemd/system";
 
-    my $systemd_dir = "$rootdir/$systemd_dir_rel";
-
-    my $etc_systemd_dir = "$rootdir/etc/systemd/system";
-
     my $systemd_getty_service_rel = "$systemd_dir_rel/getty\@.service";
 
-    my $systemd_getty_service = "$rootdir/$systemd_getty_service_rel";
+    return if !$self->ct_file_exists($systemd_getty_service_rel);
 
-    return if ! -f $systemd_getty_service;
-
-    my $raw = PVE::Tools::file_get_contents($systemd_getty_service);
+    my $raw = $self->ct_file_get_contents($systemd_getty_service_rel);
 
     my $systemd_container_getty_service_rel = "$systemd_dir_rel/container-getty\@.service";
-    my $systemd_container_getty_service =  "$rootdir/$systemd_container_getty_service_rel";
 
     # systemd on CenoOS 7.1 is too old (version 205), so there is no
     # container-getty service
-    if (! -f $systemd_container_getty_service) {
+    if (!$self->ct_file_exists($systemd_container_getty_service_rel)) {
 	if ($raw =~ s!^ConditionPathExists=/dev/tty0$!ConditionPathExists=/dev/tty!m) {
-	    PVE::Tools::file_set_contents($systemd_getty_service, $raw);
+	    $self->ct_file_set_contents($systemd_getty_service_rel, $raw);
 	}
     } else {
 	# undo above change (in case someone updated systemd)
 	if ($raw =~ s!^ConditionPathExists=/dev/tty$!ConditionPathExists=/dev/tty0!m) {
-	    PVE::Tools::file_set_contents($systemd_getty_service, $raw);
+	    $self->ct_file_set_contents($systemd_getty_service_rel, $raw);
 	}
     }
 
     my $ttycount = PVE::LXC::get_tty_count($conf);
 
     for (my $i = 1; $i < 7; $i++) {
-	my $tty_service_lnk = "$etc_systemd_dir/getty.target.wants/getty\@tty$i.service";
+	my $tty_service_lnk = "/etc/systemd/system/getty.target.wants/getty\@tty$i.service";
 	if ($i > $ttycount) {
-	    unlink $tty_service_lnk;
+	    $self->ct_unlink($tty_service_lnk);
 	} else {
-	    if (! -l $tty_service_lnk) {
-		unlink $tty_service_lnk;
-		symlink($systemd_getty_service_rel, $tty_service_lnk);
+	    if (!$self->ct_is_symlink($tty_service_lnk)) {
+		$self->ct_unlink($tty_service_lnk);
+		$self->ct_symlink($systemd_getty_service_rel, $tty_service_lnk);
 	    }
 	}
     }
@@ -251,14 +238,12 @@ sub setup_systemd_console {
 sub setup_systemd_networkd {
     my ($self, $conf) = @_;
 
-    my $rootdir = $self->{rootdir};
-
     foreach my $k (keys %$conf) {
 	next if $k !~ m/^net(\d+)$/;
 	my $d = PVE::LXC::parse_lxc_network($conf->{$k});
 	next if !$d->{name};
 
-	my $filename = "$rootdir/etc/systemd/network/$d->{name}.network";
+	my $filename = "/etc/systemd/network/$d->{name}.network";
 
 	my $data = <<"DATA";
 [Match]
@@ -309,38 +294,37 @@ DATA
 	$data .= "DHCP = $DHCPMODES[$dhcp]\n";
 	$data .= $routes if $routes;
 
-	PVE::Tools::file_set_contents($filename, $data);
+	$self->ct_file_set_contents($filename, $data);
     }
 }
 
 sub setup_securetty {
     my ($self, $conf, @add) = @_;
 
-    my $rootdir = $self->{rootdir};
-    my $filename = "$rootdir/etc/securetty";
-    my $data = PVE::Tools::file_get_contents($filename);
+    my $filename = "/etc/securetty";
+    my $data = $self->ct_file_get_contents($filename);
     chomp $data; $data .= "\n";
     foreach my $dev (@add) {
 	if ($data !~ m!^\Q$dev\E\s*$!m) {
 	    $data .= "$dev\n"; 
 	}
     }
-    PVE::Tools::file_set_contents($filename, $data);
+    $self->ct_file_set_contents($filename, $data);
 }
 
 my $replacepw  = sub {
-    my ($file, $user, $epw, $shadow) = @_;
+    my ($self, $file, $user, $epw, $shadow) = @_;
 
     my $tmpfile = "$file.$$";
 
     eval  {
-	my $src = IO::File->new("<$file") ||
+	my $src = $self->ct_open_file_read($file) ||
 	    die "unable to open file '$file' - $!";
 
-	my $st = File::stat::stat($src) ||
+	my $st = $self->ct_stat($src) ||
 	    die "unable to stat file - $!";
 
-	my $dst = IO::File->new(">$tmpfile") ||
+	my $dst = $self->ct_open_file_write($tmpfile) ||
 	    die "unable to open file '$tmpfile' - $!";
 
 	# copy owner and permissions
@@ -366,23 +350,21 @@ my $replacepw  = sub {
 	$dst->close() || die "close '$tmpfile' failed - $!\n";
     };
     if (my $err = $@) {
-	unlink $tmpfile;
+	$self->ct_unlink($tmpfile);
     } else {
-	rename $tmpfile, $file;
-	unlink $tmpfile; # in case rename fails
+	$self->ct_rename($tmpfile, $file);
+	$self->ct_unlink($tmpfile); # in case rename fails
     }	
 };
 
 sub set_user_password {
     my ($self, $conf, $user, $opt_password) = @_;
 
-    my $rootdir = $self->{rootdir};
+    my $pwfile = "/etc/passwd";
 
-    my $pwfile = "$rootdir/etc/passwd";
+    return if !$self->ct_file_exists($pwfile);
 
-    return if ! -f $pwfile;
-
-    my $shadow = "$rootdir/etc/shadow";
+    my $shadow = "/etc/shadow";
     
     if (defined($opt_password)) {
 	if ($opt_password !~ m/^\$/) {
@@ -393,32 +375,29 @@ sub set_user_password {
 	$opt_password = '*';
     }
     
-    if (-f $shadow) {
-	&$replacepw ($shadow, $user, $opt_password, 1);
-	&$replacepw ($pwfile, $user, 'x');
+    if ($self->ct_file_exists($shadow)) {
+	&$replacepw ($self, $shadow, $user, $opt_password, 1);
+	&$replacepw ($self, $pwfile, $user, 'x');
     } else {
-	&$replacepw ($pwfile, $user, $opt_password);
+	&$replacepw ($self, $pwfile, $user, $opt_password);
     }
 }
 
 my $randomize_crontab = sub {
     my ($self, $conf) = @_;
 
-    my $rootdir = $self->{rootdir};
-
     my @files;
     # Note: dir_glob_foreach() untaints filenames!
-    my $cron_dir = "$rootdir/etc/cron.d";
-    PVE::Tools::dir_glob_foreach($cron_dir, qr/[A-Z\-\_a-z0-9]+/, sub {
+    PVE::Tools::dir_glob_foreach("/etc/cron.d", qr/[A-Z\-\_a-z0-9]+/, sub {
 	my ($name) = @_;
-	push @files, "$cron_dir/$name";
+	push @files, "/etc/cron.d/$name";
     });
 
-    my $crontab_fn = "$rootdir/etc/crontab";
-    unshift @files, $crontab_fn if -f $crontab_fn;
+    my $crontab_fn = "/etc/crontab";
+    unshift @files, $crontab_fn if $self->ct_file_exists($crontab_fn);
     
     foreach my $filename (@files) {
-	my $data = PVE::Tools::file_get_contents($filename);
+	my $data = $self->ct_file_get_contents($filename);
  	my $new = '';
 	foreach my $line (split(/\n/, $data)) {
 	    # we only randomize minutes for root crontab entries
@@ -430,18 +409,14 @@ my $randomize_crontab = sub {
 		$new .= "$line\n";
 	    }
 	}
-	PVE::Tools::file_set_contents($filename, $new);
+	$self->ct_file_set_contents($filename, $new);
    }
 };
 
 sub rewrite_ssh_host_keys {
     my ($self, $conf) = @_;
 
-    my $rootdir = $self->{rootdir};
-
-    my $etc_ssh_dir = "$rootdir/etc/ssh";
-
-    return if ! -d $etc_ssh_dir;
+    return if !$self->ct_is_directory('/etc/ssh');
     
     my $keynames = {
 	rsa1 => 'ssh_host_key',
@@ -454,12 +429,15 @@ sub rewrite_ssh_host_keys {
     my $hostname = $conf->{hostname} || 'localhost';
     $hostname =~ s/\..*$//;
 
+    # Since we don't want to replace host keys let's make sure in_chroot is set
+    die "internal error: not protected" if !$self->{in_chroot};
+
     foreach my $keytype (keys %$keynames) {
 	my $basename = $keynames->{$keytype};
-	unlink "${etc_ssh_dir}/$basename";
-	unlink "${etc_ssh_dir}/$basename.pub";
+	$self->ct_unlink("/etc/ssh/$basename");
+	$self->ct_unlink("/etc/ssh/$basename.pub");
 	print "Creating SSH host key '$basename' - this may take some time ...\n";
-	my $cmd = ['ssh-keygen', '-q', '-f', "${etc_ssh_dir}/$basename", '-t', $keytype,
+	my $cmd = ['ssh-keygen', '-q', '-f', "/etc/ssh/$basename", '-t', $keytype,
 		   '-N', '', '-C', "root\@$hostname"];
 	PVE::Tools::run_command($cmd);
     }
@@ -493,75 +471,83 @@ sub post_create_hook {
     # fixme: what else ?
 }
 
+# File access wrappers for container setup code.
+# For user-namespace support these might need to take uid and gid maps into account.
+
 sub ct_mkdir {
     my ($self, $file, $mask) = @_;
-    my $root = $self->{rootdir};
-    $file //= $_; # emulate mkdir parameters
-    return CORE::mkdir("$root/$file", $mask) if defined ($mask);
-    return CORE::mkdir("$root/$file");
+    # mkdir goes by parameter count - an `undef' mode acts like a mode of 0000
+    return CORE::mkdir($file, $mask) if defined ($mask);
+    return CORE::mkdir($file);
 }
 
 sub ct_unlink {
-    my $self = shift;
-    my $root = $self->{rootdir};
-    return CORE::unlink("$root/$_") if !@_; # emulate unlink parameters
-    return CORE::unlink(map { "$root/$_" } @_);
+    my ($self, @files) = @_;
+    foreach my $file (@files) {
+	CORE::unlink($file);
+    }
+}
+
+sub ct_rename {
+    my ($self, $old, $new) = @_;
+    CORE::rename($old, $new);
 }
 
-sub ct_open_file {
+sub ct_open_file_read {
     my $self = shift;
-    my $file = $self->{rootdir} . '/' . shift;
-    return IO::File->new($file, @_);
+    my $file = shift;
+    return IO::File->new($file, O_RDONLY, @_);
 }
 
-sub ct_make_path {
+sub ct_open_file_write {
     my $self = shift;
-    my $root = $self->{rootdir};
-    my $opt = pop;
-    $opt = "$root/$opt" if ref($opt) ne 'HASH';
-    return File::Path::make_path(map { "$root/$_" } @_, $opt);
+    my $file = shift;
+    return IO::File->new($file, O_WRONLY | O_CREAT, @_);
 }
 
-sub ct_mkpath {
+sub ct_make_path {
     my $self = shift;
-    my $root = $self->{rootdir};
-
-    my $first = shift;
-    return File::Path::mkpath(map { "$root/$_" } @$first, @_) if ref($first) eq 'ARRAY';
-    unshift @_, $first;
-    my $last = pop;
-    return File::Path::mkpath(map { "$root/$_" } @_, $last) if ref($last) eq 'HASH';
-    return File::Path::mkpath(map { "$root/$_" } (@_, $last||()));
+    File::Path::make_path(@_);
 }
 
 sub ct_symlink {
     my ($self, $old, $new) = @_;
-    my $root = $self->{rootdir};
-    return CORE::symlink($old, "$root/$new");
+    return CORE::symlink($old, $new);
 }
 
 sub ct_file_exists {
     my ($self, $file) = @_;
-    my $root = $self->{rootdir};
-    return -f "$root/$file";
+    return -f $file;
+}
+
+sub ct_is_directory {
+    my ($self, $file) = @_;
+    return -d $file;
+}
+
+sub ct_is_symlink {
+    my ($self, $file) = @_;
+    return -l $file;
+}
+
+sub ct_stat {
+    my ($self, $file) = @_;
+    return File::stat::stat($file);
 }
 
 sub ct_file_read_firstline {
     my ($self, $file) = @_;
-    my $root = $self->{rootdir};
-    return PVE::Tools::file_read_firstline("$root/$file");
+    return PVE::Tools::file_read_firstline($file);
 }
 
 sub ct_file_get_contents {
     my ($self, $file) = @_;
-    my $root = $self->{rootdir};
-    return PVE::Tools::file_get_contents("$root/$file");
+    return PVE::Tools::file_get_contents($file);
 }
 
 sub ct_file_set_contents {
     my ($self, $file, $data) = @_;
-    my $root = $self->{rootdir};
-    return PVE::Tools::file_set_contents("$root/$file", $data);
+    return PVE::Tools::file_set_contents($file, $data);
 }
 
 1;
diff --git a/src/PVE/LXC/Setup/Debian.pm b/src/PVE/LXC/Setup/Debian.pm
index 2db8066..0c3448e 100644
--- a/src/PVE/LXC/Setup/Debian.pm
+++ b/src/PVE/LXC/Setup/Debian.pm
@@ -238,7 +238,7 @@ sub setup_network {
 	$section = undef;
     };
 
-    if (my $fh = $self->ct_open_file($filename, "r")) {
+    if (my $fh = $self->ct_open_file_read($filename)) {
 	while (defined (my $line = <$fh>)) {
 	    chomp $line;
 	    if ($line =~ m/^#/) {
diff --git a/src/PVE/LXC/Setup/Redhat.pm b/src/PVE/LXC/Setup/Redhat.pm
index b85b8a2..33f70a6 100644
--- a/src/PVE/LXC/Setup/Redhat.pm
+++ b/src/PVE/LXC/Setup/Redhat.pm
@@ -85,7 +85,7 @@ sub template_fixup {
     if ($self->{version} < 7) {
 	# re-create emissing files for tty
 
-	$self->ct_mkpath('/etc/init');
+	$self->ct_make_path('/etc/init');
 
 	my $filename = "/etc/init/tty.conf";
 	$self->ct_file_set_contents($filename, $tty_conf)
@@ -187,7 +187,7 @@ sub setup_network {
 
     my ($gw, $gw6);
 
-    $self->ct_mkpath('/etc/sysconfig/network-scripts');
+    $self->ct_make_path('/etc/sysconfig/network-scripts');
 
     foreach my $k (keys %$conf) {
 	next if $k !~ m/^net(\d+)$/;
-- 
2.1.4




More information about the pve-devel mailing list