1 { config, lib, options, pkgs, ... }:
6 cfg = config.services.exhibitor;
7 opt = options.services.exhibitor;
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}
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;
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;
38 s3CommonOptions = { s3region = cfg.s3Region; s3credentials = cfg.s3Credentials; };
39 cliOptionsPerConfig = {
41 s3config = "${cfg.s3Config.bucketName}:${cfg.s3Config.objectKey}";
42 s3configprefix = cfg.s3Config.configPrefix;
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
53 fsconfigdir = cfg.fsConfigDir;
54 fsconfiglockprefix = cfg.fsConfigLockPrefix;
55 fsConfigName = fsConfigName;
58 noneconfigdir = configDir;
61 cliOptions = concatStringsSep " " (mapAttrsToList (k: v: "--${k} ${v}") (filterAttrs (k: v: v != null && v != "") (cliOptionsCommon //
62 cliOptionsPerConfig.${cfg.configType} //
64 optionalAttrs cfg.s3Backup { s3backup = "true"; } //
65 optionalAttrs cfg.fileSystemBackup { filesystembackup = "true"; }
70 services.exhibitor = {
71 enable = mkEnableOption (lib.mdDoc "exhibitor server");
73 # See https://github.com/soabase/exhibitor/wiki/Running-Exhibitor for what these mean
74 # General options for any type of config
78 description = lib.mdDoc ''
79 The port for exhibitor to listen on and communicate with other exhibitors.
84 default = "/var/exhibitor";
85 description = lib.mdDoc ''
86 Baseline directory for exhibitor runtime config.
89 configType = mkOption {
90 type = types.enum [ "file" "s3" "zookeeper" "none" ];
91 description = lib.mdDoc ''
92 Which configuration type you want to use. Additional config will be
93 required depending on which type you are using.
97 type = types.nullOr types.str;
98 description = lib.mdDoc ''
99 Hostname to use and advertise
103 nodeModification = mkOption {
105 description = lib.mdDoc ''
106 Whether the Explorer UI will allow nodes to be modified (use with caution).
110 configCheckMs = mkOption {
112 description = lib.mdDoc ''
113 Period (ms) to check for shared config updates.
117 headingText = mkOption {
118 type = types.nullOr types.str;
119 description = lib.mdDoc ''
120 Extra text to display in UI header
124 jqueryStyle = mkOption {
125 type = types.enum [ "red" "black" "custom" ];
126 description = lib.mdDoc ''
127 Styling used for the JQuery-based UI.
131 logLines = mkOption {
133 description = lib.mdDoc ''
134 Max lines of logging to keep in memory for display.
140 description = lib.mdDoc ''
141 ZooKeeper will be queried once a minute for its state via the 'mntr' four
142 letter word (this requires ZooKeeper 3.4.x+). Servo will be used to publish
149 description = lib.mdDoc ''
150 Connection timeout (ms) for ZK connections.
154 autoManageInstances = mkOption {
156 description = lib.mdDoc ''
157 Automatically manage ZooKeeper instances in the ensemble
161 zkDataDir = mkOption {
163 default = "${cfg.baseDir}/zkData";
164 defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkData"'';
165 description = lib.mdDoc ''
166 The Zookeeper data directory
169 zkLogDir = mkOption {
171 default = "${cfg.baseDir}/zkLogs";
172 defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkLogs"'';
173 description = lib.mdDoc ''
174 The Zookeeper logs directory
177 extraConf = mkOption {
180 description = lib.mdDoc ''
181 Extra Exhibitor configuration to put in the ZooKeeper config file.
184 zkExtraCfg = mkOption {
186 default = "initLimit=5&syncLimit=2&tickTime=2000";
187 description = lib.mdDoc ''
188 Extra options to pass into Zookeeper
191 zkClientPort = mkOption {
194 description = lib.mdDoc ''
195 Zookeeper client port
198 zkConnectPort = mkOption {
201 description = lib.mdDoc ''
202 The port to use for followers to talk to each other.
205 zkElectionPort = mkOption {
208 description = lib.mdDoc ''
209 The port for Zookeepers to use for leader election.
212 zkCleanupPeriod = mkOption {
215 description = lib.mdDoc ''
216 How often (in milliseconds) to run the Zookeeper log cleanup task.
219 zkServersSpec = mkOption {
220 type = types.listOf types.str;
222 description = lib.mdDoc ''
223 Zookeeper server spec for all servers in the ensemble.
225 example = [ "S:1:zk1.example.com" "S:2:zk2.example.com" "S:3:zk3.example.com" "O:4:zk-observer.example.com" ];
229 s3Backup = mkOption {
232 description = lib.mdDoc ''
233 Whether to enable backups to S3
236 fileSystemBackup = mkOption {
239 description = lib.mdDoc ''
240 Enables file system backup of ZooKeeper log files
244 # Options for using zookeeper configType
245 zkConfigConnect = mkOption {
246 type = types.listOf types.str;
247 description = lib.mdDoc ''
248 The initial connection string for ZooKeeper shared config storage
250 example = ["host1:2181" "host2:2181"];
252 zkConfigExhibitorPath = mkOption {
254 description = lib.mdDoc ''
255 If the ZooKeeper shared config is also running Exhibitor, the URI path for the REST call
259 zkConfigExhibitorPort = mkOption {
260 type = types.nullOr types.int;
261 description = lib.mdDoc ''
262 If the ZooKeeper shared config is also running Exhibitor, the port that
263 Exhibitor is listening on. IMPORTANT: if this value is not set it implies
264 that Exhibitor is not being used on the ZooKeeper shared config.
267 zkConfigPollMs = mkOption {
269 description = lib.mdDoc ''
270 The period in ms to check for changes in the config ensemble
278 description = lib.mdDoc ''
279 Retry sleep time connecting to the ZooKeeper config
282 retryQuantity = mkOption {
285 description = lib.mdDoc ''
286 Retries connecting to the ZooKeeper config
290 zkConfigZPath = mkOption {
292 description = lib.mdDoc ''
293 The base ZPath that Exhibitor should use
295 example = "/exhibitor/config";
298 # Config options for s3 configType
300 bucketName = mkOption {
302 description = lib.mdDoc ''
303 Bucket name to store config
306 objectKey = mkOption {
308 description = lib.mdDoc ''
309 S3 key name to store the config
312 configPrefix = mkOption {
314 description = lib.mdDoc ''
315 When using AWS S3 shared config files, the prefix to use for values such as locks
317 default = "exhibitor-";
321 # The next two are used for either s3backup or s3 configType
322 s3Credentials = mkOption {
323 type = types.nullOr types.path;
324 description = lib.mdDoc ''
325 Optional credentials to use for s3backup or s3config. Argument is the path
326 to an AWS credential properties file with two properties:
327 com.netflix.exhibitor.s3.access-key-id and com.netflix.exhibitor.s3.access-secret-key
331 s3Region = mkOption {
332 type = types.nullOr types.str;
333 description = lib.mdDoc ''
334 Optional region for S3 calls
339 # Config options for file config type
340 fsConfigDir = mkOption {
342 description = lib.mdDoc ''
343 Directory to store Exhibitor properties (cannot be used with s3config).
344 Exhibitor uses file system locks so you can specify a shared location
345 so as to enable complete ensemble management.
348 fsConfigLockPrefix = mkOption {
350 description = lib.mdDoc ''
351 A prefix for a locking mechanism used in conjunction with fsconfigdir
353 default = "exhibitor-lock-";
355 fsConfigName = mkOption {
357 description = lib.mdDoc ''
358 The name of the file to store config in
360 default = "exhibitor.properties";
365 config = mkIf cfg.enable {
366 systemd.services.exhibitor = {
367 description = "Exhibitor Daemon";
368 wantedBy = [ "multi-user.target" ];
369 after = [ "network.target" ];
371 ZOO_LOG_DIR = cfg.baseDir;
375 Exhibitor is a bit un-nixy. It wants to present to you a user interface in order to
376 mutate the configuration of both itself and ZooKeeper, and to coordinate changes
377 among the members of the Zookeeper ensemble. I'm going for a different approach here,
378 which is to manage all the configuration via nix and have it write out the configuration
379 files that exhibitor will use, and to reduce the amount of inter-exhibitor orchestration.
382 ${pkgs.exhibitor}/bin/startExhibitor.sh ${cliOptions}
385 PermissionsStartOnly = true;
387 # This is a bit wonky, but the reason for this is that Exhibitor tries to write to
388 # ${cfg.baseDir}/zookeeper/bin/../conf/zoo.cfg
389 # I want everything but the conf directory to be in the immutable nix store, and I want defaults
391 # If I symlink the bin directory in, then bin/../ will resolve to the parent of the symlink in the
392 # immutable nix store. Bind mounting a writable conf over the existing conf might work, but it gets very
393 # messy with trying to copy the existing out into a mutable store.
394 # Another option is to try to patch upstream exhibitor, but the current package just pulls down the
395 # prebuild JARs off of Maven, rather than building them ourselves, as Maven support in Nix isn't
396 # very mature. So, it seems like a reasonable compromise is to just copy out of the immutable store
397 # just before starting the service, so we're running binaries from the immutable store, but we work around
398 # Exhibitor's desire to mutate its current installation.
400 mkdir -m 0700 -p ${cfg.baseDir}/zookeeper
401 # Not doing a chown -R to keep the base ZK files owned by root
402 chown zookeeper ${cfg.baseDir} ${cfg.baseDir}/zookeeper
403 cp -Rf ${pkgs.zookeeper}/* ${cfg.baseDir}/zookeeper
404 chown -R zookeeper ${cfg.baseDir}/zookeeper/conf
405 chmod -R u+w ${cfg.baseDir}/zookeeper/conf
406 replace_what=$(echo ${pkgs.zookeeper} | sed 's/[\/&]/\\&/g')
407 replace_with=$(echo ${cfg.baseDir}/zookeeper | sed 's/[\/&]/\\&/g')
408 sed -i 's/'"$replace_what"'/'"$replace_with"'/g' ${cfg.baseDir}/zookeeper/bin/zk*.sh
411 users.users.zookeeper = {
412 uid = config.ids.uids.zookeeper;
413 description = "Zookeeper daemon user";