summary refs log tree commit diff
path: root/nixos/modules
diff options
context:
space:
mode:
authorEelco Dolstra <eelco.dolstra@logicblox.com>2014-03-19 19:55:05 +0100
committerEelco Dolstra <eelco.dolstra@logicblox.com>2014-03-24 12:19:27 +0100
commitba88db3cd332e439dd2090b64abb7b9942b5fc94 (patch)
tree12aa33997e94ac1e1e03d4d3c9f6733a4eef4ccf /nixos/modules
parent0cca0f477f168cee994b2c90d20b713f2bb67d85 (diff)
downloadnixlib-ba88db3cd332e439dd2090b64abb7b9942b5fc94.tar
nixlib-ba88db3cd332e439dd2090b64abb7b9942b5fc94.tar.gz
nixlib-ba88db3cd332e439dd2090b64abb7b9942b5fc94.tar.bz2
nixlib-ba88db3cd332e439dd2090b64abb7b9942b5fc94.tar.lz
nixlib-ba88db3cd332e439dd2090b64abb7b9942b5fc94.tar.xz
nixlib-ba88db3cd332e439dd2090b64abb7b9942b5fc94.tar.zst
nixlib-ba88db3cd332e439dd2090b64abb7b9942b5fc94.zip
Add support for imperative container management
The command nixos-container can now create containers.  For instance,
the following creates and starts a container named ‘database’:

  $ nixos-container create database

The configuration of the container is stored in
/var/lib/containers/<name>/etc/nixos/configuration.nix.  After editing
the configuration, you can make the changes take effect by doing

  $ nixos-container update database

The container can also be destroyed:

  $ nixos-container destroy database

