nixos/saunafs: add module + test

Changed files
+411
nixos
modules
services
network-filesystems
tests
+1
nixos/modules/module-list.nix
···
./services/network-filesystems/rsyncd.nix
./services/network-filesystems/samba-wsdd.nix
./services/network-filesystems/samba.nix
./services/network-filesystems/tahoe.nix
./services/network-filesystems/u9fs.nix
./services/network-filesystems/webdav-server-rs.nix
···
./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
nixos/modules/services/network-filesystems/saunafs.nix
···
···
+
{
+
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
nixos/tests/all-tests.nix
···
samba-wsdd = handleTest ./samba-wsdd.nix {};
sane = handleTest ./sane.nix {};
sanoid = handleTest ./sanoid.nix {};
scaphandre = handleTest ./scaphandre.nix {};
schleuder = handleTest ./schleuder.nix {};
scion-freestanding-deployment = handleTest ./scion/freestanding-deployment {};
···
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
nixos/tests/saunafs.nix
···
···
+
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")
+
'';
+
}
+
)