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