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