2023-01-03 14:20:02 +00:00
|
|
|
{ config, lib, pkgs, utils, sane-lib, ... }:
|
2022-12-30 14:45:34 +00:00
|
|
|
with lib;
|
|
|
|
let
|
2023-01-03 14:20:02 +00:00
|
|
|
path-lib = sane-lib.path;
|
2023-01-04 00:59:52 +00:00
|
|
|
sane-types = sane-lib.types;
|
2022-12-30 14:45:34 +00:00
|
|
|
cfg = config.sane.fs;
|
|
|
|
|
2023-01-03 02:45:23 +00:00
|
|
|
mountNameFor = path: "${utils.escapeSystemdPath path}.mount";
|
2022-12-31 11:31:16 +00:00
|
|
|
serviceNameFor = path: "ensure-${utils.escapeSystemdPath path}";
|
|
|
|
|
2022-12-30 14:45:34 +00:00
|
|
|
# sane.fs."<path>" top-level options
|
2022-12-31 12:18:27 +00:00
|
|
|
fsEntry = types.submodule ({ name, config, ...}: let
|
2023-01-03 14:20:02 +00:00
|
|
|
parent = path-lib.parent name;
|
|
|
|
has-parent = path-lib.hasParent name;
|
2022-12-30 14:45:34 +00:00
|
|
|
parent-cfg = if has-parent then cfg."${parent}" else {};
|
2023-01-04 06:08:27 +00:00
|
|
|
parent-acl = if has-parent then parent-cfg.generated.acl else {};
|
2022-12-30 14:45:34 +00:00
|
|
|
in {
|
|
|
|
options = {
|
|
|
|
dir = mkOption {
|
2023-01-04 06:08:27 +00:00
|
|
|
type = types.nullOr dirEntry;
|
2023-01-03 02:45:23 +00:00
|
|
|
default = null;
|
|
|
|
};
|
2023-01-04 02:51:07 +00:00
|
|
|
symlink = mkOption {
|
2023-01-06 15:26:39 +00:00
|
|
|
type = types.nullOr (symlinkEntryFor name);
|
2023-01-04 02:51:07 +00:00
|
|
|
default = null;
|
|
|
|
};
|
2023-01-04 06:08:27 +00:00
|
|
|
generated = mkOption {
|
|
|
|
type = generatedEntry;
|
|
|
|
default = {};
|
|
|
|
};
|
|
|
|
mount = mkOption {
|
|
|
|
type = types.nullOr (mountEntryFor name);
|
|
|
|
default = null;
|
2023-01-04 04:32:20 +00:00
|
|
|
};
|
2023-01-04 11:22:26 +00:00
|
|
|
wantedBy = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
|
|
|
description = ''
|
|
|
|
list of units or targets which, when activated, should trigger this fs entry to be created.
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
wantedBeforeBy = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
|
|
|
description = ''
|
|
|
|
list of units or targets which, when activated, should first start and wait for this fs entry to be created.
|
|
|
|
if this unit fails, it will not block the targets in this list.
|
|
|
|
'';
|
|
|
|
};
|
2022-12-31 11:31:16 +00:00
|
|
|
unit = mkOption {
|
2022-12-30 14:45:34 +00:00
|
|
|
type = types.str;
|
2022-12-31 11:31:16 +00:00
|
|
|
description = "name of the systemd unit which ensures this entry";
|
2022-12-30 14:45:34 +00:00
|
|
|
};
|
|
|
|
};
|
2023-01-04 06:08:27 +00:00
|
|
|
config = let
|
|
|
|
default-acl = {
|
|
|
|
user = lib.mkDefault (parent-acl.user or "root");
|
|
|
|
group = lib.mkDefault (parent-acl.group or "root");
|
|
|
|
mode = lib.mkDefault (parent-acl.mode or "0755");
|
|
|
|
};
|
|
|
|
in {
|
2022-12-30 14:45:34 +00:00
|
|
|
# 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`).
|
2023-01-04 06:08:27 +00:00
|
|
|
generated.depends = if has-parent then [ parent-cfg.unit ] else [];
|
|
|
|
|
|
|
|
# populate generated items from `dir` or `symlink` shorthands
|
|
|
|
generated.acl = lib.mkMerge [
|
|
|
|
default-acl
|
|
|
|
(lib.mkIf (config.dir != null)
|
|
|
|
(sane-lib.filterNonNull config.dir.acl))
|
|
|
|
(lib.mkIf (config.symlink != null)
|
|
|
|
(sane-lib.filterNonNull config.symlink.acl))
|
|
|
|
];
|
2023-01-04 07:14:01 +00:00
|
|
|
|
|
|
|
# actually generate the item
|
2023-01-04 06:08:27 +00:00
|
|
|
generated.script = lib.mkMerge [
|
|
|
|
(lib.mkIf (config.dir != null) (ensureDirScript name config.dir))
|
|
|
|
(lib.mkIf (config.symlink != null) (ensureSymlinkScript name config.symlink))
|
|
|
|
];
|
|
|
|
|
|
|
|
# make the unit file which generates the underlying thing available so that `mount` can use it.
|
|
|
|
generated.unit = (serviceNameFor name) + ".service";
|
2023-01-03 02:45:23 +00:00
|
|
|
|
2023-01-06 13:27:27 +00:00
|
|
|
# if we were asked to mount, make sure we create the dir that we mount over
|
|
|
|
dir = lib.mkIf (config.mount != null) {};
|
|
|
|
|
2022-12-31 12:18:27 +00:00
|
|
|
# if defaulted, this module is responsible for finalizing the entry.
|
2023-01-03 02:45:23 +00:00
|
|
|
# the user could override this if, say, they finalize some aspect of the entry
|
|
|
|
# with a custom service.
|
2023-01-04 02:51:07 +00:00
|
|
|
unit = lib.mkDefault (
|
|
|
|
if config.mount != null then
|
|
|
|
config.mount.unit
|
2023-01-04 06:08:27 +00:00
|
|
|
else
|
|
|
|
config.generated.unit
|
2023-01-04 02:51:07 +00:00
|
|
|
);
|
2022-12-30 14:45:34 +00:00
|
|
|
};
|
|
|
|
});
|
2023-01-03 02:45:23 +00:00
|
|
|
|
2023-01-04 07:14:01 +00:00
|
|
|
# options which can be set in dir/symlink generated items,
|
|
|
|
# with intention that they just propagate down
|
|
|
|
propagatedGenerateMod = {
|
2023-01-04 06:08:27 +00:00
|
|
|
options = {
|
|
|
|
acl = mkOption {
|
|
|
|
type = sane-types.aclOverride;
|
|
|
|
default = {};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-01-04 07:14:01 +00:00
|
|
|
# sane.fs."<path>".dir sub-options
|
|
|
|
# takes no special options
|
|
|
|
dirEntry = types.submodule propagatedGenerateMod;
|
|
|
|
|
2023-01-06 15:26:39 +00:00
|
|
|
symlinkEntryFor = path: types.submodule ({ config, ...}: {
|
2023-01-04 06:08:27 +00:00
|
|
|
options = {
|
2023-01-04 07:14:01 +00:00
|
|
|
inherit (propagatedGenerateMod.options) acl;
|
2023-01-04 06:08:27 +00:00
|
|
|
target = mkOption {
|
2023-01-06 15:26:39 +00:00
|
|
|
type = types.coercedTo types.package toString types.str;
|
2023-01-04 06:08:27 +00:00
|
|
|
description = "fs path to link to";
|
|
|
|
};
|
2023-01-06 15:26:39 +00:00
|
|
|
text = mkOption {
|
|
|
|
type = types.nullOr types.str;
|
|
|
|
default = null;
|
|
|
|
description = "create a file in the /nix/store with the provided text and use that as the target";
|
|
|
|
};
|
2023-01-04 06:08:27 +00:00
|
|
|
};
|
2023-01-06 15:26:39 +00:00
|
|
|
config = {
|
|
|
|
target = lib.mkIf (config.text != null) (
|
|
|
|
pkgs.writeText (path-lib.leaf path) config.text
|
|
|
|
);
|
|
|
|
};
|
|
|
|
});
|
2023-01-04 06:08:27 +00:00
|
|
|
|
|
|
|
generatedEntry = types.submodule {
|
2023-01-03 02:45:23 +00:00
|
|
|
options = {
|
|
|
|
acl = mkOption {
|
2023-01-04 00:59:52 +00:00
|
|
|
type = sane-types.acl;
|
2023-01-03 02:45:23 +00:00
|
|
|
};
|
2023-01-04 06:08:27 +00:00
|
|
|
depends = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
description = ''
|
|
|
|
list of systemd units needed to be run before this item can be generated.
|
|
|
|
'';
|
|
|
|
default = [];
|
|
|
|
};
|
|
|
|
script.script = mkOption {
|
|
|
|
type = types.lines;
|
|
|
|
};
|
|
|
|
script.scriptArgs = mkOption {
|
|
|
|
type = types.listOf types.str;
|
|
|
|
default = [];
|
|
|
|
};
|
2022-12-31 12:18:27 +00:00
|
|
|
unit = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
description = "name of the systemd unit which ensures this directory";
|
|
|
|
};
|
|
|
|
};
|
2022-12-30 14:45:34 +00:00
|
|
|
};
|
|
|
|
|
2023-01-03 02:45:23 +00:00
|
|
|
# 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;
|
|
|
|
};
|
2023-01-04 11:22:26 +00:00
|
|
|
depends = mkOption {
|
2023-01-03 07:04:49 +00:00
|
|
|
type = types.listOf types.str;
|
2023-01-04 11:22:26 +00:00
|
|
|
description = ''
|
|
|
|
list of systemd units needed to be run before this entry can be mounted
|
|
|
|
'';
|
2023-01-03 07:04:49 +00:00
|
|
|
default = [];
|
|
|
|
};
|
2023-01-03 02:45:23 +00:00
|
|
|
unit = mkOption {
|
|
|
|
type = types.str;
|
|
|
|
description = "name of the systemd unit which mounts this path";
|
|
|
|
default = mountNameFor path;
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-01-04 11:22:26 +00:00
|
|
|
mkGeneratedConfig = path: opt: let
|
|
|
|
gen-opt = opt.generated;
|
2023-01-04 06:08:27 +00:00
|
|
|
wrapper = generateWrapperScript path gen-opt;
|
|
|
|
in {
|
2022-12-31 11:31:16 +00:00
|
|
|
systemd.services."${serviceNameFor path}" = {
|
2022-12-30 14:45:34 +00:00
|
|
|
description = "prepare ${path}";
|
2022-12-31 12:18:27 +00:00
|
|
|
serviceConfig.Type = "oneshot";
|
|
|
|
|
2023-01-04 06:08:27 +00:00
|
|
|
script = wrapper.script;
|
2023-03-22 19:52:04 +00:00
|
|
|
scriptArgs = escapeShellArgs wrapper.scriptArgs;
|
2022-12-31 12:18:27 +00:00
|
|
|
|
2023-01-04 06:08:27 +00:00
|
|
|
after = gen-opt.depends;
|
|
|
|
wants = gen-opt.depends;
|
2022-12-30 14:45:34 +00:00
|
|
|
# prevent systemd making this unit implicitly dependent on sysinit.target.
|
|
|
|
# see: <https://www.freedesktop.org/software/systemd/man/systemd.special.html>
|
|
|
|
unitConfig.DefaultDependencies = "no";
|
2022-12-31 12:18:27 +00:00
|
|
|
|
2023-01-04 11:22:26 +00:00
|
|
|
before = opt.wantedBeforeBy;
|
|
|
|
wantedBy = opt.wantedBy ++ opt.wantedBeforeBy;
|
2022-12-30 14:45:34 +00:00
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-01-04 02:51:07 +00:00
|
|
|
# given a mountEntry definition, evaluate its toplevel `config` output.
|
2023-01-03 02:45:23 +00:00
|
|
|
mkMountConfig = path: opt: (let
|
2023-01-04 03:57:24 +00:00
|
|
|
device = config.fileSystems."${path}".device;
|
|
|
|
underlying = cfg."${device}";
|
|
|
|
isBind = opt.mount.bind != null;
|
|
|
|
ifBind = lib.mkIf isBind;
|
2023-01-04 12:12:30 +00:00
|
|
|
# before mounting:
|
|
|
|
# - create the target directory
|
|
|
|
# - prepare the source directory -- assuming it's not an external device
|
|
|
|
# - satisfy any user-specified prerequisites ("depends")
|
|
|
|
requires = [ opt.generated.unit ]
|
|
|
|
++ (if lib.hasPrefix "/dev/disk/" device then [] else [ underlying.unit ])
|
|
|
|
++ opt.mount.depends;
|
2023-01-03 02:45:23 +00:00
|
|
|
in {
|
2023-01-04 03:57:24 +00:00
|
|
|
fileSystems."${path}" = {
|
|
|
|
device = ifBind opt.mount.bind;
|
|
|
|
options = (if isBind then ["bind"] else [])
|
|
|
|
++ [
|
2023-01-04 11:22:26 +00:00
|
|
|
# disable defaults: don't require this to be mount as part of local-fs.target
|
|
|
|
# we'll handle that stuff precisely.
|
|
|
|
"noauto"
|
|
|
|
"nofail"
|
2023-01-04 03:57:24 +00:00
|
|
|
# x-systemd options documented here:
|
|
|
|
# - <https://www.freedesktop.org/software/systemd/man/systemd.mount.html>
|
|
|
|
]
|
2023-01-04 12:12:30 +00:00
|
|
|
++ (builtins.map (unit: "x-systemd.requires=${unit}") requires)
|
2023-01-04 11:22:26 +00:00
|
|
|
++ (builtins.map (unit: "x-systemd.before=${unit}") opt.wantedBeforeBy)
|
|
|
|
++ (builtins.map (unit: "x-systemd.wanted-by=${unit}") (opt.wantedBy ++ opt.wantedBeforeBy));
|
2023-01-04 03:57:24 +00:00
|
|
|
noCheck = ifBind true;
|
2023-01-03 02:45:23 +00:00
|
|
|
};
|
|
|
|
});
|
|
|
|
|
2023-01-04 02:51:07 +00:00
|
|
|
|
2023-01-09 11:14:59 +00:00
|
|
|
mkFsConfig = path: opt: lib.mkMerge [
|
2023-01-04 11:22:26 +00:00
|
|
|
(mkGeneratedConfig path opt)
|
2023-01-09 11:14:59 +00:00
|
|
|
(lib.mkIf (opt.mount != null) (mkMountConfig path opt))
|
2023-01-03 02:45:23 +00:00
|
|
|
];
|
|
|
|
|
2023-01-04 06:08:27 +00:00
|
|
|
generateWrapperScript = path: gen-opt: {
|
|
|
|
script = ''
|
|
|
|
fspath="$1"
|
|
|
|
acluser="$2"
|
|
|
|
aclgroup="$3"
|
|
|
|
aclmode="$4"
|
|
|
|
shift 4
|
|
|
|
|
2023-01-04 06:28:44 +00:00
|
|
|
# ensure any things created by the user script have the desired mode.
|
|
|
|
# chmod doesn't work on symlinks, so we *have* to use this umask approach.
|
2023-01-04 07:14:01 +00:00
|
|
|
decmask=$(( 0777 - "$aclmode" ))
|
|
|
|
octmask=$(printf "%o" "$decmask")
|
|
|
|
umask "$octmask"
|
2023-01-04 06:28:44 +00:00
|
|
|
|
2023-01-04 06:08:27 +00:00
|
|
|
# try to chmod/chown the result even if the user script errors
|
|
|
|
_status=0
|
|
|
|
trap "_status=\$?" ERR
|
|
|
|
|
|
|
|
${gen-opt.script.script}
|
|
|
|
|
2023-01-04 06:28:44 +00:00
|
|
|
# claim ownership of the new thing (DON'T traverse symlinks)
|
2023-01-04 07:14:01 +00:00
|
|
|
chown --no-dereference "$acluser:$aclgroup" "$fspath"
|
2023-01-04 08:12:53 +00:00
|
|
|
# AS LONG AS IT'S NOT A SYMLINK, try to fix perms in case the entity existed before this script was called
|
|
|
|
if ! test -L "$fspath"
|
|
|
|
then
|
|
|
|
chmod "$aclmode" "$fspath"
|
|
|
|
fi
|
|
|
|
|
2023-01-04 06:08:27 +00:00
|
|
|
exit "$_status"
|
|
|
|
'';
|
|
|
|
scriptArgs = [ path gen-opt.acl.user gen-opt.acl.group gen-opt.acl.mode ] ++ gen-opt.script.scriptArgs;
|
|
|
|
};
|
|
|
|
|
2022-12-30 14:45:34 +00:00
|
|
|
# systemd/shell script used to create and set perms for a specific dir
|
2023-01-04 06:08:27 +00:00
|
|
|
ensureDirScript = path: dir-cfg: {
|
|
|
|
script = ''
|
|
|
|
dirpath="$1"
|
|
|
|
|
|
|
|
if ! test -d "$dirpath"
|
|
|
|
then
|
|
|
|
# if the directory *doesn't* exist, try creating it
|
|
|
|
# if we fail to create it, ensure we raced with something else and that it's actually a directory
|
|
|
|
mkdir "$dirpath" || test -d "$dirpath"
|
|
|
|
fi
|
|
|
|
'';
|
|
|
|
scriptArgs = [ path ];
|
|
|
|
};
|
2022-12-30 14:45:34 +00:00
|
|
|
|
2023-01-04 02:51:07 +00:00
|
|
|
# systemd/shell script used to create a symlink
|
2023-01-04 06:08:27 +00:00
|
|
|
ensureSymlinkScript = path: link-cfg: {
|
|
|
|
script = ''
|
|
|
|
lnfrom="$1"
|
|
|
|
lnto="$2"
|
2023-01-04 02:51:07 +00:00
|
|
|
|
2023-01-06 14:51:25 +00:00
|
|
|
# ln is clever when there's something else at the place we want to create the link
|
|
|
|
# only create the link if nothing's there or what is there is another link,
|
|
|
|
# otherwise you'll get links at unexpected fs locations
|
|
|
|
! test -e "$lnfrom" || test -L "$lnfrom" && ln -sf --no-dereference "$lnto" "$lnfrom"
|
2023-01-04 06:08:27 +00:00
|
|
|
'';
|
|
|
|
scriptArgs = [ path link-cfg.target ];
|
|
|
|
};
|
2023-01-04 02:51:07 +00:00
|
|
|
|
2022-12-30 14:45:34 +00:00
|
|
|
# return all ancestors of this path.
|
|
|
|
# e.g. ancestorsOf "/foo/bar/baz" => [ "/" "/foo" "/foo/bar" ]
|
2023-01-06 09:56:06 +00:00
|
|
|
ancestorsOf = path: lib.init (path-lib.walk "/" path);
|
2022-12-30 14:45:34 +00:00
|
|
|
|
|
|
|
# attrsOf fsEntry type which for every entry ensures that all ancestor entries are created.
|
|
|
|
# we do this with a custom type to ensure that users can access `config.sane.fs."/parent/path"`
|
|
|
|
# when inferred.
|
|
|
|
fsTree = let
|
|
|
|
baseType = types.attrsOf fsEntry;
|
|
|
|
# merge is called once, with all collected `sane.fs` definitions passed and we coalesce those
|
|
|
|
# into a single value `x` as if the user had wrote simply `sane.fs = x` in a single location.
|
|
|
|
# so option defaulting and such happens *after* `merge` is called.
|
|
|
|
merge = loc: defs: let
|
|
|
|
# loc is the location of the option holding this type, e.g. ["sane" "fs"].
|
|
|
|
# each def is an { value = attrsOf fsEntry instance; file = "..."; }
|
|
|
|
pathsForDef = def: attrNames def.value;
|
|
|
|
origPaths = concatLists (builtins.map pathsForDef defs);
|
|
|
|
extraPaths = concatLists (builtins.map ancestorsOf origPaths);
|
|
|
|
extraDefs = builtins.map (p: {
|
|
|
|
file = ./.;
|
|
|
|
value = {
|
|
|
|
"${p}".dir = {};
|
|
|
|
};
|
|
|
|
}) extraPaths;
|
|
|
|
in
|
|
|
|
baseType.merge loc (defs ++ extraDefs);
|
|
|
|
in
|
|
|
|
lib.mkOptionType {
|
|
|
|
inherit merge;
|
|
|
|
name = "fsTree";
|
|
|
|
description = "attrset representation of a file-system tree";
|
|
|
|
# ensure that every path is in canonical form, else we might get duplicates and subtle errors
|
2023-01-03 14:20:02 +00:00
|
|
|
check = tree: builtins.all (p: p == path-lib.norm p) (builtins.attrNames tree);
|
2022-12-30 14:45:34 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
in {
|
|
|
|
options = {
|
|
|
|
sane.fs = mkOption {
|
|
|
|
# type = types.attrsOf fsEntry;
|
2023-05-08 09:50:10 +00:00
|
|
|
# TODO: can we use `types.lazyAttrsOf fsEntry`??
|
|
|
|
# - this exists specifically to let attrs reference eachother
|
2022-12-30 14:45:34 +00:00
|
|
|
type = fsTree;
|
|
|
|
default = {};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2023-01-09 09:42:17 +00:00
|
|
|
config =
|
|
|
|
let
|
|
|
|
configs = lib.mapAttrsToList mkFsConfig cfg;
|
|
|
|
take = f: {
|
|
|
|
systemd.services = f.systemd.services;
|
|
|
|
fileSystems = f.fileSystems;
|
|
|
|
};
|
|
|
|
in take (sane-lib.mkTypedMerge take configs);
|
2022-12-30 14:45:34 +00:00
|
|
|
}
|