Containers are now executed using a template unit,
‘container@.service’, so the unit in this example would be
‘container@database.service’.
Diffstat (limited to 'nixos/modules')
-rw-r--r--nixos/modules/virtualisation/containers.nix166
-rw-r--r--nixos/modules/virtualisation/nixos-container.sh106
2 files changed, 185 insertions, 87 deletions
diff --git a/nixos/modules/virtualisation/containers.nix b/nixos/modules/virtualisation/containers.nix
index e718398815d2..573a3d1c5848 100644
--- a/nixos/modules/virtualisation/containers.nix
+++ b/nixos/modules/virtualisation/containers.nix
@@ -42,13 +42,6 @@ in
         {
           options = {
 
-            root = mkOption {
-              type = types.path;
-              description = ''
-                The root directory of the container.
-              '';
-            };
-
             config = mkOption {
               description = ''
                 A specification of the desired configuration of this
@@ -103,9 +96,7 @@ in
           };
 
           config = mkMerge
-            [ { root = mkDefault "/var/lib/containers/${name}";
-              }
-              (mkIf options.config.isDefined {
+            [ (mkIf options.config.isDefined {
                 path = (import ../../lib/eval-config.nix {
                   modules =
                     let extraConfig =
@@ -126,12 +117,10 @@ in
       example = literalExample
         ''
           { webserver =
-              { root = "/containers/webserver";
-                path = "/nix/var/nix/profiles/webserver";
+              { path = "/nix/var/nix/profiles/webserver";
               };
             database =
-              { root = "/containers/database";
-                config =
+              { config =
                   { config, pkgs, ... }:
                   { services.postgresql.enable = true;
                     services.postgresql.package = pkgs.postgresql92;
@@ -153,78 +142,76 @@ in
 
   config = {
 
-    systemd.services = mapAttrs' (name: cfg:
-      let
-        # FIXME: interface names have a maximum length.
-        ifaceHost = "c-${name}";
-        ifaceCont = "ctmp-${name}";
-        ns = "net-${name}";
-      in
-      nameValuePair "container-${name}" {
-        description = "Container '${name}'";
-
-        wantedBy = [ "multi-user.target" ];
+    systemd.services."container@" =
+      { description = "Container '%I'";
 
-        unitConfig.RequiresMountsFor = [ cfg.root ];
+        unitConfig.RequiresMountsFor = [ "/var/lib/containers/%I" ];
 
         path = [ pkgs.iproute ];
 
-        preStart =
+        environment.INSTANCE = "%I";
+
+        script =
           ''
-            mkdir -p -m 0755 ${cfg.root}/etc
-            if ! [ -e ${cfg.root}/etc/os-release ]; then
-              touch ${cfg.root}/etc/os-release
+            root="/var/lib/containers/$INSTANCE"
+            mkdir -p -m 0755 "$root/etc"
+            if ! [ -e "$root/etc/os-release" ]; then
+              touch "$root/etc/os-release"
             fi
 
             mkdir -p -m 0755 \
-              /nix/var/nix/profiles/per-container/${name} \
-              /nix/var/nix/gcroots/per-container/${name}
-          ''
+              "/nix/var/nix/profiles/per-container/$INSTANCE" \
+              "/nix/var/nix/gcroots/per-container/$INSTANCE"
 
-          + optionalString (cfg.root != "/var/lib/containers/${name}") ''
-            ln -sfn "${cfg.root}" "/var/lib/containers/${name}"
-          ''
+            SYSTEM_PATH=/nix/var/nix/profiles/system
+            if [ -f "/etc/containers/$INSTANCE.conf" ]; then
+              . "/etc/containers/$INSTANCE.conf"
+            fi
 
-          + optionalString cfg.privateNetwork ''
             # Cleanup from last time.
-            ip netns del ${ns} 2> /dev/null || true
-            ip link del ${ifaceHost} 2> /dev/null || true
-            ip link del ${ifaceCont} 2> /dev/null || true
-
-            # Create a pair of virtual ethernet devices.  On the host,
-            # we get ‘c-<container-name’, and on the guest, we get
-            # ‘eth0’.
-            set -x
-            ip link add ${ifaceHost} type veth peer name ${ifaceCont}
-            ip netns add ${ns}
-            ip link set ${ifaceCont} netns ${ns}
-            ip netns exec ${ns} ip link set ${ifaceCont} name eth0
-            ip netns exec ${ns} ip link set dev eth0 up
-            ip link set dev ${ifaceHost} up
-            ${optionalString (cfg.hostAddress != null) ''
-              ip addr add ${cfg.hostAddress} dev ${ifaceHost}
-              ip netns exec ${ns} ip route add ${cfg.hostAddress} dev eth0
-              ip netns exec ${ns} ip route add default via ${cfg.hostAddress}
-            ''}
-            ${optionalString (cfg.localAddress != null) ''
-              ip netns exec ${ns} ip addr add ${cfg.localAddress} dev eth0
-              ip route add ${cfg.localAddress} dev ${ifaceHost}
-            ''}
-          '';
+            ifaceHost=c-$INSTANCE
+            ifaceCont=ctmp-$INSTANCE
+            ns=net-$INSTANCE
+            ip netns del $ns 2> /dev/null || true
+            ip link del $ifaceHost 2> /dev/null || true
+            ip link del $ifaceCont 2> /dev/null || true
+
+            if [ "$PRIVATE_NETWORK" = 1 ]; then
+              # Create a pair of virtual ethernet devices.  On the host,
+              # we get ‘c-<container-name’, and on the guest, we get
+              # ‘eth0’.
+              ip link add $ifaceHost type veth peer name $ifaceCont
+              ip netns add $ns
+              ip link set $ifaceCont netns $ns
+              ip netns exec $ns ip link set $ifaceCont name eth0
+              ip netns exec $ns ip link set dev eth0 up
+              ip link set dev $ifaceHost up
+              if [ -n "$HOST_ADDRESS" ]; then
+                ip addr add $HOST_ADDRESS dev $ifaceHost
+                ip netns exec $ns ip route add $HOST_ADDRESS dev eth0
+                ip netns exec $ns ip route add default via $HOST_ADDRESS
+              fi
+              if [ -n "$LOCAL_ADDRESS" ]; then
+                ip netns exec $ns ip addr add $LOCAL_ADDRESS dev eth0
+                ip route add $LOCAL_ADDRESS dev $ifaceHost
+              fi
+              runInNetNs="${runInNetns}/bin/run-in-netns $ns"
+              extraFlags="--capability=CAP_NET_ADMIN"
+            fi
 
-        serviceConfig.ExecStart =
-          (optionalString cfg.privateNetwork "${runInNetns}/bin/run-in-netns ${ns} ")
-          + "${config.systemd.package}/bin/systemd-nspawn"
-          + (optionalString cfg.privateNetwork " --capability=CAP_NET_ADMIN")
-          + " -M ${name} -D ${cfg.root}"
-          + " --bind-ro=/nix/store --bind-ro=/nix/var/nix/db --bind-ro=/nix/var/nix/daemon-socket"
-          + " --bind=/nix/var/nix/profiles/per-container/${name}:/nix/var/nix/profiles"
-          + " --bind=/nix/var/nix/gcroots/per-container/${name}:/nix/var/nix/gcroots"
-          + " ${cfg.path}/init";
+            exec $runInNetNs ${config.systemd.package}/bin/systemd-nspawn \
+              -M "$INSTANCE" -D "/var/lib/containers/$INSTANCE" $extraFlags \
+              --bind-ro=/nix/store \
+              --bind-ro=/nix/var/nix/db \
+              --bind-ro=/nix/var/nix/daemon-socket \
+              --bind="/nix/var/nix/profiles/per-container/$INSTANCE:/nix/var/nix/profiles" \
+              --bind="/nix/var/nix/gcroots/per-container/$INSTANCE:/nix/var/nix/gcroots" \
+              "$SYSTEM_PATH/init"
+          '';
 
         preStop =
           ''
-            pid="$(cat /sys/fs/cgroup/systemd/machine/${name}.nspawn/system/tasks 2> /dev/null)"
+            pid="$(cat /sys/fs/cgroup/systemd/machine/$INSTANCE.nspawn/system/tasks 2> /dev/null)"
             if [ -n "$pid" ]; then
               # Send the RTMIN+3 signal, which causes the container
               # systemd to start halt.target.
@@ -240,13 +227,38 @@ in
             fi
           '';
 
-        reloadIfChanged = true;
+        restartIfChanged = false;
+        #reloadIfChanged = true; # FIXME
 
-        serviceConfig.ExecReload =
-          "${pkgs.bash}/bin/bash -c '"
-          + "echo ${cfg.path}/bin/switch-to-configuration test "
-          + "| ${pkgs.socat}/bin/socat unix:${cfg.root}/var/lib/root-shell.socket -'";
+        serviceConfig.ExecReload = pkgs.writeScript "reload-container"
+          ''
+            #! ${pkgs.stdenv.shell} -e
+            SYSTEM_PATH=/nix/var/nix/profiles/system
+            if [ -f "/etc/containers/$INSTANCE.conf" ]; then
+              . "/etc/containers/$INSTANCE.conf"
+            fi
+            echo $SYSTEM_PATH/bin/switch-to-configuration test | \
+              ${pkgs.socat}/bin/socat unix:/var/lib/containers/$INSTANCE/var/lib/root-shell.socket -
+          '';
+      };
 
+    # Generate a configuration file in /etc/containers for each
+    # container so that container@.target can get the container
+    # configuration.
+    environment.etc = mapAttrs' (name: cfg: nameValuePair "containers/${name}.conf"
+      { text =
+          ''
+            SYSTEM_PATH=${cfg.path}
+            ${optionalString cfg.privateNetwork ''
+              PRIVATE_NETWORK=1
+              ${optionalString (cfg.hostAddress != null) ''
+                HOST_ADDRESS=${cfg.hostAddress}
+              ''}
+              ${optionalString (cfg.localAddress != null) ''
+                LOCAL_ADDRESS=${cfg.localAddress}
+              ''}
+            ''}
+          '';
       }) config.containers;
 
     # Generate /etc/hosts entries for the containers.
@@ -257,5 +269,7 @@ in
 
     environment.systemPackages = optional (config.containers != {}) nixos-container;
 
+    system.build.foo = nixos-container;
+
   };
 }
diff --git a/nixos/modules/virtualisation/nixos-container.sh b/nixos/modules/virtualisation/nixos-container.sh
index f4e34588d6eb..c72b6fb92814 100644
--- a/nixos/modules/virtualisation/nixos-container.sh
+++ b/nixos/modules/virtualisation/nixos-container.sh
@@ -1,15 +1,21 @@
 #! @bash@/bin/sh -e
 
 usage() {
-    echo "Usage: $0 login <container-name>" >&2
+    echo "Usage: $0 create <container-name> [--config <filename>]" >&2
+    echo "       $0 update <container-name>" >&2
+    echo "       $0 destroy <container-name>" >&2
+    echo "       $0 login <container-name>" >&2
     echo "       $0 root-shell <container-name>" >&2
+    echo "       $0 set-root-password <container-name> <password>" >&2
 }
 
-args="`getopt --options '' -l help -- "$@"`"
+args="`getopt --options '' -l help -l config: -- "$@"`"
 eval "set -- $args"
+extraConfigFile=
 while [ $# -gt 0 ]; do
     case "$1" in
         (--help) usage; exit 0;;
+        (--config) shift; extraConfigFile=$1;;
         (--) shift; break;;
         (*) break;;
     esac
@@ -28,26 +34,104 @@ getContainerRoot() {
     fi
 }
 
-if [ $action = login ]; then
+container="$1"
+if [ -z "$container" ]; then usage; exit 1; fi
+shift
 
-    container="$1"
-    if [ -z "$container" ]; then usage; exit 1; fi
-    shift
+if [ $action = create ]; then
+
+    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 = [ <nixpkgs/nixos/modules/virtualisation/container-login.nix> $extraConfigFile ];
+}"
+    configFile="$root/etc/nixos/configuration.nix"
+    echo "$config" > "$configFile"
+
+    nix-env -p "$profileDir/system" -I "nixos-config=$configFile" -f '<nixpkgs/nixos>' --set -A system
+
+    # Allocate a new /8 network in the 10.233.* range.
+    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" <<EOF
+PRIVATE_NETWORK=1
+HOST_ADDRESS=$hostAddress
+LOCAL_ADDRESS=$localAddress
+EOF
+
+    echo "starting container@$container.service..." >&2
+    systemctl start "container@$container.service"
+
+elif [ $action = update ]; then
 
     getContainerRoot
 
-    exec @socat@/bin/socat "unix:$root/var/lib/login.socket" -,echo=0,raw
+    configFile="$root/etc/nixos/configuration.nix"
+    profileDir="/nix/var/nix/profiles/per-container/$container"
 
-elif [ $action = root-shell ]; then
+    nix-env -p "$profileDir/system" -I "nixos-config=$configFile" -f '<nixpkgs/nixos>' --set -A system
 
-    container="$1"
-    if [ -z "$container" ]; then usage; exit 1; fi
-    shift
+    echo "reloading container@$container.service..." >&2
+    systemctl reload "container@$container.service"
+
+elif [ $action = destroy ]; then
 
     getContainerRoot
 
+    confFile="/etc/containers/$container.conf"
+    if [ ! -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"
+
+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" -
+
 else
     echo "$0: unknown action ‘$action’" >&2
     exit 1