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