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’.
This commit is contained in:
parent
0cca0f477f
commit
ba88db3cd3
@ -42,13 +42,6 @@ in
|
|||||||
{
|
{
|
||||||
options = {
|
options = {
|
||||||
|
|
||||||
root = mkOption {
|
|
||||||
type = types.path;
|
|
||||||
description = ''
|
|
||||||
The root directory of the container.
|
|
||||||
'';
|
|
||||||
};
|
|
||||||
|
|
||||||
config = mkOption {
|
config = mkOption {
|
||||||
description = ''
|
description = ''
|
||||||
A specification of the desired configuration of this
|
A specification of the desired configuration of this
|
||||||
@ -103,9 +96,7 @@ in
|
|||||||
};
|
};
|
||||||
|
|
||||||
config = mkMerge
|
config = mkMerge
|
||||||
[ { root = mkDefault "/var/lib/containers/${name}";
|
[ (mkIf options.config.isDefined {
|
||||||
}
|
|
||||||
(mkIf options.config.isDefined {
|
|
||||||
path = (import ../../lib/eval-config.nix {
|
path = (import ../../lib/eval-config.nix {
|
||||||
modules =
|
modules =
|
||||||
let extraConfig =
|
let extraConfig =
|
||||||
@ -126,12 +117,10 @@ in
|
|||||||
example = literalExample
|
example = literalExample
|
||||||
''
|
''
|
||||||
{ webserver =
|
{ webserver =
|
||||||
{ root = "/containers/webserver";
|
{ path = "/nix/var/nix/profiles/webserver";
|
||||||
path = "/nix/var/nix/profiles/webserver";
|
|
||||||
};
|
};
|
||||||
database =
|
database =
|
||||||
{ root = "/containers/database";
|
{ config =
|
||||||
config =
|
|
||||||
{ config, pkgs, ... }:
|
{ config, pkgs, ... }:
|
||||||
{ services.postgresql.enable = true;
|
{ services.postgresql.enable = true;
|
||||||
services.postgresql.package = pkgs.postgresql92;
|
services.postgresql.package = pkgs.postgresql92;
|
||||||
@ -153,78 +142,76 @@ in
|
|||||||
|
|
||||||
config = {
|
config = {
|
||||||
|
|
||||||
systemd.services = mapAttrs' (name: cfg:
|
systemd.services."container@" =
|
||||||
let
|
{ description = "Container '%I'";
|
||||||
# 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" ];
|
unitConfig.RequiresMountsFor = [ "/var/lib/containers/%I" ];
|
||||||
|
|
||||||
unitConfig.RequiresMountsFor = [ cfg.root ];
|
|
||||||
|
|
||||||
path = [ pkgs.iproute ];
|
path = [ pkgs.iproute ];
|
||||||
|
|
||||||
preStart =
|
environment.INSTANCE = "%I";
|
||||||
|
|
||||||
|
script =
|
||||||
''
|
''
|
||||||
mkdir -p -m 0755 ${cfg.root}/etc
|
root="/var/lib/containers/$INSTANCE"
|
||||||
if ! [ -e ${cfg.root}/etc/os-release ]; then
|
mkdir -p -m 0755 "$root/etc"
|
||||||
touch ${cfg.root}/etc/os-release
|
if ! [ -e "$root/etc/os-release" ]; then
|
||||||
|
touch "$root/etc/os-release"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p -m 0755 \
|
mkdir -p -m 0755 \
|
||||||
/nix/var/nix/profiles/per-container/${name} \
|
"/nix/var/nix/profiles/per-container/$INSTANCE" \
|
||||||
/nix/var/nix/gcroots/per-container/${name}
|
"/nix/var/nix/gcroots/per-container/$INSTANCE"
|
||||||
''
|
|
||||||
|
|
||||||
+ optionalString (cfg.root != "/var/lib/containers/${name}") ''
|
SYSTEM_PATH=/nix/var/nix/profiles/system
|
||||||
ln -sfn "${cfg.root}" "/var/lib/containers/${name}"
|
if [ -f "/etc/containers/$INSTANCE.conf" ]; then
|
||||||
''
|
. "/etc/containers/$INSTANCE.conf"
|
||||||
|
fi
|
||||||
|
|
||||||
+ optionalString cfg.privateNetwork ''
|
|
||||||
# Cleanup from last time.
|
# Cleanup from last time.
|
||||||
ip netns del ${ns} 2> /dev/null || true
|
ifaceHost=c-$INSTANCE
|
||||||
ip link del ${ifaceHost} 2> /dev/null || true
|
ifaceCont=ctmp-$INSTANCE
|
||||||
ip link del ${ifaceCont} 2> /dev/null || true
|
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
|
||||||
|
|
||||||
# Create a pair of virtual ethernet devices. On the host,
|
if [ "$PRIVATE_NETWORK" = 1 ]; then
|
||||||
# we get ‘c-<container-name’, and on the guest, we get
|
# Create a pair of virtual ethernet devices. On the host,
|
||||||
# ‘eth0’.
|
# we get ‘c-<container-name’, and on the guest, we get
|
||||||
set -x
|
# ‘eth0’.
|
||||||
ip link add ${ifaceHost} type veth peer name ${ifaceCont}
|
ip link add $ifaceHost type veth peer name $ifaceCont
|
||||||
ip netns add ${ns}
|
ip netns add $ns
|
||||||
ip link set ${ifaceCont} netns ${ns}
|
ip link set $ifaceCont netns $ns
|
||||||
ip netns exec ${ns} ip link set ${ifaceCont} name eth0
|
ip netns exec $ns ip link set $ifaceCont name eth0
|
||||||
ip netns exec ${ns} ip link set dev eth0 up
|
ip netns exec $ns ip link set dev eth0 up
|
||||||
ip link set dev ${ifaceHost} up
|
ip link set dev $ifaceHost up
|
||||||
${optionalString (cfg.hostAddress != null) ''
|
if [ -n "$HOST_ADDRESS" ]; then
|
||||||
ip addr add ${cfg.hostAddress} dev ${ifaceHost}
|
ip addr add $HOST_ADDRESS dev $ifaceHost
|
||||||
ip netns exec ${ns} ip route add ${cfg.hostAddress} dev eth0
|
ip netns exec $ns ip route add $HOST_ADDRESS dev eth0
|
||||||
ip netns exec ${ns} ip route add default via ${cfg.hostAddress}
|
ip netns exec $ns ip route add default via $HOST_ADDRESS
|
||||||
''}
|
fi
|
||||||
${optionalString (cfg.localAddress != null) ''
|
if [ -n "$LOCAL_ADDRESS" ]; then
|
||||||
ip netns exec ${ns} ip addr add ${cfg.localAddress} dev eth0
|
ip netns exec $ns ip addr add $LOCAL_ADDRESS dev eth0
|
||||||
ip route add ${cfg.localAddress} dev ${ifaceHost}
|
ip route add $LOCAL_ADDRESS dev $ifaceHost
|
||||||
''}
|
fi
|
||||||
|
runInNetNs="${runInNetns}/bin/run-in-netns $ns"
|
||||||
|
extraFlags="--capability=CAP_NET_ADMIN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
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"
|
||||||
'';
|
'';
|
||||||
|
|
||||||
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";
|
|
||||||
|
|
||||||
preStop =
|
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
|
if [ -n "$pid" ]; then
|
||||||
# Send the RTMIN+3 signal, which causes the container
|
# Send the RTMIN+3 signal, which causes the container
|
||||||
# systemd to start halt.target.
|
# systemd to start halt.target.
|
||||||
@ -240,13 +227,38 @@ in
|
|||||||
fi
|
fi
|
||||||
'';
|
'';
|
||||||
|
|
||||||
reloadIfChanged = true;
|
restartIfChanged = false;
|
||||||
|
#reloadIfChanged = true; # FIXME
|
||||||
|
|
||||||
serviceConfig.ExecReload =
|
serviceConfig.ExecReload = pkgs.writeScript "reload-container"
|
||||||
"${pkgs.bash}/bin/bash -c '"
|
''
|
||||||
+ "echo ${cfg.path}/bin/switch-to-configuration test "
|
#! ${pkgs.stdenv.shell} -e
|
||||||
+ "| ${pkgs.socat}/bin/socat unix:${cfg.root}/var/lib/root-shell.socket -'";
|
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;
|
}) config.containers;
|
||||||
|
|
||||||
# Generate /etc/hosts entries for the containers.
|
# Generate /etc/hosts entries for the containers.
|
||||||
@ -257,5 +269,7 @@ in
|
|||||||
|
|
||||||
environment.systemPackages = optional (config.containers != {}) nixos-container;
|
environment.systemPackages = optional (config.containers != {}) nixos-container;
|
||||||
|
|
||||||
|
system.build.foo = nixos-container;
|
||||||
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,21 @@
|
|||||||
#! @bash@/bin/sh -e
|
#! @bash@/bin/sh -e
|
||||||
|
|
||||||
usage() {
|
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 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"
|
eval "set -- $args"
|
||||||
|
extraConfigFile=
|
||||||
while [ $# -gt 0 ]; do
|
while [ $# -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
(--help) usage; exit 0;;
|
(--help) usage; exit 0;;
|
||||||
|
(--config) shift; extraConfigFile=$1;;
|
||||||
(--) shift; break;;
|
(--) shift; break;;
|
||||||
(*) break;;
|
(*) break;;
|
||||||
esac
|
esac
|
||||||
@ -28,26 +34,104 @@ getContainerRoot() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ $action = login ]; then
|
container="$1"
|
||||||
|
if [ -z "$container" ]; then usage; exit 1; fi
|
||||||
|
shift
|
||||||
|
|
||||||
container="$1"
|
if [ $action = create ]; then
|
||||||
if [ -z "$container" ]; then usage; exit 1; fi
|
|
||||||
shift
|
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
|
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 '<nixpkgs/nixos>' --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 [ ! -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
|
exec @socat@/bin/socat "unix:$root/var/lib/login.socket" -,echo=0,raw
|
||||||
|
|
||||||
elif [ $action = root-shell ]; then
|
elif [ $action = root-shell ]; then
|
||||||
|
|
||||||
container="$1"
|
|
||||||
if [ -z "$container" ]; then usage; exit 1; fi
|
|
||||||
shift
|
|
||||||
|
|
||||||
getContainerRoot
|
getContainerRoot
|
||||||
|
|
||||||
exec @socat@/bin/socat "unix:$root/var/lib/root-shell.socket" -
|
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
|
else
|
||||||
echo "$0: unknown action ‘$action’" >&2
|
echo "$0: unknown action ‘$action’" >&2
|
||||||
exit 1
|
exit 1
|
||||||
|
Loading…
x
Reference in New Issue
Block a user