1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.ceph;
9
10 # function that translates "camelCaseOptions" to "camel case options", credits to tilpner in #nixos@freenode
11 expandCamelCase = lib.replaceStrings lib.upperChars (map (s: " ${s}") lib.lowerChars);
12 expandCamelCaseAttrs = lib.mapAttrs' (name: value: lib.nameValuePair (expandCamelCase name) value);
13
14 makeServices =
15 daemonType: daemonIds:
16 lib.mkMerge (
17 map (daemonId: {
18 "ceph-${daemonType}-${daemonId}" =
19 makeService daemonType daemonId cfg.global.clusterName
20 cfg.${daemonType}.package;
21 }) daemonIds
22 );
23
24 makeService =
25 daemonType: daemonId: clusterName: ceph:
26 let
27 stateDirectory = "ceph/${
28 if daemonType == "rgw" then "radosgw" else daemonType
29 }/${clusterName}-${daemonId}";
30 in
31 {
32 enable = true;
33 description = "Ceph ${
34 builtins.replaceStrings lib.lowerChars lib.upperChars daemonType
35 } daemon ${daemonId}";
36 after = [
37 "network-online.target"
38 "time-sync.target"
39 ] ++ lib.optional (daemonType == "osd") "ceph-mon.target";
40 wants = [
41 "network-online.target"
42 "time-sync.target"
43 ];
44 partOf = [ "ceph-${daemonType}.target" ];
45 wantedBy = [ "ceph-${daemonType}.target" ];
46
47 path = [ pkgs.getopt ];
48
49 # Don't start services that are not yet initialized
50 unitConfig.ConditionPathExists = "/var/lib/${stateDirectory}/keyring";
51 startLimitBurst =
52 if daemonType == "osd" then
53 30
54 else if
55 lib.elem daemonType [
56 "mgr"
57 "mds"
58 ]
59 then
60 3
61 else
62 5;
63 startLimitIntervalSec = 60 * 30; # 30 mins
64
65 serviceConfig =
66 {
67 LimitNOFILE = 1048576;
68 LimitNPROC = 1048576;
69 Environment = "CLUSTER=${clusterName}";
70 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID";
71 PrivateDevices = "yes";
72 PrivateTmp = "true";
73 ProtectHome = "true";
74 ProtectSystem = "full";
75 Restart = "on-failure";
76 StateDirectory = stateDirectory;
77 User = "ceph";
78 Group = if daemonType == "osd" then "disk" else "ceph";
79 ExecStart = ''
80 ${ceph.out}/bin/${if daemonType == "rgw" then "radosgw" else "ceph-${daemonType}"} \
81 -f --cluster ${clusterName} --id ${daemonId}'';
82 }
83 // lib.optionalAttrs (daemonType == "osd") {
84 ExecStartPre = "${ceph.lib}/libexec/ceph/ceph-osd-prestart.sh --id ${daemonId} --cluster ${clusterName}";
85 RestartSec = "20s";
86 PrivateDevices = "no"; # osd needs disk access
87 }
88 // lib.optionalAttrs (daemonType == "mon") {
89 RestartSec = "10";
90 };
91 };
92
93 makeTarget = daemonType: {
94 "ceph-${daemonType}" = {
95 description = "Ceph target allowing to start/stop all ceph-${daemonType} services at once";
96 partOf = [ "ceph.target" ];
97 wantedBy = [ "ceph.target" ];
98 before = [ "ceph.target" ];
99 unitConfig.StopWhenUnneeded = true;
100 };
101 };
102in
103{
104 options.services.ceph = {
105 # Ceph has a monolithic configuration file but different sections for
106 # each daemon, a separate client section and a global section
107 enable = lib.mkEnableOption "Ceph global configuration";
108
109 global = {
110 fsid = lib.mkOption {
111 type = lib.types.str;
112 example = ''
113 433a2193-4f8a-47a0-95d2-209d7ca2cca5
114 '';
115 description = ''
116 Filesystem ID, a generated uuid, its must be generated and set before
117 attempting to start a cluster
118 '';
119 };
120
121 clusterName = lib.mkOption {
122 type = lib.types.str;
123 default = "ceph";
124 description = ''
125 Name of cluster
126 '';
127 };
128
129 mgrModulePath = lib.mkOption {
130 type = lib.types.path;
131 default = "${pkgs.ceph.lib}/lib/ceph/mgr";
132 defaultText = lib.literalExpression ''"''${pkgs.ceph.lib}/lib/ceph/mgr"'';
133 description = ''
134 Path at which to find ceph-mgr modules.
135 '';
136 };
137
138 monInitialMembers = lib.mkOption {
139 type = with lib.types; nullOr commas;
140 default = null;
141 example = ''
142 node0, node1, node2
143 '';
144 description = ''
145 List of hosts that will be used as monitors at startup.
146 '';
147 };
148
149 monHost = lib.mkOption {
150 type = with lib.types; nullOr commas;
151 default = null;
152 example = ''
153 10.10.0.1, 10.10.0.2, 10.10.0.3
154 '';
155 description = ''
156 List of hostname shortnames/IP addresses of the initial monitors.
157 '';
158 };
159
160 maxOpenFiles = lib.mkOption {
161 type = lib.types.int;
162 default = 131072;
163 description = ''
164 Max open files for each OSD daemon.
165 '';
166 };
167
168 authClusterRequired = lib.mkOption {
169 type = lib.types.enum [
170 "cephx"
171 "none"
172 ];
173 default = "cephx";
174 description = ''
175 Enables requiring daemons to authenticate with eachother in the cluster.
176 '';
177 };
178
179 authServiceRequired = lib.mkOption {
180 type = lib.types.enum [
181 "cephx"
182 "none"
183 ];
184 default = "cephx";
185 description = ''
186 Enables requiring clients to authenticate with the cluster to access services in the cluster (e.g. radosgw, mds or osd).
187 '';
188 };
189
190 authClientRequired = lib.mkOption {
191 type = lib.types.enum [
192 "cephx"
193 "none"
194 ];
195 default = "cephx";
196 description = ''
197 Enables requiring the cluster to authenticate itself to the client.
198 '';
199 };
200
201 publicNetwork = lib.mkOption {
202 type = with lib.types; nullOr commas;
203 default = null;
204 example = ''
205 10.20.0.0/24, 192.168.1.0/24
206 '';
207 description = ''
208 A comma-separated list of subnets that will be used as public networks in the cluster.
209 '';
210 };
211
212 clusterNetwork = lib.mkOption {
213 type = with lib.types; nullOr commas;
214 default = null;
215 example = ''
216 10.10.0.0/24, 192.168.0.0/24
217 '';
218 description = ''
219 A comma-separated list of subnets that will be used as cluster networks in the cluster.
220 '';
221 };
222
223 rgwMimeTypesFile = lib.mkOption {
224 type = with lib.types; nullOr path;
225 default = "${pkgs.mailcap}/etc/mime.types";
226 defaultText = lib.literalExpression ''"''${pkgs.mailcap}/etc/mime.types"'';
227 description = ''
228 Path to mime types used by radosgw.
229 '';
230 };
231 };
232
233 extraConfig = lib.mkOption {
234 type = with lib.types; attrsOf str;
235 default = { };
236 example = {
237 "ms bind ipv6" = "true";
238 };
239 description = ''
240 Extra configuration to add to the global section. Use for setting values that are common for all daemons in the cluster.
241 '';
242 };
243
244 mgr = {
245 enable = lib.mkEnableOption "Ceph MGR daemon";
246 daemons = lib.mkOption {
247 type = with lib.types; listOf str;
248 default = [ ];
249 example = [
250 "name1"
251 "name2"
252 ];
253 description = ''
254 A list of names for manager daemons that should have a service created. The names correspond
255 to the id part in ceph i.e. [ "name1" ] would result in mgr.name1
256 '';
257 };
258 package = lib.mkPackageOption pkgs "ceph" { };
259 extraConfig = lib.mkOption {
260 type = with lib.types; attrsOf str;
261 default = { };
262 description = ''
263 Extra configuration to add to the global section for manager daemons.
264 '';
265 };
266 };
267
268 mon = {
269 enable = lib.mkEnableOption "Ceph MON daemon";
270 daemons = lib.mkOption {
271 type = with lib.types; listOf str;
272 default = [ ];
273 example = [
274 "name1"
275 "name2"
276 ];
277 description = ''
278 A list of monitor daemons that should have a service created. The names correspond
279 to the id part in ceph i.e. [ "name1" ] would result in mon.name1
280 '';
281 };
282 package = lib.mkPackageOption pkgs "ceph" { };
283 extraConfig = lib.mkOption {
284 type = with lib.types; attrsOf str;
285 default = { };
286 description = ''
287 Extra configuration to add to the monitor section.
288 '';
289 };
290 };
291
292 osd = {
293 enable = lib.mkEnableOption "Ceph OSD daemon";
294 daemons = lib.mkOption {
295 type = with lib.types; listOf str;
296 default = [ ];
297 example = [
298 "name1"
299 "name2"
300 ];
301 description = ''
302 A list of OSD daemons that should have a service created. The names correspond
303 to the id part in ceph i.e. [ "name1" ] would result in osd.name1
304 '';
305 };
306 package = lib.mkPackageOption pkgs "ceph" { };
307 extraConfig = lib.mkOption {
308 type = with lib.types; attrsOf str;
309 default = {
310 "osd journal size" = "10000";
311 "osd pool default size" = "3";
312 "osd pool default min size" = "2";
313 "osd pool default pg num" = "200";
314 "osd pool default pgp num" = "200";
315 "osd crush chooseleaf type" = "1";
316 };
317 description = ''
318 Extra configuration to add to the OSD section.
319 '';
320 };
321 };
322
323 mds = {
324 enable = lib.mkEnableOption "Ceph MDS daemon";
325 daemons = lib.mkOption {
326 type = with lib.types; listOf str;
327 default = [ ];
328 example = [
329 "name1"
330 "name2"
331 ];
332 description = ''
333 A list of metadata service daemons that should have a service created. The names correspond
334 to the id part in ceph i.e. [ "name1" ] would result in mds.name1
335 '';
336 };
337 package = lib.mkPackageOption pkgs "ceph" { };
338 extraConfig = lib.mkOption {
339 type = with lib.types; attrsOf str;
340 default = { };
341 description = ''
342 Extra configuration to add to the MDS section.
343 '';
344 };
345 };
346
347 rgw = {
348 enable = lib.mkEnableOption "Ceph RadosGW daemon";
349 package = lib.mkPackageOption pkgs "ceph" { };
350 daemons = lib.mkOption {
351 type = with lib.types; listOf str;
352 default = [ ];
353 example = [
354 "name1"
355 "name2"
356 ];
357 description = ''
358 A list of rados gateway daemons that should have a service created. The names correspond
359 to the id part in ceph i.e. [ "name1" ] would result in client.name1, radosgw daemons
360 aren't daemons to cluster in the sense that OSD, MGR or MON daemons are. They are simply
361 daemons, from ceph, that uses the cluster as a backend.
362 '';
363 };
364 };
365
366 client = {
367 enable = lib.mkEnableOption "Ceph client configuration";
368 extraConfig = lib.mkOption {
369 type = with lib.types; attrsOf (attrsOf str);
370 default = { };
371 example = lib.literalExpression ''
372 {
373 # This would create a section for a radosgw daemon named node0 and related
374 # configuration for it
375 "client.radosgw.node0" = { "some config option" = "true"; };
376 };
377 '';
378 description = ''
379 Extra configuration to add to the client section. Configuration for rados gateways
380 would be added here, with their own sections, see example.
381 '';
382 };
383 };
384 };
385
386 config = lib.mkIf config.services.ceph.enable {
387 assertions = [
388 {
389 assertion = cfg.global.fsid != "";
390 message = "fsid has to be set to a valid uuid for the cluster to function";
391 }
392 {
393 assertion = cfg.mon.enable -> cfg.mon.daemons != [ ];
394 message = "have to set id of atleast one MON if you're going to enable Monitor";
395 }
396 {
397 assertion = cfg.mds.enable -> cfg.mds.daemons != [ ];
398 message = "have to set id of atleast one MDS if you're going to enable Metadata Service";
399 }
400 {
401 assertion = cfg.osd.enable -> cfg.osd.daemons != [ ];
402 message = "have to set id of atleast one OSD if you're going to enable OSD";
403 }
404 {
405 assertion = cfg.mgr.enable -> cfg.mgr.daemons != [ ];
406 message = "have to set id of atleast one MGR if you're going to enable MGR";
407 }
408 ];
409
410 warnings =
411 lib.optional (cfg.global.monInitialMembers == null)
412 "Not setting up a list of members in monInitialMembers requires that you set the host variable for each mon daemon or else the cluster won't function";
413
414 environment.etc."ceph/ceph.conf".text =
415 let
416 # Merge the extraConfig set for mgr daemons, as mgr don't have their own section
417 globalSection = expandCamelCaseAttrs (
418 cfg.global // cfg.extraConfig // lib.optionalAttrs cfg.mgr.enable cfg.mgr.extraConfig
419 );
420 # Remove all name-value pairs with null values from the attribute set to avoid making empty sections in the ceph.conf
421 globalSection' = lib.filterAttrs (name: value: value != null) globalSection;
422 totalConfig =
423 {
424 global = globalSection';
425 }
426 // lib.optionalAttrs (cfg.mon.enable && cfg.mon.extraConfig != { }) { mon = cfg.mon.extraConfig; }
427 // lib.optionalAttrs (cfg.mds.enable && cfg.mds.extraConfig != { }) { mds = cfg.mds.extraConfig; }
428 // lib.optionalAttrs (cfg.osd.enable && cfg.osd.extraConfig != { }) { osd = cfg.osd.extraConfig; }
429 // lib.optionalAttrs (cfg.client.enable && cfg.client.extraConfig != { }) cfg.client.extraConfig;
430 in
431 lib.generators.toINI { } totalConfig;
432
433 users.users.ceph = {
434 uid = config.ids.uids.ceph;
435 description = "Ceph daemon user";
436 group = "ceph";
437 extraGroups = [ "disk" ];
438 };
439
440 users.groups.ceph = {
441 gid = config.ids.gids.ceph;
442 };
443
444 systemd.services =
445 let
446 services =
447 [ ]
448 ++ lib.optional cfg.mon.enable (makeServices "mon" cfg.mon.daemons)
449 ++ lib.optional cfg.mds.enable (makeServices "mds" cfg.mds.daemons)
450 ++ lib.optional cfg.osd.enable (makeServices "osd" cfg.osd.daemons)
451 ++ lib.optional cfg.rgw.enable (makeServices "rgw" cfg.rgw.daemons)
452 ++ lib.optional cfg.mgr.enable (makeServices "mgr" cfg.mgr.daemons);
453 in
454 lib.mkMerge services;
455
456 systemd.targets =
457 let
458 targets =
459 [
460 {
461 ceph = {
462 description = "Ceph target allowing to start/stop all ceph service instances at once";
463 wantedBy = [ "multi-user.target" ];
464 unitConfig.StopWhenUnneeded = true;
465 };
466 }
467 ]
468 ++ lib.optional cfg.mon.enable (makeTarget "mon")
469 ++ lib.optional cfg.mds.enable (makeTarget "mds")
470 ++ lib.optional cfg.osd.enable (makeTarget "osd")
471 ++ lib.optional cfg.rgw.enable (makeTarget "rgw")
472 ++ lib.optional cfg.mgr.enable (makeTarget "mgr");
473 in
474 lib.mkMerge targets;
475
476 systemd.tmpfiles.settings."10-ceph" =
477 let
478 defaultConfig = {
479 user = "ceph";
480 group = "ceph";
481 };
482 in
483 {
484 "/etc/ceph".d = defaultConfig;
485 "/run/ceph".d = defaultConfig // {
486 mode = "0770";
487 };
488 "/var/lib/ceph".d = defaultConfig;
489 "/var/lib/ceph/mgr".d = lib.mkIf (cfg.mgr.enable) defaultConfig;
490 "/var/lib/ceph/mon".d = lib.mkIf (cfg.mon.enable) defaultConfig;
491 "/var/lib/ceph/osd".d = lib.mkIf (cfg.osd.enable) defaultConfig;
492 };
493 };
494}