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 ''
76 ${lib.optionalString (
77 !isNull cfg.secretFile
78 ) "install -Dm 0600 \${CREDENTIALS_DIRECTORY}/devpi-secret ${runtimeDir}/${secretsFileName}"}
79
80 if [ -f ${serverDir}/.nodeinfo ]; then
81 # already initialized the package index, exit gracefully
82 exit 0
83 fi
84 ${cfg.package}/bin/devpi-init --serverdir ${serverDir} ''
85 + lib.optionalString cfg.replica "--role=replica --master-url=${cfg.primaryUrl}";
86
87 serviceConfig = {
88 LoadCredential = lib.mkIf (!isNull cfg.secretFile) [
89 "devpi-secret:${cfg.secretFile}"
90 ];
91 Restart = "always";
92 ExecStart =
93 let
94 args =
95 [
96 "--request-timeout=5"
97 "--serverdir=${serverDir}"
98 "--host=${cfg.host}"
99 "--port=${builtins.toString cfg.port}"
100 ]
101 ++ lib.optionals (!isNull cfg.secretFile) [
102 "--secretfile=${runtimeDir}/${secretsFileName}"
103 ]
104 ++ (
105 if cfg.replica then
106 [
107 "--role=replica"
108 "--master-url=${cfg.primaryUrl}"
109 ]
110 else
111 [ "--role=master" ]
112 );
113 in
114 "${cfg.package}/bin/devpi-server ${lib.concatStringsSep " " args}";
115 DynamicUser = true;
116 StateDirectory = stateDirName;
117 RuntimeDirectory = stateDirName;
118 PrivateDevices = true;
119 PrivateTmp = true;
120 ProtectHome = true;
121 ProtectSystem = "strict";
122 };
123 };
124
125 networking.firewall = lib.mkIf cfg.openFirewall {
126 allowedTCPPorts = [ cfg.port ];
127 };
128 };
129
130 meta.maintainers = [ lib.maintainers.cafkafk ];
131}