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