Implement initial containers module
This commit is contained in:
@@ -9,5 +9,6 @@
|
||||
server = ./server.nix;
|
||||
deploy-rs = ./deploy-rs.nix;
|
||||
secrets = ./secrets.nix;
|
||||
containers = ./containers.nix;
|
||||
};
|
||||
}
|
||||
|
@@ -31,6 +31,15 @@ let
|
||||
}
|
||||
];
|
||||
};
|
||||
asContainer = extendModules {
|
||||
# TODO: see previous
|
||||
specialArgs = { inherit baseModules; };
|
||||
modules = [
|
||||
{
|
||||
boot.isContainer = true;
|
||||
}
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
options = with lib.types; {
|
||||
@@ -46,13 +55,19 @@ in
|
||||
inherit (asDevVM) type;
|
||||
default = { };
|
||||
visible = "shallow";
|
||||
description = "Configuration as a development VM";
|
||||
description = "Configuration as a development VM.";
|
||||
};
|
||||
asISO = mkOption {
|
||||
inherit (asISO) type;
|
||||
default = { };
|
||||
visible = "shallow";
|
||||
description = "Configuration as a bootable .iso image";
|
||||
description = "Configuration as a bootable .iso image.";
|
||||
};
|
||||
asContainer = mkOption {
|
||||
inherit (asContainer) type;
|
||||
default = { };
|
||||
visible = "shallow";
|
||||
description = "Configuration as a container.";
|
||||
};
|
||||
|
||||
buildAs = options.system.build;
|
||||
@@ -76,6 +91,7 @@ in
|
||||
# The meta.mainProgram should probably be set upstream but oh well...
|
||||
devVM = recursiveUpdate config.my.asDevVM.system.build.vm { meta.mainProgram = "run-${config.system.name}-vm"; };
|
||||
iso = config.my.asISO.system.build.isoImage;
|
||||
container = config.my.asContainer.system.build.toplevel;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
@@ -97,18 +97,15 @@ in
|
||||
|
||||
networking = {
|
||||
domain = mkDefault "int.nul.ie";
|
||||
useDHCP = mkDefault false;
|
||||
useDHCP = false;
|
||||
enableIPv6 = mkDefault true;
|
||||
};
|
||||
virtualisation = {
|
||||
forwardPorts = flatten [
|
||||
(optional config.services.openssh.openFirewall { from = "host"; host.port = 2222; guest.port = 22; })
|
||||
];
|
||||
useNetworkd = mkDefault true;
|
||||
};
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
bash-completion
|
||||
vim
|
||||
ldns
|
||||
];
|
||||
|
||||
programs = {
|
||||
@@ -146,6 +143,11 @@ in
|
||||
})
|
||||
(mkIf config.my.build.isDevVM {
|
||||
networking.interfaces.eth0.useDHCP = mkDefault true;
|
||||
virtualisation = {
|
||||
forwardPorts = flatten [
|
||||
(optional config.services.openssh.openFirewall { from = "host"; host.port = 2222; guest.port = 22; })
|
||||
];
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
|
161
nixos/modules/containers.nix
Normal file
161
nixos/modules/containers.nix
Normal file
@@ -0,0 +1,161 @@
|
||||
{ lib, options, config, systems, ... }:
|
||||
let
|
||||
inherit (builtins) attrNames attrValues mapAttrs;
|
||||
inherit (lib) concatMapStringsSep filterAttrs mkDefault mkIf mkMerge mkAliasDefinitions mkVMOverride mkAfter;
|
||||
inherit (lib.my) mkOpt';
|
||||
|
||||
cfg = config.my.containers;
|
||||
|
||||
devVMKeyPath = "/run/dev.key";
|
||||
|
||||
containerOpts = with lib.types; { name, ... }: {
|
||||
options = {
|
||||
system = mkOpt' unspecified systems."${name}".configuration.config.my.buildAs.container
|
||||
"Top-level system configuration.";
|
||||
opts = mkOpt' lib.my.naiveModule { } "Options to pass to `containers.*name*`.";
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
options.my.containers = with lib.types; {
|
||||
networking = {
|
||||
bridgeName = mkOpt' str "containers" "Name of host bridge.";
|
||||
hostAddresses = mkOpt' (either str (listOf str)) "172.16.137.1/24" "Addresses for the host bridge.";
|
||||
};
|
||||
persistDir = mkOpt' str "/persist/containers" "Where to store container persistence data.";
|
||||
instances = mkOpt' (attrsOf (submodule containerOpts)) { } "Individual containers.";
|
||||
};
|
||||
|
||||
config = mkMerge [
|
||||
(mkIf (cfg.instances != { }) {
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.systemd.network.enable;
|
||||
message = "Containers currently require systemd-networkd!";
|
||||
}
|
||||
];
|
||||
|
||||
my.firewall.trustedInterfaces = [ cfg.networking.bridgeName ];
|
||||
|
||||
systemd = {
|
||||
network = {
|
||||
netdevs."25-container-bridge".netdevConfig = {
|
||||
Name = cfg.networking.bridgeName;
|
||||
Kind = "bridge";
|
||||
};
|
||||
# Based on the pre-installed 80-container-vz
|
||||
networks."80-container-vb" = {
|
||||
matchConfig = {
|
||||
Name = "vb-*";
|
||||
Driver = "veth";
|
||||
};
|
||||
networkConfig = {
|
||||
# systemd LLDP doesn't work on bridge interfaces
|
||||
LLDP = true;
|
||||
EmitLLDP = "customer-bridge";
|
||||
# Although nspawn will set the veth's master, systemd will clear it (systemd 250 adds a `KeepMaster`
|
||||
# to avoid this)
|
||||
Bridge = cfg.networking.bridgeName;
|
||||
};
|
||||
};
|
||||
networks."80-containers-bridge" = {
|
||||
matchConfig = {
|
||||
Name = cfg.networking.bridgeName;
|
||||
Driver = "bridge";
|
||||
};
|
||||
networkConfig = {
|
||||
Address = cfg.networking.hostAddresses;
|
||||
DHCPServer = true;
|
||||
# TODO: Configuration for routed IPv6 (and maybe IPv4)
|
||||
IPMasquerade = "both";
|
||||
IPv6SendRA = true;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
tmpfiles.rules = map (n: "d ${cfg.persistDir}/${n} 0755 root root") (attrNames cfg.instances);
|
||||
};
|
||||
|
||||
containers = mapAttrs (n: c: mkMerge [
|
||||
{
|
||||
path = "/nix/var/nix/profiles/per-container/${n}";
|
||||
ephemeral = true;
|
||||
autoStart = mkDefault true;
|
||||
bindMounts = {
|
||||
"/persist" = {
|
||||
hostPath = "${cfg.persistDir}/${n}";
|
||||
isReadOnly = false;
|
||||
};
|
||||
};
|
||||
|
||||
privateNetwork = true;
|
||||
hostBridge = cfg.networking.bridgeName;
|
||||
additionalCapabilities = [ "CAP_NET_ADMIN" ];
|
||||
}
|
||||
c.opts
|
||||
|
||||
(mkIf config.my.build.isDevVM {
|
||||
path = mkVMOverride c.system;
|
||||
bindMounts."${devVMKeyPath}" = {
|
||||
hostPath = config.my.secrets.vmKeyPath;
|
||||
isReadOnly = true;
|
||||
};
|
||||
})
|
||||
]) cfg.instances;
|
||||
})
|
||||
|
||||
# Inside container
|
||||
(mkIf config.boot.isContainer {
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.systemd.network.enable;
|
||||
message = "Containers currently require systemd-networkd!";
|
||||
}
|
||||
];
|
||||
|
||||
my = {
|
||||
tmproot.enable = true;
|
||||
};
|
||||
|
||||
system.activationScripts = {
|
||||
# impermanence will throw a fit and bail the whole activation script if this already exists (the container
|
||||
# start script pre-creates it for some reason)
|
||||
clearMachineId.text = "rm -f /etc/machine-id";
|
||||
createPersistentStorageDirs.deps = [ "clearMachineId" ];
|
||||
|
||||
# Ordinarily I think the Nix daemon does this but ofc it doesn't in the container
|
||||
createNixPerUserDirs = {
|
||||
text =
|
||||
let
|
||||
users = attrValues (filterAttrs (_: u: u.isNormalUser) config.users.users);
|
||||
in
|
||||
concatMapStringsSep "\n"
|
||||
(u: ''install -d -o ${u.name} -g ${u.group} /nix/var/nix/{profiles,gcroots}/per-user/"${u.name}"'') users;
|
||||
deps = [ "users" "groups" ];
|
||||
};
|
||||
};
|
||||
|
||||
networking = {
|
||||
useHostResolvConf = false;
|
||||
};
|
||||
# Based on the pre-installed 80-container-host0
|
||||
systemd.network.networks."80-container-eth0" = {
|
||||
matchConfig = {
|
||||
Name = "eth0";
|
||||
Virtualization = "container";
|
||||
};
|
||||
networkConfig = {
|
||||
DHCP = "yes";
|
||||
LLDP = true;
|
||||
EmitLLDP = "customer-bridge";
|
||||
};
|
||||
dhcpConfig = {
|
||||
UseTimezone = true;
|
||||
};
|
||||
};
|
||||
|
||||
# If the host is a dev VM
|
||||
age.identityPaths = [ devVMKeyPath ];
|
||||
})
|
||||
];
|
||||
}
|
@@ -4,6 +4,7 @@ let
|
||||
inherit (lib.my) parseIPPort mkOpt' mkBoolOpt';
|
||||
|
||||
cfg = config.my.firewall;
|
||||
iptCfg = config.networking.firewall;
|
||||
in
|
||||
{
|
||||
options.my.firewall = with lib.types; {
|
||||
@@ -31,9 +32,9 @@ in
|
||||
enable = true;
|
||||
ruleset =
|
||||
let
|
||||
trusted' = "{ ${concatStringsSep ", " cfg.trustedInterfaces} }";
|
||||
openTCP = cfg.tcp.allowed ++ config.networking.firewall.allowedTCPPorts;
|
||||
openUDP = cfg.udp.allowed ++ config.networking.firewall.allowedUDPPorts;
|
||||
trusted' = "{ ${concatStringsSep ", " (cfg.trustedInterfaces ++ iptCfg.trustedInterfaces)} }";
|
||||
openTCP = cfg.tcp.allowed ++ iptCfg.allowedTCPPorts;
|
||||
openUDP = cfg.udp.allowed ++ iptCfg.allowedUDPPorts;
|
||||
in
|
||||
''
|
||||
table inet filter {
|
||||
|
@@ -8,6 +8,7 @@ let
|
||||
in
|
||||
{
|
||||
options.my.secrets = with lib.types; {
|
||||
vmKeyPath = mkOpt' str "/tmp/xchg/dev.key" "Path to dev key when in a dev VM.";
|
||||
key = mkOpt' (nullOr str) null "Public key that secrets for this system should be encrypted for.";
|
||||
files = mkOpt' (attrsOf unspecified) { } "Secrets to decrypt with agenix.";
|
||||
};
|
||||
@@ -19,7 +20,7 @@ in
|
||||
} // opts) cfg.files;
|
||||
}
|
||||
(mkIf config.my.build.isDevVM {
|
||||
age.identityPaths = [ "/tmp/xchg/dev.key" ];
|
||||
age.identityPaths = [ cfg.vmKeyPath ];
|
||||
})
|
||||
];
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
{ config, lib, ... }:
|
||||
{ lib, pkgs, config, ... }:
|
||||
let
|
||||
inherit (lib) mkIf mkDefault;
|
||||
inherit (lib.my) mkBoolOpt';
|
||||
inherit (lib.my) mkBoolOpt' mkDefault';
|
||||
|
||||
cfg = config.my.server;
|
||||
uname = if config.my.user.enable then config.my.user.config.name else "root";
|
||||
@@ -17,6 +17,12 @@ in
|
||||
my.user.homeConfig = {
|
||||
my.gui.enable = false;
|
||||
};
|
||||
|
||||
documentation.nixos.enable = mkDefault' false;
|
||||
|
||||
environment.systemPackages = with pkgs; [
|
||||
tcpdump
|
||||
];
|
||||
};
|
||||
|
||||
meta.buildDocsInSandbox = false;
|
||||
|
@@ -1,10 +1,12 @@
|
||||
{ lib, pkgs, config, ... }:
|
||||
{ lib, pkgs, options, config, ... }:
|
||||
let
|
||||
inherit (lib) optionalString concatStringsSep concatMap concatMapStringsSep mkIf mkDefault mkMerge mkVMOverride;
|
||||
inherit (lib)
|
||||
optionalString concatStringsSep concatMap concatMapStringsSep mkIf mkDefault mkMerge mkForce mkVMOverride
|
||||
mkAliasDefinitions;
|
||||
inherit (lib.my) mkOpt' mkBoolOpt' mkVMOverride';
|
||||
|
||||
cfg = config.my.tmproot;
|
||||
enablePersistence = cfg.persistDir != null;
|
||||
enablePersistence = cfg.persistence.dir != null;
|
||||
|
||||
showUnsaved =
|
||||
''
|
||||
@@ -58,8 +60,11 @@ in
|
||||
options = with lib.types; {
|
||||
my.tmproot = {
|
||||
enable = mkBoolOpt' true "Whether to enable tmproot.";
|
||||
persistDir = mkOpt' (nullOr str) "/persist" "Path where persisted files are stored.";
|
||||
size = mkOpt' str "2G" "Size of tmpfs root";
|
||||
persistence = {
|
||||
dir = mkOpt' (nullOr str) "/persist" "Path where persisted files are stored.";
|
||||
config = mkOpt' options.environment.persistence.type.nestedTypes.elemType { } "Persistence configuration";
|
||||
};
|
||||
unsaved = {
|
||||
showMotd = mkBoolOpt' true "Whether to show unsaved files with `dynamic-motd`.";
|
||||
ignore = mkOpt' (listOf str) [ ] "Path prefixes to ignore if unsaved.";
|
||||
@@ -77,33 +82,53 @@ in
|
||||
}
|
||||
];
|
||||
|
||||
my.tmproot.unsaved.ignore = [
|
||||
"/tmp"
|
||||
my.tmproot = {
|
||||
unsaved.ignore = [
|
||||
"/tmp"
|
||||
|
||||
# setup-etc.pl will create this for us
|
||||
"/etc/NIXOS"
|
||||
# setup-etc.pl will create this for us
|
||||
"/etc/NIXOS"
|
||||
|
||||
# Once mutableUsers is disabled, we should be all clear here
|
||||
"/etc/passwd"
|
||||
"/etc/group"
|
||||
"/etc/shadow"
|
||||
"/etc/subuid"
|
||||
"/etc/subgid"
|
||||
# Once mutableUsers is disabled, we should be all clear here
|
||||
"/etc/passwd"
|
||||
"/etc/group"
|
||||
"/etc/shadow"
|
||||
"/etc/subuid"
|
||||
"/etc/subgid"
|
||||
|
||||
# Lock file for /etc/{passwd,shadow}
|
||||
"/etc/.pwd.lock"
|
||||
# Lock file for /etc/{passwd,shadow}
|
||||
"/etc/.pwd.lock"
|
||||
|
||||
# systemd last updated? I presume they'll get updated on boot...
|
||||
"/etc/.updated"
|
||||
"/var/.updated"
|
||||
# systemd last updated? I presume they'll get updated on boot...
|
||||
"/etc/.updated"
|
||||
"/var/.updated"
|
||||
|
||||
# Specifies obsolete files that should be deleted on activation - we'll never have those!
|
||||
"/etc/.clean"
|
||||
# Specifies obsolete files that should be deleted on activation - we'll never have those!
|
||||
"/etc/.clean"
|
||||
|
||||
# These are set in environment.etc by the sshd module, but because their mode needs to be changed,
|
||||
# setup-etc will copy them instead of symlinking
|
||||
"/etc/ssh/authorized_keys.d"
|
||||
];
|
||||
# These are set in environment.etc by the sshd module, but because their mode needs to be changed,
|
||||
# setup-etc will copy them instead of symlinking
|
||||
"/etc/ssh/authorized_keys.d"
|
||||
];
|
||||
persistence.config = {
|
||||
# In impermanence the key in `environment.persistence.*` (aka name passed the attrsOf submodule) sets the
|
||||
# default value, so we need to override it when we mkAliasDefinitions
|
||||
_module.args.name = mkForce cfg.persistence.dir;
|
||||
|
||||
hideMounts = mkDefault true;
|
||||
directories = [
|
||||
"/var/log"
|
||||
# In theory we'd include only the files needed individually (i.e. the {U,G}ID map files that track deleted
|
||||
# users and groups), but `update-users-groups.pl` actually deletes the original files for "atomic update".
|
||||
# Also the script runs before impermanence does.
|
||||
"/var/lib/nixos"
|
||||
"/var/lib/systemd"
|
||||
];
|
||||
files = [
|
||||
"/etc/machine-id"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
environment.systemPackages = [
|
||||
(pkgs.writeScriptBin "tmproot-unsaved" showUnsaved)
|
||||
@@ -119,7 +144,7 @@ in
|
||||
echo -e "\t\e[31;1;4mWarning:\e[0m $count file(s) on / will be lost on shutdown!"
|
||||
echo -e '\tTo see them, run `tmproot-unsaved` as root.'
|
||||
${optionalString enablePersistence ''
|
||||
echo -e '\tAdd these files to `environment.persistence."${cfg.persistDir}"` to keep them!'
|
||||
echo -e '\tAdd these files to `my.tmproot.persistence.config` to keep them!'
|
||||
''}
|
||||
echo -e "\tIf they don't need to be kept, add them to \`my.tmproot.unsaved.ignore\`."
|
||||
echo
|
||||
@@ -149,8 +174,8 @@ in
|
||||
{
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.fileSystems ? "${cfg.persistDir}";
|
||||
message = "The 'fileSystems' option does not specify your persistence file system (${cfg.persistDir}).";
|
||||
assertion = (config.fileSystems ? "${cfg.persistence.dir}") || config.boot.isContainer;
|
||||
message = "The 'fileSystems' option does not specify your persistence file system (${cfg.persistence.dir}).";
|
||||
}
|
||||
];
|
||||
|
||||
@@ -172,7 +197,7 @@ in
|
||||
sourceDir = "${d.persistentStoragePath}${d.directory}";
|
||||
in
|
||||
''([ "$device" = "/mnt-root${sourceDir}" ] && ensurePersistSource "${sourceDir}" "${d.mode}")'')
|
||||
config.environment.persistence."${cfg.persistDir}".directories}
|
||||
cfg.persistence.config.directories}
|
||||
|
||||
waitDevice "$@"
|
||||
}
|
||||
@@ -181,33 +206,20 @@ in
|
||||
alias waitDevice=_waitDevice
|
||||
'';
|
||||
|
||||
environment.persistence."${cfg.persistDir}" = {
|
||||
hideMounts = mkDefault true;
|
||||
directories = [
|
||||
"/var/log"
|
||||
# In theory we'd include only the files needed individually (i.e. the {U,G}ID map files that track deleted
|
||||
# users and groups), but `update-users-groups.pl` actually deletes the original files for "atomic update".
|
||||
# Also the script runs before impermanence does.
|
||||
"/var/lib/nixos"
|
||||
"/var/lib/systemd"
|
||||
];
|
||||
files = [
|
||||
"/etc/machine-id"
|
||||
];
|
||||
};
|
||||
environment.persistence."${cfg.persistence.dir}" = mkAliasDefinitions options.my.tmproot.persistence.config;
|
||||
|
||||
virtualisation = {
|
||||
diskImage = "./.vms/${config.system.name}-persist.qcow2";
|
||||
};
|
||||
}
|
||||
(mkIf config.services.openssh.enable {
|
||||
environment.persistence."${cfg.persistDir}".files =
|
||||
my.tmproot.persistence.config.files =
|
||||
concatMap (k: [ k.path "${k.path}.pub" ]) config.services.openssh.hostKeys;
|
||||
})
|
||||
(mkIf config.my.build.isDevVM {
|
||||
fileSystems = mkVMOverride {
|
||||
# Hijack the "root" device for persistence in the VM
|
||||
"${cfg.persistDir}" = {
|
||||
"${cfg.persistence.dir}" = {
|
||||
device = config.virtualisation.bootDevice;
|
||||
neededForBoot = true;
|
||||
};
|
||||
|
@@ -5,6 +5,7 @@ let
|
||||
|
||||
cfg = config.my.user;
|
||||
user' = cfg.config;
|
||||
user = config.users.users.${user'.name};
|
||||
in
|
||||
{
|
||||
options.my.user = with lib.types; {
|
||||
@@ -37,9 +38,33 @@ in
|
||||
in mkIf (shell != null) (mkDefault' shell);
|
||||
openssh.authorizedKeys.keyFiles = [ lib.my.sshKeyFiles.me ];
|
||||
};
|
||||
# In order for this option to evaluate on its own, home-manager expects the `name` (which is derived from the
|
||||
# parent attr name) to be the users name, aka `home-manager.users.<name>`
|
||||
homeConfig = { _module.args.name = lib.mkForce user'.name; };
|
||||
homeConfig = {
|
||||
# In order for this option to evaluate on its own, home-manager expects the `name` (which is derived from the
|
||||
# parent attr name) to be the users name, aka `home-manager.users.<name>`
|
||||
_module.args.name = lib.mkForce user'.name;
|
||||
};
|
||||
};
|
||||
tmproot.persistence.config =
|
||||
let
|
||||
perms = {
|
||||
mode = "0700";
|
||||
user = user.name;
|
||||
group = user.group;
|
||||
};
|
||||
in {
|
||||
files = map (file: {
|
||||
inherit file;
|
||||
parentDirectory = perms;
|
||||
}) [
|
||||
"/home/${user'.name}/.bash_history"
|
||||
];
|
||||
directories = map (directory: {
|
||||
inherit directory;
|
||||
} // perms) [
|
||||
# Persist all of fish; it's not easy to persist just the history fish won't let you move it to a different
|
||||
# directory. Also it does some funny stuff and can't really be a symlink it seems.
|
||||
"/home/${user'.name}/.local/share/fish"
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
|
Reference in New Issue
Block a user