1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.zammad;
7 settingsFormat = pkgs.formats.yaml { };
8 filterNull = filterAttrs (_: v: v != null);
9 serviceConfig = {
10 Type = "simple";
11 Restart = "always";
12
13 User = "zammad";
14 Group = "zammad";
15 PrivateTmp = true;
16 StateDirectory = "zammad";
17 WorkingDirectory = cfg.dataDir;
18 };
19 environment = {
20 RAILS_ENV = "production";
21 NODE_ENV = "production";
22 RAILS_SERVE_STATIC_FILES = "true";
23 RAILS_LOG_TO_STDOUT = "true";
24 };
25 databaseConfig = settingsFormat.generate "database.yml" cfg.database.settings;
26in
27{
28
29 options = {
30 services.zammad = {
31 enable = mkEnableOption (lib.mdDoc "Zammad, a web-based, open source user support/ticketing solution");
32
33 package = mkOption {
34 type = types.package;
35 default = pkgs.zammad;
36 defaultText = literalExpression "pkgs.zammad";
37 description = lib.mdDoc "Zammad package to use.";
38 };
39
40 dataDir = mkOption {
41 type = types.path;
42 default = "/var/lib/zammad";
43 description = lib.mdDoc ''
44 Path to a folder that will contain Zammad working directory.
45 '';
46 };
47
48 host = mkOption {
49 type = types.str;
50 default = "127.0.0.1";
51 example = "192.168.23.42";
52 description = lib.mdDoc "Host address.";
53 };
54
55 openPorts = mkOption {
56 type = types.bool;
57 default = false;
58 description = lib.mdDoc "Whether to open firewall ports for Zammad";
59 };
60
61 port = mkOption {
62 type = types.port;
63 default = 3000;
64 description = lib.mdDoc "Web service port.";
65 };
66
67 websocketPort = mkOption {
68 type = types.port;
69 default = 6042;
70 description = lib.mdDoc "Websocket service port.";
71 };
72
73 database = {
74 type = mkOption {
75 type = types.enum [ "PostgreSQL" "MySQL" ];
76 default = "PostgreSQL";
77 example = "MySQL";
78 description = lib.mdDoc "Database engine to use.";
79 };
80
81 host = mkOption {
82 type = types.nullOr types.str;
83 default = {
84 PostgreSQL = "/run/postgresql";
85 MySQL = "localhost";
86 }.${cfg.database.type};
87 defaultText = literalExpression ''
88 {
89 PostgreSQL = "/run/postgresql";
90 MySQL = "localhost";
91 }.''${config.services.zammad.database.type};
92 '';
93 description = lib.mdDoc ''
94 Database host address.
95 '';
96 };
97
98 port = mkOption {
99 type = types.nullOr types.port;
100 default = null;
101 description = lib.mdDoc "Database port. Use `null` for default port.";
102 };
103
104 name = mkOption {
105 type = types.str;
106 default = "zammad";
107 description = lib.mdDoc ''
108 Database name.
109 '';
110 };
111
112 user = mkOption {
113 type = types.nullOr types.str;
114 default = "zammad";
115 description = lib.mdDoc "Database user.";
116 };
117
118 passwordFile = mkOption {
119 type = types.nullOr types.path;
120 default = null;
121 example = "/run/keys/zammad-dbpassword";
122 description = lib.mdDoc ''
123 A file containing the password for {option}`services.zammad.database.user`.
124 '';
125 };
126
127 createLocally = mkOption {
128 type = types.bool;
129 default = true;
130 description = lib.mdDoc "Whether to create a local database automatically.";
131 };
132
133 settings = mkOption {
134 type = settingsFormat.type;
135 default = { };
136 example = literalExpression ''
137 {
138 }
139 '';
140 description = lib.mdDoc ''
141 The {file}`database.yml` configuration file as key value set.
142 See \<TODO\>
143 for list of configuration parameters.
144 '';
145 };
146 };
147
148 secretKeyBaseFile = mkOption {
149 type = types.nullOr types.path;
150 default = null;
151 example = "/run/keys/secret_key_base";
152 description = lib.mdDoc ''
153 The path to a file containing the
154 `secret_key_base` secret.
155
156 Zammad uses `secret_key_base` to encrypt
157 the cookie store, which contains session data, and to digest
158 user auth tokens.
159
160 Needs to be a 64 byte long string of hexadecimal
161 characters. You can generate one by running
162
163 ```
164 openssl rand -hex 64 >/path/to/secret_key_base_file
165 ```
166
167 This should be a string, not a nix path, since nix paths are
168 copied into the world-readable nix store.
169 '';
170 };
171 };
172 };
173
174 config = mkIf cfg.enable {
175
176 services.zammad.database.settings = {
177 production = mapAttrs (_: v: mkDefault v) (filterNull {
178 adapter = {
179 PostgreSQL = "postgresql";
180 MySQL = "mysql2";
181 }.${cfg.database.type};
182 database = cfg.database.name;
183 pool = 50;
184 timeout = 5000;
185 encoding = "utf8";
186 username = cfg.database.user;
187 host = cfg.database.host;
188 port = cfg.database.port;
189 });
190 };
191
192 networking.firewall.allowedTCPPorts = mkIf cfg.openPorts [
193 config.services.zammad.port
194 config.services.zammad.websocketPort
195 ];
196
197 users.users.zammad = {
198 isSystemUser = true;
199 home = cfg.dataDir;
200 group = "zammad";
201 };
202
203 users.groups.zammad = { };
204
205 assertions = [
206 {
207 assertion = cfg.database.createLocally -> cfg.database.user == "zammad";
208 message = "services.zammad.database.user must be set to \"zammad\" if services.zammad.database.createLocally is set to true";
209 }
210 {
211 assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
212 message = "a password cannot be specified if services.zammad.database.createLocally is set to true";
213 }
214 ];
215
216 services.mysql = optionalAttrs (cfg.database.createLocally && cfg.database.type == "MySQL") {
217 enable = true;
218 package = mkDefault pkgs.mariadb;
219 ensureDatabases = [ cfg.database.name ];
220 ensureUsers = [
221 {
222 name = cfg.database.user;
223 ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; };
224 }
225 ];
226 };
227
228 services.postgresql = optionalAttrs (cfg.database.createLocally && cfg.database.type == "PostgreSQL") {
229 enable = true;
230 ensureDatabases = [ cfg.database.name ];
231 ensureUsers = [
232 {
233 name = cfg.database.user;
234 ensurePermissions = { "DATABASE ${cfg.database.name}" = "ALL PRIVILEGES"; };
235 }
236 ];
237 };
238
239 systemd.services.zammad-web = {
240 inherit environment;
241 serviceConfig = serviceConfig // {
242 # loading all the gems takes time
243 TimeoutStartSec = 1200;
244 };
245 after = [
246 "network.target"
247 "postgresql.service"
248 ];
249 requires = [
250 "postgresql.service"
251 ];
252 description = "Zammad web";
253 wantedBy = [ "multi-user.target" ];
254 preStart = ''
255 # Blindly copy the whole project here.
256 chmod -R +w .
257 rm -rf ./public/assets/
258 rm -rf ./tmp/*
259 rm -rf ./log/*
260 cp -r --no-preserve=owner ${cfg.package}/* .
261 chmod -R +w .
262 # config file
263 cp ${databaseConfig} ./config/database.yml
264 chmod -R +w .
265 ${optionalString (cfg.database.passwordFile != null) ''
266 {
267 echo -n " password: "
268 cat ${cfg.database.passwordFile}
269 } >> ./config/database.yml
270 ''}
271 ${optionalString (cfg.secretKeyBaseFile != null) ''
272 {
273 echo "production: "
274 echo -n " secret_key_base: "
275 cat ${cfg.secretKeyBaseFile}
276 } > ./config/secrets.yml
277 ''}
278
279 if [ `${config.services.postgresql.package}/bin/psql \
280 --host ${cfg.database.host} \
281 ${optionalString
282 (cfg.database.port != null)
283 "--port ${toString cfg.database.port}"} \
284 --username ${cfg.database.user} \
285 --dbname ${cfg.database.name} \
286 --command "SELECT COUNT(*) FROM pg_class c \
287 JOIN pg_namespace s ON s.oid = c.relnamespace \
288 WHERE s.nspname NOT IN ('pg_catalog', 'pg_toast', 'information_schema') \
289 AND s.nspname NOT LIKE 'pg_temp%';" | sed -n 3p` -eq 0 ]; then
290 echo "Initialize database"
291 ./bin/rake --no-system db:migrate
292 ./bin/rake --no-system db:seed
293 else
294 echo "Migrate database"
295 ./bin/rake --no-system db:migrate
296 fi
297 echo "Done"
298 '';
299 script = "./script/rails server -b ${cfg.host} -p ${toString cfg.port}";
300 };
301
302 systemd.services.zammad-websocket = {
303 inherit serviceConfig environment;
304 after = [ "zammad-web.service" ];
305 requires = [ "zammad-web.service" ];
306 description = "Zammad websocket";
307 wantedBy = [ "multi-user.target" ];
308 script = "./script/websocket-server.rb -b ${cfg.host} -p ${toString cfg.websocketPort} start";
309 };
310
311 systemd.services.zammad-scheduler = {
312 inherit environment;
313 serviceConfig = serviceConfig // { Type = "forking"; };
314 after = [ "zammad-web.service" ];
315 requires = [ "zammad-web.service" ];
316 description = "Zammad scheduler";
317 wantedBy = [ "multi-user.target" ];
318 script = "./script/scheduler.rb start";
319 };
320 };
321
322 meta.maintainers = with lib.maintainers; [ garbas taeer ];
323}