diff --git a/modules/external-storage/default.nix b/modules/external-storage/default.nix new file mode 100644 index 0000000..f07e162 --- /dev/null +++ b/modules/external-storage/default.nix @@ -0,0 +1,142 @@ +{ config, lib, pkgs, ... }: + +let + s3qlWithSystemd = pkgs.s3ql.overrideAttrs (old: { + propagatedBuildInputs = old.propagatedBuildInputs ++ [ + pkgs.python3Packages.systemd + ]; + }); + + cfg = config.services.external-storage; + + create = lib.flip lib.mapAttrs'; +in + +{ + options = { + services.external-storage = { + fileSystems = lib.mkOption { + description = "S3QL-based filesystems on top of CIFS mountpoints."; + default = {}; + type = with lib.types; lazyAttrsOf (submodule ./filesystem-type.nix); + }; + underlays = lib.mkOption { + description = "CIFS underlays for S3QL filesystems."; + default = {}; + type = with lib.types; lazyAttrsOf (submodule ./underlay-type.nix); + }; + }; + }; + + config = { + boot.supportedFilesystems = [ "cifs" ]; + + age.secrets = lib.mkMerge [ + (create cfg.underlays (name: ul: lib.nameValuePair "cifsCredentials-${name}" { file = ul.credentialsFile; })) + (create cfg.fileSystems (name: fs: lib.nameValuePair "storageEncryptionKey-${name}" { file = fs.encryptionKeyFile; })) + ]; + + 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" + "seal" + "_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 + underlayPath = cfg.underlays.${fs.underlay}.mountpoint; + 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 + ]; + + unitConfig.RequiresMountsFor = underlayPath; + + serviceConfig = let + commonOptions = [ + "--cachedir" fs.cacheDir + "--authfile" config.age.secrets."storageEncryptionKey-${name}".path + ]; + in { + Type = "notify"; + + ExecStartPre = map lib.escapeShellArgs [ + [ + (pkgs.writeShellScript "create-s3ql-filesystem" '' + if ! test -e ${underlayPath}/s3ql_passphrase; then + echo Creating new S3QL filesystem on ${underlayPath} + ${pkgs.gnugrep}/bin/grep -m1 fs-passphrase: '${config.age.secrets."storageEncryptionKey-${name}".path}' \ + | cut -d' ' -f2- \ + | ${s3qlWithSystemd}/bin/mkfs.s3ql ${lib.escapeShellArgs commonOptions} -L '${name}' 'local://${underlayPath}' + fi + '') + ] + [ + "${pkgs.coreutils}/bin/install" "-dm755" fs.mountpoint + ] + ([ + "${s3qlWithSystemd}/bin/fsck.s3ql" + "local://${underlayPath}" + "--compress" "none" + ] ++ commonOptions) + ]; + + ExecStart = lib.escapeShellArgs ([ + "${s3qlWithSystemd}/bin/mount.s3ql" + "local://${underlayPath}" + fs.mountpoint + "--fs-name" "${fs.unitName}" + "--allow-other" + "--systemd" "--fg" + "--log" "none" + "--compress" "none" + ] ++ commonOptions); + + ExecStop = lib.escapeShellArgs [ + "${s3qlWithSystemd}/bin/umount.s3ql" + "--log" "none" + fs.mountpoint + ]; + + # fsck and unmounting might take a while + TimeoutStartSec = "6h"; + TimeoutStopSec = "900s"; + + # s3ql only handles SIGINT + KillSignal = "SIGINT"; + + Restart = "on-failure"; + RestartSec = "10s"; + }; + }; + }); + }; + }; +} diff --git a/modules/external-storage/filesystem-type.nix b/modules/external-storage/filesystem-type.nix new file mode 100644 index 0000000..e35a68f --- /dev/null +++ b/modules/external-storage/filesystem-type.nix @@ -0,0 +1,30 @@ +{ lib, name, ... }: + +with lib; + +{ + options = { + mountpoint = mkOption { + type = types.path; + }; + unitName = mkOption { + type = types.str; + default = "remote-storage-${name}"; + }; + unitDescription = mkOption { + type = types.str; + default = "Remote Storage | ${name}"; + }; + encryptionKeyFile = mkOption { + type = types.path; + }; + cacheDir = mkOption { + type = types.path; + default = "/var/cache/remote-storage/${name}"; + }; + underlay = mkOption { + type = types.str; + default = "default"; + }; + }; +} diff --git a/modules/external-storage/underlay-type.nix b/modules/external-storage/underlay-type.nix new file mode 100644 index 0000000..d29ea57 --- /dev/null +++ b/modules/external-storage/underlay-type.nix @@ -0,0 +1,32 @@ +{ config, lib, name, ... }: + +with lib; + +{ + options = { + mountpoint = mkOption { + type = types.path; + default = "/mnt/remote-storage-backends/${name}"; + }; + storageBoxAccount = mkOption { + type = types.str; + # Private Void's main Storage Box + default = "u357754"; + }; + host = mkOption { + type = types.str; + default = "${config.storageBoxAccount}.your-storagebox.de"; + }; + subUser = mkOption { + type = types.str; + example = "sub1"; + }; + credentialsFile = mkOption { + type = types.path; + }; + path = mkOption { + type = types.path; + default = "/"; + }; + }; +} diff --git a/modules/part.nix b/modules/part.nix index 17566a3..7b9c3e7 100644 --- a/modules/part.nix +++ b/modules/part.nix @@ -11,6 +11,7 @@ in deploy-rs-receiver = ./deploy-rs-receiver; effect-receiver = ./effect-receiver; enterprise = ./enterprise; + external-storage = ./external-storage; fail2ban = ./fail2ban; hydra = ./hydra; hyprspace = ./hyprspace; @@ -44,6 +45,7 @@ in consul-service-registry deploy-rs-receiver effect-receiver + external-storage fail2ban motd nix-config-server