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