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