1{
2 pkgs,
3 lib,
4 config,
5 ...
6}:
7let
8 cfg = config.services.devpi-server;
9
10 secretsFileName = "devpi-secret-file";
11
12 stateDirName = "devpi";
13
14 runtimeDir = "/run/${stateDirName}";
15 serverDir = "/var/lib/${stateDirName}";
16in
17{
18 options.services.devpi-server = {
19 enable = lib.mkEnableOption "Devpi Server";
20
21 package = lib.mkPackageOption pkgs "devpi-server" { };
22
23 primaryUrl = lib.mkOption {
24 type = lib.types.str;
25 description = "Url for the primary node. Required option for replica nodes.";
26 };
27
28 replica = lib.mkOption {
29 type = lib.types.bool;
30 default = false;
31 description = ''
32 Run node as a replica.
33 Requires the secretFile option and the primaryUrl to be enabled.
34 '';
35 };
36
37 secretFile = lib.mkOption {
38 type = lib.types.nullOr lib.types.path;
39 default = null;
40 description = ''
41 Path to a shared secret file used for synchronization,
42 Required for all nodes in a replica/primary setup.
43 '';
44 };
45
46 host = lib.mkOption {
47 type = lib.types.str;
48 default = "localhost";
49 description = ''
50 domain/ip address to listen on
51 '';
52 };
53
54 port = lib.mkOption {
55 type = lib.types.port;
56 default = 3141;
57 description = "The port on which Devpi Server will listen.";
58 };
59
60 openFirewall = lib.mkEnableOption "opening the default ports in the firewall for Devpi Server";
61 };
62
63 config = lib.mkIf cfg.enable {
64
65 systemd.services.devpi-server = {
66 enable = true;
67 description = "devpi PyPI-compatible server";
68 documentation = [ "https://devpi.net/docs/devpi/devpi/stable/+d/index.html" ];
69 wants = [ "network-online.target" ];
70 wantedBy = [ "multi-user.target" ];
71 after = [ "network-online.target" ];
72 # Since at least devpi-server 6.10.0, devpi requires the secrets file to
73 # have 0600 permissions.
74 preStart = ''
75 ${lib.optionalString (
76 !isNull cfg.secretFile
77 ) "install -Dm 0600 \${CREDENTIALS_DIRECTORY}/devpi-secret ${runtimeDir}/${secretsFileName}"}
78
79 if [ -f ${serverDir}/.nodeinfo ]; then
80 # already initialized the package index, exit gracefully
81 exit 0
82 fi
83 ${cfg.package}/bin/devpi-init --serverdir ${serverDir} ''
84 + lib.optionalString cfg.replica "--role=replica --master-url=${cfg.primaryUrl}";
85
86 serviceConfig = {
87 LoadCredential = lib.mkIf (!isNull cfg.secretFile) [
88 "devpi-secret:${cfg.secretFile}"
89 ];
90 Restart = "always";
91 ExecStart =
92 let
93 args = [
94 "--request-timeout=5"
95 "--serverdir=${serverDir}"
96 "--host=${cfg.host}"
97 "--port=${builtins.toString cfg.port}"
98 ]
99 ++ lib.optionals (!isNull cfg.secretFile) [
100 "--secretfile=${runtimeDir}/${secretsFileName}"
101 ]
102 ++ (
103 if cfg.replica then
104 [
105 "--role=replica"
106 "--master-url=${cfg.primaryUrl}"
107 ]
108 else
109 [ "--role=master" ]
110 );
111 in
112 "${cfg.package}/bin/devpi-server ${lib.concatStringsSep " " args}";
113 DynamicUser = true;
114 StateDirectory = stateDirName;
115 RuntimeDirectory = stateDirName;
116 PrivateDevices = true;
117 PrivateTmp = true;
118 ProtectHome = true;
119 ProtectSystem = "strict";
120 };
121 };
122
123 networking.firewall = lib.mkIf cfg.openFirewall {
124 allowedTCPPorts = [ cfg.port ];
125 };
126 };
127
128 meta.maintainers = [ lib.maintainers.cafkafk ];
129}