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 }
84 // extraConfig;
85 };
86
87in
88{
89 ###### interface
90
91 options = {
92 services.saunafs = {
93 masterHost = lib.mkOption {
94 type = lib.types.str;
95 default = null;
96 description = "IP or hostname name of master host.";
97 };
98
99 sfsUser = lib.mkOption {
100 type = lib.types.str;
101 default = "saunafs";
102 description = "Run daemons as user.";
103 };
104
105 client.enable = lib.mkEnableOption "Saunafs client";
106
107 master = {
108 enable = lib.mkOption {
109 type = lib.types.bool;
110 description = ''
111 Enable Saunafs master daemon.
112
113 You need to run `sfsmaster-init` on a freshly installed master server to
114 initialize the `DATA_PATH` directory.
115 '';
116 default = false;
117 };
118
119 exports = lib.mkOption {
120 type = with lib.types; listOf str;
121 default = null;
122 description = "Paths to exports file (see {manpage}`sfsexports.cfg(5)`).";
123 example = lib.literalExpression ''
124 [ "* / rw,alldirs,admin,maproot=0:0" ];
125 '';
126 };
127
128 openFirewall = lib.mkOption {
129 type = lib.types.bool;
130 description = "Whether to automatically open the necessary ports in the firewall.";
131 default = false;
132 };
133
134 settings = lib.mkOption {
135 type = lib.types.submodule {
136 freeformType = settingsFormat.type;
137
138 options.DATA_PATH = lib.mkOption {
139 type = lib.types.str;
140 default = "/var/lib/saunafs/master";
141 description = "Data storage directory.";
142 };
143 };
144
145 description = "Contents of config file ({manpage}`sfsmaster.cfg(5)`).";
146 };
147 };
148
149 metalogger = {
150 enable = lib.mkEnableOption "Saunafs metalogger daemon";
151
152 settings = lib.mkOption {
153 type = lib.types.submodule {
154 freeformType = settingsFormat.type;
155
156 options.DATA_PATH = lib.mkOption {
157 type = lib.types.str;
158 default = "/var/lib/saunafs/metalogger";
159 description = "Data storage directory";
160 };
161 };
162
163 description = "Contents of metalogger config file (see {manpage}`sfsmetalogger.cfg(5)`).";
164 };
165 };
166
167 chunkserver = {
168 enable = lib.mkEnableOption "Saunafs chunkserver daemon";
169
170 openFirewall = lib.mkOption {
171 type = lib.types.bool;
172 description = "Whether to automatically open the necessary ports in the firewall.";
173 default = false;
174 };
175
176 hdds = lib.mkOption {
177 type = with lib.types; listOf str;
178 default = null;
179
180 example = lib.literalExpression ''
181 [ "/mnt/hdd1" ];
182 '';
183
184 description = ''
185 Mount points to be used by chunkserver for storage (see {manpage}`sfshdd.cfg(5)`).
186
187 Note, that these mount points must writeable by the user defined by the saunafs user.
188 '';
189 };
190
191 settings = lib.mkOption {
192 type = lib.types.submodule {
193 freeformType = settingsFormat.type;
194
195 options.DATA_PATH = lib.mkOption {
196 type = lib.types.str;
197 default = "/var/lib/saunafs/chunkserver";
198 description = "Directory for chunck meta data";
199 };
200 };
201
202 description = "Contents of chunkserver config file (see {manpage}`sfschunkserver.cfg(5)`).";
203 };
204 };
205 };
206 };
207
208 ###### implementation
209
210 config =
211 lib.mkIf (cfg.client.enable || cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable)
212 {
213
214 warnings = [
215 (lib.mkIf (cfg.sfsUser == "root") "Running saunafs services as root is not recommended.")
216 ];
217
218 # Service settings
219 services.saunafs = {
220 master.settings = lib.mkIf cfg.master.enable {
221 WORKING_USER = cfg.sfsUser;
222 EXPORTS_FILENAME = toString (
223 pkgs.writeText "sfsexports.cfg" (lib.concatStringsSep "\n" cfg.master.exports)
224 );
225 };
226
227 metalogger.settings = lib.mkIf cfg.metalogger.enable {
228 WORKING_USER = cfg.sfsUser;
229 MASTER_HOST = cfg.masterHost;
230 };
231
232 chunkserver.settings = lib.mkIf cfg.chunkserver.enable {
233 WORKING_USER = cfg.sfsUser;
234 MASTER_HOST = cfg.masterHost;
235 HDD_CONF_FILENAME = toString (
236 pkgs.writeText "sfshdd.cfg" (lib.concatStringsSep "\n" cfg.chunkserver.hdds)
237 );
238 };
239 };
240
241 # Create system user account for daemons
242 users =
243 lib.mkIf
244 (cfg.sfsUser != "root" && (cfg.master.enable || cfg.metalogger.enable || cfg.chunkserver.enable))
245 {
246 users."${cfg.sfsUser}" = {
247 isSystemUser = true;
248 description = "saunafs daemon user";
249 group = "saunafs";
250 };
251 groups."${cfg.sfsUser}" = { };
252 };
253
254 environment.systemPackages =
255 (lib.optional cfg.client.enable pkgs.saunafs) ++ (lib.optional cfg.master.enable initTool);
256
257 networking.firewall.allowedTCPPorts =
258 (lib.optionals cfg.master.openFirewall [
259 9419
260 9420
261 9421
262 ])
263 ++ (lib.optional cfg.chunkserver.openFirewall 9422);
264
265 # Ensure storage directories exist
266 systemd.tmpfiles.rules =
267 lib.optional cfg.master.enable "d ${cfg.master.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -"
268 ++ lib.optional cfg.metalogger.enable "d ${cfg.metalogger.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -"
269 ++ lib.optional cfg.chunkserver.enable "d ${cfg.chunkserver.settings.DATA_PATH} 0700 ${cfg.sfsUser} ${cfg.sfsUser} -";
270
271 # Service definitions
272 systemd.services.sfs-master = lib.mkIf cfg.master.enable (
273 systemdService "master" {
274 TimeoutStartSec = 1800;
275 TimeoutStopSec = 1800;
276 Restart = "no";
277 } masterCfg
278 );
279
280 systemd.services.sfs-metalogger = lib.mkIf cfg.metalogger.enable (
281 systemdService "metalogger" { Restart = "on-abort"; } metaloggerCfg
282 );
283
284 systemd.services.sfs-chunkserver = lib.mkIf cfg.chunkserver.enable (
285 systemdService "chunkserver" { Restart = "on-abort"; } chunkserverCfg
286 );
287 };
288}