at 23.05-pre 18 kB view raw
1{ config, lib, pkgs, ... }: 2 3let 4 inherit (lib) 5 concatStringsSep 6 flip 7 literalMD 8 literalExpression 9 optionalAttrs 10 optionals 11 recursiveUpdate 12 mdDoc 13 mkEnableOption 14 mkIf 15 mkOption 16 types 17 versionAtLeast 18 ; 19 20 cfg = config.services.cassandra; 21 22 defaultUser = "cassandra"; 23 24 cassandraConfig = flip recursiveUpdate cfg.extraConfig ( 25 { 26 commitlog_sync = "batch"; 27 commitlog_sync_batch_window_in_ms = 2; 28 start_native_transport = cfg.allowClients; 29 cluster_name = cfg.clusterName; 30 partitioner = "org.apache.cassandra.dht.Murmur3Partitioner"; 31 endpoint_snitch = "SimpleSnitch"; 32 data_file_directories = [ "${cfg.homeDir}/data" ]; 33 commitlog_directory = "${cfg.homeDir}/commitlog"; 34 saved_caches_directory = "${cfg.homeDir}/saved_caches"; 35 } // optionalAttrs (cfg.seedAddresses != [ ]) { 36 seed_provider = [ 37 { 38 class_name = "org.apache.cassandra.locator.SimpleSeedProvider"; 39 parameters = [{ seeds = concatStringsSep "," cfg.seedAddresses; }]; 40 } 41 ]; 42 } // optionalAttrs (versionAtLeast cfg.package.version "3") { 43 hints_directory = "${cfg.homeDir}/hints"; 44 } 45 ); 46 47 cassandraConfigWithAddresses = cassandraConfig // ( 48 if cfg.listenAddress == null 49 then { listen_interface = cfg.listenInterface; } 50 else { listen_address = cfg.listenAddress; } 51 ) // ( 52 if cfg.rpcAddress == null 53 then { rpc_interface = cfg.rpcInterface; } 54 else { rpc_address = cfg.rpcAddress; } 55 ); 56 57 cassandraEtc = pkgs.stdenv.mkDerivation { 58 name = "cassandra-etc"; 59 60 cassandraYaml = builtins.toJSON cassandraConfigWithAddresses; 61 cassandraEnvPkg = "${cfg.package}/conf/cassandra-env.sh"; 62 cassandraLogbackConfig = pkgs.writeText "logback.xml" cfg.logbackConfig; 63 64 passAsFile = [ "extraEnvSh" ]; 65 inherit (cfg) extraEnvSh; 66 67 buildCommand = '' 68 mkdir -p "$out" 69 70 echo "$cassandraYaml" > "$out/cassandra.yaml" 71 ln -s "$cassandraLogbackConfig" "$out/logback.xml" 72 73 ( cat "$cassandraEnvPkg" 74 echo "# lines from services.cassandra.extraEnvSh: " 75 cat "$extraEnvShPath" 76 ) > "$out/cassandra-env.sh" 77 78 # Delete default JMX Port, otherwise we can't set it using env variable 79 sed -i '/JMX_PORT="7199"/d' "$out/cassandra-env.sh" 80 81 # Delete default password file 82 sed -i '/-Dcom.sun.management.jmxremote.password.file=\/etc\/cassandra\/jmxremote.password/d' "$out/cassandra-env.sh" 83 ''; 84 }; 85 86 defaultJmxRolesFile = 87 builtins.foldl' 88 (left: right: left + right) "" 89 (map (role: "${role.username} ${role.password}") cfg.jmxRoles); 90 91 fullJvmOptions = 92 cfg.jvmOpts 93 ++ optionals (cfg.jmxRoles != [ ]) [ 94 "-Dcom.sun.management.jmxremote.authenticate=true" 95 "-Dcom.sun.management.jmxremote.password.file=${cfg.jmxRolesFile}" 96 ] ++ optionals cfg.remoteJmx [ 97 "-Djava.rmi.server.hostname=${cfg.rpcAddress}" 98 ]; 99 100in 101{ 102 options.services.cassandra = { 103 104 enable = mkEnableOption (lib.mdDoc '' 105 Apache Cassandra Scalable and highly available database. 106 ''); 107 108 clusterName = mkOption { 109 type = types.str; 110 default = "Test Cluster"; 111 description = mdDoc '' 112 The name of the cluster. 113 This setting prevents nodes in one logical cluster from joining 114 another. All nodes in a cluster must have the same value. 115 ''; 116 }; 117 118 user = mkOption { 119 type = types.str; 120 default = defaultUser; 121 description = mdDoc "Run Apache Cassandra under this user."; 122 }; 123 124 group = mkOption { 125 type = types.str; 126 default = defaultUser; 127 description = mdDoc "Run Apache Cassandra under this group."; 128 }; 129 130 homeDir = mkOption { 131 type = types.path; 132 default = "/var/lib/cassandra"; 133 description = mdDoc '' 134 Home directory for Apache Cassandra. 135 ''; 136 }; 137 138 package = mkOption { 139 type = types.package; 140 default = pkgs.cassandra; 141 defaultText = literalExpression "pkgs.cassandra"; 142 example = literalExpression "pkgs.cassandra_3_11"; 143 description = mdDoc '' 144 The Apache Cassandra package to use. 145 ''; 146 }; 147 148 jvmOpts = mkOption { 149 type = types.listOf types.str; 150 default = [ ]; 151 description = mdDoc '' 152 Populate the `JVM_OPT` environment variable. 153 ''; 154 }; 155 156 listenAddress = mkOption { 157 type = types.nullOr types.str; 158 default = "127.0.0.1"; 159 example = null; 160 description = mdDoc '' 161 Address or interface to bind to and tell other Cassandra nodes 162 to connect to. You _must_ change this if you want multiple 163 nodes to be able to communicate! 164 165 Set {option}`listenAddress` OR {option}`listenInterface`, not both. 166 167 Leaving it blank leaves it up to 168 `InetAddress.getLocalHost()`. This will always do the "Right 169 Thing" _if_ the node is properly configured (hostname, name 170 resolution, etc), and the Right Thing is to use the address 171 associated with the hostname (it might not be). 172 173 Setting {option}`listenAddress` to `0.0.0.0` is always wrong. 174 ''; 175 }; 176 177 listenInterface = mkOption { 178 type = types.nullOr types.str; 179 default = null; 180 example = "eth1"; 181 description = mdDoc '' 182 Set `listenAddress` OR `listenInterface`, not both. Interfaces 183 must correspond to a single address, IP aliasing is not 184 supported. 185 ''; 186 }; 187 188 rpcAddress = mkOption { 189 type = types.nullOr types.str; 190 default = "127.0.0.1"; 191 example = null; 192 description = mdDoc '' 193 The address or interface to bind the native transport server to. 194 195 Set {option}`rpcAddress` OR {option}`rpcInterface`, not both. 196 197 Leaving {option}`rpcAddress` blank has the same effect as on 198 {option}`listenAddress` (i.e. it will be based on the configured hostname 199 of the node). 200 201 Note that unlike {option}`listenAddress`, you can specify `"0.0.0.0"`, but you 202 must also set `extraConfig.broadcast_rpc_address` to a value other 203 than `"0.0.0.0"`. 204 205 For security reasons, you should not expose this port to the 206 internet. Firewall it if needed. 207 ''; 208 }; 209 210 rpcInterface = mkOption { 211 type = types.nullOr types.str; 212 default = null; 213 example = "eth1"; 214 description = mdDoc '' 215 Set {option}`rpcAddress` OR {option}`rpcInterface`, not both. Interfaces must 216 correspond to a single address, IP aliasing is not supported. 217 ''; 218 }; 219 220 logbackConfig = mkOption { 221 type = types.lines; 222 default = '' 223 <configuration scan="false"> 224 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender"> 225 <encoder> 226 <pattern>%-5level %date{HH:mm:ss,SSS} %msg%n</pattern> 227 </encoder> 228 </appender> 229 230 <root level="INFO"> 231 <appender-ref ref="STDOUT" /> 232 </root> 233 234 <logger name="com.thinkaurelius.thrift" level="ERROR"/> 235 </configuration> 236 ''; 237 description = mdDoc '' 238 XML logback configuration for cassandra 239 ''; 240 }; 241 242 seedAddresses = mkOption { 243 type = types.listOf types.str; 244 default = [ "127.0.0.1" ]; 245 description = mdDoc '' 246 The addresses of hosts designated as contact points in the cluster. A 247 joining node contacts one of the nodes in the seeds list to learn the 248 topology of the ring. 249 Set to `[ "127.0.0.1" ]` for a single node cluster. 250 ''; 251 }; 252 253 allowClients = mkOption { 254 type = types.bool; 255 default = true; 256 description = mdDoc '' 257 Enables or disables the native transport server (CQL binary protocol). 258 This server uses the same address as the {option}`rpcAddress`, 259 but the port it uses is not `rpc_port` but 260 `native_transport_port`. See the official Cassandra 261 docs for more information on these variables and set them using 262 {option}`extraConfig`. 263 ''; 264 }; 265 266 extraConfig = mkOption { 267 type = types.attrs; 268 default = { }; 269 example = 270 { 271 commitlog_sync_batch_window_in_ms = 3; 272 }; 273 description = mdDoc '' 274 Extra options to be merged into {file}`cassandra.yaml` as nix attribute set. 275 ''; 276 }; 277 278 extraEnvSh = mkOption { 279 type = types.lines; 280 default = ""; 281 example = literalExpression ''"CLASSPATH=$CLASSPATH:''${extraJar}"''; 282 description = mdDoc '' 283 Extra shell lines to be appended onto {file}`cassandra-env.sh`. 284 ''; 285 }; 286 287 fullRepairInterval = mkOption { 288 type = types.nullOr types.str; 289 default = "3w"; 290 example = null; 291 description = mdDoc '' 292 Set the interval how often full repairs are run, i.e. 293 {command}`nodetool repair --full` is executed. See 294 <https://cassandra.apache.org/doc/latest/operating/repair.html> 295 for more information. 296 297 Set to `null` to disable full repairs. 298 ''; 299 }; 300 301 fullRepairOptions = mkOption { 302 type = types.listOf types.str; 303 default = [ ]; 304 example = [ "--partitioner-range" ]; 305 description = mdDoc '' 306 Options passed through to the full repair command. 307 ''; 308 }; 309 310 incrementalRepairInterval = mkOption { 311 type = types.nullOr types.str; 312 default = "3d"; 313 example = null; 314 description = mdDoc '' 315 Set the interval how often incremental repairs are run, i.e. 316 {command}`nodetool repair` is executed. See 317 <https://cassandra.apache.org/doc/latest/operating/repair.html> 318 for more information. 319 320 Set to `null` to disable incremental repairs. 321 ''; 322 }; 323 324 incrementalRepairOptions = mkOption { 325 type = types.listOf types.str; 326 default = [ ]; 327 example = [ "--partitioner-range" ]; 328 description = mdDoc '' 329 Options passed through to the incremental repair command. 330 ''; 331 }; 332 333 maxHeapSize = mkOption { 334 type = types.nullOr types.str; 335 default = null; 336 example = "4G"; 337 description = mdDoc '' 338 Must be left blank or set together with {option}`heapNewSize`. 339 If left blank a sensible value for the available amount of RAM and CPU 340 cores is calculated. 341 342 Override to set the amount of memory to allocate to the JVM at 343 start-up. For production use you may wish to adjust this for your 344 environment. `MAX_HEAP_SIZE` is the total amount of memory dedicated 345 to the Java heap. `HEAP_NEWSIZE` refers to the size of the young 346 generation. 347 348 The main trade-off for the young generation is that the larger it 349 is, the longer GC pause times will be. The shorter it is, the more 350 expensive GC will be (usually). 351 ''; 352 }; 353 354 heapNewSize = mkOption { 355 type = types.nullOr types.str; 356 default = null; 357 example = "800M"; 358 description = mdDoc '' 359 Must be left blank or set together with {option}`heapNewSize`. 360 If left blank a sensible value for the available amount of RAM and CPU 361 cores is calculated. 362 363 Override to set the amount of memory to allocate to the JVM at 364 start-up. For production use you may wish to adjust this for your 365 environment. `HEAP_NEWSIZE` refers to the size of the young 366 generation. 367 368 The main trade-off for the young generation is that the larger it 369 is, the longer GC pause times will be. The shorter it is, the more 370 expensive GC will be (usually). 371 372 The example `HEAP_NEWSIZE` assumes a modern 8-core+ machine for decent pause 373 times. If in doubt, and if you do not particularly want to tweak, go with 374 100 MB per physical CPU core. 375 ''; 376 }; 377 378 mallocArenaMax = mkOption { 379 type = types.nullOr types.int; 380 default = null; 381 example = 4; 382 description = mdDoc '' 383 Set this to control the amount of arenas per-thread in glibc. 384 ''; 385 }; 386 387 remoteJmx = mkOption { 388 type = types.bool; 389 default = false; 390 description = mdDoc '' 391 Cassandra ships with JMX accessible *only* from localhost. 392 To enable remote JMX connections set to true. 393 394 Be sure to also enable authentication and/or TLS. 395 See: <https://wiki.apache.org/cassandra/JmxSecurity> 396 ''; 397 }; 398 399 jmxPort = mkOption { 400 type = types.int; 401 default = 7199; 402 description = mdDoc '' 403 Specifies the default port over which Cassandra will be available for 404 JMX connections. 405 For security reasons, you should not expose this port to the internet. 406 Firewall it if needed. 407 ''; 408 }; 409 410 jmxRoles = mkOption { 411 default = [ ]; 412 description = mdDoc '' 413 Roles that are allowed to access the JMX (e.g. {command}`nodetool`) 414 BEWARE: The passwords will be stored world readable in the nix store. 415 It's recommended to use your own protected file using 416 {option}`jmxRolesFile` 417 418 Doesn't work in versions older than 3.11 because they don't like that 419 it's world readable. 420 ''; 421 type = types.listOf (types.submodule { 422 options = { 423 username = mkOption { 424 type = types.str; 425 description = lib.mdDoc "Username for JMX"; 426 }; 427 password = mkOption { 428 type = types.str; 429 description = lib.mdDoc "Password for JMX"; 430 }; 431 }; 432 }); 433 }; 434 435 jmxRolesFile = mkOption { 436 type = types.nullOr types.path; 437 default = 438 if versionAtLeast cfg.package.version "3.11" 439 then pkgs.writeText "jmx-roles-file" defaultJmxRolesFile 440 else null; 441 defaultText = literalMD ''generated configuration file if version is at least 3.11, otherwise `null`''; 442 example = "/var/lib/cassandra/jmx.password"; 443 description = lib.mdDoc '' 444 Specify your own jmx roles file. 445 446 Make sure the permissions forbid "others" from reading the file if 447 you're using Cassandra below version 3.11. 448 ''; 449 }; 450 }; 451 452 config = mkIf cfg.enable { 453 assertions = [ 454 { 455 assertion = (cfg.listenAddress == null) != (cfg.listenInterface == null); 456 message = "You have to set either listenAddress or listenInterface"; 457 } 458 { 459 assertion = (cfg.rpcAddress == null) != (cfg.rpcInterface == null); 460 message = "You have to set either rpcAddress or rpcInterface"; 461 } 462 { 463 assertion = (cfg.maxHeapSize == null) == (cfg.heapNewSize == null); 464 message = "If you set either of maxHeapSize or heapNewSize you have to set both"; 465 } 466 { 467 assertion = cfg.remoteJmx -> cfg.jmxRolesFile != null; 468 message = '' 469 If you want JMX available remotely you need to set a password using 470 <literal>jmxRoles</literal> or <literal>jmxRolesFile</literal> if 471 using Cassandra older than v3.11. 472 ''; 473 } 474 ]; 475 users = mkIf (cfg.user == defaultUser) { 476 users.${defaultUser} = { 477 group = cfg.group; 478 home = cfg.homeDir; 479 createHome = true; 480 uid = config.ids.uids.cassandra; 481 description = "Cassandra service user"; 482 }; 483 groups.${defaultUser}.gid = config.ids.gids.cassandra; 484 }; 485 486 systemd.services.cassandra = { 487 description = "Apache Cassandra service"; 488 after = [ "network.target" ]; 489 environment = { 490 CASSANDRA_CONF = "${cassandraEtc}"; 491 JVM_OPTS = builtins.concatStringsSep " " fullJvmOptions; 492 MAX_HEAP_SIZE = toString cfg.maxHeapSize; 493 HEAP_NEWSIZE = toString cfg.heapNewSize; 494 MALLOC_ARENA_MAX = toString cfg.mallocArenaMax; 495 LOCAL_JMX = if cfg.remoteJmx then "no" else "yes"; 496 JMX_PORT = toString cfg.jmxPort; 497 }; 498 wantedBy = [ "multi-user.target" ]; 499 serviceConfig = { 500 User = cfg.user; 501 Group = cfg.group; 502 ExecStart = "${cfg.package}/bin/cassandra -f"; 503 SuccessExitStatus = 143; 504 }; 505 }; 506 507 systemd.services.cassandra-full-repair = { 508 description = "Perform a full repair on this Cassandra node"; 509 after = [ "cassandra.service" ]; 510 requires = [ "cassandra.service" ]; 511 serviceConfig = { 512 User = cfg.user; 513 Group = cfg.group; 514 ExecStart = 515 concatStringsSep " " 516 ([ 517 "${cfg.package}/bin/nodetool" 518 "repair" 519 "--full" 520 ] ++ cfg.fullRepairOptions); 521 }; 522 }; 523 524 systemd.timers.cassandra-full-repair = 525 mkIf (cfg.fullRepairInterval != null) { 526 description = "Schedule full repairs on Cassandra"; 527 wantedBy = [ "timers.target" ]; 528 timerConfig = { 529 OnBootSec = cfg.fullRepairInterval; 530 OnUnitActiveSec = cfg.fullRepairInterval; 531 Persistent = true; 532 }; 533 }; 534 535 systemd.services.cassandra-incremental-repair = { 536 description = "Perform an incremental repair on this cassandra node."; 537 after = [ "cassandra.service" ]; 538 requires = [ "cassandra.service" ]; 539 serviceConfig = { 540 User = cfg.user; 541 Group = cfg.group; 542 ExecStart = 543 concatStringsSep " " 544 ([ 545 "${cfg.package}/bin/nodetool" 546 "repair" 547 ] ++ cfg.incrementalRepairOptions); 548 }; 549 }; 550 551 systemd.timers.cassandra-incremental-repair = 552 mkIf (cfg.incrementalRepairInterval != null) { 553 description = "Schedule incremental repairs on Cassandra"; 554 wantedBy = [ "timers.target" ]; 555 timerConfig = { 556 OnBootSec = cfg.incrementalRepairInterval; 557 OnUnitActiveSec = cfg.incrementalRepairInterval; 558 Persistent = true; 559 }; 560 }; 561 }; 562 563 meta.maintainers = with lib.maintainers; [ roberth ]; 564}