1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
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
86in
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}