grafana-alloy: don't build the frontend twice
[NixPkgs.git] / nixos / modules / services / databases / cassandra.nix
blobc8fce9b939db16fcbd98095358990510ca7dfba2
1 { config, lib, pkgs, ... }:
3 let
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     ;
20   cfg = config.services.cassandra;
22   atLeast3 = versionAtLeast cfg.package.version "3";
23   atLeast3_11 = versionAtLeast cfg.package.version "3.11";
24   atLeast4 = versionAtLeast cfg.package.version "4";
26   defaultUser = "cassandra";
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   );
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   );
61   cassandraEtc = pkgs.stdenv.mkDerivation {
62     name = "cassandra-etc";
64     cassandraYaml = builtins.toJSON cassandraConfigWithAddresses;
65     cassandraEnvPkg = "${cfg.package}/conf/cassandra-env.sh";
66     cassandraLogbackConfig = pkgs.writeText "logback.xml" cfg.logbackConfig;
68     passAsFile = [ "extraEnvSh" ];
69     inherit (cfg) extraEnvSh package;
71     buildCommand = ''
72       mkdir -p "$out"
74       echo "$cassandraYaml" > "$out/cassandra.yaml"
75       ln -s "$cassandraLogbackConfig" "$out/logback.xml"
77       ( cat "$cassandraEnvPkg"
78         echo "# lines from services.cassandra.extraEnvSh: "
79         cat "$extraEnvShPath"
80       ) > "$out/cassandra-env.sh"
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"
85       # Delete default password file
86       sed -i '/-Dcom.sun.management.jmxremote.password.file=\/etc\/cassandra\/jmxremote.password/d' "$out/cassandra-env.sh"
88       ${lib.optionalString atLeast4 ''
89         cp $package/conf/jvm*.options $out/
90       ''}
91     '';
92   };
94   defaultJmxRolesFile =
95     builtins.foldl'
96       (left: right: left + right) ""
97       (map (role: "${role.username} ${role.password}") cfg.jmxRoles);
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     ];
112   commonEnv = {
113     # Sufficient for cassandra 2.x, 3.x
114     CASSANDRA_CONF = "${cassandraEtc}";
116     # Required since cassandra 4
117     CASSANDRA_LOGBACK_CONF = "${cassandraEtc}/logback.xml";
118   };
122   options.services.cassandra = {
124     enable = mkEnableOption ''
125       Apache Cassandra – Scalable and highly available database
126     '';
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     };
138     user = mkOption {
139       type = types.str;
140       default = defaultUser;
141       description = "Run Apache Cassandra under this user.";
142     };
144     group = mkOption {
145       type = types.str;
146       default = defaultUser;
147       description = "Run Apache Cassandra under this group.";
148     };
150     homeDir = mkOption {
151       type = types.path;
152       default = "/var/lib/cassandra";
153       description = ''
154         Home directory for Apache Cassandra.
155       '';
156     };
158     package = mkPackageOption pkgs "cassandra" {
159       example = "cassandra_3_11";
160     };
162     jvmOpts = mkOption {
163       type = types.listOf types.str;
164       default = [ ];
165       description = ''
166         Populate the `JVM_OPT` environment variable.
167       '';
168     };
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!
179         Set {option}`listenAddress` OR {option}`listenInterface`, not both.
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).
187         Setting {option}`listenAddress` to `0.0.0.0` is always wrong.
188       '';
189     };
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     };
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.
209         Set {option}`rpcAddress` OR {option}`rpcInterface`, not both.
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).
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"`.
219         For security reasons, you should not expose this port to the
220         internet. Firewall it if needed.
221       '';
222     };
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     };
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>
244           <root level="INFO">
245             <appender-ref ref="STDOUT" />
246           </root>
248           <logger name="com.thinkaurelius.thrift" level="ERROR"/>
249         </configuration>
250       '';
251       description = ''
252         XML logback configuration for cassandra
253       '';
254     };
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     };
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     };
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     };
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     };
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.
311         Set to `null` to disable full repairs.
312       '';
313     };
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     };
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.
334         Set to `null` to disable incremental repairs.
335       '';
336     };
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     };
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.
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.
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     };
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.
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.
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).
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     };
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     };
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.
408         Be sure to also enable authentication and/or TLS.
409         See: <https://wiki.apache.org/cassandra/JmxSecurity>
410       '';
411     };
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     };
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`
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     };
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.
460         Make sure the permissions forbid "others" from reading the file if
461         you're using Cassandra below version 3.11.
462       '';
463     };
464   };
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     };
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     };
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     };
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       };
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     };
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   };
578   meta.maintainers = with lib.maintainers; [ roberth ];