Skip to content

Commit

Permalink
treewide: revamp the persistence module
Browse files Browse the repository at this point in the history
Massive changes have been accomplished here. Gone is the large unwieldy bash script and in is a smaller and significantly simpler script that executes within systemd. This has one major benefit of parallelizing the mounting, which can have some pretty decent speedups. Another major benefit is that we have more fine-tuned control over the execution of our logic. This allowed us to insert it earlier in the initrd than even when NixOS activation occurs.

Alongside it comes a persist copy script, which will attempt to perform a copy of any file contents that exist during shutdown for the first time in order to create a trivial directory tree of the persisted entries. The main reason for this was simply to have a set of permissions to reference during mounting, but performing a mostly safe copy was good too. Regrettably there is no way to assure our copied content is complete, but that's simply not something we can handle at this stage anyways.

Permissions have been completely thrown out from the module. Instead, it is encouraged that permissions are managed on the filesystem itself. Is this bad for reproducibility? You could say, but I argue that permissions are stateful and shouldn't have been mingled here anyways. Software should be responsible for setting the permissions it needs and those permissions will be copied and respected through this setup. This is more than enough.

Since a lot of stuff within this module only works in a sane manner thanks to our persist being on "/nix", we're throwing out the ability to change it entirely. Most of our stuff works because "/sysroot/nix" is mounted in the sysroot early on alongside "/sysroot". Other mounts are not so lucky. Thinking of an implementation to force this to happen as early as possible whilst also thinking about the abstractions needed was simply too annoying, so I will not be considering it at all.
  • Loading branch information
Frontear committed Dec 7, 2024
1 parent e5d4e27 commit d952a91
Show file tree
Hide file tree
Showing 16 changed files with 437 additions and 266 deletions.
28 changes: 4 additions & 24 deletions lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -60,28 +60,8 @@
outputsToRemove = [ "inputs" "outputs" "sourceInfo" ];
in (removeSystemAttr (removeAttrs flake outputsToRemove)));

types = (
let
mkPathOption = { name, prefix }: prev.mkOptionType {
inherit name;
description = "An absolute path, prefixed with ${prefix}.";
descriptionClass = "nonRestrictiveClause";

check = (x:
prev.isStringLike x &&
prev.substring 0 1 x == prefix
);
merge = prev.mergeEqualOption;
};
in {
systemPath = mkPathOption {
name = "systemPath";
prefix = "/";
};

userPath = mkPathOption {
name = "userPath";
prefix = "~";
};
} // prev.types);
types = rec {
systemPath = prev.types.path;
userPath = prev.types.either systemPath (prev.types.strMatching "~/.+");
} // prev.types;
}
134 changes: 134 additions & 0 deletions modules/common/persist/config.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
{
config,
lib,
pkgs,
utils,
...
}:
let
cfg = config.my.persist;

