diff --git a/nixos/modules/virtualisation/containers.nix b/nixos/modules/virtualisation/containers.nix index 16df108c21ec..9964cd431cd7 100644 --- a/nixos/modules/virtualisation/containers.nix +++ b/nixos/modules/virtualisation/containers.nix @@ -18,8 +18,9 @@ let name = "nixos-container"; dir = "bin"; isExecutable = true; - src = ./nixos-container.sh; - inherit (pkgs) bash socat; + src = ./nixos-container.pl; + perl = "${pkgs.perl}/bin/perl -I${pkgs.perlPackages.FileSlurp}/lib/perl5/site_perl"; + inherit (pkgs) socat; }; in diff --git a/nixos/modules/virtualisation/nixos-container.pl b/nixos/modules/virtualisation/nixos-container.pl new file mode 100644 index 000000000000..dfc856e8b667 --- /dev/null +++ b/nixos/modules/virtualisation/nixos-container.pl @@ -0,0 +1,198 @@ +#! @perl@ + +use strict; +use File::Path; +use File::Slurp; +use Fcntl ':flock'; +use Getopt::Long qw(:config gnu_getopt); + +my $socat = '@socat@/bin/socat'; + +# Parse the command line. + +sub showHelp { + print < [--config ] [--ensure-unique-name] + nixos-container destroy + nixos-container start + nixos-container stop + nixos-container login + nixos-container root-shell + nixos-container set-root-password + nixos-container show-ip +EOF + exit 0; +} + +my $ensureUniqueName = 0; +my $extraConfig = ""; + +GetOptions( + "help" => sub { showHelp() }, + "ensure-unique-name" => \$ensureUniqueName, + "config=s" => \$extraConfig + ) or exit 1; + +my $action = $ARGV[0] or die "$0: no action specified\n"; + + +# Execute the selected action. + +mkpath("/etc/containers", 0, 0755); +mkpath("/var/lib/containers", 0, 0700); + +if ($action eq "list") { + foreach my $confFile (glob "/etc/containers/*.conf") { + $confFile =~ /\/([^\/]+).conf$/ or next; + print "$1\n"; + } + exit 0; +} + +my $containerName = $ARGV[1] or die "$0: no container name specified\n"; +$containerName =~ /^[a-zA-Z0-9\-]+$/ or die "$0: invalid container name\n"; + +if ($action eq "create") { + # Acquire an exclusive lock to prevent races with other + # invocations of ‘nixos-container create’. + my $lockFN = "/run/lock/nixos-container"; + open(my $lock, '>>', $lockFN) or die "$0: opening $lockFN: $!"; + flock($lock, LOCK_EX) or die "$0: could not lock $lockFN: $!"; + + my $confFile = "/etc/containers/$containerName.conf"; + my $root = "/var/lib/containers/$containerName"; + + # Maybe generate a unique name. + if ($ensureUniqueName) { + my $base = $containerName; + for (my $nr = 0; ; $nr++) { + $containerName = "$base-$nr"; + $confFile = "/etc/containers/$containerName.conf"; + $root = "/var/lib/containers/$containerName"; + last unless -e $confFile || -e $root; + } + } + + die "$0: container ‘$containerName’ already exists\n" if -e $confFile; + + # Get an unused IP address. + my %usedIPs; + foreach my $confFile2 (glob "/etc/containers/*.conf") { + my $s = read_file($confFile2) or die; + $usedIPs{$1} = 1 if $s =~ /^HOST_ADDRESS=([0-9\.]+)$/m; + $usedIPs{$1} = 1 if $s =~ /^LOCAL_ADDRESS=([0-9\.]+)$/m; + } + + my ($ipPrefix, $hostAddress, $localAddress); + for (my $nr = 1; $nr < 255; $nr++) { + $ipPrefix = "10.233.$nr"; + $hostAddress = "$ipPrefix.1"; + $localAddress = "$ipPrefix.2"; + last unless $usedIPs{$hostAddress} || $usedIPs{$localAddress}; + $ipPrefix = undef; + } + + die "$0: out of IP addresses\n" unless defined $ipPrefix; + + my @conf; + push @conf, "PRIVATE_NETWORK=1\n"; + push @conf, "HOST_ADDRESS=$hostAddress\n"; + push @conf, "LOCAL_ADDRESS=$localAddress\n"; + write_file($confFile, \@conf); + + close($lock); + + print STDERR "host IP is $hostAddress, container IP is $localAddress\n"; + + mkpath("$root/etc/nixos", 0, 0755); + + my $nixosConfig = < ]; + $extraConfig +} +EOF + my $nixosConfigFile = "$root/etc/nixos/configuration.nix"; + write_file($nixosConfigFile, $nixosConfig); + + # The per-container directory is restricted to prevent users on + # the host from messing with guest users who happen to have the + # same uid. + my $profileDir = "/nix/var/nix/profiles/per-container"; + mkpath($profileDir, 0, 0700); + $profileDir = "$profileDir/$containerName"; + mkpath($profileDir, 0, 0755); + + system("nix-env", "-p", "$profileDir/system", + "-I", "nixos-config=$nixosConfigFile", "-f", "", + "--set", "-A", "system") == 0 + or die "$0: failed to build initial container configuration\n"; + + print "$containerName\n" if $ensureUniqueName; + exit 0; +} + +my $confFile = "/etc/containers/$containerName.conf"; +die "$0: container ‘$containerName’ does not exist\n" if !-e $confFile; + +sub stopContainer { + system("systemctl", "stop", "container\@$containerName") == 0 + or die "$0: failed to stop container\n"; +} + +if ($action eq "destroy") { + my $root = "/var/lib/containers/$containerName"; + my $profileDir = "/nix/var/nix/profiles/per-container/$containerName"; + + my $status = `systemctl show 'container\@$containerName'`; + stopContainer if $status =~ /ActiveState=active/; + + rmtree($profileDir) if -e $profileDir; + rmtree($root) if -e $root; + unlink($confFile) or die; +} + +elsif ($action eq "start") { + system("systemctl", "start", "container\@$containerName") == 0 + or die "$0: failed to start container\n"; +} + +elsif ($action eq "stop") { + stopContainer; +} + +elsif ($action eq "login") { + exec($socat, "unix:/var/lib/containers/$containerName/var/lib/login.socket", "-,echo=0,raw"); +} + +elsif ($action eq "root-shell") { + exec($socat, "unix:/var/lib/containers/$containerName/var/lib/root-shell.socket", "-"); +} + +elsif ($action eq "set-root-password") { + # FIXME: don't get password from the command line. + my $password = $ARGV[2] or die "$0: no password given\n"; + open(SOCAT, "|-", $socat, "unix:/var/lib/containers/$containerName/var/lib/root-shell.socket", "-"); + print SOCAT "passwd\n"; + print SOCAT "$password\n"; + print SOCAT "$password\n"; + close(SOCAT); +} + +elsif ($action eq "show-ip") { + my $s = read_file($confFile) or die; + $s =~ /^LOCAL_ADDRESS=([0-9\.]+)$/m or die "$0: cannot get IP address\n"; + print "$1\n"; +} + +else { + die "$0: unknown action ‘$action’\n"; +} diff --git a/nixos/modules/virtualisation/nixos-container.sh b/nixos/modules/virtualisation/nixos-container.sh deleted file mode 100644 index 47abf96072f3..000000000000 --- a/nixos/modules/virtualisation/nixos-container.sh +++ /dev/null @@ -1,171 +0,0 @@ -#! @bash@/bin/sh -e - -usage() { - echo "Usage: $0 list" >&2 - echo " $0 create [--config ] [--ensure-unique-name]" >&2 - echo " $0 update " >&2 - echo " $0 destroy " >&2 - echo " $0 login " >&2 - echo " $0 root-shell " >&2 - echo " $0 set-root-password " >&2 - echo " $0 show-ip " >&2 -} - -args="`getopt --options '' -l help -l config: -l ensure-unique-name -- "$@"`" -eval "set -- $args" -extraConfig= -ensureUniqueName= -while [ $# -gt 0 ]; do - case "$1" in - (--help) usage; exit 0;; - (--config) shift; extraConfig=$1;; - (--ensure-unique-name) ensureUniqueName=1;; - (--) shift; break;; - (*) break;; - esac - shift -done - -action="$1" -if [ -z "$action" ]; then usage; exit 1; fi -shift - -getContainerRoot() { - root="/var/lib/containers/$container" - if ! [ -d "$root" ]; then - echo "$0: container ‘$container’ does not exist" >&2 - exit 1 - fi -} - -if [ $action = list ]; then - for i in $(cd /etc/containers && echo *.conf); do - echo "$(basename "$i" .conf)" - done - exit 0 -fi - -container="$1" -if [ -z "$container" ]; then usage; exit 1; fi -shift - -if [ $action = create ]; then - - if [ -n "$ensureUniqueName" ]; then - # FIXME: race - nr=0 - while [ -e "/etc/containers/$container-$nr.conf" -o -e "/var/lib/containers/$container-$nr" ]; do - : $((nr++)) - done - container="$container-$nr" - fi - - confFile="/etc/containers/$container.conf" - root="/var/lib/containers/$container" - - if [ -e "$confFile" -o -e "$root/nix" ]; then - echo "$0: container ‘$container’ already exists" >&2 - exit 1 - fi - - profileDir="/nix/var/nix/profiles/per-container/$container" - mkdir -m 0755 -p "$root/etc/nixos" "$profileDir" - - config=" -{ config, pkgs, ... }: - -with pkgs.lib; - -{ boot.isContainer = true; - security.initialRootPassword = mkDefault \"!\"; - networking.hostName = mkDefault \"$container\"; - networking.useDHCP = false; - imports = [ ]; - $extraConfig -}" - configFile="$root/etc/nixos/configuration.nix" - echo "$config" > "$configFile" - - nix-env -p "$profileDir/system" -I "nixos-config=$configFile" -f '' --set -A system - - # Allocate a new /8 network in the 10.233.* range. FIXME: race - network="$(sed -e 's/.*_ADDRESS=10\.233\.\(.*\)\..*/\1/; t; d' /etc/containers/*.conf | sort -n | tail -n1)" - if [ -z "$network" ]; then network=0; else : $((network++)); fi - - hostAddress="10.233.$network.1" - localAddress="10.233.$network.2" - echo "host IP is $hostAddress, container IP is $localAddress" >&2 - - cat > "$confFile" <&2 - systemctl start "container@$container.service" - - # Print generated container name on stdout. - if [ -n "$ensureUniqueName" ]; then - echo "$container" - fi - -elif [ $action = update ]; then - - getContainerRoot - - configFile="$root/etc/nixos/configuration.nix" - profileDir="/nix/var/nix/profiles/per-container/$container" - - nix-env -p "$profileDir/system" -I "nixos-config=$configFile" -f '' --set -A system - - echo "reloading container@$container.service..." >&2 - systemctl reload "container@$container.service" - -elif [ $action = destroy ]; then - - getContainerRoot - - confFile="/etc/containers/$container.conf" - if [ -e "$confFile" -a ! -w "$confFile" ]; then - echo "$0: cannot destroy declarative container (remove it from your configuration.nix instead)" - exit 1 - fi - - if systemctl show "container@$container.service" | grep -q ActiveState=active; then - echo "stopping container@$container.service..." >&2 - systemctl stop "container@$container.service" - fi - - rm -f "$confFile" - rm -rf "$root" - -elif [ $action = login ]; then - - getContainerRoot - exec @socat@/bin/socat "unix:$root/var/lib/login.socket" -,echo=0,raw - -elif [ $action = root-shell ]; then - - getContainerRoot - exec @socat@/bin/socat "unix:$root/var/lib/root-shell.socket" - - -elif [ $action = set-root-password ]; then - - password="$1" - if [ -z "$password" ]; then usage; exit 1; fi - - # FIXME: not very secure. - getContainerRoot - (echo "passwd"; echo "$password"; echo "$password") | @socat@/bin/socat "unix:$root/var/lib/root-shell.socket" - - -elif [ $action = show-ip ]; then - - getContainerRoot - . "/etc/containers/$container.conf" - echo "$LOCAL_ADDRESS" - -else - echo "$0: unknown action ‘$action’" >&2 - exit 1 -fi