modules/tmproot: Make persistence optional

This commit is contained in:
Jack O'Sullivan 2022-02-13 18:36:57 +00:00
parent 7dec8bb56b
commit e306da7ce6
3 changed files with 108 additions and 85 deletions

View File

@ -1,7 +1,7 @@
{ lib, extendModules, modulesPath, baseModules, options, config, ... }: { lib, extendModules, modulesPath, baseModules, options, config, ... }:
let let
inherit (lib) recursiveUpdate mkOption; inherit (lib) recursiveUpdate mkOption mkDefault;
inherit (lib.my) mkBoolOpt'; inherit (lib.my) mkBoolOpt' dummyOption;
cfg = config.my.build; cfg = config.my.build;
@ -17,19 +17,31 @@ let
}; };
in in
{ {
options.my = with lib.types; { options = with lib.types; {
boot.isDevVM = mkBoolOpt' false "Whether the system is a development VM."; my = {
build = options.system.build; boot.isDevVM = mkBoolOpt' false "Whether the system is a development VM.";
asDevVM = mkOption { build = options.system.build;
inherit (asDevVM) type; asDevVM = mkOption {
default = { }; inherit (asDevVM) type;
visible = "shallow"; default = { };
description = "Configuration as a development VM"; visible = "shallow";
description = "Configuration as a development VM";
};
};
# Forward declare options that won't exist until the VM module is actually imported
virtualisation = {
diskImage = dummyOption;
}; };
}; };
config.my.build = { config = {
# The meta.mainProgram should probably be set upstream but oh well... virtualisation = {
devVM = recursiveUpdate config.my.asDevVM.system.build.vm { meta.mainProgram = "run-${config.system.name}-vm"; }; diskImage = mkDefault "./.vms/${config.system.name}.qcow2";
};
my.build = {
# 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"; };
};
}; };
} }

View File

