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}