at 23.05-pre 18 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.redis; 7 8 mkValueString = value: 9 if value == true then "yes" 10 else if value == false then "no" 11 else generators.mkValueStringDefault { } value; 12 13 redisConfig = settings: pkgs.writeText "redis.conf" (generators.toKeyValue { 14 listsAsDuplicateKeys = true; 15 mkKeyValue = generators.mkKeyValueDefault { inherit mkValueString; } " "; 16 } settings); 17 18 redisName = name: "redis" + optionalString (name != "") ("-"+name); 19 enabledServers = filterAttrs (name: conf: conf.enable) config.services.redis.servers; 20 21in { 22 imports = [ 23 (mkRemovedOptionModule [ "services" "redis" "user" ] "The redis module now is hardcoded to the redis user.") 24 (mkRemovedOptionModule [ "services" "redis" "dbpath" ] "The redis module now uses /var/lib/redis as data directory.") 25 (mkRemovedOptionModule [ "services" "redis" "dbFilename" ] "The redis module now uses /var/lib/redis/dump.rdb as database dump location.") 26 (mkRemovedOptionModule [ "services" "redis" "appendOnlyFilename" ] "This option was never used.") 27 (mkRemovedOptionModule [ "services" "redis" "pidFile" ] "This option was removed.") 28 (mkRemovedOptionModule [ "services" "redis" "extraConfig" ] "Use services.redis.servers.*.settings instead.") 29 (mkRenamedOptionModule [ "services" "redis" "enable"] [ "services" "redis" "servers" "" "enable" ]) 30 (mkRenamedOptionModule [ "services" "redis" "port"] [ "services" "redis" "servers" "" "port" ]) 31 (mkRenamedOptionModule [ "services" "redis" "openFirewall"] [ "services" "redis" "servers" "" "openFirewall" ]) 32 (mkRenamedOptionModule [ "services" "redis" "bind"] [ "services" "redis" "servers" "" "bind" ]) 33 (mkRenamedOptionModule [ "services" "redis" "unixSocket"] [ "services" "redis" "servers" "" "unixSocket" ]) 34 (mkRenamedOptionModule [ "services" "redis" "unixSocketPerm"] [ "services" "redis" "servers" "" "unixSocketPerm" ]) 35 (mkRenamedOptionModule [ "services" "redis" "logLevel"] [ "services" "redis" "servers" "" "logLevel" ]) 36 (mkRenamedOptionModule [ "services" "redis" "logfile"] [ "services" "redis" "servers" "" "logfile" ]) 37 (mkRenamedOptionModule [ "services" "redis" "syslog"] [ "services" "redis" "servers" "" "syslog" ]) 38 (mkRenamedOptionModule [ "services" "redis" "databases"] [ "services" "redis" "servers" "" "databases" ]) 39 (mkRenamedOptionModule [ "services" "redis" "maxclients"] [ "services" "redis" "servers" "" "maxclients" ]) 40 (mkRenamedOptionModule [ "services" "redis" "save"] [ "services" "redis" "servers" "" "save" ]) 41 (mkRenamedOptionModule [ "services" "redis" "slaveOf"] [ "services" "redis" "servers" "" "slaveOf" ]) 42 (mkRenamedOptionModule [ "services" "redis" "masterAuth"] [ "services" "redis" "servers" "" "masterAuth" ]) 43 (mkRenamedOptionModule [ "services" "redis" "requirePass"] [ "services" "redis" "servers" "" "requirePass" ]) 44 (mkRenamedOptionModule [ "services" "redis" "requirePassFile"] [ "services" "redis" "servers" "" "requirePassFile" ]) 45 (mkRenamedOptionModule [ "services" "redis" "appendOnly"] [ "services" "redis" "servers" "" "appendOnly" ]) 46 (mkRenamedOptionModule [ "services" "redis" "appendFsync"] [ "services" "redis" "servers" "" "appendFsync" ]) 47 (mkRenamedOptionModule [ "services" "redis" "slowLogLogSlowerThan"] [ "services" "redis" "servers" "" "slowLogLogSlowerThan" ]) 48 (mkRenamedOptionModule [ "services" "redis" "slowLogMaxLen"] [ "services" "redis" "servers" "" "slowLogMaxLen" ]) 49 (mkRenamedOptionModule [ "services" "redis" "settings"] [ "services" "redis" "servers" "" "settings" ]) 50 ]; 51 52 ###### interface 53 54 options = { 55 56 services.redis = { 57 package = mkOption { 58 type = types.package; 59 default = pkgs.redis; 60 defaultText = literalExpression "pkgs.redis"; 61 description = lib.mdDoc "Which Redis derivation to use."; 62 }; 63 64 vmOverCommit = mkEnableOption (lib.mdDoc '' 65 setting of vm.overcommit_memory to 1 66 (Suggested for Background Saving: http://redis.io/topics/faq) 67 ''); 68 69 servers = mkOption { 70 type = with types; attrsOf (submodule ({config, name, ...}@args: { 71 options = { 72 enable = mkEnableOption (lib.mdDoc '' 73 Redis server. 74 75 Note that the NixOS module for Redis disables kernel support 76 for Transparent Huge Pages (THP), 77 because this features causes major performance problems for Redis, 78 e.g. (https://redis.io/topics/latency). 79 ''); 80 81 user = mkOption { 82 type = types.str; 83 default = redisName name; 84 defaultText = literalExpression '' 85 if name == "" then "redis" else "redis-''${name}" 86 ''; 87 description = lib.mdDoc "The username and groupname for redis-server."; 88 }; 89 90 port = mkOption { 91 type = types.port; 92 default = if name == "" then 6379 else 0; 93 defaultText = literalExpression ''if name == "" then 6379 else 0''; 94 description = lib.mdDoc '' 95 The TCP port to accept connections. 96 If port 0 is specified Redis will not listen on a TCP socket. 97 ''; 98 }; 99 100 openFirewall = mkOption { 101 type = types.bool; 102 default = false; 103 description = lib.mdDoc '' 104 Whether to open ports in the firewall for the server. 105 ''; 106 }; 107 108 extraParams = mkOption { 109 type = with types; listOf str; 110 default = []; 111 description = lib.mdDoc "Extra parameters to append to redis-server invocation"; 112 example = [ "--sentinel" ]; 113 }; 114 115 bind = mkOption { 116 type = with types; nullOr str; 117 default = "127.0.0.1"; 118 description = lib.mdDoc '' 119 The IP interface to bind to. 120 `null` means "all interfaces". 121 ''; 122 example = "192.0.2.1"; 123 }; 124 125 unixSocket = mkOption { 126 type = with types; nullOr path; 127 default = "/run/${redisName name}/redis.sock"; 128 defaultText = literalExpression '' 129 if name == "" then "/run/redis/redis.sock" else "/run/redis-''${name}/redis.sock" 130 ''; 131 description = lib.mdDoc "The path to the socket to bind to."; 132 }; 133 134 unixSocketPerm = mkOption { 135 type = types.int; 136 default = 660; 137 description = lib.mdDoc "Change permissions for the socket"; 138 example = 600; 139 }; 140 141 logLevel = mkOption { 142 type = types.str; 143 default = "notice"; # debug, verbose, notice, warning 144 example = "debug"; 145 description = lib.mdDoc "Specify the server verbosity level, options: debug, verbose, notice, warning."; 146 }; 147 148 logfile = mkOption { 149 type = types.str; 150 default = "/dev/null"; 151 description = lib.mdDoc "Specify the log file name. Also 'stdout' can be used to force Redis to log on the standard output."; 152 example = "/var/log/redis.log"; 153 }; 154 155 syslog = mkOption { 156 type = types.bool; 157 default = true; 158 description = lib.mdDoc "Enable logging to the system logger."; 159 }; 160 161 databases = mkOption { 162 type = types.int; 163 default = 16; 164 description = lib.mdDoc "Set the number of databases."; 165 }; 166 167 maxclients = mkOption { 168 type = types.int; 169 default = 10000; 170 description = lib.mdDoc "Set the max number of connected clients at the same time."; 171 }; 172 173 save = mkOption { 174 type = with types; listOf (listOf int); 175 default = [ [900 1] [300 10] [60 10000] ]; 176 description = mdDoc '' 177 The schedule in which data is persisted to disk, represented as a list of lists where the first element represent the amount of seconds and the second the number of changes. 178 179 If set to the empty list (`[]`) then RDB persistence will be disabled (useful if you are using AOF or don't want any persistence). 180 ''; 181 }; 182 183 slaveOf = mkOption { 184 type = with types; nullOr (submodule ({ ... }: { 185 options = { 186 ip = mkOption { 187 type = str; 188 description = lib.mdDoc "IP of the Redis master"; 189 example = "192.168.1.100"; 190 }; 191 192 port = mkOption { 193 type = port; 194 description = lib.mdDoc "port of the Redis master"; 195 default = 6379; 196 }; 197 }; 198 })); 199 200 default = null; 201 description = lib.mdDoc "IP and port to which this redis instance acts as a slave."; 202 example = { ip = "192.168.1.100"; port = 6379; }; 203 }; 204 205 masterAuth = mkOption { 206 type = with types; nullOr str; 207 default = null; 208 description = lib.mdDoc ''If the master is password protected (using the requirePass configuration) 209 it is possible to tell the slave to authenticate before starting the replication synchronization 210 process, otherwise the master will refuse the slave request. 211 (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE)''; 212 }; 213 214 requirePass = mkOption { 215 type = with types; nullOr str; 216 default = null; 217 description = lib.mdDoc '' 218 Password for database (STORED PLAIN TEXT, WORLD-READABLE IN NIX STORE). 219 Use requirePassFile to store it outside of the nix store in a dedicated file. 220 ''; 221 example = "letmein!"; 222 }; 223 224 requirePassFile = mkOption { 225 type = with types; nullOr path; 226 default = null; 227 description = lib.mdDoc "File with password for the database."; 228 example = "/run/keys/redis-password"; 229 }; 230 231 appendOnly = mkOption { 232 type = types.bool; 233 default = false; 234 description = lib.mdDoc "By default data is only periodically persisted to disk, enable this option to use an append-only file for improved persistence."; 235 }; 236 237 appendFsync = mkOption { 238 type = types.str; 239 default = "everysec"; # no, always, everysec 240 description = lib.mdDoc "How often to fsync the append-only log, options: no, always, everysec."; 241 }; 242 243 slowLogLogSlowerThan = mkOption { 244 type = types.int; 245 default = 10000; 246 description = lib.mdDoc "Log queries whose execution take longer than X in milliseconds."; 247 example = 1000; 248 }; 249 250 slowLogMaxLen = mkOption { 251 type = types.int; 252 default = 128; 253 description = lib.mdDoc "Maximum number of items to keep in slow log."; 254 }; 255 256 settings = mkOption { 257 # TODO: this should be converted to freeformType 258 type = with types; attrsOf (oneOf [ bool int str (listOf str) ]); 259 default = {}; 260 description = lib.mdDoc '' 261 Redis configuration. Refer to 262 <https://redis.io/topics/config> 263 for details on supported values. 264 ''; 265 example = literalExpression '' 266 { 267 loadmodule = [ "/path/to/my_module.so" "/path/to/other_module.so" ]; 268 } 269 ''; 270 }; 271 }; 272 config.settings = mkMerge [ 273 { 274 port = config.port; 275 daemonize = false; 276 supervised = "systemd"; 277 loglevel = config.logLevel; 278 logfile = config.logfile; 279 syslog-enabled = config.syslog; 280 databases = config.databases; 281 maxclients = config.maxclients; 282 save = if config.save == [] 283 then ''""'' # Disable saving with `save = ""` 284 else map 285 (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") 286 config.save; 287 dbfilename = "dump.rdb"; 288 dir = "/var/lib/${redisName name}"; 289 appendOnly = config.appendOnly; 290 appendfsync = config.appendFsync; 291 slowlog-log-slower-than = config.slowLogLogSlowerThan; 292 slowlog-max-len = config.slowLogMaxLen; 293 } 294 (mkIf (config.bind != null) { bind = config.bind; }) 295 (mkIf (config.unixSocket != null) { 296 unixsocket = config.unixSocket; 297 unixsocketperm = toString config.unixSocketPerm; 298 }) 299 (mkIf (config.slaveOf != null) { slaveof = "${config.slaveOf.ip} ${toString config.slaveOf.port}"; }) 300 (mkIf (config.masterAuth != null) { masterauth = config.masterAuth; }) 301 (mkIf (config.requirePass != null) { requirepass = config.requirePass; }) 302 ]; 303 })); 304 description = lib.mdDoc "Configuration of multiple `redis-server` instances."; 305 default = {}; 306 }; 307 }; 308 309 }; 310 311 312 ###### implementation 313 314 config = mkIf (enabledServers != {}) { 315 316 assertions = attrValues (mapAttrs (name: conf: { 317 assertion = conf.requirePass != null -> conf.requirePassFile == null; 318 message = '' 319 You can only set one services.redis.servers.${name}.requirePass 320 or services.redis.servers.${name}.requirePassFile 321 ''; 322 }) enabledServers); 323 324 boot.kernel.sysctl = mkMerge [ 325 { "vm.nr_hugepages" = "0"; } 326 ( mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; } ) 327 ]; 328 329 networking.firewall.allowedTCPPorts = concatMap (conf: 330 optional conf.openFirewall conf.port 331 ) (attrValues enabledServers); 332 333 environment.systemPackages = [ cfg.package ]; 334 335 users.users = mapAttrs' (name: conf: nameValuePair (redisName name) { 336 description = "System user for the redis-server instance ${name}"; 337 isSystemUser = true; 338 group = redisName name; 339 }) enabledServers; 340 users.groups = mapAttrs' (name: conf: nameValuePair (redisName name) { 341 }) enabledServers; 342 343 systemd.services = mapAttrs' (name: conf: nameValuePair (redisName name) { 344 description = "Redis Server - ${redisName name}"; 345 346 wantedBy = [ "multi-user.target" ]; 347 after = [ "network.target" ]; 348 349 serviceConfig = { 350 ExecStart = "${cfg.package}/bin/redis-server /var/lib/${redisName name}/redis.conf ${escapeShellArgs conf.extraParams}"; 351 ExecStartPre = "+"+pkgs.writeShellScript "${redisName name}-prep-conf" (let 352 redisConfVar = "/var/lib/${redisName name}/redis.conf"; 353 redisConfRun = "/run/${redisName name}/nixos.conf"; 354 redisConfStore = redisConfig conf.settings; 355 in '' 356 touch "${redisConfVar}" "${redisConfRun}" 357 chown '${conf.user}' "${redisConfVar}" "${redisConfRun}" 358 chmod 0600 "${redisConfVar}" "${redisConfRun}" 359 if [ ! -s ${redisConfVar} ]; then 360 echo 'include "${redisConfRun}"' > "${redisConfVar}" 361 fi 362 echo 'include "${redisConfStore}"' > "${redisConfRun}" 363 ${optionalString (conf.requirePassFile != null) '' 364 { 365 echo -n "requirepass " 366 cat ${escapeShellArg conf.requirePassFile} 367 } >> "${redisConfRun}" 368 ''} 369 ''); 370 Type = "notify"; 371 # User and group 372 User = conf.user; 373 Group = conf.user; 374 # Runtime directory and mode 375 RuntimeDirectory = redisName name; 376 RuntimeDirectoryMode = "0750"; 377 # State directory and mode 378 StateDirectory = redisName name; 379 StateDirectoryMode = "0700"; 380 # Access write directories 381 UMask = "0077"; 382 # Capabilities 383 CapabilityBoundingSet = ""; 384 # Security 385 NoNewPrivileges = true; 386 # Process Properties 387 LimitNOFILE = mkDefault "${toString (conf.maxclients + 32)}"; 388 # Sandboxing 389 ProtectSystem = "strict"; 390 ProtectHome = true; 391 PrivateTmp = true; 392 PrivateDevices = true; 393 PrivateUsers = true; 394 ProtectClock = true; 395 ProtectHostname = true; 396 ProtectKernelLogs = true; 397 ProtectKernelModules = true; 398 ProtectKernelTunables = true; 399 ProtectControlGroups = true; 400 RestrictAddressFamilies = 401 optionals (conf.port != 0) ["AF_INET" "AF_INET6"] ++ 402 optional (conf.unixSocket != null) "AF_UNIX"; 403 RestrictNamespaces = true; 404 LockPersonality = true; 405 MemoryDenyWriteExecute = true; 406 RestrictRealtime = true; 407 RestrictSUIDSGID = true; 408 PrivateMounts = true; 409 # System Call Filtering 410 SystemCallArchitectures = "native"; 411 SystemCallFilter = "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @privileged @resources @setuid"; 412 }; 413 }) enabledServers; 414 415 }; 416}