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}