1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 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 ); 58in 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.users = singleton { 336 name = "ceph"; 337 uid = config.ids.uids.ceph; 338 description = "Ceph daemon user"; 339 }; 340 341 users.groups = 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}