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}