@ -1,9 +1,10 @@
{ lib, pkgs, config, ... }: { lib, pkgs, config, ... }:
let let
inherit (lib) concatStringsSep concatMap concatMapStringsSep mkIf mkDefault mkMerge mkVMOverride; inherit (lib) optionalString concatStringsSep concatMap concatMapStringsSep mkIf mkDefault mkMerge mkVMOverride;
inherit (lib.my) mkOpt' mkBoolOpt' mkVMOverride' dummyOption; inherit (lib.my) mkOpt' mkBoolOpt' mkVMOverride';
cfg = config.my.tmproot; cfg = config.my.tmproot;
enablePersistence = cfg.persistDir != null;
showUnsaved = showUnsaved =
'' ''
@ -56,27 +57,18 @@ in
options = with lib.types; { options = with lib.types; {
my.tmproot = { my.tmproot = {
enable = mkBoolOpt' true "Whether to enable tmproot."; enable = mkBoolOpt' true "Whether to enable tmproot.";
persistDir = mkOpt' str "/persist" "Path where persisted files are stored."; persistDir = mkOpt' (nullOr str) "/persist" "Path where persisted files are stored.";
size = mkOpt' str "2G" "Size of tmpfs root"; size = mkOpt' str "2G" "Size of tmpfs root";
unsaved = { unsaved = {
showMotd = mkBoolOpt' true "Whether to show unsaved files with `dynamic-motd`."; showMotd = mkBoolOpt' true "Whether to show unsaved files with `dynamic-motd`.";
ignore = mkOpt' (listOf str) [ ] "Path prefixes to ignore if unsaved."; ignore = mkOpt' (listOf str) [ ] "Path prefixes to ignore if unsaved.";
}; };
}; };
# Forward declare options that won't exist until the VM module is actually imported
virtualisation = {
diskImage = dummyOption;
};
}; };
config = mkIf cfg.enable (mkMerge [ config = mkIf cfg.enable (mkMerge [
{ {
assertions = [ assertions = [
{
assertion = config.fileSystems ? "${cfg.persistDir}";
message = "The 'fileSystems' option does not specify your persistence file system (${cfg.persistDir}).";
}
{ {
# I mean you probably _could_, but if you're doing tmproot... come on # I mean you probably _could_, but if you're doing tmproot... come on
assertion = !config.users.mutableUsers; assertion = !config.users.mutableUsers;
@ -112,48 +104,6 @@ in
(pkgs.writeScriptBin "tmproot-unsaved" showUnsaved) (pkgs.writeScriptBin "tmproot-unsaved" showUnsaved)
]; ];
# Catch non-existent source directories that are needed for boot (see `pathsNeededForBoot` in
# nixos/lib/util.nix). We do this by monkey-patching the `waitDevice` function that would otherwise hang.
boot.initrd.postDeviceCommands =
''
ensurePersistSource() {
[ -e "/mnt-root$1" ] && return
echo "Persistent source directory $1 does not exist, creating..."
install -dm "$2" "/mnt-root$1" || fail
}
_waitDevice() {
local device="$1"
${concatMapStringsSep " || \\\n " (d:
let
sourceDir = "${d.persistentStoragePath}${d.directory}";
in
''([ "$device" = "/mnt-root${sourceDir}" ] && ensurePersistSource "${sourceDir}" "${d.mode}")'')
config.environment.persistence."${cfg.persistDir}".directories}
waitDevice "$@"
}
type waitDevice > /dev/null || (echo "waitDevice is missing!"; fail)
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"
];
};
my.dynamic-motd.script = mkIf cfg.unsaved.showMotd my.dynamic-motd.script = mkIf cfg.unsaved.showMotd
'' ''
tmprootUnsaved() { tmprootUnsaved() {
@ -163,8 +113,10 @@ in
echo echo
echo -e "\t\e[31;1;4mWarning:\e[0m $count file(s) on / will be lost on shutdown!" 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.' echo -e '\tTo see them, run `tmproot-unsaved` as root.'
echo -e '\tAdd these files to `environment.persistence."${cfg.persistDir}"` to keep them!' ${optionalString enablePersistence ''
echo -e '\tOtherwise, they can be ignored by adding to `my.tmproot.unsaved.ignore`.' echo -e '\tAdd these files to `environment.persistence."${cfg.persistDir}"` to keep them!'
''}
echo -e "\tIf they don't need to be kept, add them to \`my.tmproot.unsaved.ignore\`."
echo echo
} }
@ -172,15 +124,8 @@ in
''; '';
fileSystems."/" = rootDef; fileSystems."/" = rootDef;
virtualisation = {
diskImage = "./.vms/${config.system.name}-persist.qcow2";
};
} }
(mkIf config.services.openssh.enable {
environment.persistence."${cfg.persistDir}".files =
concatMap (k: [ k.path "${k.path}.pub" ]) config.services.openssh.hostKeys;
})
(mkIf config.networking.resolvconf.enable { (mkIf config.networking.resolvconf.enable {
my.tmproot.unsaved.ignore = [ "/etc/resolv.conf" ]; my.tmproot.unsaved.ignore = [ "/etc/resolv.conf" ];
}) })
@ -192,12 +137,77 @@ in
fileSystems = mkVMOverride { fileSystems = mkVMOverride {
"/" = mkVMOverride' rootDef; "/" = mkVMOverride' rootDef;
# Hijack the "root" device for persistence in the VM
"${cfg.persistDir}" = {
device = config.virtualisation.bootDevice;
neededForBoot = true;
};
}; };
}) })
(mkIf enablePersistence (mkMerge [
{
assertions = [
{
assertion = config.fileSystems ? "${cfg.persistDir}";
message = "The 'fileSystems' option does not specify your persistence file system (${cfg.persistDir}).";
}
];
# Catch non-existent source directories that are needed for boot (see `pathsNeededForBoot` in
# nixos/lib/util.nix). We do this by monkey-patching the `waitDevice` function that would otherwise hang.
boot.initrd.postDeviceCommands =
''
ensurePersistSource() {
[ -e "/mnt-root$1" ] && return
echo "Persistent source directory $1 does not exist, creating..."
install -dm "$2" "/mnt-root$1" || fail
}
_waitDevice() {
local device="$1"
${concatMapStringsSep " || \\\n " (d:
let
sourceDir = "${d.persistentStoragePath}${d.directory}";
in
''([ "$device" = "/mnt-root${sourceDir}" ] && ensurePersistSource "${sourceDir}" "${d.mode}")'')
config.environment.persistence."${cfg.persistDir}".directories}
waitDevice "$@"
}
type waitDevice > /dev/null || (echo "waitDevice is missing!"; fail)
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"
];
};
virtualisation = {
diskImage = "./.vms/${config.system.name}-persist.qcow2";
};
}
(mkIf config.services.openssh.enable {
environment.persistence."${cfg.persistDir}".files =
concatMap (k: [ k.path "${k.path}.pub" ]) config.services.openssh.hostKeys;
})
(mkIf config.my.boot.isDevVM {
fileSystems = mkVMOverride {
# Hijack the "root" device for persistence in the VM
"${cfg.persistDir}" = {
device = config.virtualisation.bootDevice;
neededForBoot = true;
};
};
})
]))
]); ]);
} }

View File

@ -1,13 +1,14 @@
{ lib, pkgsFlakes, inputs, modules }: { lib, pkgsFlakes, inputs, modules }:
let let
inherit (builtins) attrValues mapAttrs; inherit (builtins) attrValues mapAttrs;
inherit (lib) mkDefault; inherit (lib) optionals mkDefault;
mkSystem = mkSystem =
name: { name: {
system, system,
nixpkgs ? "unstable", nixpkgs ? "unstable",
config, config,
docCustom ? true,
}: }:
let let
pkgsFlake = pkgsFlakes.${nixpkgs}; pkgsFlake = pkgsFlakes.${nixpkgs};
@ -31,8 +32,8 @@ let
inputs.impermanence.nixosModule inputs.impermanence.nixosModule
inputs.agenix.nixosModules.age inputs.agenix.nixosModules.age
inputs.home-manager.nixosModule inputs.home-manager.nixosModule
] ++ modules; ] ++ (optionals docCustom modules);
modules = [ modules = (optionals (!docCustom) modules) ++ [
{ {
_module.args = { inherit system inputs; }; _module.args = { inherit system inputs; };
system.name = name; system.name = name;