Unverified Commit d22d60f3 authored by Markus Kowalewski's avatar Markus Kowalewski
Browse files

nixos/saunafs: add module + test

parent b44c8b4c
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -966,6 +966,7 @@
  ./services/network-filesystems/rsyncd.nix
  ./services/network-filesystems/samba-wsdd.nix
  ./services/network-filesystems/samba.nix
  ./services/network-filesystems/saunafs.nix
  ./services/network-filesystems/tahoe.nix
  ./services/network-filesystems/u9fs.nix
  ./services/network-filesystems/webdav-server-rs.nix
+287 −0
Original line number Diff line number Diff line
{
  config,
  lib,
  pkgs,
  ...
}:

let
  cfg = config.services.saunafs;

  settingsFormat =
    let
      listSep = " ";
      allowedTypes = with lib.types; [
        bool
        int
        float
        str
      ];
      valueToString =
        val:
        if lib.isList val then
          lib.concatStringsSep listSep (map (x: valueToString x) val)
        else if lib.isBool val then
          (if val then "1" else "0")
        else
          toString val;

    in
    {
      type =
        let
          valueType =
            lib.types.oneOf (
              [
                (lib.types.listOf valueType)
              ]
              ++ allowedTypes
            )
            // {
              description = "Flat key-value file";
            };
        in
        lib.types.attrsOf valueType;

      generate =
        name: value:
        pkgs.writeText name (
          lib.concatStringsSep "\n" (lib.mapAttrsToList (key: val: "${key} = ${valueToString val}") value)
        );
    };

  initTool = pkgs.writeShellScriptBin "sfsmaster-init" ''
    if [ ! -e ${cfg.master.settings.DATA_PATH}/metadata.sfs ]; then
      cp --update=none ${pkgs.saunafs}/var/lib/saunafs/metadata.sfs.empty ${cfg.master.settings.DATA_PATH}/metadata.sfs
      chmod +w ${cfg.master.settings.DATA_PATH}/metadata.sfs
    fi
  '';

  # master config file
  masterCfg = settingsFormat.generate "sfsmaster.cfg" cfg.master.settings;

  # metalogger config file
  metaloggerCfg = settingsFormat.generate "sfsmetalogger.cfg" cfg.metalogger.settings;

  # chunkserver config file
  chunkserverCfg = settingsFormat.generate "sfschunkserver.cfg" cfg.chunkserver.settings;

  # generic template for all daemons
  systemdService = name: extraConfig: configFile: {
    wantedBy = [ "multi-user.target" ];
    wants = [ "network-online.target" ];
    after = [
      "network.target"
      "network-online.target"
    ];

    serviceConfig = {
      Type = "forking";
      ExecStart = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} start";
      ExecStop = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} stop";
      ExecReload = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} reload";
    } // extraConfig;
  };

