fs: add a "mount.bind" option & use it for impermanence bind-mounts
This commit is contained in:
parent
be222c1d70
commit
edf6bd4455
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
)
|
||||
|
|
Loading…
Reference in New Issue
Block a user