2023-11-02 00:13:50 +02:00
|
|
|
{ config, depot, lib, pkgs, ... }:
|
2023-08-23 01:55:48 +03:00
|
|
|
|
|
|
|
let
|
2023-11-02 00:13:50 +02:00
|
|
|
inherit (depot.packages) s3ql;
|
2023-08-23 01:55:48 +03:00
|
|
|
|
|
|
|
cfg = config.services.external-storage;
|
|
|
|
|
2024-06-06 00:44:55 +03:00
|
|
|
cfgAge = config.age;
|
|
|
|
|
2023-08-23 01:55:48 +03:00
|
|
|
create = lib.flip lib.mapAttrs';
|
2024-08-03 03:58:20 +03:00
|
|
|
|
|
|
|
createFiltered = pred: attrs: f: create (lib.filterAttrs pred attrs) f;
|
2023-08-23 01:55:48 +03:00
|
|
|
in
|
|
|
|
|
|
|
|
{
|
2023-08-23 18:02:22 +03:00
|
|
|
imports = [
|
|
|
|
./strict-mounts.nix
|
|
|
|
];
|
|
|
|
|
2023-08-23 01:55:48 +03:00
|
|
|
options = {
|
|
|
|
services.external-storage = {
|
|
|
|
fileSystems = lib.mkOption {
|
|
|
|
description = "S3QL-based filesystems on top of CIFS mountpoints.";
|
|
|
|
default = {};
|
2024-08-03 03:58:20 +03:00
|
|
|
type = with lib.types; lazyAttrsOf (submodule ({ config, name, ... }: let
|
|
|
|
authFile = if config.locksmithSecret != null then
|
|
|
|
"/run/locksmith/${config.locksmithSecret}"
|
|
|
|
else
|
|
|
|
cfgAge.secrets."storageAuth-${name}".path;
|
|
|
|
in {
|
2024-06-06 00:44:55 +03:00
|
|
|
imports = [ ./filesystem-type.nix ];
|
|
|
|
backend = lib.mkIf (config.underlay != null) "local://${cfg.underlays.${config.underlay}.mountpoint}";
|
|
|
|
commonArgs = [
|
|
|
|
"--cachedir" config.cacheDir
|
2024-08-03 03:58:20 +03:00
|
|
|
"--authfile" authFile
|
2024-06-06 00:44:55 +03:00
|
|
|
] ++ (lib.optionals (config.backendOptions != []) [ "--backend-options" (lib.concatStringsSep "," config.backendOptions) ]);
|
|
|
|
}));
|
2023-08-23 01:55:48 +03:00
|
|
|
};
|
|
|
|
underlays = lib.mkOption {
|
|
|
|
description = "CIFS underlays for S3QL filesystems.";
|
|
|
|
default = {};
|
|
|
|
type = with lib.types; lazyAttrsOf (submodule ./underlay-type.nix);
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
|
|
|
config = {
|
2024-06-06 00:44:55 +03:00
|
|
|
system.extraIncantations = {
|
|
|
|
runS3qlUpgrade = i: filesystem: let
|
|
|
|
fs = cfg.fileSystems.${filesystem};
|
|
|
|
in i.execShellWith [ s3ql ] ''
|
|
|
|
echo yes | ${lib.escapeShellArgs
|
|
|
|
([
|
|
|
|
"${s3ql}/bin/s3qladm"
|
|
|
|
] ++ fs.commonArgs ++ [
|
|
|
|
"upgrade"
|
|
|
|
fs.backend
|
|
|
|
])
|
|
|
|
}
|
|
|
|
'';
|
|
|
|
};
|
|
|
|
|
2023-11-02 03:46:40 +02:00
|
|
|
boot.supportedFilesystems = lib.mkIf (cfg.underlays != {}) [ "cifs" ];
|
2023-08-23 01:55:48 +03:00
|
|
|
|
|
|
|
age.secrets = lib.mkMerge [
|
|
|
|
(create cfg.underlays (name: ul: lib.nameValuePair "cifsCredentials-${name}" { file = ul.credentialsFile; }))
|
2024-08-03 03:58:20 +03:00
|
|
|
(createFiltered (_: fs: fs.locksmithSecret == null) cfg.fileSystems (name: fs: lib.nameValuePair "storageAuth-${name}" { file = fs.authFile; }))
|
2023-08-23 01:55:48 +03:00
|
|
|
];
|
|
|
|
|
2024-08-03 03:58:20 +03:00
|
|
|
services.locksmith.waitForSecrets = createFiltered (_: fs: fs.locksmithSecret != null) cfg.fileSystems (name: fs: {
|
|
|
|
name = fs.unitName;
|
|
|
|
value = [ fs.locksmithSecret ];
|
|
|
|
});
|
|
|
|
|
2023-08-23 01:55:48 +03:00
|
|
|
fileSystems = create cfg.underlays (name: ul: {
|
|
|
|
name = ul.mountpoint;
|
|
|
|
value = {
|
|
|
|
fsType = "cifs";
|
|
|
|
device = "//${ul.host}/${ul.storageBoxAccount}-${ul.subUser}${ul.path}";
|
|
|
|
options = [
|
|
|
|
"credentials=${config.age.secrets."cifsCredentials-${name}".path}"
|
|
|
|
"dir_mode=0700"
|
|
|
|
"file_mode=0600"
|
2023-08-28 03:19:39 +03:00
|
|
|
"uid=${toString ul.uid}"
|
|
|
|
"gid=${toString ul.gid}"
|
|
|
|
"forceuid"
|
|
|
|
"forcegid"
|
2023-08-23 01:55:48 +03:00
|
|
|
"seal"
|
2023-08-23 02:46:18 +03:00
|
|
|
"hard"
|
|
|
|
"resilienthandles"
|
|
|
|
"cache=loose"
|
2023-08-23 01:55:48 +03:00
|
|
|
"_netdev"
|
|
|
|
"x-systemd.automount"
|
|
|
|
];
|
|
|
|
};
|
|
|
|
});
|
|
|
|
systemd = {
|
|
|
|
tmpfiles.rules = lib.mapAttrsToList (_: fs: "d '${fs.cacheDir}' 0700 root root - -") cfg.fileSystems;
|
|
|
|
|
|
|
|
mounts = lib.mapAttrsToList (name: fs: {
|
|
|
|
where = fs.mountpoint;
|
|
|
|
what = name;
|
|
|
|
requires = [ "${fs.unitName}.service" ];
|
|
|
|
after = [ "${fs.unitName}.service" ];
|
|
|
|
}) cfg.fileSystems;
|
|
|
|
|
|
|
|
services = create cfg.fileSystems (name: fs: {
|
|
|
|
name = fs.unitName;
|
|
|
|
value = let
|
2023-11-02 03:46:40 +02:00
|
|
|
isUnderlay = fs.underlay != null;
|
|
|
|
|
2024-08-10 00:46:11 +03:00
|
|
|
backendParts = lib.strings.match "([a-z0-9]*)://([^/]*)/([^/]*)(/.*)?" fs.backend;
|
|
|
|
|
|
|
|
fsType = if isUnderlay then "local" else lib.head backendParts;
|
|
|
|
|
|
|
|
s3Endpoint = assert fsType == "s3c4"; lib.elemAt backendParts 1;
|
|
|
|
|
|
|
|
s3Bucket = assert fsType == "s3c4"; lib.elemAt backendParts 2;
|
2024-06-06 00:04:33 +03:00
|
|
|
|
2024-06-06 00:44:55 +03:00
|
|
|
localBackendPath = if isUnderlay then cfg.underlays.${fs.underlay}.mountpoint else lib.head (lib.strings.match "[a-z0-9]*://(/.*)" fs.backend);
|
2023-08-23 01:55:48 +03:00
|
|
|
in {
|
|
|
|
description = fs.unitDescription;
|
|
|
|
wantedBy = [ "multi-user.target" ];
|
|
|
|
wants = [ "remote-fs.target" ];
|
|
|
|
before = [ "remote-fs.target" ];
|
|
|
|
|
|
|
|
# used by umount.s3ql
|
|
|
|
path = with pkgs; [
|
|
|
|
psmisc
|
|
|
|
util-linux
|
|
|
|
];
|
|
|
|
|
2024-06-06 00:04:33 +03:00
|
|
|
unitConfig.RequiresMountsFor = lib.mkIf isUnderlay localBackendPath;
|
2023-08-23 01:55:48 +03:00
|
|
|
|
2024-06-06 00:44:55 +03:00
|
|
|
serviceConfig = {
|
2023-08-23 01:55:48 +03:00
|
|
|
Type = "notify";
|
|
|
|
|
|
|
|
ExecStartPre = map lib.escapeShellArgs [
|
|
|
|
[
|
2023-11-02 03:46:40 +02:00
|
|
|
(let
|
2024-08-10 00:46:11 +03:00
|
|
|
authFile = if fs.locksmithSecret != null then
|
|
|
|
"/run/locksmith/${fs.locksmithSecret}"
|
|
|
|
else
|
|
|
|
cfgAge.secrets."storageAuth-${name}".path;
|
2023-11-02 03:46:40 +02:00
|
|
|
mkfsEncrypted = ''
|
2024-08-10 00:46:11 +03:00
|
|
|
${pkgs.gnugrep}/bin/grep -m1 fs-passphrase: '${authFile}' \
|
2023-08-23 01:55:48 +03:00
|
|
|
| cut -d' ' -f2- \
|
2024-06-06 00:44:55 +03:00
|
|
|
| ${s3ql}/bin/mkfs.s3ql ${lib.escapeShellArgs fs.commonArgs} -L '${name}' '${fs.backend}'
|
2023-11-02 03:46:40 +02:00
|
|
|
'';
|
|
|
|
|
|
|
|
mkfsPlain = ''
|
2024-06-06 00:44:55 +03:00
|
|
|
${s3ql}/bin/mkfs.s3ql ${lib.escapeShellArgs fs.commonArgs} --plain -L '${name}' '${fs.backend}'
|
2023-11-02 03:46:40 +02:00
|
|
|
'';
|
|
|
|
|
|
|
|
detectFs = {
|
2024-06-06 00:04:33 +03:00
|
|
|
local = "test -e ${localBackendPath}/s3ql_metadata";
|
2024-08-10 00:46:11 +03:00
|
|
|
s3c4 = pkgs.writeShellScript "detect-s3ql-filesystem" ''
|
|
|
|
export AWS_ACCESS_KEY_ID="$(${pkgs.gnugrep}/bin/grep -m1 backend-login: '${authFile}' | cut -d' ' -f2-)"
|
|
|
|
export AWS_SECRET_ACCESS_KEY="$(${pkgs.gnugrep}/bin/grep -m1 backend-password: '${authFile}' | cut -d' ' -f2-)"
|
|
|
|
${pkgs.s5cmd}/bin/s5cmd --endpoint-url https://${s3Endpoint}/ ls 's3://${s3Bucket}/s3ql_params' >/dev/null
|
|
|
|
'';
|
2023-11-02 03:46:40 +02:00
|
|
|
}.${fsType} or null;
|
|
|
|
in pkgs.writeShellScript "create-s3ql-filesystem" (lib.optionalString (detectFs != null) ''
|
|
|
|
if ! ${detectFs}; then
|
2024-06-06 00:44:55 +03:00
|
|
|
echo Creating new S3QL filesystem on ${fs.backend}
|
2023-11-02 03:46:40 +02:00
|
|
|
${if fs.encrypt then mkfsEncrypted else mkfsPlain}
|
2023-08-23 01:55:48 +03:00
|
|
|
fi
|
2023-11-02 03:46:40 +02:00
|
|
|
''))
|
2023-08-23 01:55:48 +03:00
|
|
|
]
|
|
|
|
[
|
|
|
|
"${pkgs.coreutils}/bin/install" "-dm755" fs.mountpoint
|
|
|
|
]
|
|
|
|
([
|
2023-11-02 00:13:50 +02:00
|
|
|
"${s3ql}/bin/fsck.s3ql"
|
2024-06-06 00:44:55 +03:00
|
|
|
fs.backend
|
2023-08-23 01:55:48 +03:00
|
|
|
"--compress" "none"
|
2024-06-06 00:44:55 +03:00
|
|
|
] ++ fs.commonArgs)
|
2023-08-23 01:55:48 +03:00
|
|
|
];
|
|
|
|
|
|
|
|
ExecStart = lib.escapeShellArgs ([
|
2023-11-02 00:13:50 +02:00
|
|
|
"${s3ql}/bin/mount.s3ql"
|
2024-06-06 00:44:55 +03:00
|
|
|
fs.backend
|
2023-08-23 01:55:48 +03:00
|
|
|
fs.mountpoint
|
|
|
|
"--fs-name" "${fs.unitName}"
|
|
|
|
"--allow-other"
|
|
|
|
"--systemd" "--fg"
|
|
|
|
"--log" "none"
|
|
|
|
"--compress" "none"
|
2024-06-06 00:44:55 +03:00
|
|
|
] ++ fs.commonArgs);
|
2023-08-23 01:55:48 +03:00
|
|
|
|
2024-06-06 01:41:32 +03:00
|
|
|
ExecStop = pkgs.writeShellScript "umount-s3ql-filesystem" ''
|
|
|
|
if grep -qw '${fs.mountpoint}' /proc/self/mounts; then
|
|
|
|
${s3ql}/bin/umount.s3ql --log none '${fs.mountpoint}'
|
|
|
|
else
|
|
|
|
echo Filesystem already unmounted.
|
|
|
|
fi
|
2024-06-06 03:18:52 +03:00
|
|
|
echo "Waiting for MainPID ($MAINPID) to die..."
|
|
|
|
tail --pid=$MAINPID -f /dev/null
|
2024-06-06 01:41:32 +03:00
|
|
|
'';
|
2023-08-23 01:55:48 +03:00
|
|
|
|
|
|
|
# fsck and unmounting might take a while
|
|
|
|
TimeoutStartSec = "6h";
|
|
|
|
TimeoutStopSec = "900s";
|
|
|
|
|
|
|
|
# s3ql only handles SIGINT
|
|
|
|
KillSignal = "SIGINT";
|
|
|
|
|
|
|
|
Restart = "on-failure";
|
|
|
|
RestartSec = "10s";
|
2023-08-27 02:44:21 +03:00
|
|
|
|
|
|
|
# see https://www.rath.org/s3ql-docs/man/fsck.html
|
|
|
|
SuccessExitStatus = [ 128 ];
|
2023-08-23 01:55:48 +03:00
|
|
|
};
|
|
|
|
};
|
|
|
|
});
|
|
|
|
};
|
|
|
|
};
|
|
|
|
}
|