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