at 23.11-pre 7.2 kB view raw
1{ config, lib, pkgs, ... }: 2 3with lib; 4 5let 6 cfg = config.services.uwsgi; 7 8 isEmperor = cfg.instance.type == "emperor"; 9 10 imperialPowers = 11 [ 12 # spawn other user processes 13 "CAP_SETUID" "CAP_SETGID" 14 "CAP_SYS_CHROOT" 15 # transfer capabilities 16 "CAP_SETPCAP" 17 # create other user sockets 18 "CAP_CHOWN" 19 ]; 20 21 buildCfg = name: c: 22 let 23 plugins' = 24 if any (n: !any (m: m == n) cfg.plugins) (c.plugins or []) 25 then throw "`plugins` attribute in uWSGI configuration contains plugins not in config.services.uwsgi.plugins" 26 else c.plugins or cfg.plugins; 27 plugins = unique plugins'; 28 29 hasPython = v: filter (n: n == "python${v}") plugins != []; 30 hasPython2 = hasPython "2"; 31 hasPython3 = hasPython "3"; 32 33 python = 34 if hasPython2 && hasPython3 then 35 throw "`plugins` attribute in uWSGI configuration shouldn't contain both python2 and python3" 36 else if hasPython2 then cfg.package.python2 37 else if hasPython3 then cfg.package.python3 38 else null; 39 40 pythonEnv = python.withPackages (c.pythonPackages or (self: [])); 41 42 uwsgiCfg = { 43 uwsgi = 44 if c.type == "normal" 45 then { 46 inherit plugins; 47 } // removeAttrs c [ "type" "pythonPackages" ] 48 // optionalAttrs (python != null) { 49 pyhome = "${pythonEnv}"; 50 env = 51 # Argh, uwsgi expects list of key-values there instead of a dictionary. 52 let envs = partition (hasPrefix "PATH=") (c.env or []); 53 oldPaths = map (x: substring (stringLength "PATH=") (stringLength x) x) envs.right; 54 paths = oldPaths ++ [ "${pythonEnv}/bin" ]; 55 in [ "PATH=${concatStringsSep ":" paths}" ] ++ envs.wrong; 56 } 57 else if isEmperor 58 then { 59 emperor = if builtins.typeOf c.vassals != "set" then c.vassals 60 else pkgs.buildEnv { 61 name = "vassals"; 62 paths = mapAttrsToList buildCfg c.vassals; 63 }; 64 } // removeAttrs c [ "type" "vassals" ] 65 else throw "`type` attribute in uWSGI configuration should be either 'normal' or 'emperor'"; 66 }; 67 68 in pkgs.writeTextDir "${name}.json" (builtins.toJSON uwsgiCfg); 69 70in { 71 72 options = { 73 services.uwsgi = { 74 75 enable = mkOption { 76 type = types.bool; 77 default = false; 78 description = lib.mdDoc "Enable uWSGI"; 79 }; 80 81 runDir = mkOption { 82 type = types.path; 83 default = "/run/uwsgi"; 84 description = lib.mdDoc "Where uWSGI communication sockets can live"; 85 }; 86 87 package = mkOption { 88 type = types.package; 89 internal = true; 90 }; 91 92 instance = mkOption { 93 type = with types; let 94 valueType = nullOr (oneOf [ 95 bool 96 int 97 float 98 str 99 (lazyAttrsOf valueType) 100 (listOf valueType) 101 (mkOptionType { 102 name = "function"; 103 description = "function"; 104 check = x: isFunction x; 105 merge = mergeOneOption; 106 }) 107 ]) // { 108 description = "Json value or lambda"; 109 emptyValue.value = {}; 110 }; 111 in valueType; 112 default = { 113 type = "normal"; 114 }; 115 example = literalExpression '' 116 { 117 type = "emperor"; 118 vassals = { 119 moin = { 120 type = "normal"; 121 pythonPackages = self: with self; [ moinmoin ]; 122 socket = "''${config.services.uwsgi.runDir}/uwsgi.sock"; 123 }; 124 }; 125 } 126 ''; 127 description = lib.mdDoc '' 128 uWSGI configuration. It awaits an attribute `type` inside which can be either 129 `normal` or `emperor`. 130 131 For `normal` mode you can specify `pythonPackages` as a function 132 from libraries set into a list of libraries. `pythonpath` will be set accordingly. 133 134 For `emperor` mode, you should use `vassals` attribute 135 which should be either a set of names and configurations or a path to a directory. 136 137 Other attributes will be used in configuration file as-is. Notice that you can redefine 138 `plugins` setting here. 139 ''; 140 }; 141 142 plugins = mkOption { 143 type = types.listOf types.str; 144 default = []; 145 description = lib.mdDoc "Plugins used with uWSGI"; 146 }; 147 148 user = mkOption { 149 type = types.str; 150 default = "uwsgi"; 151 description = lib.mdDoc "User account under which uWSGI runs."; 152 }; 153 154 group = mkOption { 155 type = types.str; 156 default = "uwsgi"; 157 description = lib.mdDoc "Group account under which uWSGI runs."; 158 }; 159 160 capabilities = mkOption { 161 type = types.listOf types.str; 162 apply = caps: caps ++ optionals isEmperor imperialPowers; 163 default = [ ]; 164 example = literalExpression '' 165 [ 166 "CAP_NET_BIND_SERVICE" # bind on ports <1024 167 "CAP_NET_RAW" # open raw sockets 168 ] 169 ''; 170 description = lib.mdDoc '' 171 Grant capabilities to the uWSGI instance. See the 172 `capabilities(7)` for available values. 173 174 ::: {.note} 175 uWSGI runs as an unprivileged user (even as Emperor) with the minimal 176 capabilities required. This option can be used to add fine-grained 177 permissions without running the service as root. 178 179 When in Emperor mode, any capability to be inherited by a vassal must 180 be specified again in the vassal configuration using `cap`. 181 See the uWSGI [docs](https://uwsgi-docs.readthedocs.io/en/latest/Capabilities.html) 182 for more information. 183 ::: 184 ''; 185 }; 186 }; 187 }; 188 189 config = mkIf cfg.enable { 190 systemd.tmpfiles.rules = optional (cfg.runDir != "/run/uwsgi") '' 191 d ${cfg.runDir} 775 ${cfg.user} ${cfg.group} 192 ''; 193 194 systemd.services.uwsgi = { 195 wantedBy = [ "multi-user.target" ]; 196 serviceConfig = { 197 User = cfg.user; 198 Group = cfg.group; 199 Type = "notify"; 200 ExecStart = "${cfg.package}/bin/uwsgi --json ${buildCfg "server" cfg.instance}/server.json"; 201 ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; 202 ExecStop = "${pkgs.coreutils}/bin/kill -INT $MAINPID"; 203 NotifyAccess = "main"; 204 KillSignal = "SIGQUIT"; 205 AmbientCapabilities = cfg.capabilities; 206 CapabilityBoundingSet = cfg.capabilities; 207 RuntimeDirectory = mkIf (cfg.runDir == "/run/uwsgi") "uwsgi"; 208 }; 209 }; 210 211 users.users = optionalAttrs (cfg.user == "uwsgi") { 212 uwsgi = { 213 group = cfg.group; 214 uid = config.ids.uids.uwsgi; 215 }; 216 }; 217 218 users.groups = optionalAttrs (cfg.group == "uwsgi") { 219 uwsgi.gid = config.ids.gids.uwsgi; 220 }; 221 222 services.uwsgi.package = pkgs.uwsgi.override { 223 plugins = unique cfg.plugins; 224 }; 225 }; 226}