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
|
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);
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.
|
||||||
|
|
|
@ -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));
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue
Block a user