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