1{ config, lib, pkgs, ... }:
2
3let
4 inherit (lib)
5 concatStringsSep
6 flip
7 literalMD
8 literalExpression
9 optionalAttrs
10 optionals
11 recursiveUpdate
12 mkEnableOption
13 mkPackageOption
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 ''
125 Apache Cassandra – Scalable and highly available database
126 '';
127
128 clusterName = mkOption {
129 type = types.str;
130 default = "Test Cluster";
131 description = ''
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 = "Run Apache Cassandra under this user.";
142 };
143
144 group = mkOption {
145 type = types.str;
146 default = defaultUser;
147 description = "Run Apache Cassandra under this group.";
148 };
149
150 homeDir = mkOption {
151 type = types.path;
152 default = "/var/lib/cassandra";
153 description = ''
154 Home directory for Apache Cassandra.
155 '';
156 };
157
158 package = mkPackageOption pkgs "cassandra" {
159 example = "cassandra_3_11";
160 };
161
162 jvmOpts = mkOption {
163 type = types.listOf types.str;
164 default = [ ];
165 description = ''
166 Populate the `JVM_OPT` environment variable.
167 '';
168 };
169
170 listenAddress = mkOption {
171 type = types.nullOr types.str;
172 default = "127.0.0.1";
173 example = null;
174 description = ''
175 Address or interface to bind to and tell other Cassandra nodes
176 to connect to. You _must_ change this if you want multiple
177 nodes to be able to communicate!
178
179 Set {option}`listenAddress` OR {option}`listenInterface`, not both.
180
181 Leaving it blank leaves it up to
182 `InetAddress.getLocalHost()`. This will always do the "Right
183 Thing" _if_ the node is properly configured (hostname, name
184 resolution, etc), and the Right Thing is to use the address
185 associated with the hostname (it might not be).
186
187 Setting {option}`listenAddress` to `0.0.0.0` is always wrong.
188 '';
189 };
190
191 listenInterface = mkOption {
192 type = types.nullOr types.str;
193 default = null;
194 example = "eth1";
195 description = ''
196 Set `listenAddress` OR `listenInterface`, not both. Interfaces
197 must correspond to a single address, IP aliasing is not
198 supported.
199 '';
200 };
201
202 rpcAddress = mkOption {
203 type = types.nullOr types.str;
204 default = "127.0.0.1";
205 example = null;
206 description = ''
207 The address or interface to bind the native transport server to.
208
209 Set {option}`rpcAddress` OR {option}`rpcInterface`, not both.
210
211 Leaving {option}`rpcAddress` blank has the same effect as on
212 {option}`listenAddress` (i.e. it will be based on the configured hostname
213 of the node).
214
215 Note that unlike {option}`listenAddress`, you can specify `"0.0.0.0"`, but you
216 must also set `extraConfig.broadcast_rpc_address` to a value other
217 than `"0.0.0.0"`.
218
219 For security reasons, you should not expose this port to the
220 internet. Firewall it if needed.
221 '';
222 };
223
224 rpcInterface = mkOption {
225 type = types.nullOr types.str;
226 default = null;
227 example = "eth1";
228 description = ''
229 Set {option}`rpcAddress` OR {option}`rpcInterface`, not both. Interfaces must
230 correspond to a single address, IP aliasing is not supported.
231 '';
232 };
233
234 logbackConfig = mkOption {
235 type = types.lines;
236 default = ''
237 <configuration scan="false">
238 <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
239 <encoder>
240 <pattern>%-5level %date{HH:mm:ss,SSS} %msg%n</pattern>
241 </encoder>
242 </appender>
243
244 <root level="INFO">
245 <appender-ref ref="STDOUT" />
246 </root>
247
248 <logger name="com.thinkaurelius.thrift" level="ERROR"/>
249 </configuration>
250 '';
251 description = ''
252 XML logback configuration for cassandra
253 '';
254 };
255
256 seedAddresses = mkOption {
257 type = types.listOf types.str;
258 default = [ "127.0.0.1" ];
259 description = ''
260 The addresses of hosts designated as contact points in the cluster. A
261 joining node contacts one of the nodes in the seeds list to learn the
262 topology of the ring.
263 Set to `[ "127.0.0.1" ]` for a single node cluster.
264 '';
265 };
266
267 allowClients = mkOption {
268 type = types.bool;
269 default = true;
270 description = ''
271 Enables or disables the native transport server (CQL binary protocol).
272 This server uses the same address as the {option}`rpcAddress`,
273 but the port it uses is not `rpc_port` but
274 `native_transport_port`. See the official Cassandra
275 docs for more information on these variables and set them using
276 {option}`extraConfig`.
277 '';
278 };
279
280 extraConfig = mkOption {
281 type = types.attrs;
282 default = { };
283 example =
284 {
285 commitlog_sync_batch_window_in_ms = 3;
286 };
287 description = ''
288 Extra options to be merged into {file}`cassandra.yaml` as nix attribute set.
289 '';
290 };
291
292 extraEnvSh = mkOption {
293 type = types.lines;
294 default = "";
295 example = literalExpression ''"CLASSPATH=$CLASSPATH:''${extraJar}"'';
296 description = ''
297 Extra shell lines to be appended onto {file}`cassandra-env.sh`.
298 '';
299 };
300
301 fullRepairInterval = mkOption {
302 type = types.nullOr types.str;
303 default = "3w";
304 example = null;
305 description = ''
306 Set the interval how often full repairs are run, i.e.
307 {command}`nodetool repair --full` is executed. See
308 <https://cassandra.apache.org/doc/latest/operating/repair.html>
309 for more information.
310
311 Set to `null` to disable full repairs.
312 '';
313 };
314
315 fullRepairOptions = mkOption {
316 type = types.listOf types.str;
317 default = [ ];
318 example = [ "--partitioner-range" ];
319 description = ''
320 Options passed through to the full repair command.
321 '';
322 };
323
324 incrementalRepairInterval = mkOption {
325 type = types.nullOr types.str;
326 default = "3d";
327 example = null;
328 description = ''
329 Set the interval how often incremental repairs are run, i.e.
330 {command}`nodetool repair` is executed. See
331 <https://cassandra.apache.org/doc/latest/operating/repair.html>
332 for more information.
333
334 Set to `null` to disable incremental repairs.
335 '';
336 };
337
338 incrementalRepairOptions = mkOption {
339 type = types.listOf types.str;
340 default = [ ];
341 example = [ "--partitioner-range" ];
342 description = ''
343 Options passed through to the incremental repair command.
344 '';
345 };
346
347 maxHeapSize = mkOption {
348 type = types.nullOr types.str;
349 default = null;
350 example = "4G";
351 description = ''
352 Must be left blank or set together with {option}`heapNewSize`.
353 If left blank a sensible value for the available amount of RAM and CPU
354 cores is calculated.
355
356 Override to set the amount of memory to allocate to the JVM at
357 start-up. For production use you may wish to adjust this for your
358 environment. `MAX_HEAP_SIZE` is the total amount of memory dedicated
359 to the Java heap. `HEAP_NEWSIZE` refers to the size of the young
360 generation.
361
362 The main trade-off for the young generation is that the larger it
363 is, the longer GC pause times will be. The shorter it is, the more
364 expensive GC will be (usually).
365 '';
366 };
367
368 heapNewSize = mkOption {
369 type = types.nullOr types.str;
370 default = null;
371 example = "800M";
372 description = ''
373 Must be left blank or set together with {option}`heapNewSize`.
374 If left blank a sensible value for the available amount of RAM and CPU
375 cores is calculated.
376
377 Override to set the amount of memory to allocate to the JVM at
378 start-up. For production use you may wish to adjust this for your
379 environment. `HEAP_NEWSIZE` refers to the size of the young
380 generation.
381
382 The main trade-off for the young generation is that the larger it
383 is, the longer GC pause times will be. The shorter it is, the more
384 expensive GC will be (usually).
385
386 The example `HEAP_NEWSIZE` assumes a modern 8-core+ machine for decent pause
387 times. If in doubt, and if you do not particularly want to tweak, go with
388 100 MB per physical CPU core.
389 '';
390 };
391
392 mallocArenaMax = mkOption {
393 type = types.nullOr types.int;
394 default = null;
395 example = 4;
396 description = ''
397 Set this to control the amount of arenas per-thread in glibc.
398 '';
399 };
400
401 remoteJmx = mkOption {
402 type = types.bool;
403 default = false;
404 description = ''
405 Cassandra ships with JMX accessible *only* from localhost.
406 To enable remote JMX connections set to true.
407
408 Be sure to also enable authentication and/or TLS.
409 See: <https://wiki.apache.org/cassandra/JmxSecurity>
410 '';
411 };
412
413 jmxPort = mkOption {
414 type = types.int;
415 default = 7199;
416 description = ''
417 Specifies the default port over which Cassandra will be available for
418 JMX connections.
419 For security reasons, you should not expose this port to the internet.
420 Firewall it if needed.
421 '';
422 };
423
424 jmxRoles = mkOption {
425 default = [ ];
426 description = ''
427 Roles that are allowed to access the JMX (e.g. {command}`nodetool`)
428 BEWARE: The passwords will be stored world readable in the nix store.
429 It's recommended to use your own protected file using
430 {option}`jmxRolesFile`
431
432 Doesn't work in versions older than 3.11 because they don't like that
433 it's world readable.
434 '';
435 type = types.listOf (types.submodule {
436 options = {
437 username = mkOption {
438 type = types.str;
439 description = "Username for JMX";
440 };
441 password = mkOption {
442 type = types.str;
443 description = "Password for JMX";
444 };
445 };
446 });
447 };
448
449 jmxRolesFile = mkOption {
450 type = types.nullOr types.path;
451 default =
452 if atLeast3_11
453 then pkgs.writeText "jmx-roles-file" defaultJmxRolesFile
454 else null;
455 defaultText = literalMD ''generated configuration file if version is at least 3.11, otherwise `null`'';
456 example = "/var/lib/cassandra/jmx.password";
457 description = ''
458 Specify your own jmx roles file.
459
460 Make sure the permissions forbid "others" from reading the file if
461 you're using Cassandra below version 3.11.
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> or <literal>jmxRolesFile</literal> if
485 using Cassandra older than v3.11.
486 '';
487 }
488 ];
489 users = mkIf (cfg.user == defaultUser) {
490 users.${defaultUser} = {
491 group = cfg.group;
492 home = cfg.homeDir;
493 createHome = true;
494 uid = config.ids.uids.cassandra;
495 description = "Cassandra service user";
496 };
497 groups.${defaultUser}.gid = config.ids.gids.cassandra;
498 };
499
500 systemd.services.cassandra = {
501 description = "Apache Cassandra service";
502 after = [ "network.target" ];
503 environment = commonEnv // {
504 JVM_OPTS = builtins.concatStringsSep " " fullJvmOptions;
505 MAX_HEAP_SIZE = toString cfg.maxHeapSize;
506 HEAP_NEWSIZE = toString cfg.heapNewSize;
507 MALLOC_ARENA_MAX = toString cfg.mallocArenaMax;
508 LOCAL_JMX = if cfg.remoteJmx then "no" else "yes";
509 JMX_PORT = toString cfg.jmxPort;
510 };
511 wantedBy = [ "multi-user.target" ];
512 serviceConfig = {
513 User = cfg.user;
514 Group = cfg.group;
515 ExecStart = "${cfg.package}/bin/cassandra -f";
516 SuccessExitStatus = 143;
517 };
518 };
519
520 systemd.services.cassandra-full-repair = {
521 description = "Perform a full repair on this Cassandra node";
522 after = [ "cassandra.service" ];
523 requires = [ "cassandra.service" ];
524 environment = commonEnv;
525 serviceConfig = {
526 User = cfg.user;
527 Group = cfg.group;
528 ExecStart =
529 concatStringsSep " "
530 ([
531 "${cfg.package}/bin/nodetool"
532 "repair"
533 "--full"
534 ] ++ cfg.fullRepairOptions);
535 };
536 };
537
538 systemd.timers.cassandra-full-repair =
539 mkIf (cfg.fullRepairInterval != null) {
540 description = "Schedule full repairs on Cassandra";
541 wantedBy = [ "timers.target" ];
542 timerConfig = {
543 OnBootSec = cfg.fullRepairInterval;
544 OnUnitActiveSec = cfg.fullRepairInterval;
545 Persistent = true;
546 };
547 };
548
549 systemd.services.cassandra-incremental-repair = {
550 description = "Perform an incremental repair on this cassandra node.";
551 after = [ "cassandra.service" ];
552 requires = [ "cassandra.service" ];
553 environment = commonEnv;
554 serviceConfig = {
555 User = cfg.user;
556 Group = cfg.group;
557 ExecStart =
558 concatStringsSep " "
559 ([
560 "${cfg.package}/bin/nodetool"
561 "repair"
562 ] ++ cfg.incrementalRepairOptions);
563 };
564 };
565
566 systemd.timers.cassandra-incremental-repair =
567 mkIf (cfg.incrementalRepairInterval != null) {
568 description = "Schedule incremental repairs on Cassandra";
569 wantedBy = [ "timers.target" ];
570 timerConfig = {
571 OnBootSec = cfg.incrementalRepairInterval;
572 OnUnitActiveSec = cfg.incrementalRepairInterval;
573 Persistent = true;
574 };
575 };
576 };
577
578 meta.maintainers = with lib.maintainers; [ roberth ];
579}