1{
2 config,
3 lib,
4 pkgs,
5 buildEnv,
6 ...
7}:
8
9let
10 cfg = config.services.peering-manager;
11
12 pythonFmt = pkgs.formats.pythonVars { };
13 settingsFile = pythonFmt.generate "peering-manager-settings.py" cfg.settings;
14 extraConfigFile = pkgs.writeTextFile {
15 name = "peering-manager-extraConfig.py";
16 text = cfg.extraConfig;
17 };
18 configFile = pkgs.concatText "configuration.py" [
19 settingsFile
20 extraConfigFile
21 ];
22 finalConfigFile =
23 if (cfg.environmentFile != null) then "/var/lib/peering-manager/configuration.py" else configFile;
24
25 pkg =
26 (pkgs.peering-manager.overrideAttrs (old: {
27 postInstall = ''
28 ln -s ${finalConfigFile} $out/opt/peering-manager/peering_manager/configuration.py
29 ''
30 + lib.optionalString cfg.enableLdap ''
31 ln -s ${cfg.ldapConfigPath} $out/opt/peering-manager/peering_manager/ldap_config.py
32 '';
33 })).override
34 {
35 inherit (cfg) plugins;
36 };
37 peeringManagerManageScript = pkgs.writeScriptBin "peering-manager-manage" ''
38 #!${pkgs.stdenv.shell}
39 export PYTHONPATH=${pkg.pythonPath}
40 sudo -u peering-manager ${pkg}/bin/peering-manager "$@"
41 '';
42
43in
44{
45 options.services.peering-manager = with lib; {
46 enable = mkOption {
47 type = types.bool;
48 default = false;
49 description = ''
50 Enable Peering Manager.
51
52 This module requires a reverse proxy that serves `/static` separately.
53 See this [example](https://github.com/peering-manager/contrib/blob/main/nginx.conf) on how to configure this.
54 '';
55 };
56
57 environmentFile = mkOption {
58 type = with types; nullOr path;
59 default = null;
60 example = "/run/secrets/peering-manager.env";
61 description = ''
62 Environment file as defined in {manpage}`systemd.exec(5)`.
63
64 Secrets may be passed to the service without adding them to the world-readable
65 Nix store, by specifying placeholder variables as the option value in Nix and
66 setting these variables accordingly in the environment file.
67
68 ```
69 # snippet of peering-manager-related config
70 services.peering-manager.settings.SOCIAL_AUTH_OIDC_SECRET = "$PM_OIDC_SECRET";
71 ```
72
73 ```
74 # content of the environment file
75 PM_OIDC_SECRET=topsecret
76 ```
77
78 Note that this file needs to be available on the host on which
79 `peering-manager` is running.
80 '';
81 };
82
83 enableScheduledTasks = mkOption {
84 type = types.bool;
85 default = true;
86 description = ''
87 Set up [scheduled tasks](https://peering-manager.readthedocs.io/en/stable/setup/8-scheduled-tasks/)
88 '';
89 };
90
91 listenAddress = mkOption {
92 type = types.str;
93 default = "[::1]";
94 description = ''
95 Address the server will listen on.
96 '';
97 };
98
99 port = mkOption {
100 type = types.port;
101 default = 8001;
102 description = ''
103 Port the server will listen on.
104 '';
105 };
106
107 plugins = mkOption {
108 type = types.functionTo (types.listOf types.package);
109 default = _: [ ];
110 defaultText = literalExpression ''
111 python3Packages: with python3Packages; [];
112 '';
113 description = ''
114 List of plugin packages to install.
115 '';
116 };
117
118 secretKeyFile = mkOption {
119 type = types.path;
120 description = ''
121 Path to a file containing the secret key.
122 '';
123 };
124
125 peeringdbApiKeyFile = mkOption {
126 type = with types; nullOr path;
127 default = null;
128 description = ''
129 Path to a file containing the PeeringDB API key.
130 '';
131 };
132
133 settings = lib.mkOption {
134 description = ''
135 Configuration options to set in `configuration.py`.
136 See the [documentation](https://peering-manager.readthedocs.io/en/stable/configuration/optional-settings/) for more possible options.
137 '';
138
139 default = { };
140
141 type = lib.types.submodule {
142 freeformType = pythonFmt.type;
143
144 options = {
145 ALLOWED_HOSTS = lib.mkOption {
146 type = with lib.types; listOf str;
147 default = [ "*" ];
148 description = ''
149 A list of valid fully-qualified domain names (FQDNs) and/or IP
150 addresses that can be used to reach the peering manager service.
151 '';
152 };
153 };
154 };
155 };
156
157 extraConfig = mkOption {
158 type = types.lines;
159 default = "";
160 description = ''
161 Additional lines of configuration appended to the `configuration.py`.
162 See the [documentation](https://peering-manager.readthedocs.io/en/stable/configuration/optional-settings/) for more possible options.
163 '';
164 };
165
166 enableLdap = mkOption {
167 type = types.bool;
168 default = false;
169 description = ''
170 Enable LDAP-Authentication for Peering Manager.
171
172 This requires a configuration file being pass through `ldapConfigPath`.
173 '';
174 };
175
176 ldapConfigPath = mkOption {
177 type = types.path;
178 description = ''
179 Path to the Configuration-File for LDAP-Authentication, will be loaded as `ldap_config.py`.
180 See the [documentation](https://peering-manager.readthedocs.io/en/stable/setup/6-ldap/#configuration) for possible options.
181 '';
182 };
183 };
184
185 imports = [
186 (lib.mkRemovedOptionModule [ "services" "peering-manager" "enableOidc" ] ''
187 The enableOidc option has been removed, since peering-manager has OIDC support builtin since version >= 1.9.0.
188
189 Make sure to update your OIDC configuration according to the documentation:
190 https://peering-manager.readthedocs.io/en/v1.9.3/administration/authentication/oidc/
191 '')
192 (lib.mkRemovedOptionModule [ "services" "peering-manager" "oidcConfigPath" ] ''
193 The oidcConfigPath option has been removed, since peering-manager has OIDC support builtin since version >= 1.9.0.
194
195 The new config settings for OIDC are explained in the documentation:
196 https://peering-manager.readthedocs.io/en/v1.9.3/administration/authentication/oidc/
197 '')
198 ];
199
200 config = lib.mkIf cfg.enable {
201 services.peering-manager = {
202 settings = {
203 DATABASE = {
204 NAME = "peering-manager";
205 USER = "peering-manager";
206 HOST = "/run/postgresql";
207 };
208
209 # Redis database settings. Redis is used for caching and for queuing background tasks such as webhook events. A separate
210 # configuration exists for each. Full connection details are required in both sections, and it is strongly recommended
211 # to use two separate database IDs.
212 REDIS = {
213 tasks = {
214 UNIX_SOCKET_PATH = config.services.redis.servers.peering-manager.unixSocket;
215 DATABASE = 0;
216 };
217 caching = {
218 UNIX_SOCKET_PATH = config.services.redis.servers.peering-manager.unixSocket;
219 DATABASE = 1;
220 };
221 };
222 };
223
224 extraConfig = ''
225 with open("${cfg.secretKeyFile}", "r") as file:
226 SECRET_KEY = file.readline()
227 ''
228 + lib.optionalString (cfg.peeringdbApiKeyFile != null) ''
229 with open("${cfg.peeringdbApiKeyFile}", "r") as file:
230 PEERINGDB_API_KEY = file.readline()
231 '';
232
233 plugins = (ps: (lib.optionals cfg.enableLdap [ ps.django-auth-ldap ]));
234 };
235
236 system.build.peeringManagerPkg = pkg;
237
238 services.redis.servers.peering-manager.enable = true;
239
240 services.postgresql = {
241 enable = true;
242 ensureDatabases = [ "peering-manager" ];
243 ensureUsers = [
244 {
245 name = "peering-manager";
246 ensureDBOwnership = true;
247 }
248 ];
249 };
250
251 environment.systemPackages = [ peeringManagerManageScript ];
252
253 systemd.targets.peering-manager = {
254 description = "Target for all Peering Manager services";
255 wantedBy = [ "multi-user.target" ];
256 wants = [ "network-online.target" ];
257 after = [
258 "network-online.target"
259 "redis-peering-manager.service"
260 ];
261 };
262
263 systemd.services =
264 let
265 defaults = {
266 environment = {
267 PYTHONPATH = pkg.pythonPath;
268 };
269 serviceConfig = {
270 WorkingDirectory = "/var/lib/peering-manager";
271 User = "peering-manager";
272 Group = "peering-manager";
273 StateDirectory = "peering-manager";
274 StateDirectoryMode = "0750";
275 Restart = "on-failure";
276 };
277 };
278 in
279 {
280 peering-manager-config = lib.mkIf (cfg.environmentFile != null) (
281 lib.recursiveUpdate defaults {
282 description = "Peering Manager config file setup";
283 wantedBy = [ "peering-manager.target" ];
284 serviceConfig = {
285 Type = "oneshot";
286 EnvironmentFile = [ cfg.environmentFile ];
287 ExecStart = "${lib.getExe pkgs.envsubst} -i ${configFile} -o ${finalConfigFile}";
288 };
289 }
290 );
291
292 peering-manager-migration = lib.recursiveUpdate defaults {
293 description = "Peering Manager migrations";
294 wantedBy = [ "peering-manager.target" ];
295 after = lib.mkIf (cfg.environmentFile != null) [ "peering-manager-config.service" ];
296 serviceConfig = {
297 Type = "oneshot";
298 ExecStart = "${pkg}/bin/peering-manager migrate";
299 };
300 };
301
302 peering-manager = lib.recursiveUpdate defaults {
303 description = "Peering Manager WSGI Service";
304 wantedBy = [ "peering-manager.target" ];
305 after = [
306 "peering-manager-migration.service"
307 ]
308 ++ lib.optionals (cfg.environmentFile != null) [ "peering-manager-config.service" ];
309
310 preStart = ''
311 ${pkg}/bin/peering-manager remove_stale_contenttypes --no-input
312 '';
313
314 serviceConfig = {
315 ExecStart = ''
316 ${pkg.python.pkgs.gunicorn}/bin/gunicorn peering_manager.wsgi \
317 --bind ${cfg.listenAddress}:${toString cfg.port} \
318 --pythonpath ${pkg}/opt/peering-manager
319 '';
320 };
321 };
322
323 peering-manager-rq = lib.recursiveUpdate defaults {
324 description = "Peering Manager Request Queue Worker";
325 wantedBy = [ "peering-manager.target" ];
326 after = [ "peering-manager.service" ];
327 serviceConfig.ExecStart = "${pkg}/bin/peering-manager rqworker high default low";
328 };
329
330 peering-manager-housekeeping = lib.recursiveUpdate defaults {
331 description = "Peering Manager housekeeping job";
332 after = [ "peering-manager.service" ];
333 serviceConfig = {
334 Type = "oneshot";
335 ExecStart = "${pkg}/bin/peering-manager housekeeping";
336 };
337 };
338
339 peering-manager-peeringdb-sync = lib.recursiveUpdate defaults {
340 description = "PeeringDB sync";
341 after = [ "peering-manager.service" ];
342 serviceConfig = {
343 Type = "oneshot";
344 ExecStart = "${pkg}/bin/peering-manager peeringdb_sync";
345 };
346 };
347
348 peering-manager-prefix-fetch = lib.recursiveUpdate defaults {
349 description = "Fetch IRR AS-SET prefixes";
350 after = [ "peering-manager.service" ];
351 serviceConfig = {
352 Type = "oneshot";
353 ExecStart = "${pkg}/bin/peering-manager grab_prefixes";
354 };
355 };
356
357 peering-manager-configuration-deployment = lib.recursiveUpdate defaults {
358 description = "Push configuration to routers";
359 after = [ "peering-manager.service" ];
360 serviceConfig = {
361 Type = "oneshot";
362 ExecStart = "${pkg}/bin/peering-manager configure_routers";
363 };
364 };
365
366 peering-manager-session-poll = lib.recursiveUpdate defaults {
367 description = "Poll peering sessions from routers";
368 after = [ "peering-manager.service" ];
369 serviceConfig = {
370 Type = "oneshot";
371 ExecStart = "${pkg}/bin/peering-manager poll_bgp_sessions";
372 };
373 };
374 };
375
376 systemd.timers = {
377 peering-manager-housekeeping = {
378 description = "Run Peering Manager housekeeping job";
379 wantedBy = [ "timers.target" ];
380 timerConfig.OnCalendar = "daily";
381 };
382
383 peering-manager-peeringdb-sync = {
384 enable = lib.mkDefault cfg.enableScheduledTasks;
385 description = "Sync PeeringDB at 2:30";
386 wantedBy = [ "timers.target" ];
387 timerConfig.OnCalendar = "02:30:00";
388 };
389
390 peering-manager-prefix-fetch = {
391 enable = lib.mkDefault cfg.enableScheduledTasks;
392 description = "Fetch IRR AS-SET prefixes at 4:30";
393 wantedBy = [ "timers.target" ];
394 timerConfig.OnCalendar = "04:30:00";
395 };
396
397 peering-manager-configuration-deployment = {
398 enable = lib.mkDefault cfg.enableScheduledTasks;
399 description = "Push router configuration every hour 5 minutes before full hour";
400 wantedBy = [ "timers.target" ];
401 timerConfig.OnCalendar = "*:55:00";
402 };
403
404 peering-manager-session-poll = {
405 enable = lib.mkDefault cfg.enableScheduledTasks;
406 description = "Poll peering sessions from routers every hour";
407 wantedBy = [ "timers.target" ];
408 timerConfig.OnCalendar = "*:00:00";
409 };
410 };
411
412 users.users.peering-manager = {
413 home = "/var/lib/peering-manager";
414 isSystemUser = true;
415 group = "peering-manager";
416 };
417 users.groups.peering-manager = { };
418 users.groups."${config.services.redis.servers.peering-manager.user}".members = [
419 "peering-manager"
420 ];
421 };
422
423 meta.maintainers = with lib.maintainers; [ yuka ];
424}