persist-helper = lib.getExe (pkgs.callPackage ./package {});
in {
config = lib.mkIf cfg.enable {
# Automatically add a default sane set of directories
# to persist on a standard system.
my.persist = {
directories = [
"/var/lib" # contains persistent information about system state for services
"/var/log" # logging.. fairly straightforward, you'd always want this.
] ++ lib.optionals config.security.sudo.enable [
"/var/db/sudo/lectured" # preferential.
];

files = [
"/etc/machine-id" # systemd uses this to match up system-specific data.
];
};

# Kill the `systemd-machine-id-commit` service, introduced in systemd 256.7.
#
# This service detects when `/etc/machine-id` seems to be in danger of being
# lost, and attempts to persist it to a writable medium. I don't know the
# details of what it considers "a writable medium", however we do know that
# our setup causes this unit to fire.
#
# In our case, choosing to persist `/etc/machine-id` (default behaviour)
# causes this service to think our file is at risk of disappearing, and as
# a result, tries to persist it. However, it cannot determine a place to
# save it, which causes the service to fail.
#
# This doesn't matter at all for our setup because we have the confidence
# in knowing the file is safe. If for some reason it's not, then that was
# a concious decision by the user and they can handle the problems with it.
boot.initrd.systemd.suppressedUnits = [ "systemd-machine-id-commit.service" ];
systemd.suppressedSystemUnits = [ "systemd-machine-id-commit.service" ];

# Create services that will attempt to mount our units in parallel
# from within the initrd.
#
# The motivation behind performing this in the initrd is to avoid
# conflicts with files that needed to exist earlier. For example,
# if we want to persist `/var/log` but do so too late, the journal
# will already have created this directory and written to it. Any
# attempts at a bind here will result in a loss of data, or race
# conditions between attempting to copy and bind on top. This is
# just too messy.
#
# Furthermore, at a conceptual level, one could think of these bind
# mounts like real files that were brought up thanks to the sysroot
# mount. After all, on a non-ephemeral root system, these files would
# be brought with the root, so why not make it almost seem like that?
#
# Note that there isn't a hard dependency on when these must finish.
# This is because there's no immediate importance to force them to finish
# fast since nothing in the initrd (to my knowledge) should cause problems
# for these mounts.
boot.initrd.systemd.storePaths = [ persist-helper ];
boot.initrd.systemd.services = lib.listToAttrs (map (path: {
name = "persist-mount-${utils.escapeSystemdPath path}";
value = {
unitConfig = {
DefaultDependencies = "no";

After = "sysroot.mount";
Before = "initrd-root-fs.target";

ConditionPathExists = [
"/sysroot/${cfg.volume}/${path}"
"!/sysroot/${path}"
];

RequiresMountsFor = [ "/sysroot/${cfg.volume}" ];
};

serviceConfig = {
ExecStart = "${persist-helper} 'mount'"
+ " '/sysroot/${cfg.volume}' '/sysroot' '${path}'";
};

requiredBy = [ "initrd-root-fs.target" ];
};
}) cfg.toplevel);

# Perform a possibly mostly safe copy of file contents as they are
# found during the shutdown phase of the system.
#
# There is no truly safe way to perform this without losing some minimal
# amount of data during the copy, especially for services that refuse to
# let their fd's go. We cannot reasonably do anything at this stage, so
# we instead opt to perform a one-off copy to obtain the initial set of
# files from whatever we wanted to persist.
#
# The expectation here is that this directory won't be too big given the
# root is mostly expected to be a tmpfs, and even if the files are big
# and the copy takes a long time, we can justify it by remembering that
# this copy won't happen again. It's a one-off service to obtain the bare
# minimum level of directories (mostly for the permissions) so that a
# reasonable directory tree is produced in the persistent location.
systemd.services = lib.listToAttrs (map (path: {
name = "persist-copy-${utils.escapeSystemdPath path}";
value = {
unitConfig = {
DefaultDependencies = "no";

Before = "final.target";

ConditionPathExists = [
"${path}"
"!${cfg.volume}/${path}"
];

RequiresMountsFor = [ "${cfg.volume}" ];
};

serviceConfig = {
ExecStart = "${persist-helper} 'copy' '/' '${cfg.volume}'"
+ " '${path}'";
};

requiredBy = [ "final.target" ];
};
}) cfg.toplevel);
};
}
130 changes: 0 additions & 130 deletions modules/common/persist/module-impl.nix

This file was deleted.

71 changes: 3 additions & 68 deletions modules/common/persist/module.nix
Original file line number Diff line number Diff line change
@@ -1,74 +1,9 @@
{
lib,
...
}:
let
mkPathOption = user: group: mode: type: apply: {
options = {
path = lib.mkOption {
default = null;

inherit apply type;
};

user = lib.mkOption {
default = user;

type = with lib.types; passwdEntry str;
};

group = lib.mkOption {
default = group;

type = with lib.types; str;
};

mode = lib.mkOption {
default = mode;

type = with lib.types; str;
};
};
};

mkPersistOption = user: group: type: apply: {
enable = lib.mkEnableOption "impermanence";

volume = lib.mkOption {
default = "/nix/persist";

type = with lib.types; path;
};

directories = lib.mkOption {
default = [];

type = with lib.types; listOf (coercedTo str (path: { inherit path; }) (submodule (mkPathOption user group "755" type apply)));
};

files = lib.mkOption {
default = [];

type = with lib.types; listOf (coercedTo str (path: { inherit path; }) (submodule (mkPathOption user group "644" type apply)));
};
};
in {
{
imports = [
./module-impl.nix
./options.nix
./config.nix
];

options.my.persist = mkPersistOption "root" "root" (with lib.types; systemPath) (x: x);

config = {
# Add the module into the home-manager context.
home-manager.sharedModules = lib.singleton (
{
osConfig,
config,
...
}:
{
options.my.persist = lib.removeAttrs (mkPersistOption config.home.username osConfig.users.extraUsers.${config.home.username}.group (with lib.types; userPath) (lib.replaceStrings [ "~" ] [ config.home.homeDirectory ])) [ "enable" "volume" ]; # strip enable and volume options, they are irrelevant for home-manager.
});
};
}
Loading

0 comments on commit d952a91

Please sign in to comment.