at 23.11-pre 17 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, ... }: { 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 inherit (config) port logfile databases maxclients appendOnly; 275 daemonize = false; 276 supervised = "systemd"; 277 loglevel = config.logLevel; 278 syslog-enabled = config.syslog; 279 save = if config.save == [] 280 then ''""'' # Disable saving with `save = ""` 281 else map 282 (d: "${toString (builtins.elemAt d 0)} ${toString (builtins.elemAt d 1)}") 283 config.save; 284 dbfilename = "dump.rdb"; 285 dir = "/var/lib/${redisName name}"; 286 appendfsync = config.appendFsync; 287 slowlog-log-slower-than = config.slowLogLogSlowerThan; 288 slowlog-max-len = config.slowLogMaxLen; 289 } 290 (mkIf (config.bind != null) { inherit (config) bind; }) 291 (mkIf (config.unixSocket != null) { 292 unixsocket = config.unixSocket; 293 unixsocketperm = toString config.unixSocketPerm; 294 }) 295 (mkIf (config.slaveOf != null) { slaveof = "${config.slaveOf.ip} ${toString config.slaveOf.port}"; }) 296 (mkIf (config.masterAuth != null) { masterauth = config.masterAuth; }) 297 (mkIf (config.requirePass != null) { requirepass = config.requirePass; }) 298 ]; 299 })); 300 description = lib.mdDoc "Configuration of multiple `redis-server` instances."; 301 default = {}; 302 }; 303 }; 304 305 }; 306 307 308 ###### implementation 309 310 config = mkIf (enabledServers != {}) { 311 312 assertions = attrValues (mapAttrs (name: conf: { 313 assertion = conf.requirePass != null -> conf.requirePassFile == null; 314 message = '' 315 You can only set one services.redis.servers.${name}.requirePass 316 or services.redis.servers.${name}.requirePassFile 317 ''; 318 }) enabledServers); 319 320 boot.kernel.sysctl = mkMerge [ 321 { "vm.nr_hugepages" = "0"; } 322 ( mkIf cfg.vmOverCommit { "vm.overcommit_memory" = "1"; } ) 323 ]; 324 325 networking.firewall.allowedTCPPorts = concatMap (conf: 326 optional conf.openFirewall conf.port 327 ) (attrValues enabledServers); 328 329 environment.systemPackages = [ cfg.package ]; 330 331 users.users = mapAttrs' (name: conf: nameValuePair (redisName name) { 332 description = "System user for the redis-server instance ${name}"; 333 isSystemUser = true; 334 group = redisName name; 335 }) enabledServers; 336 users.groups = mapAttrs' (name: conf: nameValuePair (redisName name) { 337 }) enabledServers; 338 339 systemd.services = mapAttrs' (name: conf: nameValuePair (redisName name) { 340 description = "Redis Server - ${redisName name}"; 341 342 wantedBy = [ "multi-user.target" ]; 343 after = [ "network.target" ]; 344 345 serviceConfig = { 346 ExecStart = "${cfg.package}/bin/redis-server /var/lib/${redisName name}/redis.conf ${escapeShellArgs conf.extraParams}"; 347 ExecStartPre = "+"+pkgs.writeShellScript "${redisName name}-prep-conf" (let 348 redisConfVar = "/var/lib/${redisName name}/redis.conf"; 349 redisConfRun = "/run/${redisName name}/nixos.conf"; 350 redisConfStore = redisConfig conf.settings; 351 in '' 352 touch "${redisConfVar}" "${redisConfRun}" 353 chown '${conf.user}' "${redisConfVar}" "${redisConfRun}" 354 chmod 0600 "${redisConfVar}" "${redisConfRun}" 355 if [ ! -s ${redisConfVar} ]; then 356 echo 'include "${redisConfRun}"' > "${redisConfVar}" 357 fi 358 echo 'include "${redisConfStore}"' > "${redisConfRun}" 359 ${optionalString (conf.requirePassFile != null) '' 360 { 361 echo -n "requirepass " 362 cat ${escapeShellArg conf.requirePassFile} 363 } >> "${redisConfRun}" 364 ''} 365 ''); 366 Type = "notify"; 367 # User and group 368 User = conf.user; 369 Group = conf.user; 370 # Runtime directory and mode 371 RuntimeDirectory = redisName name; 372 RuntimeDirectoryMode = "0750"; 373 # State directory and mode 374 StateDirectory = redisName name; 375 StateDirectoryMode = "0700"; 376 # Access write directories 377 UMask = "0077"; 378 # Capabilities 379 CapabilityBoundingSet = ""; 380 # Security 381 NoNewPrivileges = true; 382 # Process Properties 383 LimitNOFILE = mkDefault "${toString (conf.maxclients + 32)}"; 384 # Sandboxing 385 ProtectSystem = "strict"; 386 ProtectHome = true; 387 PrivateTmp = true; 388 PrivateDevices = true; 389 PrivateUsers = true; 390 ProtectClock = true; 391 ProtectHostname = true; 392 ProtectKernelLogs = true; 393 ProtectKernelModules = true; 394 ProtectKernelTunables = true; 395 ProtectControlGroups = true; 396 RestrictAddressFamilies = 397 optionals (conf.port != 0) ["AF_INET" "AF_INET6"] ++ 398 optional (conf.unixSocket != null) "AF_UNIX"; 399 RestrictNamespaces = true; 400 LockPersonality = true; 401 MemoryDenyWriteExecute = true; 402 RestrictRealtime = true; 403 RestrictSUIDSGID = true; 404 PrivateMounts = true; 405 # System Call Filtering 406 SystemCallArchitectures = "native"; 407 SystemCallFilter = "~@cpu-emulation @debug @keyring @memlock @mount @obsolete @privileged @resources @setuid"; 408 }; 409 }) enabledServers; 410 411 }; 412}