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 "Ceph global configuration"; 76 77 global = { 78 fsid = mkOption { 79 type = types.str; 80 example = '' 81 433a2193-4f8a-47a0-95d2-209d7ca2cca5 82 ''; 83 description = '' 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 = '' 93 Name of cluster 94 ''; 95 }; 96 97 mgrModulePath = mkOption { 98 type = types.path; 99 default = "${pkgs.ceph.lib}/lib/ceph/mgr"; 100 description = '' 101 Path at which to find ceph-mgr modules. 102 ''; 103 }; 104 105 monInitialMembers = mkOption { 106 type = with types; nullOr commas; 107 default = null; 108 example = '' 109 node0, node1, node2 110 ''; 111 description = '' 112 List of hosts that will be used as monitors at startup. 113 ''; 114 }; 115 116 monHost = mkOption { 117 type = with types; nullOr commas; 118 default = null; 119 example = '' 120 10.10.0.1, 10.10.0.2, 10.10.0.3 121 ''; 122 description = '' 123 List of hostname shortnames/IP addresses of the initial monitors. 124 ''; 125 }; 126 127 maxOpenFiles = mkOption { 128 type = types.int; 129 default = 131072; 130 description = '' 131 Max open files for each OSD daemon. 132 ''; 133 }; 134 135 authClusterRequired = mkOption { 136 type = types.enum [ "cephx" "none" ]; 137 default = "cephx"; 138 description = '' 139 Enables requiring daemons to authenticate with eachother in the cluster. 140 ''; 141 }; 142 143 authServiceRequired = mkOption { 144 type = types.enum [ "cephx" "none" ]; 145 default = "cephx"; 146 description = '' 147 Enables requiring clients to authenticate with the cluster to access services in the cluster (e.g. radosgw, mds or osd). 148 ''; 149 }; 150 151 authClientRequired = mkOption { 152 type = types.enum [ "cephx" "none" ]; 153 default = "cephx"; 154 description = '' 155 Enables requiring the cluster to authenticate itself to the client. 156 ''; 157 }; 158 159 publicNetwork = mkOption { 160 type = with types; nullOr commas; 161 default = null; 162 example = '' 163 10.20.0.0/24, 192.168.1.0/24 164 ''; 165 description = '' 166 A comma-separated list of subnets that will be used as public networks in the cluster. 167 ''; 168 }; 169 170 clusterNetwork = mkOption { 171 type = with types; nullOr commas; 172 default = null; 173 example = '' 174 10.10.0.0/24, 192.168.0.0/24 175 ''; 176 description = '' 177 A comma-separated list of subnets that will be used as cluster networks in the cluster. 178 ''; 179 }; 180 181 rgwMimeTypesFile = mkOption { 182 type = with types; nullOr path; 183 default = "${pkgs.mime-types}/etc/mime.types"; 184 description = '' 185 Path to mime types used by radosgw. 186 ''; 187 }; 188 }; 189 190 extraConfig = mkOption { 191 type = with types; attrsOf str; 192 default = {}; 193 example = '' 194 { 195 "ms bind ipv6" = "true"; 196 }; 197 ''; 198 description = '' 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 "Ceph MGR daemon"; 205 daemons = mkOption { 206 type = with types; listOf str; 207 default = []; 208 example = '' 209 [ "name1" "name2" ]; 210 ''; 211 description = '' 212 A list of names for manager daemons that should have a service created. The names correspond 213 to the id part in ceph i.e. [ "name1" ] would result in mgr.name1 214 ''; 215 }; 216 extraConfig = mkOption { 217 type = with types; attrsOf str; 218 default = {}; 219 description = '' 220 Extra configuration to add to the global section for manager daemons. 221 ''; 222 }; 223 }; 224 225 mon = { 226 enable = mkEnableOption "Ceph MON daemon"; 227 daemons = mkOption { 228 type = with types; listOf str; 229 default = []; 230 example = '' 231 [ "name1" "name2" ]; 232 ''; 233 description = '' 234 A list of monitor daemons that should have a service created. The names correspond 235 to the id part in ceph i.e. [ "name1" ] would result in mon.name1 236 ''; 237 }; 238 extraConfig = mkOption { 239 type = with types; attrsOf str; 240 default = {}; 241 description = '' 242 Extra configuration to add to the monitor section. 243 ''; 244 }; 245 }; 246 247 osd = { 248 enable = mkEnableOption "Ceph OSD daemon"; 249 daemons = mkOption { 250 type = with types; listOf str; 251 default = []; 252 example = '' 253 [ "name1" "name2" ]; 254 ''; 255 description = '' 256 A list of OSD daemons that should have a service created. The names correspond 257 to the id part in ceph i.e. [ "name1" ] would result in osd.name1 258 ''; 259 }; 260 261 extraConfig = mkOption { 262 type = with types; attrsOf str; 263 default = { 264 "osd journal size" = "10000"; 265 "osd pool default size" = "3"; 266 "osd pool default min size" = "2"; 267 "osd pool default pg num" = "200"; 268 "osd pool default pgp num" = "200"; 269 "osd crush chooseleaf type" = "1"; 270 }; 271 description = '' 272 Extra configuration to add to the OSD section. 273 ''; 274 }; 275 }; 276 277 mds = { 278 enable = mkEnableOption "Ceph MDS daemon"; 279 daemons = mkOption { 280 type = with types; listOf str; 281 default = []; 282 example = '' 283 [ "name1" "name2" ]; 284 ''; 285 description = '' 286 A list of metadata service daemons that should have a service created. The names correspond 287 to the id part in ceph i.e. [ "name1" ] would result in mds.name1 288 ''; 289 }; 290 extraConfig = mkOption { 291 type = with types; attrsOf str; 292 default = {}; 293 description = '' 294 Extra configuration to add to the MDS section. 295 ''; 296 }; 297 }; 298 299 rgw = { 300 enable = mkEnableOption "Ceph RadosGW daemon"; 301 daemons = mkOption { 302 type = with types; listOf str; 303 default = []; 304 example = '' 305 [ "name1" "name2" ]; 306 ''; 307 description = '' 308 A list of rados gateway daemons that should have a service created. The names correspond 309 to the id part in ceph i.e. [ "name1" ] would result in client.name1, radosgw daemons 310 aren't daemons to cluster in the sense that OSD, MGR or MON daemons are. They are simply 311 daemons, from ceph, that uses the cluster as a backend. 312 ''; 313 }; 314 }; 315 316 client = { 317 enable = mkEnableOption "Ceph client configuration"; 318 extraConfig = mkOption { 319 type = with types; attrsOf (attrsOf str); 320 default = {}; 321 example = '' 322 { 323 # This would create a section for a radosgw daemon named node0 and related 324 # configuration for it 325 "client.radosgw.node0" = { "some config option" = "true"; }; 326 }; 327 ''; 328 description = '' 329 Extra configuration to add to the client section. Configuration for rados gateways 330 would be added here, with their own sections, see example. 331 ''; 332 }; 333 }; 334 }; 335 336 config = mkIf config.services.ceph.enable { 337 assertions = [ 338 { assertion = cfg.global.fsid != ""; 339 message = "fsid has to be set to a valid uuid for the cluster to function"; 340 } 341 { assertion = cfg.mon.enable == true -> cfg.mon.daemons != []; 342 message = "have to set id of atleast one MON if you're going to enable Monitor"; 343 } 344 { assertion = cfg.mds.enable == true -> cfg.mds.daemons != []; 345 message = "have to set id of atleast one MDS if you're going to enable Metadata Service"; 346 } 347 { assertion = cfg.osd.enable == true -> cfg.osd.daemons != []; 348 message = "have to set id of atleast one OSD if you're going to enable OSD"; 349 } 350 { assertion = cfg.mgr.enable == true -> cfg.mgr.daemons != []; 351 message = "have to set id of atleast one MGR if you're going to enable MGR"; 352 } 353 ]; 354 355 warnings = optional (cfg.global.monInitialMembers == null) 356 "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"; 357 358 environment.etc."ceph/ceph.conf".text = let 359 # Merge the extraConfig set for mgr daemons, as mgr don't have their own section 360 globalSection = expandCamelCaseAttrs (cfg.global // cfg.extraConfig // optionalAttrs cfg.mgr.enable cfg.mgr.extraConfig); 361 # Remove all name-value pairs with null values from the attribute set to avoid making empty sections in the ceph.conf 362 globalSection' = filterAttrs (name: value: value != null) globalSection; 363 totalConfig = { 364 global = globalSection'; 365 } // optionalAttrs (cfg.mon.enable && cfg.mon.extraConfig != {}) { mon = cfg.mon.extraConfig; } 366 // optionalAttrs (cfg.mds.enable && cfg.mds.extraConfig != {}) { mds = cfg.mds.extraConfig; } 367 // optionalAttrs (cfg.osd.enable && cfg.osd.extraConfig != {}) { osd = cfg.osd.extraConfig; } 368 // optionalAttrs (cfg.client.enable && cfg.client.extraConfig != {}) cfg.client.extraConfig; 369 in 370 generators.toINI {} totalConfig; 371 372 users.users.ceph = { 373 uid = config.ids.uids.ceph; 374 description = "Ceph daemon user"; 375 group = "ceph"; 376 extraGroups = [ "disk" ]; 377 }; 378 379 users.groups.ceph = { 380 gid = config.ids.gids.ceph; 381 }; 382 383 systemd.services = let 384 services = [] 385 ++ optional cfg.mon.enable (makeServices "mon" cfg.mon.daemons) 386 ++ optional cfg.mds.enable (makeServices "mds" cfg.mds.daemons) 387 ++ optional cfg.osd.enable (makeServices "osd" cfg.osd.daemons) 388 ++ optional cfg.rgw.enable (makeServices "rgw" cfg.rgw.daemons) 389 ++ optional cfg.mgr.enable (makeServices "mgr" cfg.mgr.daemons); 390 in 391 mkMerge services; 392 393 systemd.targets = let 394 targets = [ 395 { ceph = { 396 description = "Ceph target allowing to start/stop all ceph service instances at once"; 397 wantedBy = [ "multi-user.target" ]; 398 unitConfig.StopWhenUnneeded = true; 399 }; } ] 400 ++ optional cfg.mon.enable (makeTarget "mon") 401 ++ optional cfg.mds.enable (makeTarget "mds") 402 ++ optional cfg.osd.enable (makeTarget "osd") 403 ++ optional cfg.rgw.enable (makeTarget "rgw") 404 ++ optional cfg.mgr.enable (makeTarget "mgr"); 405 in 406 mkMerge targets; 407 408 systemd.tmpfiles.rules = [ 409 "d /etc/ceph - ceph ceph - -" 410 "d /run/ceph 0770 ceph ceph -" 411 "d /var/lib/ceph - ceph ceph - -"] 412 ++ optionals cfg.mgr.enable [ "d /var/lib/ceph/mgr - ceph ceph - -"] 413 ++ optionals cfg.mon.enable [ "d /var/lib/ceph/mon - ceph ceph - -"] 414 ++ optionals cfg.osd.enable [ "d /var/lib/ceph/osd - ceph ceph - -"]; 415 }; 416}