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}