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