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