1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.cassandra;
7 defaultUser = "cassandra";
8 cassandraConfig = flip recursiveUpdate cfg.extraConfig
9 ({ commitlog_sync = "batch";
10 commitlog_sync_batch_window_in_ms = 2;
11 partitioner = "org.apache.cassandra.dht.Murmur3Partitioner";
12 endpoint_snitch = "SimpleSnitch";
13 seed_provider =
14 [{ class_name = "org.apache.cassandra.locator.SimpleSeedProvider";
15 parameters = [ { seeds = "127.0.0.1"; } ];
16 }];
17 data_file_directories = [ "${cfg.homeDir}/data" ];
18 commitlog_directory = "${cfg.homeDir}/commitlog";
19 saved_caches_directory = "${cfg.homeDir}/saved_caches";
20 } // (if builtins.compareVersions cfg.package.version "3" >= 0
21 then { hints_directory = "${cfg.homeDir}/hints"; }
22 else {})
23 );
24 cassandraConfigWithAddresses = cassandraConfig //
25 ( if isNull cfg.listenAddress
26 then { listen_interface = cfg.listenInterface; }
27 else { listen_address = cfg.listenAddress; }
28 ) // (
29 if isNull cfg.rpcAddress
30 then { rpc_interface = cfg.rpcInterface; }
31 else { rpc_address = cfg.rpcAddress; }
32 );
33 cassandraEtc = pkgs.stdenv.mkDerivation
34 { name = "cassandra-etc";
35 cassandraYaml = builtins.toJSON cassandraConfigWithAddresses;
36 cassandraEnvPkg = "${cfg.package}/conf/cassandra-env.sh";
37 buildCommand = ''
38 mkdir -p "$out"
39
40 echo "$cassandraYaml" > "$out/cassandra.yaml"
41 ln -s "$cassandraEnvPkg" "$out/cassandra-env.sh"
42 '';
43 };
44in {
45 options.services.cassandra = {
46 enable = mkEnableOption ''
47 Apache Cassandra – Scalable and highly available database.
48 '';
49 user = mkOption {
50 type = types.str;
51 default = defaultUser;
52 description = "Run Apache Cassandra under this user.";
53 };
54 group = mkOption {
55 type = types.str;
56 default = defaultUser;
57 description = "Run Apache Cassandra under this group.";
58 };
59 homeDir = mkOption {
60 type = types.path;
61 default = "/var/lib/cassandra";
62 description = ''
63 Home directory for Apache Cassandra.
64 '';
65 };
66 package = mkOption {
67 type = types.package;
68 default = pkgs.cassandra;
69 defaultText = "pkgs.cassandra";
70 example = literalExample "pkgs.cassandra_3_11";
71 description = ''
72 The Apache Cassandra package to use.
73 '';
74 };
75 jvmOpts = mkOption {
76 type = types.listOf types.str;
77 default = [];
78 description = ''
79 Populate the JVM_OPT environment variable.
80 '';
81 };
82 listenAddress = mkOption {
83 type = types.nullOr types.str;
84 default = "127.0.0.1";
85 example = literalExample "null";
86 description = ''
87 Address or interface to bind to and tell other Cassandra nodes
88 to connect to. You _must_ change this if you want multiple
89 nodes to be able to communicate!
90
91 Set listenAddress OR listenInterface, not both.
92
93 Leaving it blank leaves it up to
94 InetAddress.getLocalHost(). This will always do the Right
95 Thing _if_ the node is properly configured (hostname, name
96 resolution, etc), and the Right Thing is to use the address
97 associated with the hostname (it might not be).
98
99 Setting listen_address to 0.0.0.0 is always wrong.
100 '';
101 };
102 listenInterface = mkOption {
103 type = types.nullOr types.str;
104 default = null;
105 example = "eth1";
106 description = ''
107 Set listenAddress OR listenInterface, not both. Interfaces
108 must correspond to a single address, IP aliasing is not
109 supported.
110 '';
111 };
112 rpcAddress = mkOption {
113 type = types.nullOr types.str;
114 default = "127.0.0.1";
115 example = literalExample "null";
116 description = ''
117 The address or interface to bind the native transport server to.
118
119 Set rpcAddress OR rpcInterface, not both.
120
121 Leaving rpcAddress blank has the same effect as on
122 listenAddress (i.e. it will be based on the configured hostname
123 of the node).
124
125 Note that unlike listenAddress, you can specify 0.0.0.0, but you
126 must also set extraConfig.broadcast_rpc_address to a value other
127 than 0.0.0.0.
128
129 For security reasons, you should not expose this port to the
130 internet. Firewall it if needed.
131 '';
132 };
133 rpcInterface = mkOption {
134 type = types.nullOr types.str;
135 default = null;
136 example = "eth1";
137 description = ''
138 Set rpcAddress OR rpcInterface, not both. Interfaces must
139 correspond to a single address, IP aliasing is not supported.
140 '';
141 };
142
143 extraConfig = mkOption {
144 type = types.attrs;
145 default = {};
146 example =
147 { commitlog_sync_batch_window_in_ms = 3;
148 };
149 description = ''
150 Extra options to be merged into cassandra.yaml as nix attribute set.
151 '';
152 };
153 fullRepairInterval = mkOption {
154 type = types.nullOr types.str;
155 default = "3w";
156 example = literalExample "null";
157 description = ''
158 Set the interval how often full repairs are run, i.e.
159 `nodetool repair --full` is executed. See
160 https://cassandra.apache.org/doc/latest/operating/repair.html
161 for more information.
162
163 Set to `null` to disable full repairs.
164 '';
165 };
166 fullRepairOptions = mkOption {
167 type = types.listOf types.str;
168 default = [];
169 example = [ "--partitioner-range" ];
170 description = ''
171 Options passed through to the full repair command.
172 '';
173 };
174 incrementalRepairInterval = mkOption {
175 type = types.nullOr types.str;
176 default = "3d";
177 example = literalExample "null";
178 description = ''
179 Set the interval how often incremental repairs are run, i.e.
180 `nodetool repair` is executed. See
181 https://cassandra.apache.org/doc/latest/operating/repair.html
182 for more information.
183
184 Set to `null` to disable incremental repairs.
185 '';
186 };
187 incrementalRepairOptions = mkOption {
188 type = types.listOf types.string;
189 default = [];
190 example = [ "--partitioner-range" ];
191 description = ''
192 Options passed through to the incremental repair command.
193 '';
194 };
195 };
196
197 config = mkIf cfg.enable {
198 assertions =
199 [ { assertion =
200 ((isNull cfg.listenAddress)
201 || (isNull cfg.listenInterface)
202 ) && !((isNull cfg.listenAddress)
203 && (isNull cfg.listenInterface)
204 );
205 message = "You have to set either listenAddress or listenInterface";
206 }
207 { assertion =
208 ((isNull cfg.rpcAddress)
209 || (isNull cfg.rpcInterface)
210 ) && !((isNull cfg.rpcAddress)
211 && (isNull cfg.rpcInterface)
212 );
213 message = "You have to set either rpcAddress or rpcInterface";
214 }
215 ];
216 users = mkIf (cfg.user == defaultUser) {
217 extraUsers."${defaultUser}" =
218 { group = cfg.group;
219 home = cfg.homeDir;
220 createHome = true;
221 uid = config.ids.uids.cassandra;
222 description = "Cassandra service user";
223 };
224 extraGroups."${defaultUser}".gid = config.ids.gids.cassandra;
225 };
226
227 systemd.services.cassandra =
228 { description = "Apache Cassandra service";
229 after = [ "network.target" ];
230 environment =
231 { CASSANDRA_CONF = "${cassandraEtc}";
232 JVM_OPTS = builtins.concatStringsSep " " cfg.jvmOpts;
233 };
234 wantedBy = [ "multi-user.target" ];
235 serviceConfig =
236 { User = cfg.user;
237 Group = cfg.group;
238 ExecStart = "${cfg.package}/bin/cassandra -f";
239 SuccessExitStatus = 143;
240 };
241 };
242
243 systemd.services.cassandra-full-repair =
244 { description = "Perform a full repair on this Cassandra node";
245 after = [ "cassandra.service" ];
246 requires = [ "cassandra.service" ];
247 serviceConfig =
248 { User = cfg.user;
249 Group = cfg.group;
250 ExecStart =
251 lib.concatStringsSep " "
252 ([ "${cfg.package}/bin/nodetool" "repair" "--full"
253 ] ++ cfg.fullRepairOptions);
254 };
255 };
256 systemd.timers.cassandra-full-repair =
257 mkIf (!isNull cfg.fullRepairInterval) {
258 description = "Schedule full repairs on Cassandra";
259 wantedBy = [ "timers.target" ];
260 timerConfig =
261 { OnBootSec = cfg.fullRepairInterval;
262 OnUnitActiveSec = cfg.fullRepairInterval;
263 Persistent = true;
264 };
265 };
266
267 systemd.services.cassandra-incremental-repair =
268 { description = "Perform an incremental repair on this cassandra node.";
269 after = [ "cassandra.service" ];
270 requires = [ "cassandra.service" ];
271 serviceConfig =
272 { User = cfg.user;
273 Group = cfg.group;
274 ExecStart =
275 lib.concatStringsSep " "
276 ([ "${cfg.package}/bin/nodetool" "repair"
277 ] ++ cfg.incrementalRepairOptions);
278 };
279 };
280 systemd.timers.cassandra-incremental-repair =
281 mkIf (!isNull cfg.incrementalRepairInterval) {
282 description = "Schedule incremental repairs on Cassandra";
283 wantedBy = [ "timers.target" ];
284 timerConfig =
285 { OnBootSec = cfg.incrementalRepairInterval;
286 OnUnitActiveSec = cfg.incrementalRepairInterval;
287 Persistent = true;
288 };
289 };
290 };
291}