Skip to content

Commit 812640f

Browse files
authored
nixos/saunafs: add module + test (#347337)
2 parents 0d7e212 + 2d5bae6 commit 812640f

File tree

6 files changed

+418
-0
lines changed

6 files changed

+418
-0
lines changed

nixos/doc/manual/release-notes/rl-2411.section.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,6 +175,8 @@
175175

176176
- [Immich](https://github.com/immich-app/immich), a self-hosted photo and video backup solution. Available as [services.immich](#opt-services.immich.enable).
177177

178+
- [saunafs](https://saunafs.com) Distributed POSIX file system. Available as [services.saunafs](options.html#opt-services.saunafs).
179+
178180
- [obs-studio](https://obsproject.com/), Free and open source software for video recording and live streaming. Available as [programs.obs-studio.enable](#opt-programs.obs-studio.enable).
179181

180182
- [Veilid](https://veilid.com), a headless server that enables privacy-focused data sharing and messaging on a peer-to-peer network. Available as [services.veilid](#opt-services.veilid.enable).

nixos/modules/module-list.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -965,6 +965,7 @@
965965
./services/network-filesystems/rsyncd.nix
966966
./services/network-filesystems/samba-wsdd.nix
967967
./services/network-filesystems/samba.nix
968+
./services/network-filesystems/saunafs.nix
968969
./services/network-filesystems/tahoe.nix
969970
./services/network-filesystems/u9fs.nix
970971
./services/network-filesystems/webdav-server-rs.nix
Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
{
2+
config,
3+
lib,
4+
pkgs,
5+
...
6+
}:
7+
8+
let
9+
cfg = config.services.saunafs;
10+
11+
settingsFormat =
12+
let
13+
listSep = " ";
14+
allowedTypes = with lib.types; [
15+
bool
16+
int
17+
float
18+
str
19+
];
20+
valueToString =
21+
val:
22+
if lib.isList val then
23+
lib.concatStringsSep listSep (map (x: valueToString x) val)
24+
else if lib.isBool val then
25+
(if val then "1" else "0")
26+
else
27+
toString val;
28+
29+
in
30+
{
31+
type =
32+
let
33+
valueType =
34+
lib.types.oneOf (
35+
[
36+
(lib.types.listOf valueType)
37+
]
38+
++ allowedTypes
39+
)
40+
// {
41+
description = "Flat key-value file";
42+
};
43+
in
44+
lib.types.attrsOf valueType;
45+
46+
generate =
47+
name: value:
48+
pkgs.writeText name (
49+
lib.concatStringsSep "\n" (lib.mapAttrsToList (key: val: "${key} = ${valueToString val}") value)
50+
);
51+
};
52+
53+
initTool = pkgs.writeShellScriptBin "sfsmaster-init" ''
54+
if [ ! -e ${cfg.master.settings.DATA_PATH}/metadata.sfs ]; then
55+
cp --update=none ${pkgs.saunafs}/var/lib/saunafs/metadata.sfs.empty ${cfg.master.settings.DATA_PATH}/metadata.sfs
56+
chmod +w ${cfg.master.settings.DATA_PATH}/metadata.sfs
57+
fi
58+
'';
59+
60+
# master config file
61+
masterCfg = settingsFormat.generate "sfsmaster.cfg" cfg.master.settings;
62+
63+
# metalogger config file
64+
metaloggerCfg = settingsFormat.generate "sfsmetalogger.cfg" cfg.metalogger.settings;
65+
66+
# chunkserver config file
67+
chunkserverCfg = settingsFormat.generate "sfschunkserver.cfg" cfg.chunkserver.settings;
68+
69+
# generic template for all daemons
70+
systemdService = name: extraConfig: configFile: {
71+
wantedBy = [ "multi-user.target" ];
72+
wants = [ "network-online.target" ];
73+
after = [
74+
"network.target"
75+
"network-online.target"
76+
];
77+
78+
serviceConfig = {
79+
Type = "forking";
80+
ExecStart = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} start";
81+
ExecStop = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} stop";
82+
ExecReload = "${pkgs.saunafs}/bin/sfs${name} -c ${configFile} reload";
83+
} // extraConfig;
84+
};
85+
86+
in
87+
{
88+
###### interface
89+
90+
options = {
91+
services.saunafs = {
92+
masterHost = lib.mkOption {
93+
type = lib.types.str;
94+
default = null;
95+
description = "IP or hostname name of master host.";
96+
};
97+
98+
sfsUser = lib.mkOption {
99+
type = lib.types.str;
100+
default = "saunafs";
101+
description = "Run daemons as user.";
102+
};
103+
104+
client.enable = lib.mkEnableOption "Saunafs client";
105+
106+
master = {
107+
enable = lib.mkOption {
108+
type = lib.types.bool;
109+
description = ''
110+
Enable Saunafs master daemon.
111+
112+
You need to run `sfsmaster-init` on a freshly installed master server to
113+
initialize the `DATA_PATH` directory.
114+
'';
115+
default = false;
116+
};
117+
118+
exports = lib.mkOption {
119+
type = with lib.types; listOf str;
120+
default = null;
121+
description = "Paths to exports file (see {manpage}`sfsexports.cfg(5)`).";
122+
example = lib.literalExpression ''
123+
[ "* / rw,alldirs,admin,maproot=0:0" ];
124+
'';
125+
};
126+
127+
openFirewall = lib.mkOption {
128+
type = lib.types.bool;
129+
description = "Whether to automatically open the necessary ports in the firewall.";
130+
default = false;
131+
};
132+
133+
settings = lib.mkOption {
134+
type = lib.types.submodule {
135+
freeformType = settingsFormat.type;
136+
137+
options.DATA_PATH = lib.mkOption {
138+
type = lib.types.str;
139+
default = "/var/lib/saunafs/master";
140+
description = "Data storage directory.";
141+
};
142+
};
143+
144+
description = "Contents of config file ({manpage}`sfsmaster.cfg(5)`).";
145+
};
146+
};
147+
148+
metalogger = {
149+
enable = lib.mkEnableOption "Saunafs metalogger daemon";
150+
151+
settings = lib.mkOption {
152+
type = lib.types.submodule {
153+
freeformType = settingsFormat.type;
154+
155+
options.DATA_PATH = lib.mkOption {
156+
type = lib.types.str;
157+
default = "/var/lib/saunafs/metalogger";
158+
description = "Data storage directory";
159+
};
160+
};
161+
162+
description = "Contents of metalogger config file (see {manpage}`sfsmetalogger.cfg(5)`).";
163+
};
164+
};
165+
166+
chunkserver = {
167+
enable = lib.mkEnableOption "Saunafs chunkserver daemon";
168+
169+
openFirewall = lib.mkOption {
170+
type = lib.types.bool;
171+
description = "Whether to automatically open the necessary ports in the firewall.";
172+
default = false;
173+
};
174+
175+
hdds = lib.mkOption {
176+
type = with lib.types; listOf str;
177+
default = null;
178+
179+
example = lib.literalExpression ''
180+
[ "/mnt/hdd1" ];
181+
'';
182+
183+
description = ''
184+
Mount points to be used by chunkserver for storage (see {manpage}`sfshdd.cfg(5)`).
185+
186+
Note, that these mount points must writeable by the user defined by the saunafs user.
187+
'';
188+
};
189+
190+
settings = lib.mkOption {
191+
type = lib.types.submodule {
192+
freeformType = settingsFormat.type;
193+
194+
options.DATA_PATH = lib.mkOption {
195+
type = lib.types.str;
196+
default = "/var/lib/saunafs/chunkserver";
197+
description = "Directory for chunck meta data";
198+
};
199+
};
200+
201+
description = "Contents of chunkserver config file (see {manpage}`sfschunkserver.cfg(5)`).";
202+
};
203+
};
204+
};
205+
};
206+
207+
###### implementation
208+
209+
config =
210+
lib.mkIf (cfg.client.enable || cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable)
211+
{
212+
213+
warnings = [
214+
(lib.mkIf (cfg.sfsUser == "root") "Running saunafs services as root is not recommended.")
215+
];
216+
217+
# Service settings
218+
services.saunafs = {
219+
master.settings = lib.mkIf cfg.master.enable {
220+
WORKING_USER = cfg.sfsUser;
221+
EXPORTS_FILENAME = toString (
222+
pkgs.writeText "sfsexports.cfg" (lib.concatStringsSep "\n" cfg.master.exports)
223+
);
224+
};
225+
226+
metalogger.settings = lib.mkIf cfg.metalogger.enable {
227+
WORKING_USER = cfg.sfsUser;
228+
MASTER_HOST = cfg.masterHost;
229+
};
230+
231+
chunkserver.settings = lib.mkIf cfg.chunkserver.enable {
232+
WORKING_USER = cfg.sfsUser;
233+
MASTER_HOST = cfg.masterHost;
234+
HDD_CONF_FILENAME = toString (
235+
pkgs.writeText "sfshdd.cfg" (lib.concatStringsSep "\n" cfg.chunkserver.hdds)
236+
);
237+
};
238+
};
239+
240+
# Create system user account for daemons
241+
users =
242+
lib.mkIf
243+
(cfg.sfsUser != "root" && (cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable))
244+
{
245+
users."${cfg.sfsUser}" = {
246+
isSystemUser = true;
247+
description = "saunafs daemon user";
248+
group = "saunafs";
249+
};
250+
groups."${cfg.sfsUser}" = { };
251+
};
252+
253+
environment.systemPackages =
254+
(lib.optional cfg.client.enable pkgs.saunafs) ++ (lib.optional cfg.master.enable initTool);
255+
256+
networking.firewall.allowedTCPPorts =
257+
(lib.optionals cfg.master.openFirewall [
258+
9419
259+
9420
260+
9421
261+
])
262+
++ (lib.optional cfg.chunkserver.openFirewall 9422);
263+
264+
# Ensure storage directories exist
265+
systemd.tmpfiles.rules =
266+
lib.optional cfg.master.enable "d ${cfg.master.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -"
267+
++ lib.optional cfg.metalogger.enable "d ${cfg.metalogger.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -"
268+
++ lib.optional cfg.chunkserver.enable "d ${cfg.chunkserver.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -";
269+
270+
# Service definitions
271+
systemd.services.sfs-master = lib.mkIf cfg.master.enable (
272+
systemdService "master" {
273+
TimeoutStartSec = 1800;
274+
TimeoutStopSec = 1800;
275+
Restart = "no";
276+
} masterCfg
277+
);
278+
279+
systemd.services.sfs-metalogger = lib.mkIf cfg.metalogger.enable (
280+
systemdService "metalogger" { Restart = "on-abort"; } metaloggerCfg
281+
);
282+
283+
systemd.services.sfs-chunkserver = lib.mkIf cfg.chunkserver.enable (
284+
systemdService "chunkserver" { Restart = "on-abort"; } chunkserverCfg
285+
);
286+
};
287+
}

nixos/tests/all-tests.nix

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,7 @@ in {
889889
samba-wsdd = handleTest ./samba-wsdd.nix {};
890890
sane = handleTest ./sane.nix {};
891891
sanoid = handleTest ./sanoid.nix {};
892+
saunafs = handleTest ./saunafs.nix {};
892893
scaphandre = handleTest ./scaphandre.nix {};
893894
schleuder = handleTest ./schleuder.nix {};
894895
scion-freestanding-deployment = handleTest ./scion/freestanding-deployment {};

0 commit comments

Comments
 (0)