1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.exhibitor;
7 exhibitor = cfg.package;
8 exhibitorConfig = ''
9 zookeeper-install-directory=${cfg.baseDir}/zookeeper
10 zookeeper-data-directory=${cfg.zkDataDir}
11 zookeeper-log-directory=${cfg.zkLogDir}
12 zoo-cfg-extra=${cfg.zkExtraCfg}
13 client-port=${toString cfg.zkClientPort}
14 connect-port=${toString cfg.zkConnectPort}
15 election-port=${toString cfg.zkElectionPort}
16 cleanup-period-ms=${toString cfg.zkCleanupPeriod}
17 servers-spec=${concatStringsSep "," cfg.zkServersSpec}
18 auto-manage-instances=${toString cfg.autoManageInstances}
19 ${cfg.extraConf}
20 '';
21 # NB: toString rather than lib.boolToString on cfg.autoManageInstances is intended.
22 # Exhibitor tests if it's an integer not equal to 0, so the empty string (toString false)
23 # will operate in the same fashion as a 0.
24 configDir = pkgs.writeTextDir "exhibitor.properties" exhibitorConfig;
25 cliOptionsCommon = {
26 configtype = cfg.configType;
27 defaultconfig = "${configDir}/exhibitor.properties";
28 port = toString cfg.port;
29 hostname = cfg.hostname;
30 headingtext = if (cfg.headingText != null) then (lib.escapeShellArg cfg.headingText) else null;
31 nodemodification = lib.boolToString cfg.nodeModification;
32 configcheckms = toString cfg.configCheckMs;
33 jquerystyle = cfg.jqueryStyle;
34 loglines = toString cfg.logLines;
35 servo = lib.boolToString cfg.servo;
36 timeout = toString cfg.timeout;
37 };
38 s3CommonOptions = { s3region = cfg.s3Region; s3credentials = cfg.s3Credentials; };
39 cliOptionsPerConfig = {
40 s3 = {
41 s3config = "${cfg.s3Config.bucketName}:${cfg.s3Config.objectKey}";
42 s3configprefix = cfg.s3Config.configPrefix;
43 };
44 zookeeper = {
45 zkconfigconnect = concatStringsSep "," cfg.zkConfigConnect;
46 zkconfigexhibitorpath = cfg.zkConfigExhibitorPath;
47 zkconfigpollms = toString cfg.zkConfigPollMs;
48 zkconfigretry = "${toString cfg.zkConfigRetry.sleepMs}:${toString cfg.zkConfigRetry.retryQuantity}";
49 zkconfigzpath = cfg.zkConfigZPath;
50 zkconfigexhibitorport = toString cfg.zkConfigExhibitorPort; # NB: This might be null
51 };
52 file = {
53 fsconfigdir = cfg.fsConfigDir;
54 fsconfiglockprefix = cfg.fsConfigLockPrefix;
55 fsConfigName = fsConfigName;
56 };
57 none = {
58 noneconfigdir = configDir;
59 };
60 };
61 cliOptions = concatStringsSep " " (mapAttrsToList (k: v: "--${k} ${v}") (filterAttrs (k: v: v != null && v != "") (cliOptionsCommon //
62 cliOptionsPerConfig."${cfg.configType}" //
63 s3CommonOptions //
64 optionalAttrs cfg.s3Backup { s3backup = "true"; } //
65 optionalAttrs cfg.fileSystemBackup { filesystembackup = "true"; }
66 )));
67in
68{
69 options = {
70 services.exhibitor = {
71 enable = mkOption {
72 type = types.bool;
73 default = false;
74 description = "
75 Whether to enable the exhibitor server.
76 ";
77 };
78 # See https://github.com/soabase/exhibitor/wiki/Running-Exhibitor for what these mean
79 # General options for any type of config
80 port = mkOption {
81 type = types.int;
82 default = 8080;
83 description = ''
84 The port for exhibitor to listen on and communicate with other exhibitors.
85 '';
86 };
87 baseDir = mkOption {
88 type = types.str;
89 default = "/var/exhibitor";
90 description = ''
91 Baseline directory for exhibitor runtime config.
92 '';
93 };
94 configType = mkOption {
95 type = types.enum [ "file" "s3" "zookeeper" "none" ];
96 description = ''
97 Which configuration type you want to use. Additional config will be
98 required depending on which type you are using.
99 '';
100 };
101 hostname = mkOption {
102 type = types.nullOr types.str;
103 description = ''
104 Hostname to use and advertise
105 '';
106 default = null;
107 };
108 nodeModification = mkOption {
109 type = types.bool;
110 description = ''
111 Whether the Explorer UI will allow nodes to be modified (use with caution).
112 '';
113 default = true;
114 };
115 configCheckMs = mkOption {
116 type = types.int;
117 description = ''
118 Period (ms) to check for shared config updates.
119 '';
120 default = 30000;
121 };
122 headingText = mkOption {
123 type = types.nullOr types.str;
124 description = ''
125 Extra text to display in UI header
126 '';
127 default = null;
128 };
129 jqueryStyle = mkOption {
130 type = types.enum [ "red" "black" "custom" ];
131 description = ''
132 Styling used for the JQuery-based UI.
133 '';
134 default = "red";
135 };
136 logLines = mkOption {
137 type = types.int;
138 description = ''
139 Max lines of logging to keep in memory for display.
140 '';
141 default = 1000;
142 };
143 servo = mkOption {
144 type = types.bool;
145 description = ''
146 ZooKeeper will be queried once a minute for its state via the 'mntr' four
147 letter word (this requires ZooKeeper 3.4.x+). Servo will be used to publish
148 this data via JMX.
149 '';
150 default = false;
151 };
152 timeout = mkOption {
153 type = types.int;
154 description = ''
155 Connection timeout (ms) for ZK connections.
156 '';
157 default = 30000;
158 };
159 autoManageInstances = mkOption {
160 type = types.bool;
161 description = ''
162 Automatically manage ZooKeeper instances in the ensemble
163 '';
164 default = false;
165 };
166 zkDataDir = mkOption {
167 type = types.str;
168 default = "${cfg.baseDir}/zkData";
169 description = ''
170 The Zookeeper data directory
171 '';
172 };
173 zkLogDir = mkOption {
174 type = types.path;
175 default = "${cfg.baseDir}/zkLogs";
176 description = ''
177 The Zookeeper logs directory
178 '';
179 };
180 extraConf = mkOption {
181 type = types.str;
182 default = "";
183 description = ''
184 Extra Exhibitor configuration to put in the ZooKeeper config file.
185 '';
186 };
187 zkExtraCfg = mkOption {
188 type = types.str;
189 default = ''initLimit=5&syncLimit=2&tickTime=2000'';
190 description = ''
191 Extra options to pass into Zookeeper
192 '';
193 };
194 zkClientPort = mkOption {
195 type = types.int;
196 default = 2181;
197 description = ''
198 Zookeeper client port
199 '';
200 };
201 zkConnectPort = mkOption {
202 type = types.int;
203 default = 2888;
204 description = ''
205 The port to use for followers to talk to each other.
206 '';
207 };
208 zkElectionPort = mkOption {
209 type = types.int;
210 default = 3888;
211 description = ''
212 The port for Zookeepers to use for leader election.
213 '';
214 };
215 zkCleanupPeriod = mkOption {
216 type = types.int;
217 default = 0;
218 description = ''
219 How often (in milliseconds) to run the Zookeeper log cleanup task.
220 '';
221 };
222 zkServersSpec = mkOption {
223 type = types.listOf types.str;
224 default = [];
225 description = ''
226 Zookeeper server spec for all servers in the ensemble.
227 '';
228 example = [ "S:1:zk1.example.com" "S:2:zk2.example.com" "S:3:zk3.example.com" "O:4:zk-observer.example.com" ];
229 };
230
231 # Backup options
232 s3Backup = mkOption {
233 type = types.bool;
234 default = false;
235 description = ''
236 Whether to enable backups to S3
237 '';
238 };
239 fileSystemBackup = mkOption {
240 type = types.bool;
241 default = false;
242 description = ''
243 Enables file system backup of ZooKeeper log files
244 '';
245 };
246
247 # Options for using zookeeper configType
248 zkConfigConnect = mkOption {
249 type = types.listOf types.str;
250 description = ''
251 The initial connection string for ZooKeeper shared config storage
252 '';
253 example = ["host1:2181" "host2:2181"];
254 };
255 zkConfigExhibitorPath = mkOption {
256 type = types.string;
257 description = ''
258 If the ZooKeeper shared config is also running Exhibitor, the URI path for the REST call
259 '';
260 default = "/";
261 };
262 zkConfigExhibitorPort = mkOption {
263 type = types.nullOr types.int;
264 description = ''
265 If the ZooKeeper shared config is also running Exhibitor, the port that
266 Exhibitor is listening on. IMPORTANT: if this value is not set it implies
267 that Exhibitor is not being used on the ZooKeeper shared config.
268 '';
269 };
270 zkConfigPollMs = mkOption {
271 type = types.int;
272 description = ''
273 The period in ms to check for changes in the config ensemble
274 '';
275 default = 10000;
276 };
277 zkConfigRetry = {
278 sleepMs = mkOption {
279 type = types.int;
280 default = 1000;
281 description = ''
282 Retry sleep time connecting to the ZooKeeper config
283 '';
284 };
285 retryQuantity = mkOption {
286 type = types.int;
287 default = 3;
288 description = ''
289 Retries connecting to the ZooKeeper config
290 '';
291 };
292 };
293 zkConfigZPath = mkOption {
294 type = types.str;
295 description = ''
296 The base ZPath that Exhibitor should use
297 '';
298 example = "/exhibitor/config";
299 };
300
301 # Config options for s3 configType
302 s3Config = {
303 bucketName = mkOption {
304 type = types.str;
305 description = ''
306 Bucket name to store config
307 '';
308 };
309 objectKey = mkOption {
310 type = types.str;
311 description = ''
312 S3 key name to store the config
313 '';
314 };
315 configPrefix = mkOption {
316 type = types.str;
317 description = ''
318 When using AWS S3 shared config files, the prefix to use for values such as locks
319 '';
320 default = "exhibitor-";
321 };
322 };
323
324 # The next two are used for either s3backup or s3 configType
325 s3Credentials = mkOption {
326 type = types.nullOr types.path;
327 description = ''
328 Optional credentials to use for s3backup or s3config. Argument is the path
329 to an AWS credential properties file with two properties:
330 com.netflix.exhibitor.s3.access-key-id and com.netflix.exhibitor.s3.access-secret-key
331 '';
332 default = null;
333 };
334 s3Region = mkOption {
335 type = types.nullOr types.str;
336 description = ''
337 Optional region for S3 calls
338 '';
339 default = null;
340 };
341
342 # Config options for file config type
343 fsConfigDir = mkOption {
344 type = types.path;
345 description = ''
346 Directory to store Exhibitor properties (cannot be used with s3config).
347 Exhibitor uses file system locks so you can specify a shared location
348 so as to enable complete ensemble management.
349 '';
350 };
351 fsConfigLockPrefix = mkOption {
352 type = types.str;
353 description = ''
354 A prefix for a locking mechanism used in conjunction with fsconfigdir
355 '';
356 default = "exhibitor-lock-";
357 };
358 fsConfigName = mkOption {
359 type = types.str;
360 description = ''
361 The name of the file to store config in
362 '';
363 default = "exhibitor.properties";
364 };
365 };
366 };
367
368 config = mkIf cfg.enable {
369 systemd.services.exhibitor = {
370 description = "Exhibitor Daemon";
371 wantedBy = [ "multi-user.target" ];
372 after = [ "network.target" ];
373 environment = {
374 ZOO_LOG_DIR = cfg.baseDir;
375 };
376 serviceConfig = {
377 /***
378 Exhibitor is a bit un-nixy. It wants to present to you a user interface in order to
379 mutate the configuration of both itself and ZooKeeper, and to coordinate changes
380 among the members of the Zookeeper ensemble. I'm going for a different approach here,
381 which is to manage all the configuration via nix and have it write out the configuration
382 files that exhibitor will use, and to reduce the amount of inter-exhibitor orchestration.
383 ***/
384 ExecStart = ''
385 ${pkgs.exhibitor}/bin/startExhibitor.sh ${cliOptions}
386 '';
387 User = "zookeeper";
388 PermissionsStartOnly = true;
389 };
390 # This is a bit wonky, but the reason for this is that Exhibitor tries to write to
391 # ${cfg.baseDir}/zookeeper/bin/../conf/zoo.cfg
392 # I want everything but the conf directory to be in the immutable nix store, and I want defaults
393 # from the nix store
394 # If I symlink the bin directory in, then bin/../ will resolve to the parent of the symlink in the
395 # immutable nix store. Bind mounting a writable conf over the existing conf might work, but it gets very
396 # messy with trying to copy the existing out into a mutable store.
397 # Another option is to try to patch upstream exhibitor, but the current package just pulls down the
398 # prebuild JARs off of Maven, rather than building them ourselves, as Maven support in Nix isn't
399 # very mature. So, it seems like a reasonable compromise is to just copy out of the immutable store
400 # just before starting the service, so we're running binaries from the immutable store, but we work around
401 # Exhibitor's desire to mutate its current installation.
402 preStart = ''
403 mkdir -m 0700 -p ${cfg.baseDir}/zookeeper
404 # Not doing a chown -R to keep the base ZK files owned by root
405 chown zookeeper ${cfg.baseDir} ${cfg.baseDir}/zookeeper
406 cp -Rf ${pkgs.zookeeper}/* ${cfg.baseDir}/zookeeper
407 chown -R zookeeper ${cfg.baseDir}/zookeeper/conf
408 chmod -R u+w ${cfg.baseDir}/zookeeper/conf
409 '';
410 };
411 users.extraUsers = singleton {
412 name = "zookeeper";
413 uid = config.ids.uids.zookeeper;
414 description = "Zookeeper daemon user";
415 home = cfg.baseDir;
416 };
417 };
418}