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 cfg.${daemonType}.package; }) 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 }; 69in 70{ 71 options.services.ceph = { 72 # Ceph has a monolithic configuration file but different sections for 73 # each daemon, a separate client section and a global section 74 enable = mkEnableOption (lib.mdDoc "Ceph global configuration"); 75 76 global = { 77 fsid = mkOption { 78 type = types.str; 79 example = '' 80 433a2193-4f8a-47a0-95d2-209d7ca2cca5 81 ''; 82 description = lib.mdDoc '' 83 Filesystem ID, a generated uuid, its must be generated and set before 84 attempting to start a cluster 85 ''; 86 }; 87 88 clusterName = mkOption { 89 type = types.str; 90 default = "ceph"; 91 description = lib.mdDoc '' 92 Name of cluster 93 ''; 94 }; 95 96 mgrModulePath = mkOption { 97 type = types.path; 98 default = "${pkgs.ceph.lib}/lib/ceph/mgr"; 99 defaultText = literalExpression ''"''${pkgs.ceph.lib}/lib/ceph/mgr"''; 100 description = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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 = lib.mdDoc '' 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.mailcap}/etc/mime.types"; 184 defaultText = literalExpression ''"''${pkgs.mailcap}/etc/mime.types"''; 185 description = lib.mdDoc '' 186 Path to mime types used by radosgw. 187 ''; 188 }; 189 }; 190 191 extraConfig = mkOption { 192 type = with types; attrsOf str; 193 default = {}; 194 example = { 195 "ms bind ipv6" = "true"; 196 }; 197 description = lib.mdDoc '' 198 Extra configuration to add to the global section. Use for setting values that are common for all daemons in the cluster. 199 ''; 200 }; 201 202 mgr = { 203 enable = mkEnableOption (lib.mdDoc "Ceph MGR daemon"); 204 daemons = mkOption { 205 type = with types; listOf str; 206 default = []; 207 example = [ "name1" "name2" ]; 208 description = lib.mdDoc '' 209 A list of names for manager daemons that should have a service created. The names correspond 210 to the id part in ceph i.e. [ "name1" ] would result in mgr.name1 211 ''; 212 }; 213 package = mkPackageOptionMD pkgs "ceph" { }; 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 package = mkPackageOptionMD pkgs "ceph" { }; 235 extraConfig = mkOption { 236 type = with types; attrsOf str; 237 default = {}; 238 description = lib.mdDoc '' 239 Extra configuration to add to the monitor section. 240 ''; 241 }; 242 }; 243 244 osd = { 245 enable = mkEnableOption (lib.mdDoc "Ceph OSD daemon"); 246 daemons = mkOption { 247 type = with types; listOf str; 248 default = []; 249 example = [ "name1" "name2" ]; 250 description = lib.mdDoc '' 251 A list of OSD daemons that should have a service created. The names correspond 252 to the id part in ceph i.e. [ "name1" ] would result in osd.name1 253 ''; 254 }; 255 package = mkPackageOptionMD pkgs "ceph" { }; 256 extraConfig = mkOption { 257 type = with types; attrsOf str; 258 default = { 259 "osd journal size" = "10000"; 260 "osd pool default size" = "3"; 261 "osd pool default min size" = "2"; 262 "osd pool default pg num" = "200"; 263 "osd pool default pgp num" = "200"; 264 "osd crush chooseleaf type" = "1"; 265 }; 266 description = lib.mdDoc '' 267 Extra configuration to add to the OSD section. 268 ''; 269 }; 270 }; 271 272 mds = { 273 enable = mkEnableOption (lib.mdDoc "Ceph MDS daemon"); 274 daemons = mkOption { 275 type = with types; listOf str; 276 default = []; 277 example = [ "name1" "name2" ]; 278 description = lib.mdDoc '' 279 A list of metadata service daemons that should have a service created. The names correspond 280 to the id part in ceph i.e. [ "name1" ] would result in mds.name1 281 ''; 282 }; 283 package = mkPackageOptionMD pkgs "ceph" { }; 284 extraConfig = mkOption { 285 type = with types; attrsOf str; 286 default = {}; 287 description = lib.mdDoc '' 288 Extra configuration to add to the MDS section. 289 ''; 290 }; 291 }; 292 293 rgw = { 294 enable = mkEnableOption (lib.mdDoc "Ceph RadosGW daemon"); 295 package = mkPackageOptionMD pkgs "ceph" { }; 296 daemons = mkOption { 297 type = with types; listOf str; 298 default = []; 299 example = [ "name1" "name2" ]; 300 description = lib.mdDoc '' 301 A list of rados gateway daemons that should have a service created. The names correspond 302 to the id part in ceph i.e. [ "name1" ] would result in client.name1, radosgw daemons 303 aren't daemons to cluster in the sense that OSD, MGR or MON daemons are. They are simply 304 daemons, from ceph, that uses the cluster as a backend. 305 ''; 306 }; 307 }; 308 309 client = { 310 enable = mkEnableOption (lib.mdDoc "Ceph client configuration"); 311 extraConfig = mkOption { 312 type = with types; attrsOf (attrsOf str); 313 default = {}; 314 example = literalExpression '' 315 { 316 # This would create a section for a radosgw daemon named node0 and related 317 # configuration for it 318 "client.radosgw.node0" = { "some config option" = "true"; }; 319 }; 320 ''; 321 description = lib.mdDoc '' 322 Extra configuration to add to the client section. Configuration for rados gateways 323 would be added here, with their own sections, see example. 324 ''; 325 }; 326 }; 327 }; 328 329 config = mkIf config.services.ceph.enable { 330 assertions = [ 331 { assertion = cfg.global.fsid != ""; 332 message = "fsid has to be set to a valid uuid for the cluster to function"; 333 } 334 { assertion = cfg.mon.enable -> cfg.mon.daemons != []; 335 message = "have to set id of atleast one MON if you're going to enable Monitor"; 336 } 337 { assertion = cfg.mds.enable -> cfg.mds.daemons != []; 338 message = "have to set id of atleast one MDS if you're going to enable Metadata Service"; 339 } 340 { assertion = cfg.osd.enable -> cfg.osd.daemons != []; 341 message = "have to set id of atleast one OSD if you're going to enable OSD"; 342 } 343 { assertion = cfg.mgr.enable -> cfg.mgr.daemons != []; 344 message = "have to set id of atleast one MGR if you're going to enable MGR"; 345 } 346 ]; 347 348 warnings = optional (cfg.global.monInitialMembers == null) 349 "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"; 350 351 environment.etc."ceph/ceph.conf".text = let 352 # Merge the extraConfig set for mgr daemons, as mgr don't have their own section 353 globalSection = expandCamelCaseAttrs (cfg.global // cfg.extraConfig // optionalAttrs cfg.mgr.enable cfg.mgr.extraConfig); 354 # Remove all name-value pairs with null values from the attribute set to avoid making empty sections in the ceph.conf 355 globalSection' = filterAttrs (name: value: value != null) globalSection; 356 totalConfig = { 357 global = globalSection'; 358 } // optionalAttrs (cfg.mon.enable && cfg.mon.extraConfig != {}) { mon = cfg.mon.extraConfig; } 359 // optionalAttrs (cfg.mds.enable && cfg.mds.extraConfig != {}) { mds = cfg.mds.extraConfig; } 360 // optionalAttrs (cfg.osd.enable && cfg.osd.extraConfig != {}) { osd = cfg.osd.extraConfig; } 361 // optionalAttrs (cfg.client.enable && cfg.client.extraConfig != {}) cfg.client.extraConfig; 362 in 363 generators.toINI {} totalConfig; 364 365 users.users.ceph = { 366 uid = config.ids.uids.ceph; 367 description = "Ceph daemon user"; 368 group = "ceph"; 369 extraGroups = [ "disk" ]; 370 }; 371 372 users.groups.ceph = { 373 gid = config.ids.gids.ceph; 374 }; 375 376 systemd.services = let 377 services = [] 378 ++ optional cfg.mon.enable (makeServices "mon" cfg.mon.daemons) 379 ++ optional cfg.mds.enable (makeServices "mds" cfg.mds.daemons) 380 ++ optional cfg.osd.enable (makeServices "osd" cfg.osd.daemons) 381 ++ optional cfg.rgw.enable (makeServices "rgw" cfg.rgw.daemons) 382 ++ optional cfg.mgr.enable (makeServices "mgr" cfg.mgr.daemons); 383 in 384 mkMerge services; 385 386 systemd.targets = let 387 targets = [ 388 { ceph = { 389 description = "Ceph target allowing to start/stop all ceph service instances at once"; 390 wantedBy = [ "multi-user.target" ]; 391 unitConfig.StopWhenUnneeded = true; 392 }; } ] 393 ++ optional cfg.mon.enable (makeTarget "mon") 394 ++ optional cfg.mds.enable (makeTarget "mds") 395 ++ optional cfg.osd.enable (makeTarget "osd") 396 ++ optional cfg.rgw.enable (makeTarget "rgw") 397 ++ optional cfg.mgr.enable (makeTarget "mgr"); 398 in 399 mkMerge targets; 400 401 systemd.tmpfiles.rules = [ 402 "d /etc/ceph - ceph ceph - -" 403 "d /run/ceph 0770 ceph ceph -" 404 "d /var/lib/ceph - ceph ceph - -"] 405 ++ optionals cfg.mgr.enable [ "d /var/lib/ceph/mgr - ceph ceph - -"] 406 ++ optionals cfg.mon.enable [ "d /var/lib/ceph/mon - ceph ceph - -"] 407 ++ optionals cfg.osd.enable [ "d /var/lib/ceph/osd - ceph ceph - -"]; 408 }; 409}