in
{
  ###### interface

  options = {
    services.saunafs = {
      masterHost = lib.mkOption {
        type = lib.types.str;
        default = null;
        description = "IP or hostname name of master host.";
      };

      sfsUser = lib.mkOption {
        type = lib.types.str;
        default = "saunafs";
        description = "Run daemons as user.";
      };

      client.enable = lib.mkEnableOption "Saunafs client";

      master = {
        enable = lib.mkOption {
          type = lib.types.bool;
          description = ''
            Enable Saunafs master daemon.

            You need to run `sfsmaster-init` on a freshly installed master server to
            initialize the `DATA_PATH` directory.
          '';
          default = false;
        };

        exports = lib.mkOption {
          type = with lib.types; listOf str;
          default = null;
          description = "Paths to exports file (see {manpage}`sfsexports.cfg(5)`).";
          example = lib.literalExpression ''
            [ "* / rw,alldirs,admin,maproot=0:0" ];
          '';
        };

        openFirewall = lib.mkOption {
          type = lib.types.bool;
          description = "Whether to automatically open the necessary ports in the firewall.";
          default = false;
        };

        settings = lib.mkOption {
          type = lib.types.submodule {
            freeformType = settingsFormat.type;

            options.DATA_PATH = lib.mkOption {
              type = lib.types.str;
              default = "/var/lib/saunafs/master";
              description = "Data storage directory.";
            };
          };

          description = "Contents of config file ({manpage}`sfsmaster.cfg(5)`).";
        };
      };

      metalogger = {
        enable = lib.mkEnableOption "Saunafs metalogger daemon";

        settings = lib.mkOption {
          type = lib.types.submodule {
            freeformType = settingsFormat.type;

            options.DATA_PATH = lib.mkOption {
              type = lib.types.str;
              default = "/var/lib/saunafs/metalogger";
              description = "Data storage directory";
            };
          };

          description = "Contents of metalogger config file (see {manpage}`sfsmetalogger.cfg(5)`).";
        };
      };

      chunkserver = {
        enable = lib.mkEnableOption "Saunafs chunkserver daemon";

        openFirewall = lib.mkOption {
          type = lib.types.bool;
          description = "Whether to automatically open the necessary ports in the firewall.";
          default = false;
        };

        hdds = lib.mkOption {
          type = with lib.types; listOf str;
          default = null;

          example = lib.literalExpression ''
            [ "/mnt/hdd1" ];
          '';

          description = ''
            Mount points to be used by chunkserver for storage (see {manpage}`sfshdd.cfg(5)`).

            Note, that these mount points must writeable by the user defined by the saunafs user.
          '';
        };

        settings = lib.mkOption {
          type = lib.types.submodule {
            freeformType = settingsFormat.type;

            options.DATA_PATH = lib.mkOption {
              type = lib.types.str;
              default = "/var/lib/saunafs/chunkserver";
              description = "Directory for chunck meta data";
            };
          };

          description = "Contents of chunkserver config file (see {manpage}`sfschunkserver.cfg(5)`).";
        };
      };
    };
  };

  ###### implementation

  config =
    lib.mkIf (cfg.client.enable || cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable)
      {

        warnings = [
          (lib.mkIf (cfg.sfsUser == "root") "Running saunafs services as root is not recommended.")
        ];

        # Service settings
        services.saunafs = {
          master.settings = lib.mkIf cfg.master.enable {
            WORKING_USER = cfg.sfsUser;
            EXPORTS_FILENAME = toString (
              pkgs.writeText "sfsexports.cfg" (lib.concatStringsSep "\n" cfg.master.exports)
            );
          };

          metalogger.settings = lib.mkIf cfg.metalogger.enable {
            WORKING_USER = cfg.sfsUser;
            MASTER_HOST = cfg.masterHost;
          };

          chunkserver.settings = lib.mkIf cfg.chunkserver.enable {
            WORKING_USER = cfg.sfsUser;
            MASTER_HOST = cfg.masterHost;
            HDD_CONF_FILENAME = toString (
              pkgs.writeText "sfshdd.cfg" (lib.concatStringsSep "\n" cfg.chunkserver.hdds)
            );
          };
        };

        # Create system user account for daemons
        users =
          lib.mkIf
            (cfg.sfsUser != "root" && (cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable))
            {
              users."${cfg.sfsUser}" = {
                isSystemUser = true;
                description = "saunafs daemon user";
                group = "saunafs";
              };
              groups."${cfg.sfsUser}" = { };
            };

        environment.systemPackages =
          (lib.optional cfg.client.enable pkgs.saunafs) ++ (lib.optional cfg.master.enable initTool);

        networking.firewall.allowedTCPPorts =
          (lib.optionals cfg.master.openFirewall [
            9419
            9420
            9421
          ])
          ++ (lib.optional cfg.chunkserver.openFirewall 9422);

        # Ensure storage directories exist
        systemd.tmpfiles.rules =
          lib.optional cfg.master.enable "d ${cfg.master.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -"
          ++ lib.optional cfg.metalogger.enable "d ${cfg.metalogger.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -"
          ++ lib.optional cfg.chunkserver.enable "d ${cfg.chunkserver.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -";

        # Service definitions
        systemd.services.sfs-master = lib.mkIf cfg.master.enable (
          systemdService "master" {
            TimeoutStartSec = 1800;
            TimeoutStopSec = 1800;
            Restart = "no";
          } masterCfg
        );

        systemd.services.sfs-metalogger = lib.mkIf cfg.metalogger.enable (
          systemdService "metalogger" { Restart = "on-abort"; } metaloggerCfg
        );

        systemd.services.sfs-chunkserver = lib.mkIf cfg.chunkserver.enable (
          systemdService "chunkserver" { Restart = "on-abort"; } chunkserverCfg
        );
      };
}
+1 −0
Original line number Diff line number Diff line
@@ -889,6 +889,7 @@ in {
  samba-wsdd = handleTest ./samba-wsdd.nix {};
  sane = handleTest ./sane.nix {};
  sanoid = handleTest ./sanoid.nix {};
  saunafs = handleTest ./saunafs.nix {};
  scaphandre = handleTest ./scaphandre.nix {};
  schleuder = handleTest ./schleuder.nix {};
  scion-freestanding-deployment = handleTest ./scion/freestanding-deployment {};
+122 −0
Original line number Diff line number Diff line
import ./make-test-python.nix (
  { pkgs, lib, ... }:

  let
    master =
      { pkgs, ... }:
      {
        # data base is stored in memory
        # server may crash with default memory size
        virtualisation.memorySize = 1024;

        services.saunafs.master = {
          enable = true;
          openFirewall = true;
          exports = [
            "* / rw,alldirs,maproot=0:0"
          ];
        };
      };

    chunkserver =
      { pkgs, ... }:
      {
        virtualisation.emptyDiskImages = [ 4096 ];
        boot.initrd.postDeviceCommands = ''
          ${pkgs.e2fsprogs}/bin/mkfs.ext4 -L data /dev/vdb
        '';

        fileSystems = pkgs.lib.mkVMOverride {
          "/data" = {
            device = "/dev/disk/by-label/data";
            fsType = "ext4";
          };
        };

        services.saunafs = {
          masterHost = "master";
          chunkserver = {
            openFirewall = true;
            enable = true;
            hdds = [ "/data" ];

            # The test image is too small and gets set to "full"
            settings.HDD_LEAVE_SPACE_DEFAULT = "100M";
          };
        };
      };

    metalogger =
      { pkgs, ... }:
      {
        services.saunafs = {
          masterHost = "master";
          metalogger.enable = true;
        };
      };

    client =
      { pkgs, lib, ... }:
      {
        services.saunafs.client.enable = true;
        # systemd.tmpfiles.rules = [ "d /sfs 755 root root -" ];
        systemd.network.enable = true;

        # Use networkd to have properly functioning
        # network-online.target
        networking = {
          useDHCP = false;
          useNetworkd = true;
        };

        systemd.mounts = [
          {
            requires = [ "network-online.target" ];
            after = [ "network-online.target" ];
            wantedBy = [ "remote-fs.target" ];
            type = "saunafs";
            what = "master:/";
            where = "/sfs";
          }
        ];
      };

  in
  {
    name = "saunafs";

    meta.maintainers = [ lib.maintainers.markuskowa ];

    nodes = {
      inherit master metalogger;
      chunkserver1 = chunkserver;
      chunkserver2 = chunkserver;
      client1 = client;
      client2 = client;
    };

    testScript = ''
      # prepare master server
      master.start()
      master.wait_for_unit("multi-user.target")
      master.succeed("sfsmaster-init")
      master.succeed("systemctl restart sfs-master")
      master.wait_for_unit("sfs-master.service")

      metalogger.wait_for_unit("sfs-metalogger.service")

      # Setup chunkservers
      for chunkserver in [chunkserver1, chunkserver2]:
          chunkserver.wait_for_unit("multi-user.target")
          chunkserver.succeed("chown saunafs:saunafs /data")
          chunkserver.succeed("systemctl restart sfs-chunkserver")
          chunkserver.wait_for_unit("sfs-chunkserver.service")

      for client in [client1, client2]:
          client.wait_for_unit("multi-user.target")

      client1.succeed("echo test > /sfs/file")
      client2.succeed("grep test /sfs/file")
    '';
  }
)