Package: sbuild
Version: 0.78.1-2
Severity: wishlist

Here's my initial version of the cleaned up patch for adding a
--chroot-mode=systemd-nspawn.  Some things I'm not sure about:
- Should we maybe ping upstream and/or Debian maintainers on
https://github.com/systemd/systemd/issues/13297 to see how hard it
would be to get it fixed so I could remove that whole ugly workaround?
 (The workaround also only handles bind mount settings at present -
and for example, I've found that a lot of package builds will require
SystemCallFilter=@memlock due to a lot of crypto libraries and
utilities giving errors if they're denied access to mlock.  So I would
probably want to add that to my
/etc/systemd/nspawn/unstable-amd64-sbuild.nspawn config file.)
- It currently requires giving sudo access for systemd-run, which
essentially would open up execution of anything desired.  And the fact
that it requires NOPASSWD (because some of the commands redirect
stdin/stdout) makes things even worse.  And even if you restrict it to
e.g. "systemd-run -M unstable-amd64-sbuild*" it still seems it would
be possible to fool that with something like "sudo systemd-run -M
unstable-amd64-sbuild -M .host ~/myevilcmd".
- If you want to ignore the small patch in lib/Sbuild/Chroot.pm that's
fine.  It's not really related to the systemd-nspawn chroot mode.
- It does add a dependency on libipc-run-perl.
- It would be nice (as a future enhancement) if it would be possible
to configure this backend to start the container in --network-veth
mode and set up the host's side of the veth to forward only traffic
to/from apt-cacher-ng on localhost:3142.  Not sure how hard that would
be to accomplish.

-- 
Daniel Schepler
Description: Implement systemd-nspawn chroot mode
 Adds a chroot mode using systemd-nspawn to start a container to use
 for builds.
Author: Daniel Schepler <dschep...@gmail.com>

--- sbuild-0.78.1.orig/lib/Sbuild/Build.pm
+++ sbuild-0.78.1/lib/Sbuild/Build.pm
@@ -53,6 +53,7 @@ use Sbuild::ChrootInfoSchroot;
 use Sbuild::ChrootInfoUnshare;
 use Sbuild::ChrootInfoSudo;
 use Sbuild::ChrootInfoAutopkgtest;
+use Sbuild::ChrootInfoNspawn;
 use Sbuild::ChrootRoot;
 use Sbuild::Sysconfig qw($version $release_date);
 use Sbuild::Sysconfig;
@@ -422,6 +423,8 @@ sub run_chroot_session {
 	    $chroot_info = Sbuild::ChrootInfoAutopkgtest->new($self->get('Config'));
 	} elsif ($self->get_conf('CHROOT_MODE') eq 'unshare') {
 	    $chroot_info = Sbuild::ChrootInfoUnshare->new($self->get('Config'));
+	} elsif ($self->get_conf('CHROOT_MODE') eq 'systemd-nspawn') {
+	    $chroot_info = Sbuild::ChrootInfoNspawn->new($self->get('Config'));
 	} else {
 	    $chroot_info = Sbuild::ChrootInfoSudo->new($self->get('Config'));
 	}
