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 let
cfg = config.sane.fs; cfg = config.sane.fs;
mountNameFor = path: "${utils.escapeSystemdPath path}.mount";
serviceNameFor = path: "ensure-${utils.escapeSystemdPath path}"; serviceNameFor = path: "ensure-${utils.escapeSystemdPath path}";
# sane.fs."<path>" top-level options # sane.fs."<path>" top-level options
@ -11,33 +12,41 @@ let
has-parent = hasParent name; has-parent = hasParent name;
parent-cfg = if has-parent then cfg."${parent}" else {}; parent-cfg = if has-parent then cfg."${parent}" else {};
parent-dir = parent-cfg.dir or {}; parent-dir = parent-cfg.dir or {};
parent-acl = parent-dir.acl or {};
in { in {
options = { options = {
dir = mkOption { dir = mkOption {
type = dirEntry; type = dirEntry;
}; };
mount = mkOption {
type = types.nullOr (mountEntryFor name);
default = null;
};
unit = mkOption { unit = mkOption {
type = types.str; type = types.str;
description = "name of the systemd unit which ensures this entry"; description = "name of the systemd unit which ensures this entry";
}; };
}; };
config = { config = {
dir.user = lib.mkDefault (parent-dir.user or "root"); dir.acl.user = lib.mkDefault (parent-acl.user or "root");
dir.group = lib.mkDefault (parent-dir.group or "root"); dir.acl.group = lib.mkDefault (parent-acl.group or "root");
dir.mode = lib.mkDefault (parent-dir.mode or "0755"); 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 # 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`). # dependencies still get a dep on the parent (unless they assign with `mkForce`).
dir.depends = if has-parent then [ parent-cfg.unit ] else []; dir.depends = if has-parent then [ parent-cfg.unit ] else [];
# if defaulted, this module is responsible for creating the directory # if defaulted, this module is responsible for creating the directory
dir.unit = lib.mkDefault ((serviceNameFor name) + ".service"); dir.unit = lib.mkDefault ((serviceNameFor name) + ".service");
# if defaulted, this module is responsible for finalizing the entry. # if defaulted, this module is responsible for finalizing the entry.
# the user could override this if, say, they provide an alternate unit # the user could override this if, say, they finalize some aspect of the entry
# which finalizes the entry (by mounting it, for example). # with a custom service.
unit = lib.mkDefault config.dir.unit; 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 = { options = {
user = mkOption { user = mkOption {
type = types.str; # TODO: use uid? type = types.str; # TODO: use uid?
@ -48,6 +57,15 @@ let
mode = mkOption { mode = mkOption {
type = types.str; type = types.str;
}; };
};
};
# sane.fs."<path>".dir sub-options
dirEntry = types.submodule {
options = {
acl = mkOption {
type = acl;
};
depends = mkOption { depends = mkOption {
type = types.listOf types.str; type = types.listOf types.str;
description = "list of systemd units needed to be run before this directory can be made"; 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. # given a fsEntry definition, output the `config` attrs it generates.
mkFsConfig = path: opt: { mkDirConfig = path: opt: {
systemd.services."${serviceNameFor path}" = { systemd.services."${serviceNameFor path}" = {
description = "prepare ${path}"; description = "prepare ${path}";
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
script = ensure-dir-script; 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; after = opt.dir.depends;
wants = 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 # systemd/shell script used to create and set perms for a specific dir
ensure-dir-script = '' ensure-dir-script = ''
path="$1" path="$1"
@ -166,10 +236,5 @@ in {
}; };
}; };
config = let config = mergeTopLevel (lib.mapAttrsToList mkFsConfig cfg);
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);
};
} }

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 # ensure the fs is mounted only after the mountpoint directory is created
dir.reverseDepends = [ store.mount-unit ]; dir.reverseDepends = [ store.mount-unit ];
# HACK: this fs entry is provided by our 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}" = { sane.fs."${store.underlying.path}" = {
# don't mount until after the backing dir is setup correctly. # don't mount until after the backing dir is setup correctly.

View File

@ -96,7 +96,7 @@ in
config = mkIf cfg.enable (lib.mkMerge [ config = mkIf cfg.enable (lib.mkMerge [
{ {
# TODO: move to sane.fs, to auto-ensure all user dirs? # TODO: move to sane.fs, to auto-ensure all user dirs?
sane.fs."/home/colin".dir = { sane.fs."/home/colin".dir.acl = {
user = "colin"; user = "colin";
group = config.users.users.colin.group; group = config.users.users.colin.group;
mode = config.users.users.colin.homeMode; 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. # 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. # 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? # TODO: just add assertions that sane.fs."${backing}/${dest}".dir == sane.fs."${dest}" for each mount point?
sane.fs."/nix/persist/home/colin".dir = { sane.fs."/nix/persist/home/colin".dir.acl = config.sane.fs."/home/colin".dir.acl;
user = "colin"; sane.fs."/mnt/impermanence/crypt/clearedonboot/home/colin".dir.acl = config.sane.fs."/home/colin".dir.acl;
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;
};
} }
( (
let cfgFor = opt: let cfgFor = opt:
let 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 ]; 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 # pass through the perm/mode overrides
dir-opts = { dir-acl = {
user = lib.mkIf (opt.user != null) opt.user; user = lib.mkIf (opt.user != null) opt.user;
group = lib.mkIf (opt.group != null) opt.group; group = lib.mkIf (opt.group != null) opt.group;
mode = lib.mkIf (opt.mode != null) opt.mode; mode = lib.mkIf (opt.mode != null) opt.mode;
@ -139,27 +126,16 @@ in
# create destination and backing directory, with correct perms # create destination and backing directory, with correct perms
sane.fs."${opt.directory}" = { sane.fs."${opt.directory}" = {
# inherit perms & make sure we don't mount until after the mount point is setup correctly. # inherit perms & make sure we don't mount until after the mount point is setup correctly.
dir = dir-opts // { reverseDepends = [ mount-unit ]; }; dir.acl = dir-acl;
# HACK: anything depending on this directory should actually depend on it being mounted. mount.bind = backing-path;
unit = mount-unit;
}; };
sane.fs."${backing-path}" = { sane.fs."${backing-path}" = {
# inherit perms & make sure we don't mount until after the backing dir is setup correctly. # ensure the backing path has same perms as the mount point
dir = dir-opts // { reverseDepends = [ mount-unit ]; }; dir.acl = config.sane.fs."${opt.directory}".dir.acl;
};
# define the mountpoint.
fileSystems."${opt.directory}" = {
device = backing-path;
options = [
"bind"
];
# fsType = "bind";
noCheck = true;
}; };
}; };
cfgs = builtins.map cfgFor ingested-dirs; cfgs = builtins.map cfgFor ingested-dirs;
in { in {
fileSystems = lib.mkMerge (catAttrs "fileSystems" cfgs);
sane.fs = lib.mkMerge (catAttrs "fs" (catAttrs "sane" cfgs)); sane.fs = lib.mkMerge (catAttrs "fs" (catAttrs "sane" cfgs));
} }
) )