at 23.11-pre 15 kB view raw
1{ config, lib, options, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.exhibitor; 7 opt = options.services.exhibitor; 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 = mkEnableOption (lib.mdDoc "exhibitor server"); 72 73 # See https://github.com/soabase/exhibitor/wiki/Running-Exhibitor for what these mean 74 # General options for any type of config 75 port = mkOption { 76 type = types.port; 77 default = 8080; 78 description = lib.mdDoc '' 79 The port for exhibitor to listen on and communicate with other exhibitors. 80 ''; 81 }; 82 baseDir = mkOption { 83 type = types.str; 84 default = "/var/exhibitor"; 85 description = lib.mdDoc '' 86 Baseline directory for exhibitor runtime config. 87 ''; 88 }; 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. 94 ''; 95 }; 96 hostname = mkOption { 97 type = types.nullOr types.str; 98 description = lib.mdDoc '' 99 Hostname to use and advertise 100 ''; 101 default = null; 102 }; 103 nodeModification = mkOption { 104 type = types.bool; 105 description = lib.mdDoc '' 106 Whether the Explorer UI will allow nodes to be modified (use with caution). 107 ''; 108 default = true; 109 }; 110 configCheckMs = mkOption { 111 type = types.int; 112 description = lib.mdDoc '' 113 Period (ms) to check for shared config updates. 114 ''; 115 default = 30000; 116 }; 117 headingText = mkOption { 118 type = types.nullOr types.str; 119 description = lib.mdDoc '' 120 Extra text to display in UI header 121 ''; 122 default = null; 123 }; 124 jqueryStyle = mkOption { 125 type = types.enum [ "red" "black" "custom" ]; 126 description = lib.mdDoc '' 127 Styling used for the JQuery-based UI. 128 ''; 129 default = "red"; 130 }; 131 logLines = mkOption { 132 type = types.int; 133 description = lib.mdDoc '' 134 Max lines of logging to keep in memory for display. 135 ''; 136 default = 1000; 137 }; 138 servo = mkOption { 139 type = types.bool; 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 143 this data via JMX. 144 ''; 145 default = false; 146 }; 147 timeout = mkOption { 148 type = types.int; 149 description = lib.mdDoc '' 150 Connection timeout (ms) for ZK connections. 151 ''; 152 default = 30000; 153 }; 154 autoManageInstances = mkOption { 155 type = types.bool; 156 description = lib.mdDoc '' 157 Automatically manage ZooKeeper instances in the ensemble 158 ''; 159 default = false; 160 }; 161 zkDataDir = mkOption { 162 type = types.str; 163 default = "${cfg.baseDir}/zkData"; 164 defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkData"''; 165 description = lib.mdDoc '' 166 The Zookeeper data directory 167 ''; 168 }; 169 zkLogDir = mkOption { 170 type = types.path; 171 default = "${cfg.baseDir}/zkLogs"; 172 defaultText = literalExpression ''"''${config.${opt.baseDir}}/zkLogs"''; 173 description = lib.mdDoc '' 174 The Zookeeper logs directory 175 ''; 176 }; 177 extraConf = mkOption { 178 type = types.str; 179 default = ""; 180 description = lib.mdDoc '' 181 Extra Exhibitor configuration to put in the ZooKeeper config file. 182 ''; 183 }; 184 zkExtraCfg = mkOption { 185 type = types.str; 186 default = "initLimit=5&syncLimit=2&tickTime=2000"; 187 description = lib.mdDoc '' 188 Extra options to pass into Zookeeper 189 ''; 190 }; 191 zkClientPort = mkOption { 192 type = types.int; 193 default = 2181; 194 description = lib.mdDoc '' 195 Zookeeper client port 196 ''; 197 }; 198 zkConnectPort = mkOption { 199 type = types.int; 200 default = 2888; 201 description = lib.mdDoc '' 202 The port to use for followers to talk to each other. 203 ''; 204 }; 205 zkElectionPort = mkOption { 206 type = types.int; 207 default = 3888; 208 description = lib.mdDoc '' 209 The port for Zookeepers to use for leader election. 210 ''; 211 }; 212 zkCleanupPeriod = mkOption { 213 type = types.int; 214 default = 0; 215 description = lib.mdDoc '' 216 How often (in milliseconds) to run the Zookeeper log cleanup task. 217 ''; 218 }; 219 zkServersSpec = mkOption { 220 type = types.listOf types.str; 221 default = []; 222 description = lib.mdDoc '' 223 Zookeeper server spec for all servers in the ensemble. 224 ''; 225 example = [ "S:1:zk1.example.com" "S:2:zk2.example.com" "S:3:zk3.example.com" "O:4:zk-observer.example.com" ]; 226 }; 227 228 # Backup options 229 s3Backup = mkOption { 230 type = types.bool; 231 default = false; 232 description = lib.mdDoc '' 233 Whether to enable backups to S3 234 ''; 235 }; 236 fileSystemBackup = mkOption { 237 type = types.bool; 238 default = false; 239 description = lib.mdDoc '' 240 Enables file system backup of ZooKeeper log files 241 ''; 242 }; 243 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 249 ''; 250 example = ["host1:2181" "host2:2181"]; 251 }; 252 zkConfigExhibitorPath = mkOption { 253 type = types.str; 254 description = lib.mdDoc '' 255 If the ZooKeeper shared config is also running Exhibitor, the URI path for the REST call 256 ''; 257 default = "/"; 258 }; 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. 265 ''; 266 }; 267 zkConfigPollMs = mkOption { 268 type = types.int; 269 description = lib.mdDoc '' 270 The period in ms to check for changes in the config ensemble 271 ''; 272 default = 10000; 273 }; 274 zkConfigRetry = { 275 sleepMs = mkOption { 276 type = types.int; 277 default = 1000; 278 description = lib.mdDoc '' 279 Retry sleep time connecting to the ZooKeeper config 280 ''; 281 }; 282 retryQuantity = mkOption { 283 type = types.int; 284 default = 3; 285 description = lib.mdDoc '' 286 Retries connecting to the ZooKeeper config 287 ''; 288 }; 289 }; 290 zkConfigZPath = mkOption { 291 type = types.str; 292 description = lib.mdDoc '' 293 The base ZPath that Exhibitor should use 294 ''; 295 example = "/exhibitor/config"; 296 }; 297 298 # Config options for s3 configType 299 s3Config = { 300 bucketName = mkOption { 301 type = types.str; 302 description = lib.mdDoc '' 303 Bucket name to store config 304 ''; 305 }; 306 objectKey = mkOption { 307 type = types.str; 308 description = lib.mdDoc '' 309 S3 key name to store the config 310 ''; 311 }; 312 configPrefix = mkOption { 313 type = types.str; 314 description = lib.mdDoc '' 315 When using AWS S3 shared config files, the prefix to use for values such as locks 316 ''; 317 default = "exhibitor-"; 318 }; 319 }; 320 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 328 ''; 329 default = null; 330 }; 331 s3Region = mkOption { 332 type = types.nullOr types.str; 333 description = lib.mdDoc '' 334 Optional region for S3 calls 335 ''; 336 default = null; 337 }; 338 339 # Config options for file config type 340 fsConfigDir = mkOption { 341 type = types.path; 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. 346 ''; 347 }; 348 fsConfigLockPrefix = mkOption { 349 type = types.str; 350 description = lib.mdDoc '' 351 A prefix for a locking mechanism used in conjunction with fsconfigdir 352 ''; 353 default = "exhibitor-lock-"; 354 }; 355 fsConfigName = mkOption { 356 type = types.str; 357 description = lib.mdDoc '' 358 The name of the file to store config in 359 ''; 360 default = "exhibitor.properties"; 361 }; 362 }; 363 }; 364 365 config = mkIf cfg.enable { 366 systemd.services.exhibitor = { 367 description = "Exhibitor Daemon"; 368 wantedBy = [ "multi-user.target" ]; 369 after = [ "network.target" ]; 370 environment = { 371 ZOO_LOG_DIR = cfg.baseDir; 372 }; 373 serviceConfig = { 374 /*** 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. 380 ***/ 381 ExecStart = '' 382 ${pkgs.exhibitor}/bin/startExhibitor.sh ${cliOptions} 383 ''; 384 User = "zookeeper"; 385 PermissionsStartOnly = true; 386 }; 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 390 # from the nix store 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. 399 preStart = '' 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 409 ''; 410 }; 411 users.users.zookeeper = { 412 uid = config.ids.uids.zookeeper; 413 description = "Zookeeper daemon user"; 414 home = cfg.baseDir; 415 }; 416 }; 417}