--- sbuild-0.78.1.orig/lib/Sbuild/Chroot.pm
+++ sbuild-0.78.1/lib/Sbuild/Chroot.pm
@@ -244,10 +244,8 @@ sub get_read_file_handle {
     my $dir = "/";
     $dir = $options->{'DIR'} if defined $options->{'DIR'};
 
-    my $escapedsource = shellescape $source;
-
     my $pipe = $self->pipe_command({
-	    COMMAND => [ "sh", "-c", "cat $escapedsource" ],
+	    COMMAND => [ "cat", "--", $source ],
 	    DIR => $dir,
 	    USER => $user,
 	    PIPE => 'in'
--- /dev/null
+++ sbuild-0.78.1/lib/Sbuild/ChrootInfoNspawn.pm
@@ -0,0 +1,68 @@
+package Sbuild::ChrootInfoNspawn;
+
+use Sbuild::ChrootInfo;
+use Sbuild::ChrootNspawn;
+
+use strict;
+use warnings;
+
+BEGIN {
+    use Exporter ();
+    our (@ISA, @EXPORT);
+
+    @ISA=qw(Exporter Sbuild::ChrootInfo);
+
+    @EXPORT = qw();
+}
+
+sub new {
+    my $class = shift;
+    my $conf = shift;
+
+    my $self = $class->SUPER::new($conf);
+    bless($self, $class);
+
+    return $self;
+}
+
+sub get_info_all {
+    my $self = shift;
+
+    my $chroots = {};
+
+    open CHROOTS, '-|', $self->get_conf('MACHINECTL'), 'list-images'
+	or die 'Can\'t run machinectl list-images';
+
+    # skip header line
+    <CHROOTS>;
+
+    while (($_ = <CHROOTS>) ne "\n") {
+	chomp;
+	my @fields = split /\s+/, $_;
+	my $chroot = $fields[0];
+	$chroots->{'chroot'}->{$chroot} = 1;
+	$chroots->{'source'}->{$chroot} = 1;
+    }
+
+    # skip "N images listed"
+    <CHROOTS>;
+
+    close CHROOTS or die "Can't close machinectl list-images pipe";
+
+    $self->set('Chroots', $chroots);
+}
+
+sub _create {
+    my $self = shift;
+    my $chroot_id = shift;
+
+    my $chroot = undef;
+
+    if (defined($chroot_id)) {
+	$chroot = Sbuild::ChrootNspawn->new($self->get('Config'), $chroot_id);
+    }
+
+    return $chroot;
+}
+
+1;
--- /dev/null
+++ sbuild-0.78.1/lib/Sbuild/ChrootNspawn.pm
@@ -0,0 +1,238 @@
+package Sbuild::ChrootNspawn;
+
+use strict;
+use warnings;
+
+use IPC::Run qw(run start finish);
+use IO::Handle;
+use File::Basename qw(basename);
+
+BEGIN {
+    use Exporter ();
+    use Sbuild::Chroot;
+    our (@ISA, @EXPORT);
+
+    @ISA = qw(Exporter Sbuild::Chroot);
+
+    @EXPORT = qw();
+}
+
+sub new {
+    my $class = shift;
+    my $conf = shift;
+    my $chroot_id = shift;
+
+    my $self = $class->SUPER::new($conf, $chroot_id);
+    bless($self, $class);
+
+    return $self;
+}
+
+sub begin_session {
+    my $self = shift;
+    my $chroot = $self->get('Chroot ID');
+
+    return 0 if !defined $chroot;
+
+    my $namespace = undef;
+    if ($chroot =~ m/^(chroot|source):(.+)$/) {
+	$namespace = $1;
+	$chroot = $2;
+    }
+    my @cmd = ($self->get_conf('SUDO'), $self->get_conf('SYSTEMD_NSPAWN'), '-b',
+	       '-D', "/var/lib/machines/$chroot",
+	       '--console=passive',
+	       '--slice=machine-sbuild.slice');
+    push @cmd, '-x' if defined $namespace && $namespace eq 'chroot';
+
+    if ($self->get_conf('DEBUG')) {
+	printf STDERR "running @cmd\n";
+    }
+    my $CONTAINER_STDERR = IO::Handle->new();
+    my $h = start \@cmd,
+	'</dev/null',
+	'>/dev/null',
+	'2>pipe', $CONTAINER_STDERR;
+    my $session_id, $location;
+    if (($_ = <$CONTAINER_STDERR>) =~ m/^Spawning container (\S*) on (.*)\.$/) {
+	$session_id = $1;
+	$location = $2;
+    }
+    else {
+	die "Failed to find expected output from systemd-nspawn";
+    }
+    <$CONTAINER_STDERR>;  # consume "press ^] three times" message
+
+    $self->set('Session ID', $session_id);
+    print STDERR "Setting up chroot $chroot (session id $session_id)\n"
+	if $self->get_conf('DEBUG');
+    $self->set('Location', $location);
+    $self->set('Session Purged', 1);
+
+    $self->set('Container Handler', $h);
+    $self->set('Container Process Stderr', $CONTAINER_STDERR);
+
+    # wait for container to be ready to process commands
+    while (1) {
+	print STDERR "Checking for container's system bus availability...\n"
+	    if $self->get_conf('DEBUG');
+
+	run [$self->get_conf('SUDO'), $self->get_conf('SYSTEMD_RUN'),
+	     '-M', $session_id,
+	     '--pipe', '--quiet', '/bin/true'],
+	    '</dev/null', '>/dev/null', '2>/dev/null';
+	if ($?) {
+	    sleep(1);
+	}
+	else {
+	    last;
+	}
+    }
+
+    # work around https://github.com/systemd/systemd/issues/13297:
+    # apply bind mounts (and early in boot process in case some units
+    # within the container have dependencies on them)
+    if ($namespace eq 'chroot') {
+	return 0 if !$self->apply_bind_mounts($chroot);
+    }
+
+    # wait for container to be fully booted before running setup commands
+    print STDERR "Waiting for container to be fully booted...\n"
+	if $self->get_conf('DEBUG');
+    system($self->get_conf('SUDO'), $self->get_conf('SYSTEMD_RUN'),
+	   '-M', $session_id,
+	   '--pipe', '--quiet', '--property=After=multi-user.target',
+	   '/bin/true');
+    if ($?) {
+	print STDERR "systemd-run is failing to connect\n";
+	return 0;
+    }
+
+    return 0 if !$self->_setup_options();
+
+    return 1;
+}
+
+sub apply_bind_mounts {
+    my $self = shift;
+    my $chrootBaseName = shift;
+
+    foreach my $searchdir ('/etc/systemd/nspawn', '/run/systemd/nspawn') {
+	if (-e "$searchdir/$chrootBaseName.nspawn") {
+	    open(my $FH, '<', "$searchdir/$chrootBaseName.nspawn");
+	    while ($_ = <$FH>) {
+		chomp;
+		if (m/^\s*Bind\s*=\s*(.*)/) {
+		    my $bindDir = $1;
+		    chomp $bindDir;
+		    print STDERR "Bind mounting $bindDir\n"
+			if $self->get_conf('DEBUG');
+		    system($self->get_conf('SUDO'), $self->get_conf('MACHINECTL'), 'bind',
+			   $self->get('Session ID'), $bindDir);
+		    if ($?) {
+			print STDERR "machinectl bind failed\n";
+			close($FH);
+			return 0;
+		    }
+		}
+		elsif (m/^\s*BindReadOnly\s*=\s*(.*)/) {
+		    my $bindDir = $1;
+		    chomp $bindDir;
+		    print STDERR "Bind mounting $bindDir read-only\n"
+			if $self->get_conf('DEBUG');
+		    system($self->get_conf('SUDO'), $self->get_conf('MACHINECTL'), 'bind',
+			   '--read-only', $self->get('Session ID'), $bindDir);
+		    if ($?) {
+			print STDERR "machinectl bind --read-only failed\n";
+			close($FH);
+			return 0;
+		    }
+		}
+	    }
+	    close($FH);
+	    return 1;
+	}
+    }
+
+    # no config file found
+    return 1;
+}
+
+sub end_session {
+    my $self = shift;
+
+    return if $self->get('Session ID') eq "";
+
+    print STDERR "Cleaning up chroot (session id " . $self->get('Session ID') . ")\n"
+	if $self->get_conf('DEBUG');
+    system($self->get_conf('SUDO'), $self->get_conf('MACHINECTL'), 'poweroff',
+	   $self->get('Session ID'));
+    $self->set('Session ID', "");
+    if ($?) {
+	print STDERR "Chroot cleanup failed\n";
+	return 0;
+    }
+
+    my $CONTAINER_STDERR = $self->get('Container Process Stderr');
+    <$CONTAINER_STDERR>;  # consume "Container has been shut down" message
+    my $h = $self->get('Container Handler');
+    finish $h;
+    close($CONTAINER_STDERR);
+
+    $self->set('Container Handler', undef);
+    $self->set('Container Process Stderr', undef);
+
+    return 1;
+}
+
+sub get_command_internal {
+    my $self = shift;
+    my $options = shift;
+
+    return if $self->get('Session ID') eq "";
+
+    # Command to run. If I have a string, use it. Otherwise use the list-ref
+    my $command = $options->{'INTCOMMAND_STR'} // $options->{'INTCOMMAND'};
+
+    my $user = $options->{'USER'};
+    my $dir;                                # Directory to use (optional)
+    $dir = $self->get('Defaults')->{'DIR'} if
+	(defined($self->get('Defaults')) &&
+	 defined($self->get('Defaults')->{'DIR'}));
+    $dir = $options->{'DIR'} if
+	defined($options->{'DIR'}) && $options->{'DIR'};
+
+    if (!defined $user || $user eq "") {
+	$user = $self->get_conf('USERNAME');
+    }
+
+    if (!defined($dir)) {
+	$dir = '/';
+    }
+
+    my @cmdline = ($self->get_conf('SUDO'), $self->get_conf('SYSTEMD_RUN'),
+		   '-M', $self->get('Session ID'), '--quiet', '--pipe');
+    push @cmdline, "--uid=$user" if $user ne 'root' and $user ne '0';
+    push @cmdline, "--working-directory=$dir" if $dir ne '/';
+    push @cmdline, "--property=PrivateNetwork=yes"
+	if (defined($options->{'DISABLE_NETWORK'}) && $options->{'DISABLE_NETWORK'});
+    foreach my $envvar (sort keys %ENV) {
+	my $envvalue = $ENV{$envvar};
+	push @cmdline, "--setenv=$envvar=$envvalue";
+    }
+    if (ref $command) {
+	push @cmdline, '/usr/bin/env' unless $command->[0] =~ m:^/:;
+	push @cmdline, @$command;
+    } else {
+	push @cmdline, ('/bin/sh', '-c', $command);
+	$command = [split(/\s+/, $command)];
+    }
+
+    $options->{'USER'} = $user;
+    $options->{'COMMAND'} = $command;
+    $options->{'EXPCOMMAND'} = \@cmdline;
+    $options->{'CHDIR'} = undef;
+    $options->{'DIR'} = $dir;
+}
+
+1;
--- sbuild-0.78.1.orig/lib/Sbuild/Conf.pm
+++ sbuild-0.78.1/lib/Sbuild/Conf.pm
@@ -338,6 +338,54 @@ sub setup ($) {
 	    HELP => 'Additional command-line options for autopkgtest-virt-*',
 	    CLI_OPTIONS => ['--autopkgtest-virt-server-opt', '--autopkgtest-virt-server-opts']
 	},
+	'MACHINECTL'			=> {
+	    TYPE => 'STRING',
+	    GROUP => '__INTERNAL',
+	    CHECK => sub {
+		my $conf = shift;
+		my $entry = shift;
+		my $key = $entry->{'NAME'};
+
+		# Only validate if needed.
+		if ($conf->get('CHROOT_MODE') eq 'systemd-nspawn') {
+		    $validate_program->($conf, $entry);
+		}
+	    },
+	    DEFAULT => 'machinectl',
+	    HELP => 'Path to machinectl binary'
+	},
+	'SYSTEMD_NSPAWN'			=> {
+	    TYPE => 'STRING',
+	    GROUP => '__INTERNAL',
+	    CHECK => sub {
+		my $conf = shift;
+		my $entry = shift;
+		my $key = $entry->{'NAME'};
+
+		# Only validate if needed.
+		if ($conf->get('CHROOT_MODE') eq 'systemd-nspawn') {
+		    $validate_program->($conf, $entry);
+		}
+	    },
+	    DEFAULT => 'systemd-nspawn',
+	    HELP => 'Path to systemd-nspawn binary'
+	},
+	'SYSTEMD_RUN'				=> {
+	    TYPE => 'STRING',
+	    GROUP => '__INTERNAL',
+	    CHECK => sub {
+		my $conf = shift;
+		my $entry = shift;
+		my $key = $entry->{'NAME'};
+
+		# Only validate if needed.
+		if ($conf->get('CHROOT_MODE') eq 'systemd-nspawn') {
+		    $validate_program->($conf, $entry);
+		}
+	    },
+	    DEFAULT => 'systemd-run',
+	    HELP => 'Path to systemd-run binary'
+	},
 	# Do not check for the existance of fakeroot because it's needed
 	# inside the chroot and not on the host
 	'FAKEROOT'				=> {
@@ -699,10 +747,10 @@ sub setup ($) {
 
 		die "Bad chroot mode \'" . $conf->get('CHROOT_MODE') . "\'"
 		    if !isin($conf->get('CHROOT_MODE'),
-			     qw(schroot sudo autopkgtest unshare));
+			     qw(schroot sudo autopkgtest unshare systemd-nspawn));
 	    },
 	    DEFAULT => 'schroot',
-	    HELP => 'Mechanism to use for chroot virtualisation.  Possible value are "schroot" (default), "sudo", "autopkgtest" and "unshare".',
+	    HELP => 'Mechanism to use for chroot virtualisation.  Possible value are "schroot" (default), "sudo", "autopkgtest" and "unshare" and "systemd-nspawn".',
 	    CLI_OPTIONS => ['--chroot-mode']
 	},
 	'CHROOT_SPLIT'				=> {
--- sbuild-0.78.1.orig/lib/Sbuild/Makefile.am
+++ sbuild-0.78.1/lib/Sbuild/Makefile.am
@@ -33,12 +33,14 @@ MODULES =			\
 	ChrootSudo.pm		\
 	ChrootAutopkgtest.pm	\
 	ChrootUnshare.pm	\
+	ChrootNspawn.pm		\
 	ChrootSetup.pm		\
 	ChrootInfo.pm		\
 	ChrootInfoSchroot.pm	\
 	ChrootInfoSudo.pm	\
 	ChrootInfoAutopkgtest.pm \
 	ChrootInfoUnshare.pm	\
+	ChrootInfoNspawn.pm	\
 	Exception.pm		\
 	ResolverBase.pm		\
 	AptitudeResolver.pm	\
--- sbuild-0.78.1.orig/man/sbuild.1.in
+++ sbuild-0.78.1/man/sbuild.1.in
@@ -28,7 +28,7 @@ sbuild \- build debian packages from sou
 .RB [ \-\-archive=\fIarchive\fP ]
 .RB [ \-d \[or] \-\-dist=\fIdistribution\fP ]
 .RB [ \-c \[or] \-\-chroot=\fIchroot\fP ]
-.RB [ \-\-chroot-mode=\fIschroot|sudo|autopkgtest|unshare\fP ]
+.RB [ \-\-chroot-mode=\fIschroot|sudo|autopkgtest|unshare|systemd\-nspawn\fP ]
 .RB [ \-\-arch=\fIarchitecture\fP ]
 .RB [ \-\-arch\-any " \[or] " \-\-no\-arch\-any ]
 .RB [ \-\-build=\fIarchitecture\fP ]
@@ -274,12 +274,13 @@ This command line option sets the \fBCHR
 .BR sbuild.conf (5)
 for more information.
 .TP
-.BR "\-\-chroot-mode=\fIschroot|sudo|autopkgtest|unshare\fP"
+.BR "\-\-chroot-mode=\fIschroot|sudo|autopkgtest|unshare|systemd\-nspawn\fP"
 Select the desired chroot mode. Four values are possible: schroot (the
 default), sudo (which uses sudo to execute chroot in a directory from
 /etc/sbuild/chroot or ./chroot), autopkgtest which uses the autopkgtest-virt-* binaries
-(selectable via the \-\-autopkgtest-virt-server option) and unshare (which uses linux
-namespaces for chroot and doesn't require superuser privileges).
+(selectable via the \-\-autopkgtest-virt-server option), unshare (which uses linux
+namespaces for chroot and doesn't require superuser privileges), and
+systemd\-nspawn (which uses systemd\-nspawn to start a temporary container).
 See the section
 .BR "CHROOT MODES"
 for more information.
@@ -1204,7 +1205,7 @@ environment. Provisioning and managing t
 sbuild itself but by multiple backends. The default backend (or chroot mode) is
 schroot which is an suid binary that allows regular users to enter a chroot
 environment. But sbuild also allows one to build packages in a qemu virtual
-machine, lxc, lxd or on a remote host reached by ssh using the autopkgtest
+machine, lxc, lxd, systemd\-nspawn or on a remote host reached by ssh using the autopkgtest
 backend. The backend can be chosen using the \f[CB]--chroot-mode\fP command
 line argument or the \fI$chroot_mode\fP configuration parameter.
 .TP
@@ -1261,6 +1262,17 @@ arbitrary tarballs containing chroot env
 building. The default tarball location is in ~/.cache/sbuild/. The expected
 names are resolved in the same order as for the schroot chroot mode and can be
 overridden using the \-c or \-\-chroot options.
+.TP
+.BR systemd\-nspawn
+This is an experimental backend that uses systemd-nspawn to start an
+ephemeral container. The base chroot images are expected to be in
+subdirectories of /var/lib/machines; the expected names are resolved in the
+same order as for the schroot chroot mode and can be overridden using the
+\-c or \-\-chroot options. Each image needs to have systemd and dbus installed
+(e.g. via the \-\-include=systemd,dbus argument to debootstrap). The process
+of creating the ephemeral cloned image will be somewhat faster if the base
+image is a BTRFS subvolume. The build user needs to have sudo access to
+execute machinectl, systemd\-nspawn, and systemd\-run.
 .SH BUILD ARTIFACTS
 Sbuild is meant to be used to build architecture specific binary packages from
 a given source package. In addition, sbuild is also able to generate

Reply via email to