1#! @perl@ 2 3use strict; 4use POSIX; 5use File::Path; 6use File::Slurp; 7use Fcntl ':flock'; 8use Getopt::Long qw(:config gnu_getopt); 9 10my $nsenter = "@utillinux@/bin/nsenter"; 11my $su = "@su@"; 12 13# Ensure a consistent umask. 14umask 0022; 15 16# Parse the command line. 17 18sub showHelp { 19 print <<EOF; 20Usage: nixos-container list 21 nixos-container create <container-name> [--system-path <path>] [--config <string>] [--ensure-unique-name] [--auto-start] 22 nixos-container destroy <container-name> 23 nixos-container start <container-name> 24 nixos-container stop <container-name> 25 nixos-container status <container-name> 26 nixos-container update <container-name> [--config <string>] 27 nixos-container login <container-name> 28 nixos-container root-login <container-name> 29 nixos-container run <container-name> -- args... 30 nixos-container show-ip <container-name> 31 nixos-container show-host-key <container-name> 32EOF 33 exit 0; 34} 35 36my $systemPath; 37my $ensureUniqueName = 0; 38my $autoStart = 0; 39my $extraConfig; 40 41GetOptions( 42 "help" => sub { showHelp() }, 43 "ensure-unique-name" => \$ensureUniqueName, 44 "auto-start" => \$autoStart, 45 "system-path=s" => \$systemPath, 46 "config=s" => \$extraConfig 47 ) or exit 1; 48 49my $action = $ARGV[0] or die "$0: no action specified\n"; 50 51 52# Execute the selected action. 53 54mkpath("/etc/containers", 0, 0755); 55mkpath("/var/lib/containers", 0, 0700); 56 57if ($action eq "list") { 58 foreach my $confFile (glob "/etc/containers/*.conf") { 59 $confFile =~ /\/([^\/]+).conf$/ or next; 60 print "$1\n"; 61 } 62 exit 0; 63} 64 65my $containerName = $ARGV[1] or die "$0: no container name specified\n"; 66$containerName =~ /^[a-zA-Z0-9\-]+$/ or die "$0: invalid container name\n"; 67 68sub writeNixOSConfig { 69 my ($nixosConfigFile) = @_; 70 71 my $nixosConfig = <<EOF; 72{ config, lib, pkgs, ... }: 73 74with lib; 75 76{ boot.isContainer = true; 77 networking.hostName = mkDefault "$containerName"; 78 networking.useDHCP = false; 79 $extraConfig 80} 81EOF 82 83 write_file($nixosConfigFile, $nixosConfig); 84} 85 86if ($action eq "create") { 87 # Acquire an exclusive lock to prevent races with other 88 # invocations of ‘nixos-container create’. 89 my $lockFN = "/run/lock/nixos-container"; 90 open(my $lock, '>>', $lockFN) or die "$0: opening $lockFN: $!"; 91 flock($lock, LOCK_EX) or die "$0: could not lock $lockFN: $!"; 92 93 my $confFile = "/etc/containers/$containerName.conf"; 94 my $root = "/var/lib/containers/$containerName"; 95 96 # Maybe generate a unique name. 97 if ($ensureUniqueName) { 98 my $base = $containerName; 99 for (my $nr = 0; ; $nr++) { 100 $confFile = "/etc/containers/$containerName.conf"; 101 $root = "/var/lib/containers/$containerName"; 102 last unless -e $confFile || -e $root; 103 $containerName = "$base-$nr"; 104 } 105 } 106 107 die "$0: container ‘$containerName’ already exists\n" if -e $confFile; 108 109 # Due to interface name length restrictions, container names must 110 # be restricted too. 111 die "$0: container name ‘$containerName’ is too long\n" if length $containerName > 11; 112 113 # Get an unused IP address. 114 my %usedIPs; 115 foreach my $confFile2 (glob "/etc/containers/*.conf") { 116 my $s = read_file($confFile2) or die; 117 $usedIPs{$1} = 1 if $s =~ /^HOST_ADDRESS=([0-9\.]+)$/m; 118 $usedIPs{$1} = 1 if $s =~ /^LOCAL_ADDRESS=([0-9\.]+)$/m; 119 } 120 121 my ($ipPrefix, $hostAddress, $localAddress); 122 for (my $nr = 1; $nr < 255; $nr++) { 123 $ipPrefix = "10.233.$nr"; 124 $hostAddress = "$ipPrefix.1"; 125 $localAddress = "$ipPrefix.2"; 126 last unless $usedIPs{$hostAddress} || $usedIPs{$localAddress}; 127 $ipPrefix = undef; 128 } 129 130 die "$0: out of IP addresses\n" unless defined $ipPrefix; 131 132 my @conf; 133 push @conf, "PRIVATE_NETWORK=1\n"; 134 push @conf, "HOST_ADDRESS=$hostAddress\n"; 135 push @conf, "LOCAL_ADDRESS=$localAddress\n"; 136 push @conf, "AUTO_START=$autoStart\n"; 137 write_file($confFile, \@conf); 138 139 close($lock); 140 141 print STDERR "host IP is $hostAddress, container IP is $localAddress\n"; 142 143 # The per-container directory is restricted to prevent users on 144 # the host from messing with guest users who happen to have the 145 # same uid. 146 my $profileDir = "/nix/var/nix/profiles/per-container"; 147 mkpath($profileDir, 0, 0700); 148 $profileDir = "$profileDir/$containerName"; 149 mkpath($profileDir, 0, 0755); 150 151 # Build/set the initial configuration. 152 if (defined $systemPath) { 153 system("nix-env", "-p", "$profileDir/system", "--set", $systemPath) == 0 154 or die "$0: failed to set initial container configuration\n"; 155 } else { 156 mkpath("$root/etc/nixos", 0, 0755); 157 158 my $nixosConfigFile = "$root/etc/nixos/configuration.nix"; 159 writeNixOSConfig $nixosConfigFile; 160 161 system("nix-env", "-p", "$profileDir/system", 162 "-I", "nixos-config=$nixosConfigFile", "-f", "<nixpkgs/nixos>", 163 "--set", "-A", "system") == 0 164 or die "$0: failed to build initial container configuration\n"; 165 } 166 167 print "$containerName\n" if $ensureUniqueName; 168 exit 0; 169} 170 171my $root = "/var/lib/containers/$containerName"; 172my $profileDir = "/nix/var/nix/profiles/per-container/$containerName"; 173my $gcRootsDir = "/nix/var/nix/gcroots/per-container/$containerName"; 174my $confFile = "/etc/containers/$containerName.conf"; 175if (!-e $confFile) { 176 if ($action eq "destroy") { 177 exit 0; 178 } elsif ($action eq "status") { 179 print "gone\n"; 180 } 181 die "$0: container ‘$containerName’ does not exist\n" ; 182} 183 184sub isContainerRunning { 185 my $status = `systemctl show 'container\@$containerName'`; 186 return $status =~ /ActiveState=active/; 187} 188 189sub stopContainer { 190 system("systemctl", "stop", "container\@$containerName") == 0 191 or die "$0: failed to stop container\n"; 192} 193 194# Return the PID of the init process of the container. 195sub getLeader { 196 my $s = `machinectl show "$containerName" -p Leader`; 197 chomp $s; 198 $s =~ /^Leader=(\d+)$/ or die "unable to get container's main PID\n"; 199 return int($1); 200} 201 202# Run a command in the container. 203sub runInContainer { 204 my @args = @_; 205 my $leader = getLeader; 206 exec($nsenter, "-t", $leader, "-m", "-u", "-i", "-n", "-p", "--", @args); 207 die "cannot run ‘nsenter’: $!\n"; 208} 209 210# Remove a directory while recursively unmounting all mounted filesystems within 211# that directory and unmounting/removing that directory afterwards as well. 212# 213# NOTE: If the specified path is a mountpoint, its contents will be removed, 214# only mountpoints underneath that path will be unmounted properly. 215sub safeRemoveTree { 216 my ($path) = @_; 217 system("find", $path, "-mindepth", "1", "-xdev", 218 "(", "-type", "d", "-exec", "mountpoint", "-q", "{}", ";", ")", 219 "-exec", "umount", "-fR", "{}", "+"); 220 system("rm", "--one-file-system", "-rf", $path); 221 if (-e $path) { 222 system("umount", "-fR", $path); 223 system("rm", "--one-file-system", "-rf", $path); 224 } 225} 226 227if ($action eq "destroy") { 228 die "$0: cannot destroy declarative container (remove it from your configuration.nix instead)\n" 229 unless POSIX::access($confFile, &POSIX::W_OK); 230 231 stopContainer if isContainerRunning; 232 233 safeRemoveTree($profileDir) if -e $profileDir; 234 safeRemoveTree($gcRootsDir) if -e $gcRootsDir; 235 safeRemoveTree($root) if -e $root; 236 unlink($confFile) or die; 237} 238 239elsif ($action eq "start") { 240 system("systemctl", "start", "container\@$containerName") == 0 241 or die "$0: failed to start container\n"; 242} 243 244elsif ($action eq "stop") { 245 stopContainer; 246} 247 248elsif ($action eq "status") { 249 print isContainerRunning() ? "up" : "down", "\n"; 250} 251 252elsif ($action eq "update") { 253 my $nixosConfigFile = "$root/etc/nixos/configuration.nix"; 254 255 # FIXME: may want to be more careful about clobbering the existing 256 # configuration.nix. 257 writeNixOSConfig $nixosConfigFile if (defined $extraConfig && $extraConfig ne ""); 258 259 system("nix-env", "-p", "$profileDir/system", 260 "-I", "nixos-config=$nixosConfigFile", "-f", "<nixpkgs/nixos>", 261 "--set", "-A", "system") == 0 262 or die "$0: failed to build container configuration\n"; 263 264 if (isContainerRunning) { 265 print STDERR "reloading container...\n"; 266 system("systemctl", "reload", "container\@$containerName") == 0 267 or die "$0: failed to reload container\n"; 268 } 269} 270 271elsif ($action eq "login") { 272 exec("machinectl", "login", "--", $containerName); 273} 274 275elsif ($action eq "root-login") { 276 runInContainer("@su@", "root", "-l"); 277} 278 279elsif ($action eq "run") { 280 shift @ARGV; shift @ARGV; 281 # Escape command. 282 my $s = join(' ', map { s/'/'\\''/g; "'$_'" } @ARGV); 283 runInContainer("@su@", "root", "-l", "-c", "exec " . $s); 284} 285 286elsif ($action eq "show-ip") { 287 my $s = read_file($confFile) or die; 288 $s =~ /^LOCAL_ADDRESS=([0-9\.]+)$/m or die "$0: cannot get IP address\n"; 289 print "$1\n"; 290} 291 292elsif ($action eq "show-host-key") { 293 my $fn = "$root/etc/ssh/ssh_host_ed25519_key.pub"; 294 $fn = "$root/etc/ssh/ssh_host_ecdsa_key.pub" unless -e $fn; 295 exit 1 if ! -f $fn; 296 print read_file($fn); 297} 298 299else { 300 die "$0: unknown action ‘$action’\n"; 301}