nixos/preload: init
[NixPkgs.git] / nixos / modules / services / web-servers / uwsgi.nix
blob510582feaae104bccb1fafeb63eb82cc8eb619fe
1 { config, lib, pkgs, ... }:
3 with lib;
5 let
6   cfg = config.services.uwsgi;
8   isEmperor = cfg.instance.type == "emperor";
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     ];
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';
29       hasPython = v: filter (n: n == "python${v}") plugins != [];
30       hasPython2 = hasPython "2";
31       hasPython3 = hasPython "3";
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;
40       pythonEnv = python.withPackages (c.pythonPackages or (self: []));
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       };
68     in pkgs.writeTextDir "${name}.json" (builtins.toJSON uwsgiCfg);
70 in {
72   options = {
73     services.uwsgi = {
75       enable = mkOption {
76         type = types.bool;
77         default = false;
78         description = lib.mdDoc "Enable uWSGI";
79       };
81       runDir = mkOption {
82         type = types.path;
83         default = "/run/uwsgi";
84         description = lib.mdDoc "Where uWSGI communication sockets can live";
85       };
87       package = mkOption {
88         type = types.package;
89         internal = true;
90       };
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`.
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.
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.
137           Other attributes will be used in configuration file as-is. Notice that you can redefine
138           `plugins` setting here.
139         '';
140       };
142       plugins = mkOption {
143         type = types.listOf types.str;
144         default = [];
145         description = lib.mdDoc "Plugins used with uWSGI";
146       };
148       user = mkOption {
149         type = types.str;
150         default = "uwsgi";
151         description = lib.mdDoc "User account under which uWSGI runs.";
152       };
154       group = mkOption {
155         type = types.str;
156         default = "uwsgi";
157         description = lib.mdDoc "Group account under which uWSGI runs.";
158       };
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.
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.
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   };
189   config = mkIf cfg.enable {
190     systemd.tmpfiles.rules = optional (cfg.runDir != "/run/uwsgi") ''
191       d ${cfg.runDir} 775 ${cfg.user} ${cfg.group}
192     '';
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     };
211     users.users = optionalAttrs (cfg.user == "uwsgi") {
212       uwsgi = {
213         group = cfg.group;
214         uid = config.ids.uids.uwsgi;
215       };
216     };
218     users.groups = optionalAttrs (cfg.group == "uwsgi") {
219       uwsgi.gid = config.ids.gids.uwsgi;
220     };
222     services.uwsgi.package = pkgs.uwsgi.override {
223       plugins = unique cfg.plugins;
224     };
225   };