1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.foundationdb;
7 pkg = cfg.package;
8
9 # used for initial cluster configuration
10 initialIpAddr = if (cfg.publicAddress != "auto") then cfg.publicAddress else "127.0.0.1";
11
12 fdbServers = n:
13 concatStringsSep "\n" (map (x: "[fdbserver.${toString (x+cfg.listenPortStart)}]") (range 0 (n - 1)));
14
15 backupAgents = n:
16 concatStringsSep "\n" (map (x: "[backup_agent.${toString x}]") (range 1 n));
17
18 configFile = pkgs.writeText "foundationdb.conf" ''
19 [general]
20 cluster_file = /etc/foundationdb/fdb.cluster
21
22 [fdbmonitor]
23 restart_delay = ${toString cfg.restartDelay}
24 user = ${cfg.user}
25 group = ${cfg.group}
26
27 [fdbserver]
28 command = ${pkg}/bin/fdbserver
29 public_address = ${cfg.publicAddress}:$ID
30 listen_address = ${cfg.listenAddress}
31 datadir = ${cfg.dataDir}/$ID
32 logdir = ${cfg.logDir}
33 logsize = ${cfg.logSize}
34 maxlogssize = ${cfg.maxLogSize}
35 ${optionalString (cfg.class != null) "class = ${cfg.class}"}
36 memory = ${cfg.memory}
37 storage_memory = ${cfg.storageMemory}
38
39 ${optionalString (cfg.tls != null) ''
40 tls_plugin = ${pkg}/libexec/plugins/FDBLibTLS.so
41 tls_certificate_file = ${cfg.tls.certificate}
42 tls_key_file = ${cfg.tls.key}
43 tls_verify_peers = ${cfg.tls.allowedPeers}
44 ''}
45
46 ${optionalString (cfg.locality.machineId != null) "locality_machineid=${cfg.locality.machineId}"}
47 ${optionalString (cfg.locality.zoneId != null) "locality_zoneid=${cfg.locality.zoneId}"}
48 ${optionalString (cfg.locality.datacenterId != null) "locality_dcid=${cfg.locality.datacenterId}"}
49 ${optionalString (cfg.locality.dataHall != null) "locality_data_hall=${cfg.locality.dataHall}"}
50
51 ${fdbServers cfg.serverProcesses}
52
53 [backup_agent]
54 command = ${pkg}/libexec/backup_agent
55 ${backupAgents cfg.backupProcesses}
56 '';
57in
58{
59 options.services.foundationdb = {
60
61 enable = mkEnableOption "FoundationDB Server";
62
63 package = mkOption {
64 type = types.package;
65 description = ''
66 The FoundationDB package to use for this server. This must be specified by the user
67 in order to ensure migrations and upgrades are controlled appropriately.
68 '';
69 };
70
71 publicAddress = mkOption {
72 type = types.str;
73 default = "auto";
74 description = "Publicly visible IP address of the process. Port is determined by process ID";
75 };
76
77 listenAddress = mkOption {
78 type = types.str;
79 default = "public";
80 description = "Publicly visible IP address of the process. Port is determined by process ID";
81 };
82
83 listenPortStart = mkOption {
84 type = types.int;
85 default = 4500;
86 description = ''
87 Starting port number for database listening sockets. Every FDB process binds to a
88 subsequent port, to this number reflects the start of the overall range. e.g. having
89 8 server processes will use all ports between 4500 and 4507.
90 '';
91 };
92
93 openFirewall = mkOption {
94 type = types.bool;
95 default = false;
96 description = ''
97 Open the firewall ports corresponding to FoundationDB processes and coordinators
98 using <option>config.networking.firewall.*</option>.
99 '';
100 };
101
102 dataDir = mkOption {
103 type = types.path;
104 default = "/var/lib/foundationdb";
105 description = "Data directory. All cluster data will be put under here.";
106 };
107
108 logDir = mkOption {
109 type = types.path;
110 default = "/var/log/foundationdb";
111 description = "Log directory.";
112 };
113
114 user = mkOption {
115 type = types.str;
116 default = "foundationdb";
117 description = "User account under which FoundationDB runs.";
118 };
119
120 group = mkOption {
121 type = types.str;
122 default = "foundationdb";
123 description = "Group account under which FoundationDB runs.";
124 };
125
126 class = mkOption {
127 type = types.nullOr (types.enum [ "storage" "transaction" "stateless" ]);
128 default = null;
129 description = "Process class";
130 };
131
132 restartDelay = mkOption {
133 type = types.int;
134 default = 10;
135 description = "Number of seconds to wait before restarting servers.";
136 };
137
138 logSize = mkOption {
139 type = types.string;
140 default = "10MiB";
141 description = ''
142 Roll over to a new log file after the current log file
143 reaches the specified size.
144 '';
145 };
146
147 maxLogSize = mkOption {
148 type = types.string;
149 default = "100MiB";
150 description = ''
151 Delete the oldest log file when the total size of all log
152 files exceeds the specified size. If set to 0, old log files
153 will not be deleted.
154 '';
155 };
156
157 serverProcesses = mkOption {
158 type = types.int;
159 default = 1;
160 description = "Number of fdbserver processes to run.";
161 };
162
163 backupProcesses = mkOption {
164 type = types.int;
165 default = 1;
166 description = "Number of backup_agent processes to run for snapshots.";
167 };
168
169 memory = mkOption {
170 type = types.string;
171 default = "8GiB";
172 description = ''
173 Maximum memory used by the process. The default value is
174 <literal>8GiB</literal>. When specified without a unit,
175 <literal>MiB</literal> is assumed. This parameter does not
176 change the memory allocation of the program. Rather, it sets
177 a hard limit beyond which the process will kill itself and
178 be restarted. The default value of <literal>8GiB</literal>
179 is double the intended memory usage in the default
180 configuration (providing an emergency buffer to deal with
181 memory leaks or similar problems). It is not recommended to
182 decrease the value of this parameter below its default
183 value. It may be increased if you wish to allocate a very
184 large amount of storage engine memory or cache. In
185 particular, when the <literal>storageMemory</literal>
186 parameter is increased, the <literal>memory</literal>
187 parameter should be increased by an equal amount.
188 '';
189 };
190
191 storageMemory = mkOption {
192 type = types.string;
193 default = "1GiB";
194 description = ''
195 Maximum memory used for data storage. The default value is
196 <literal>1GiB</literal>. When specified without a unit,
197 <literal>MB</literal> is assumed. Clusters using the memory
198 storage engine will be restricted to using this amount of
199 memory per process for purposes of data storage. Memory
200 overhead associated with storing the data is counted against
201 this total. If you increase the
202 <literal>storageMemory</literal>, you should also increase
203 the <literal>memory</literal> parameter by the same amount.
204 '';
205 };
206
207 tls = mkOption {
208 default = null;
209 description = ''
210 FoundationDB Transport Security Layer (TLS) settings.
211 '';
212
213 type = types.nullOr (types.submodule ({
214 options = {
215 certificate = mkOption {
216 type = types.str;
217 description = ''
218 Path to the TLS certificate file. This certificate will
219 be offered to, and may be verified by, clients.
220 '';
221 };
222
223 key = mkOption {
224 type = types.str;
225 description = "Private key file for the certificate.";
226 };
227
228 allowedPeers = mkOption {
229 type = types.str;
230 default = "Check.Valid=1,Check.Unexpired=1";
231 description = ''
232 "Peer verification string". This may be used to adjust which TLS
233 client certificates a server will accept, as a form of user
234 authorization; for example, it may only accept TLS clients who
235 offer a certificate abiding by some locality or organization name.
236
237 For more information, please see the FoundationDB documentation.
238 '';
239 };
240 };
241 }));
242 };
243
244 locality = mkOption {
245 default = {
246 machineId = null;
247 zoneId = null;
248 datacenterId = null;
249 dataHall = null;
250 };
251
252 description = ''
253 FoundationDB locality settings.
254 '';
255
256 type = types.submodule ({
257 options = {
258 machineId = mkOption {
259 default = null;
260 type = types.nullOr types.str;
261 description = ''
262 Machine identifier key. All processes on a machine should share a
263 unique id. By default, processes on a machine determine a unique id to share.
264 This does not generally need to be set.
265 '';
266 };
267
268 zoneId = mkOption {
269 default = null;
270 type = types.nullOr types.str;
271 description = ''
272 Zone identifier key. Processes that share a zone id are
273 considered non-unique for the purposes of data replication.
274 If unset, defaults to machine id.
275 '';
276 };
277
278 datacenterId = mkOption {
279 default = null;
280 type = types.nullOr types.str;
281 description = ''
282 Data center identifier key. All processes physically located in a
283 data center should share the id. If you are depending on data
284 center based replication this must be set on all processes.
285 '';
286 };
287
288 dataHall = mkOption {
289 default = null;
290 type = types.nullOr types.str;
291 description = ''
292 Data hall identifier key. All processes physically located in a
293 data hall should share the id. If you are depending on data
294 hall based replication this must be set on all processes.
295 '';
296 };
297 };
298 });
299 };
300
301 extraReadWritePaths = mkOption {
302 default = [ ];
303 type = types.listOf types.path;
304 description = ''
305 An extra set of filesystem paths that FoundationDB can read to
306 and write from. By default, FoundationDB runs under a heavily
307 namespaced systemd environment without write access to most of
308 the filesystem outside of its data and log directories. By
309 adding paths to this list, the set of writeable paths will be
310 expanded. This is useful for allowing e.g. backups to local files,
311 which must be performed on behalf of the foundationdb service.
312 '';
313 };
314
315 pidfile = mkOption {
316 type = types.path;
317 default = "/run/foundationdb.pid";
318 description = "Path to pidfile for fdbmonitor.";
319 };
320 };
321
322 config = mkIf cfg.enable {
323 environment.systemPackages = [ pkg ];
324
325 users.users = optionalAttrs (cfg.user == "foundationdb") (singleton
326 { name = "foundationdb";
327 description = "FoundationDB User";
328 uid = config.ids.uids.foundationdb;
329 group = cfg.group;
330 });
331
332 users.groups = optionalAttrs (cfg.group == "foundationdb") (singleton
333 { name = "foundationdb";
334 gid = config.ids.gids.foundationdb;
335 });
336
337 networking.firewall.allowedTCPPortRanges = mkIf cfg.openFirewall
338 [ { from = cfg.listenPortStart;
339 to = (cfg.listenPortStart + cfg.serverProcesses) - 1;
340 }
341 ];
342
343 systemd.services.foundationdb = {
344 description = "FoundationDB Service";
345
346 after = [ "network.target" ];
347 wantedBy = [ "multi-user.target" ];
348 unitConfig =
349 { RequiresMountsFor = "${cfg.dataDir} ${cfg.logDir}";
350 };
351
352 serviceConfig =
353 let rwpaths = [ cfg.dataDir cfg.logDir cfg.pidfile "/etc/foundationdb" ]
354 ++ cfg.extraReadWritePaths;
355 in
356 { Type = "simple";
357 Restart = "always";
358 RestartSec = 5;
359 User = cfg.user;
360 Group = cfg.group;
361 PIDFile = "${cfg.pidfile}";
362
363 PermissionsStartOnly = true; # setup needs root perms
364 TimeoutSec = 120; # give reasonable time to shut down
365
366 # Security options
367 NoNewPrivileges = true;
368 ProtectHome = true;
369 ProtectSystem = "strict";
370 ProtectKernelTunables = true;
371 ProtectControlGroups = true;
372 PrivateTmp = true;
373 PrivateDevices = true;
374 ReadWritePaths = lib.concatStringsSep " " (map (x: "-" + x) rwpaths);
375 };
376
377 path = [ pkg pkgs.coreutils ];
378
379 preStart = ''
380 rm -f ${cfg.pidfile} && \
381 touch ${cfg.pidfile} && \
382 chown -R ${cfg.user}:${cfg.group} ${cfg.pidfile}
383
384 for x in "${cfg.logDir}" "${cfg.dataDir}"; do
385 [ ! -d "$x" ] && mkdir -m 0700 -vp "$x";
386 chown -R ${cfg.user}:${cfg.group} "$x";
387 done
388
389 [ ! -d /etc/foundationdb ] && \
390 mkdir -m 0775 -vp /etc/foundationdb && \
391 chown -R ${cfg.user}:${cfg.group} "/etc/foundationdb"
392
393 if [ ! -f /etc/foundationdb/fdb.cluster ]; then
394 cf=/etc/foundationdb/fdb.cluster
395 desc=$(tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c8)
396 rand=$(tr -dc A-Za-z0-9 </dev/urandom 2>/dev/null | head -c8)
397 echo ''${desc}:''${rand}@${initialIpAddr}:${builtins.toString cfg.listenPortStart} > $cf
398 chmod 0664 $cf && chown -R ${cfg.user}:${cfg.group} $cf
399 touch "${cfg.dataDir}/.first_startup"
400 fi
401 '';
402
403 script = "exec fdbmonitor --lockfile ${cfg.pidfile} --conffile ${configFile}";
404
405 postStart = ''
406 if [ -e "${cfg.dataDir}/.first_startup" ]; then
407 fdbcli --exec "configure new single memory"
408 rm -f "${cfg.dataDir}/.first_startup";
409 fi
410 '';
411 };
412 };
413
414 meta.doc = ./foundationdb.xml;
415 meta.maintainers = with lib.maintainers; [ thoughtpolice ];
416}