fs: add a "mount.bind" option & use it for impermanence bind-mounts

This commit is contained in:
colin 2023-01-03 02:45:23 +00:00
parent be222c1d70
commit edf6bd4455
3 changed files with 90 additions and 49 deletions

View File

@ -3,6 +3,7 @@ with lib;
let
cfg = config.sane.fs;
mountNameFor = path: "${utils.escapeSystemdPath path}.mount";
serviceNameFor = path: "ensure-${utils.escapeSystemdPath path}";
# sane.fs."<path>" top-level options
@ -11,33 +12,41 @@ let
has-parent = hasParent name;
parent-cfg = if has-parent then cfg."${parent}" else {};
parent-dir = parent-cfg.dir or {};
parent-acl = parent-dir.acl or {};
in {
options = {
dir = mkOption {
type = dirEntry;
};
mount = mkOption {
type = types.nullOr (mountEntryFor name);
default = null;
};
unit = mkOption {
type = types.str;
description = "name of the systemd unit which ensures this entry";
};
};
config = {
dir.user = lib.mkDefault (parent-dir.user or "root");
dir.group = lib.mkDefault (parent-dir.group or "root");
dir.mode = lib.mkDefault (parent-dir.mode or "0755");
dir.acl.user = lib.mkDefault (parent-acl.user or "root");
dir.acl.group = lib.mkDefault (parent-acl.group or "root");
dir.acl.mode = lib.mkDefault (parent-acl.mode or "0755");
# we put this here instead of as a `default` to ensure that users who specify additional
# dependencies still get a dep on the parent (unless they assign with `mkForce`).
dir.depends = if has-parent then [ parent-cfg.unit ] else [];
# if defaulted, this module is responsible for creating the directory
dir.unit = lib.mkDefault ((serviceNameFor name) + ".service");
# if defaulted, this module is responsible for finalizing the entry.
# the user could override this if, say, they provide an alternate unit
# which finalizes the entry (by mounting it, for example).
unit = lib.mkDefault config.dir.unit;
# the user could override this if, say, they finalize some aspect of the entry
# with a custom service.
unit = lib.mkDefault (if config.mount != null then
config.mount.unit
else config.dir.unit);
};
});
# sane.fs."<path>".dir sub-options
dirEntry = types.submodule {
acl = types.submodule {
options = {
user = mkOption {
type = types.str; # TODO: use uid?
@ -48,6 +57,15 @@ let
mode = mkOption {
type = types.str;
};
};
};
# sane.fs."<path>".dir sub-options
dirEntry = types.submodule {
options = {
acl = mkOption {
type = acl;
};
depends = mkOption {
type = types.listOf types.str;
description = "list of systemd units needed to be run before this directory can be made";
@ -65,14 +83,30 @@ let
};
};
# sane.fs."<path>".mount sub-options
mountEntryFor = path: types.submodule {
options = {
bind = mkOption {
type = types.nullOr types.str;
description = "fs path to bind-mount from";
default = null;
};
unit = mkOption {
type = types.str;
description = "name of the systemd unit which mounts this path";
default = mountNameFor path;
};
};
};
# given a fsEntry definition, output the `config` attrs it generates.
mkFsConfig = path: opt: {
mkDirConfig = path: opt: {
systemd.services."${serviceNameFor path}" = {
description = "prepare ${path}";
serviceConfig.Type = "oneshot";
script = ensure-dir-script;
scriptArgs = "${path} ${opt.dir.user} ${opt.dir.group} ${opt.dir.mode}";
scriptArgs = "${path} ${opt.dir.acl.user} ${opt.dir.acl.group} ${opt.dir.acl.mode}";
after = opt.dir.depends;
wants = opt.dir.depends;
@ -85,6 +119,42 @@ let
};
};
mkMountConfig = path: opt: (let
underlying = cfg."${opt.mount.bind}";
in {
fileSystems."${path}" = lib.mkIf (opt.mount.bind != null) {
device = opt.mount.bind;
options = [
"bind"
# we can't mount this until after the underlying path is prepared.
# if the underlying path disappears, this mount will be stopped.
"x-systemd.requires=${underlying.dir.unit}"
# the mount depends on its target directory being prepared
"x-systemd.requires=${opt.dir.unit}"
];
noCheck = true;
};
});
mkFsConfig = path: opt: mergeTopLevel [
(mkDirConfig path opt)
(lib.mkIf (opt.mount != null) (mkMountConfig path opt))
];
# act as `config = lib.mkMerge [ a b ]` but in a way which avoids infinite recursion,
# by extracting only specific options which are known to not be options in this module.
mergeTopLevel = items: let
# if one of the items is `lib.mkIf cond attrs`, we won't be able to index it until
# after we "push down" the mkIf to each attr.
indexable = lib.pushDownProperties (lib.mkMerge items);
# transform (listOf attrs) to (attrsOf list) by grouping each toplevel attr across lists.
top = lib.zipAttrsWith (name: lib.mkMerge) indexable;
# extract known-good top-level items in a way which errors if a module tries to define something extra.
extract = { fileSystems ? {}, systemd ? {} }@attrs: attrs;
in {
inherit (extract top) fileSystems systemd;
};
# systemd/shell script used to create and set perms for a specific dir
ensure-dir-script = ''
path="$1"
@ -166,10 +236,5 @@ in {
};
};
config = let
cfgs = builtins.attrValues (builtins.mapAttrs mkFsConfig cfg);
in {
# we can't lib.mkMerge at the top-level, so do it per-attribute
systemd = lib.mkMerge (catAttrs "systemd" cfgs);
};
config = mergeTopLevel (lib.mapAttrsToList mkFsConfig cfg);
}

View File

@ -71,7 +71,7 @@ in lib.mkIf config.sane.impermanence.enable
# ensure the fs is mounted only after the mountpoint directory is created
dir.reverseDepends = [ store.mount-unit ];
# HACK: this fs entry is provided by our mount unit.
unit = store.mount-unit;
mount.unit = store.mount-unit;
};
sane.fs."${store.underlying.path}" = {
# don't mount until after the backing dir is setup correctly.

View File

@ -96,7 +96,7 @@ in
config = mkIf cfg.enable (lib.mkMerge [
{
# TODO: move to sane.fs, to auto-ensure all user dirs?
sane.fs."/home/colin".dir = {
sane.fs."/home/colin".dir.acl = {
user = "colin";
group = config.users.users.colin.group;
mode = config.users.users.colin.homeMode;
@ -107,30 +107,17 @@ in
# what is a problem is if the user specified some other dir we don't know about here.
# like "/var", and then "/nix/persist/var" has different perms and something mounts funny.
# TODO: just add assertions that sane.fs."${backing}/${dest}".dir == sane.fs."${dest}" for each mount point?
sane.fs."/nix/persist/home/colin".dir = {
user = "colin";
group = config.users.users.colin.group;
mode = config.users.users.colin.homeMode;
};
sane.fs."/mnt/impermanence/crypt/clearedonboot/home/colin".dir = {
user = "colin";
group = config.users.users.colin.group;
mode = config.users.users.colin.homeMode;
};
sane.fs."/nix/persist/home/colin".dir.acl = config.sane.fs."/home/colin".dir.acl;
sane.fs."/mnt/impermanence/crypt/clearedonboot/home/colin".dir.acl = config.sane.fs."/home/colin".dir.acl;
}
(
let cfgFor = opt:
let
# systemd creates <path>.mount services for every fileSystems entry.
# <path> gets escaped as part of that: this code tries to guess that escaped name here.
mount-unit = "${utils.escapeSystemdPath opt.directory}.mount";
backing-path = concatPaths [ opt.store opt.directory ];
dir-unit = config.sane.fs."${opt.directory}".unit;
backing-unit = config.sane.fs."${backing-path}".unit;
# pass through the perm/mode overrides
dir-opts = {
dir-acl = {
user = lib.mkIf (opt.user != null) opt.user;
group = lib.mkIf (opt.group != null) opt.group;
mode = lib.mkIf (opt.mode != null) opt.mode;
@ -139,27 +126,16 @@ in
# create destination and backing directory, with correct perms
sane.fs."${opt.directory}" = {
# inherit perms & make sure we don't mount until after the mount point is setup correctly.
dir = dir-opts // { reverseDepends = [ mount-unit ]; };
# HACK: anything depending on this directory should actually depend on it being mounted.
unit = mount-unit;
dir.acl = dir-acl;
mount.bind = backing-path;
};
sane.fs."${backing-path}" = {
# inherit perms & make sure we don't mount until after the backing dir is setup correctly.
dir = dir-opts // { reverseDepends = [ mount-unit ]; };
};
# define the mountpoint.
fileSystems."${opt.directory}" = {
device = backing-path;
options = [
"bind"
];
# fsType = "bind";
noCheck = true;
# ensure the backing path has same perms as the mount point
dir.acl = config.sane.fs."${opt.directory}".dir.acl;
};
};
cfgs = builtins.map cfgFor ingested-dirs;
in {
fileSystems = lib.mkMerge (catAttrs "fileSystems" cfgs);
sane.fs = lib.mkMerge (catAttrs "fs" (catAttrs "sane" cfgs));
}
)