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}