biome: 1.9.2 -> 1.9.3
[NixPkgs.git] / pkgs / tools / virtualization / nixos-container / nixos-container.pl
blob5e504eca749ae6bbff1deeb50e105832bf2e69e1
1 #! @perl@/bin/perl
3 use strict;
4 use POSIX;
5 use File::Path;
6 use File::Slurp;
7 use Fcntl ':flock';
8 use Getopt::Long qw(:config gnu_getopt no_bundling);
9 use Cwd 'abs_path';
10 use Time::HiRes;
12 my $nsenter = "@utillinux@/bin/nsenter";
13 my $su = "@su@";
15 my $configurationDirectory = "@configurationDirectory@";
16 my $stateDirectory = "@stateDirectory@";
18 # Ensure a consistent umask.
19 umask 0022;
21 # Ensure $NIXOS_CONFIG is not set.
22 $ENV{"NIXOS_CONFIG"} = "";
24 # Parse the command line.
26 sub showHelp {
27 print <<EOF;
28 Usage: nixos-container list
29 nixos-container create <container-name>
30 [--nixos-path <path>]
31 [--system-path <path>]
32 [--config <string>]
33 [--config-file <path>]
34 [--flake <flakeref>]
35 [--ensure-unique-name]
36 [--auto-start]
37 [--bridge <iface>]
38 [--port <port>]
39 [--host-address <string>]
40 [--local-address <string>]
41 nixos-container destroy <container-name>
42 nixos-container restart <container-name>
43 nixos-container start <container-name>
44 nixos-container stop <container-name>
45 nixos-container terminate <container-name>
46 nixos-container status <container-name>
47 nixos-container update <container-name>
48 [--config <string>]
49 [--config-file <path>]
50 [--flake <flakeref>]
51 [--nixos-path <path>]
52 nixos-container login <container-name>
53 nixos-container root-login <container-name>
54 nixos-container run <container-name> -- args...
55 nixos-container show-ip <container-name>
56 nixos-container show-host-key <container-name>
57 EOF
58 exit 0;
61 my $systemPath;
62 my $nixosPath;
63 my $ensureUniqueName = 0;
64 my $autoStart = 0;
65 my $bridge;
66 my $port;
67 my $extraConfig;
68 my $signal;
69 my $configFile;
70 my $hostAddress;
71 my $localAddress;
72 my $flake;
73 my $flakeAttr = "container";
75 # Nix passthru flags.
76 my @nixFlags;
77 my @nixFlags2;
79 sub copyNixFlags0 { push @nixFlags, "--$_[0]"; }
80 sub copyNixFlags1 { push @nixFlags, "--$_[0]", $_[1]; }
82 # Ugly hack to handle flags that take two arguments, like --option.
83 sub copyNixFlags2 {
84 if (scalar(@nixFlags2) % 3 == 0) {
85 push @nixFlags2, "--$_[0]", $_[1];
86 } else {
87 push @nixFlags2, $_[1];
91 GetOptions(
92 "help" => sub { showHelp() },
93 "ensure-unique-name" => \$ensureUniqueName,
94 "auto-start" => \$autoStart,
95 "bridge=s" => \$bridge,
96 "port=s" => \$port,
97 "system-path=s" => \$systemPath,
98 "signal=s" => \$signal,
99 "nixos-path=s" => \$nixosPath,
100 "config=s" => \$extraConfig,
101 "config-file=s" => \$configFile,
102 "host-address=s" => \$hostAddress,
103 "local-address=s" => \$localAddress,
104 "flake=s" => \$flake,
105 # Nix passthru options.
106 "log-format=s" => \&copyNixFlags1,
107 "option=s{2}" => \&copyNixFlags2,
108 "impure" => \&copyNixFlags0,
109 "update-input=s" => \&copyNixFlags1,
110 "override-input=s{2}" => \&copyNixFlags2,
111 "commit-lock-file" => \&copyNixFlags0,
112 "no-registries" => \&copyNixFlags0,
113 "no-update-lock-file" => \&copyNixFlags0,
114 "no-write-lock-file" => \&copyNixFlags0,
115 "no-allow-dirty" => \&copyNixFlags0,
116 "recreate-lock-file" => \&copyNixFlags0,
117 ) or exit 1;
119 push @nixFlags, @nixFlags2;
121 if (defined $hostAddress and !defined $localAddress or defined $localAddress and !defined $hostAddress) {
122 die "With --host-address set, --local-address is required as well!";
125 my $action = $ARGV[0] or die "$0: no action specified\n";
127 if (defined $configFile and defined $extraConfig) {
128 die "--config and --config-file are mutually incompatible. " .
129 "Please define one or the other, but not both";
132 if (defined $flake && $flake =~ /^(.*)#([^#"]+)$/) {
133 $flake = $1;
134 $flakeAttr = $2;
137 # Execute the selected action.
139 mkpath("$configurationDirectory", 0, 0755);
140 mkpath("$stateDirectory", 0, 0700);
143 if ($action eq "list") {
144 foreach my $confFile (glob "$configurationDirectory/*.conf") {
145 # Filter libpod configuration files
146 # From 22.05 and onwards this is not an issue any more as directories dont clash
147 if($confFile eq "/etc/containers/libpod.conf" || $confFile eq "/etc/containers/containers.conf" || $confFile eq "/etc/containers/registries.conf") {
148 next
150 $confFile =~ /\/([^\/]+).conf$/ or next;
151 print "$1\n";
153 exit 0;
156 my $containerName = $ARGV[1] or die "$0: no container name specified\n";
157 $containerName =~ /^[a-zA-Z0-9_-]+$/ or die "$0: invalid container name\n";
159 sub writeNixOSConfig {
160 my ($nixosConfigFile) = @_;
162 my $localExtraConfig = "";
164 if ($extraConfig) {
165 $localExtraConfig = $extraConfig
166 } elsif ($configFile) {
167 my $resolvedFile = abs_path($configFile);
168 $localExtraConfig = "imports = [ $resolvedFile ];"
171 my $nixosConfig = <<EOF;
172 { config, lib, pkgs, ... }:
174 { boot.isContainer = true;
175 networking.hostName = lib.mkDefault "$containerName";
176 networking.useDHCP = false;
177 $localExtraConfig
181 write_file($nixosConfigFile, $nixosConfig);
184 sub buildFlake {
185 system("nix", "build", "-o", "$systemPath.tmp", @nixFlags, "--",
186 "$flake#nixosConfigurations.\"$flakeAttr\".config.system.build.toplevel") == 0
187 or die "$0: failed to build container from flake '$flake'\n";
188 $systemPath = readlink("$systemPath.tmp") or die;
189 unlink("$systemPath.tmp");
192 sub clearContainerState {
193 my ($profileDir, $gcRootsDir, $root, $configFile) = @_;
195 safeRemoveTree($profileDir) if -e $profileDir;
196 safeRemoveTree($gcRootsDir) if -e $gcRootsDir;
197 system("chattr", "-i", "$root/var/empty") if -e "$root/var/empty";
198 safeRemoveTree($root) if -e $root;
199 unlink($configFile) or die;
202 if ($action eq "create") {
203 # Acquire an exclusive lock to prevent races with other
204 # invocations of ‘nixos-container create’.
205 my $lockFN = "/run/lock/nixos-container";
206 open(my $lock, '>>', $lockFN) or die "$0: opening $lockFN: $!";
207 flock($lock, LOCK_EX) or die "$0: could not lock $lockFN: $!";
209 my $confFile = "$configurationDirectory/$containerName.conf";
210 my $root = "$stateDirectory/$containerName";
212 # Maybe generate a unique name.
213 if ($ensureUniqueName) {
214 my $base = $containerName;
215 for (my $nr = 0; ; $nr++) {
216 $confFile = "$configurationDirectory/$containerName.conf";
217 $root = "$stateDirectory/$containerName";
218 last unless -e $confFile || -e $root;
219 $containerName = "$base-$nr";
223 die "$0: container ‘$containerName’ already exists\n" if -e $confFile;
225 # Due to interface name length restrictions, container names must
226 # be restricted too.
227 die "$0: container name ‘$containerName’ is too long\n" if length $containerName > 11;
229 # Get an unused IP address.
230 my %usedIPs;
231 foreach my $confFile2 (glob "$configurationDirectory/*.conf") {
232 # Filter libpod configuration files
233 # From 22.05 and onwards this is not an issue any more as directories dont clash
234 if($confFile2 eq "/etc/containers/libpod.conf" || $confFile2 eq "/etc/containers/containers.conf" || $confFile2 eq "/etc/containers/registries.conf") {
235 next
237 my $s = read_file($confFile2) or die;
238 $usedIPs{$1} = 1 if $s =~ /^HOST_ADDRESS=([0-9\.]+)$/m;
239 $usedIPs{$1} = 1 if $s =~ /^LOCAL_ADDRESS=([0-9\.]+)$/m;
242 unless (defined $hostAddress) {
243 my $ipPrefix;
244 for (my $nr = 1; $nr < 255; $nr++) {
245 $ipPrefix = "10.233.$nr";
246 $hostAddress = "$ipPrefix.1";
247 $localAddress = "$ipPrefix.2";
248 last unless $usedIPs{$hostAddress} || $usedIPs{$localAddress};
249 $ipPrefix = undef;
252 die "$0: out of IP addresses\n" unless defined $ipPrefix;
255 my @conf;
256 push @conf, "PRIVATE_NETWORK=1\n";
257 push @conf, "HOST_ADDRESS=$hostAddress\n";
258 push @conf, "LOCAL_ADDRESS=$localAddress\n";
259 push @conf, "HOST_BRIDGE=$bridge\n";
260 push @conf, "HOST_PORT=$port\n";
261 push @conf, "AUTO_START=$autoStart\n";
262 push @conf, "FLAKE=$flake\n" if defined $flake;
263 write_file($confFile, \@conf);
265 close($lock);
267 print STDERR "host IP is $hostAddress, container IP is $localAddress\n";
269 # The per-container directory is restricted to prevent users on
270 # the host from messing with guest users who happen to have the
271 # same uid.
272 my $profileDir = "/nix/var/nix/profiles/per-container";
273 mkpath($profileDir, 0, 0700);
274 $profileDir = "$profileDir/$containerName";
275 mkpath($profileDir, 0, 0755);
277 # Build/set the initial configuration.
278 if (defined $flake) {
279 buildFlake();
282 if (defined $systemPath) {
283 system("nix-env", "-p", "$profileDir/system", "--set", $systemPath) == 0
284 or do {
285 clearContainerState($profileDir, "$profileDir/$containerName", $root, $confFile);
286 die "$0: failed to set initial container configuration\n";
288 } else {
289 mkpath("$root/etc/nixos", 0, 0755);
291 my $nixenvF = $nixosPath // "<nixpkgs/nixos>";
292 my $nixosConfigFile = "$root/etc/nixos/configuration.nix";
293 writeNixOSConfig $nixosConfigFile;
295 system("nix-env", "-p", "$profileDir/system",
296 "-I", "nixos-config=$nixosConfigFile", "-f", "$nixenvF",
297 "--set", "-A", "system", @nixFlags) == 0
298 or do {
299 clearContainerState($profileDir, "$profileDir/$containerName", $root, $confFile);
300 die "$0: failed to build initial container configuration\n"
304 print "$containerName\n" if $ensureUniqueName;
305 exit 0;
308 my $root = "$stateDirectory/$containerName";
309 my $profileDir = "/nix/var/nix/profiles/per-container/$containerName";
310 my $gcRootsDir = "/nix/var/nix/gcroots/per-container/$containerName";
311 my $confFile = "$configurationDirectory/$containerName.conf";
312 if (!-e $confFile) {
313 if ($action eq "destroy") {
314 exit 0;
315 } elsif ($action eq "status") {
316 print "gone\n";
318 die "$0: container ‘$containerName’ does not exist\n" ;
321 # Return the PID of the init process of the container.
322 sub getLeader {
323 my $s = `machinectl show "$containerName" -p Leader`;
324 chomp $s;
325 $s =~ /^Leader=(\d+)$/ or die "unable to get container's main PID\n";
326 return int($1);
329 sub isContainerRunning {
330 my $status = `systemctl show 'container\@$containerName'`;
331 return $status =~ /ActiveState=active/;
334 sub terminateContainer {
335 my $leader = getLeader;
336 system("machinectl", "terminate", $containerName) == 0
337 or die "$0: failed to terminate container\n";
338 # Wait for the leader process to exit
339 # TODO: As for any use of PIDs for process control where the process is
340 # not a direct child of ours, this can go wrong when the pid gets
341 # recycled after a PID overflow.
342 # Relying entirely on some form of UUID provided by machinectl
343 # instead of PIDs would remove this risk.
344 # See https://github.com/NixOS/nixpkgs/pull/32992#discussion_r158586048
345 while ( kill 0, $leader ) { Time::HiRes::sleep(0.1) }
348 sub startContainer {
349 system("systemctl", "start", "container\@$containerName") == 0
350 or die "$0: failed to start container\n";
353 sub stopContainer {
354 system("systemctl", "stop", "container\@$containerName") == 0
355 or die "$0: failed to stop container\n";
358 sub restartContainer {
359 stopContainer;
360 startContainer;
363 # Run a command in the container.
364 sub runInContainer {
365 my @args = @_;
366 my $leader = getLeader;
367 exec($nsenter, "-t", $leader, "-m", "-u", "-i", "-n", "-p", "--", @args);
368 die "cannot run ‘nsenter’: $!\n";
371 # Remove a directory while recursively unmounting all mounted filesystems within
372 # that directory and unmounting/removing that directory afterwards as well.
374 # NOTE: If the specified path is a mountpoint, its contents will be removed,
375 # only mountpoints underneath that path will be unmounted properly.
376 sub safeRemoveTree {
377 my ($path) = @_;
378 system("find", $path, "-mindepth", "1", "-xdev",
379 "(", "-type", "d", "-exec", "mountpoint", "-q", "{}", ";", ")",
380 "-exec", "umount", "-fR", "{}", "+");
381 system("rm", "--one-file-system", "-rf", $path);
382 if (-e $path) {
383 system("umount", "-fR", $path);
384 system("rm", "--one-file-system", "-rf", $path);
388 if ($action eq "destroy") {
389 die "$0: cannot destroy declarative container (remove it from your configuration.nix instead)\n"
390 unless POSIX::access($confFile, &POSIX::W_OK);
392 terminateContainer if (isContainerRunning);
394 clearContainerState($profileDir, $gcRootsDir, $root, $confFile);
397 elsif ($action eq "restart") {
398 restartContainer;
401 elsif ($action eq "start") {
402 startContainer;
405 elsif ($action eq "stop") {
406 stopContainer;
409 elsif ($action eq "terminate") {
410 terminateContainer;
413 elsif ($action eq "status") {
414 print isContainerRunning() ? "up" : "down", "\n";
417 elsif ($action eq "update") {
419 # Unless overriden on the command line, rebuild the flake recorded
420 # in the container config file. FIXME: read the container config
421 # in a more sensible way.
422 if (!defined $flake && !defined $configFile && !defined $extraConfig) {
423 my $s = read_file($confFile);
424 $s =~ /^FLAKE=(.*)$/m;
425 $flake = $1;
428 if (defined $flake) {
429 buildFlake();
430 system("nix-env", "-p", "$profileDir/system", "--set", $systemPath) == 0
431 or die "$0: failed to set container configuration\n";
432 } else {
434 my $nixosConfigFile = "$root/etc/nixos/configuration.nix";
436 # FIXME: may want to be more careful about clobbering the existing
437 # configuration.nix.
438 if ((defined $extraConfig && $extraConfig ne "") ||
439 (defined $configFile && $configFile ne "")) {
440 writeNixOSConfig $nixosConfigFile;
443 my $nixenvF = $nixosPath // "<nixpkgs/nixos>";
444 system("nix-env", "-p", "$profileDir/system",
445 "-I", "nixos-config=$nixosConfigFile", "-f", $nixenvF,
446 "--set", "-A", "system", @nixFlags) == 0
447 or die "$0: failed to build container configuration\n";
450 if (isContainerRunning) {
451 print STDERR "reloading container...\n";
452 system("systemctl", "reload", "container\@$containerName") == 0
453 or die "$0: failed to reload container\n";
457 elsif ($action eq "login") {
458 exec("machinectl", "login", "--", $containerName);
461 elsif ($action eq "root-login") {
462 runInContainer("@su@", "root", "-l");
465 elsif ($action eq "run") {
466 shift @ARGV; shift @ARGV;
467 # Escape command.
468 my $s = join(' ', map { s/'/'\\''/g; "'$_'" } @ARGV);
469 runInContainer("@su@", "root", "-l", "-c", "exec " . $s);
472 elsif ($action eq "show-ip") {
473 my $s = read_file($confFile) or die;
474 $s =~ /^LOCAL_ADDRESS=([0-9\.]+)(\/[0-9]+)?$/m
475 or $s =~ /^LOCAL_ADDRESS6=([0-9a-f:]+)(\/[0-9]+)?$/m
476 or die "$0: cannot get IP address\n";
477 print "$1\n";
480 elsif ($action eq "show-host-key") {
481 my $fn = "$root/etc/ssh/ssh_host_ed25519_key.pub";
482 $fn = "$root/etc/ssh/ssh_host_ecdsa_key.pub" unless -e $fn;
483 exit 1 if ! -f $fn;
484 print read_file($fn);
487 else {
488 die "$0: unknown action ‘$action\n";