1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.services.nipap;
10 iniFmt = pkgs.formats.ini { };
11
12 configFile = iniFmt.generate "nipap.conf" cfg.settings;
13
14 defaultUser = "nipap";
15 defaultAuthBackend = "local";
16 dataDir = "/var/lib/nipap";
17
18 defaultServiceConfig = {
19 WorkingDirectory = dataDir;
20 User = cfg.user;
21 Group = config.users.users."${cfg.user}".group;
22 Restart = "on-failure";
23 RestartSec = 30;
24 };
25
26 escapedHost = host: if lib.hasInfix ":" host then "[${host}]" else host;
27in
28{
29 options.services.nipap = {
30 enable = lib.mkEnableOption "global Neat IP Address Planner (NIPAP) configuration";
31
32 user = lib.mkOption {
33 type = lib.types.str;
34 description = "User to use for running NIPAP services.";
35 default = defaultUser;
36 };
37
38 settings = lib.mkOption {
39 description = ''
40 Configuration options to set in /etc/nipap/nipap.conf.
41 '';
42
43 default = { };
44
45 type = lib.types.submodule {
46 freeformType = iniFmt.type;
47
48 options = {
49 nipapd = {
50 listen = lib.mkOption {
51 type = lib.types.str;
52 default = "::1";
53 description = "IP address to bind nipapd to.";
54 };
55 port = lib.mkOption {
56 type = lib.types.port;
57 default = 1337;
58 description = "Port to bind nipapd to.";
59 };
60
61 foreground = lib.mkOption {
62 type = lib.types.bool;
63 default = true;
64 description = "Remain in foreground rather than forking to background.";
65 };
66 debug = lib.mkOption {
67 type = lib.types.bool;
68 default = false;
69 description = "Enable debug logging.";
70 };
71
72 db_host = lib.mkOption {
73 type = lib.types.str;
74 default = "";
75 description = "PostgreSQL host to connect to. Empty means use UNIX socket.";
76 };
77 db_name = lib.mkOption {
78 type = lib.types.str;
79 default = cfg.user;
80 defaultText = defaultUser;
81 description = "Name of database to use on PostgreSQL server.";
82 };
83 };
84
85 auth = {
86 default_backend = lib.mkOption {
87 type = lib.types.str;
88 default = defaultAuthBackend;
89 description = "Name of auth backend to use by default.";
90 };
91 auth_cache_timeout = lib.mkOption {
92 type = lib.types.int;
93 default = 3600;
94 description = "Seconds to store cached auth entries for.";
95 };
96 };
97 };
98 };
99 };
100
101 authBackendSettings = lib.mkOption {
102 description = ''
103 auth.backends options to set in /etc/nipap/nipap.conf.
104 '';
105
106 default = {
107 "${defaultAuthBackend}" = {
108 type = "SqliteAuth";
109 db_path = "${dataDir}/local_auth.db";
110 };
111 };
112
113 type = lib.types.submodule {
114 freeformType = iniFmt.type;
115 };
116 };
117
118 nipapd = {
119 enable = lib.mkEnableOption "nipapd server";
120 package = lib.mkPackageOption pkgs "nipap" { };
121
122 database.createLocally = lib.mkOption {
123 type = lib.types.bool;
124 default = true;
125 description = "Create a nipap database automatically.";
126 };
127 };
128
129 nipap-www = {
130 enable = lib.mkEnableOption "nipap-www server";
131 package = lib.mkPackageOption pkgs "nipap-www" { };
132
133 xmlrpcURIFile = lib.mkOption {
134 type = lib.types.nullOr lib.types.path;
135 default = null;
136 description = "Path to file containing XMLRPC URI for use by web UI - this is a secret, since it contains auth credentials. If null, it will be initialized assuming that the auth database is local.";
137 };
138
139 workers = lib.mkOption {
140 type = lib.types.int;
141 default = 4;
142 description = "Number of worker processes for Gunicorn to fork.";
143 };
144 umask = lib.mkOption {
145 type = lib.types.str;
146 default = "0";
147 description = "umask for files written by Gunicorn, including UNIX socket.";
148 };
149
150 unixSocket = lib.mkOption {
151 type = lib.types.nullOr lib.types.str;
152 default = null;
153 description = "Path to UNIX socket to bind to.";
154 example = "/run/nipap/nipap-www.sock";
155 };
156 host = lib.mkOption {
157 type = lib.types.nullOr lib.types.str;
158 default = "::";
159 description = "Host to bind to.";
160 };
161 port = lib.mkOption {
162 type = lib.types.nullOr lib.types.port;
163 default = 21337;
164 description = "Port to bind to.";
165 };
166 };
167 };
168
169 config = lib.mkIf cfg.enable (
170 lib.mkMerge [
171 {
172 systemd.tmpfiles.rules = [
173 "d '${dataDir}' - ${cfg.user} ${config.users.users."${cfg.user}".group} - -"
174 ];
175
176 environment.etc."nipap/nipap.conf" = {
177 source = configFile;
178 };
179
180 services.nipap.settings = lib.attrsets.mapAttrs' (name: value: {
181 name = "auth.backends.${name}";
182 inherit value;
183 }) cfg.authBackendSettings;
184
185 services.nipap.nipapd.enable = lib.mkDefault true;
186 services.nipap.nipap-www.enable = lib.mkDefault true;
187
188 environment.systemPackages = [
189 cfg.nipapd.package
190 ];
191 }
192 (lib.mkIf (cfg.user == defaultUser) {
193 users.users."${defaultUser}" = {
194 isSystemUser = true;
195 group = defaultUser;
196 home = dataDir;
197 };
198 users.groups."${defaultUser}" = { };
199 })
200 (lib.mkIf (cfg.nipapd.enable && cfg.nipapd.database.createLocally) {
201 services.postgresql = {
202 enable = true;
203 extensions = ps: with ps; [ ip4r ];
204 ensureUsers = [
205 {
206 name = cfg.user;
207 }
208 ];
209 ensureDatabases = [ cfg.settings.nipapd.db_name ];
210 };
211
212 systemd.services.postgresql.serviceConfig.ExecStartPost =
213 let
214 sqlFile = pkgs.writeText "nipapd-setup.sql" ''
215 CREATE EXTENSION IF NOT EXISTS ip4r;
216
217 ALTER SCHEMA public OWNER TO "${cfg.user}";
218 ALTER DATABASE "${cfg.settings.nipapd.db_name}" OWNER TO "${cfg.user}";
219 '';
220 in
221 [
222 ''
223 ${lib.getExe' config.services.postgresql.finalPackage "psql"} -d "${cfg.settings.nipapd.db_name}" -f "${sqlFile}"
224 ''
225 ];
226 })
227 (lib.mkIf cfg.nipapd.enable {
228 systemd.services.nipapd =
229 let
230 pkg = cfg.nipapd.package;
231 in
232 {
233 description = "Neat IP Address Planner";
234 after = [
235 "network.target"
236 "systemd-tmpfiles-setup.service"
237 ]
238 ++ lib.optional (cfg.settings.nipapd.db_host == "") "postgresql.target";
239 requires = lib.optional (cfg.settings.nipapd.db_host == "") "postgresql.target";
240 wantedBy = [ "multi-user.target" ];
241 preStart = lib.optionalString (cfg.settings.auth.default_backend == defaultAuthBackend) ''
242 # Create/upgrade local auth database
243 umask 077
244 ${pkg}/bin/nipap-passwd create-database >/dev/null 2>&1
245 ${pkg}/bin/nipap-passwd upgrade-database >/dev/null 2>&1
246 '';
247 serviceConfig = defaultServiceConfig // {
248 KillSignal = "SIGINT";
249 ExecStart = ''
250 ${pkg}/bin/nipapd \
251 --auto-install-db \
252 --auto-upgrade-db \
253 --foreground \
254 --no-pid-file
255 '';
256 };
257 };
258 })
259 (lib.mkIf cfg.nipap-www.enable {
260 assertions = [
261 {
262 assertion =
263 cfg.nipap-www.xmlrpcURIFile == null -> cfg.settings.auth.default_backend == defaultAuthBackend;
264 message = "If no XMLRPC URI secret file is specified, then the default auth backend must be in use to automatically generate credentials.";
265 }
266 ];
267
268 # Ensure that _something_ exists in the [www] group.
269 services.nipap.settings.www = lib.mkDefault { };
270
271 systemd.services.nipap-www =
272 let
273 pkg = cfg.nipap-www.package;
274 in
275 {
276 description = "Neat IP Address Planner web server";
277 after = [
278 "network.target"
279 "systemd-tmpfiles-setup.service"
280 ]
281 ++ lib.optional cfg.nipapd.enable "nipapd.service";
282 wantedBy = [ "multi-user.target" ];
283 environment = {
284 PYTHONPATH = pkg.pythonPath;
285 };
286 serviceConfig = defaultServiceConfig;
287 script =
288 let
289 bind =
290 if cfg.nipap-www.unixSocket != null then
291 "unix:${cfg.nipap-www.unixSocket}"
292 else
293 "${escapedHost cfg.nipap-www.host}:${toString cfg.nipap-www.port}";
294 generateXMLRPC = cfg.nipap-www.xmlrpcURIFile == null;
295 xmlrpcURIFile = if generateXMLRPC then "${dataDir}/www_xmlrpc_uri" else cfg.nipap-www.xmlrpcURIFile;
296 in
297 ''
298 test -f "${dataDir}/www_secret" || {
299 umask 0077
300 ${pkg.python}/bin/python -c "import secrets; print(secrets.token_hex())" > "${dataDir}/www_secret"
301 }
302 export FLASK_SECRET_KEY="$(cat "${dataDir}/www_secret")"
303
304 # Ensure that we have an XMLRPC URI.
305 ${
306 if generateXMLRPC then
307 ''
308 test -f "${dataDir}/www_xmlrpc_uri" || {
309 umask 0077
310 www_password="$(${pkg.python}/bin/python -c "import secrets; print(secrets.token_hex())")"
311 ${cfg.nipapd.package}/bin/nipap-passwd add --username nipap-www --password "''${www_password}" --name "User account for the web UI" --trusted
312
313 echo "http://nipap-www@${defaultAuthBackend}:''${www_password}@${escapedHost cfg.settings.nipapd.listen}:${toString cfg.settings.nipapd.port}" > "${xmlrpcURIFile}"
314 }
315 ''
316 else
317 ""
318 }
319 export FLASK_XMLRPC_URI="$(cat "${xmlrpcURIFile}")"
320
321 exec "${pkg.gunicorn}/bin/gunicorn" \
322 --preload --workers ${toString cfg.nipap-www.workers} \
323 --pythonpath "${pkg}/${pkg.python.sitePackages}" \
324 --bind ${bind} --umask ${cfg.nipap-www.umask} \
325 "nipapwww:create_app()"
326 '';
327 };
328 })
329 ]
330 );
331
332 meta.maintainers = with lib.maintainers; [ lukegb ];
333}