1{
2 config,
3 lib,
4 options,
5 pkgs,
6 ...
7}:
8
9let
10 cfg = config.services.zabbixServer;
11 opt = options.services.zabbixServer;
12 pgsql = config.services.postgresql;
13 mysql = config.services.mysql;
14
15 inherit (lib)
16 mkAfter
17 mkDefault
18 mkEnableOption
19 mkIf
20 mkMerge
21 mkOption
22 ;
23 inherit (lib)
24 attrValues
25 concatMapStringsSep
26 getName
27 literalExpression
28 optional
29 optionalAttrs
30 optionalString
31 types
32 ;
33 inherit (lib.generators) toKeyValue;
34
35 user = "zabbix";
36 group = "zabbix";
37 runtimeDir = "/run/zabbix";
38 stateDir = "/var/lib/zabbix";
39 passwordFile = "${runtimeDir}/zabbix-dbpassword.conf";
40
41 moduleEnv = pkgs.symlinkJoin {
42 name = "zabbix-server-module-env";
43 paths = attrValues cfg.modules;
44 };
45
46 configFile = pkgs.writeText "zabbix_server.conf" (
47 toKeyValue { listsAsDuplicateKeys = true; } cfg.settings
48 );
49
50 mysqlLocal = cfg.database.createLocally && cfg.database.type == "mysql";
51 pgsqlLocal = cfg.database.createLocally && cfg.database.type == "pgsql";
52
53in
54
55{
56 imports = [
57 (lib.mkRenamedOptionModule
58 [ "services" "zabbixServer" "dbServer" ]
59 [ "services" "zabbixServer" "database" "host" ]
60 )
61 (lib.mkRemovedOptionModule [
62 "services"
63 "zabbixServer"
64 "dbPassword"
65 ] "Use services.zabbixServer.database.passwordFile instead.")
66 (lib.mkRemovedOptionModule [
67 "services"
68 "zabbixServer"
69 "extraConfig"
70 ] "Use services.zabbixServer.settings instead.")
71 ];
72
73 # interface
74
75 options = {
76
77 services.zabbixServer = {
78 enable = mkEnableOption "the Zabbix Server";
79
80 package = mkOption {
81 type = types.package;
82 default =
83 if cfg.database.type == "mysql" then pkgs.zabbix.server-mysql else pkgs.zabbix.server-pgsql;
84 defaultText = literalExpression "pkgs.zabbix.server-pgsql";
85 description = "The Zabbix package to use.";
86 };
87
88 extraPackages = mkOption {
89 type = types.listOf types.package;
90 default = with pkgs; [
91 nettools
92 nmap
93 traceroute
94 ];
95 defaultText = literalExpression "[ nettools nmap traceroute ]";
96 description = ''
97 Packages to be added to the Zabbix {env}`PATH`.
98 Typically used to add executables for scripts, but can be anything.
99 '';
100 };
101
102 modules = mkOption {
103 type = types.attrsOf types.package;
104 description = "A set of modules to load.";
105 default = { };
106 example = literalExpression ''
107 {
108 "dummy.so" = pkgs.stdenv.mkDerivation {
109 name = "zabbix-dummy-module-''${cfg.package.version}";
110 src = cfg.package.src;
111 buildInputs = [ cfg.package ];
112 sourceRoot = "zabbix-''${cfg.package.version}/src/modules/dummy";
113 installPhase = '''
114 mkdir -p $out/lib
115 cp dummy.so $out/lib/
116 ''';
117 };
118 }
119 '';
120 };
121
122 database = {
123 type = mkOption {
124 type = types.enum [
125 "mysql"
126 "pgsql"
127 ];
128 example = "mysql";
129 default = "pgsql";
130 description = "Database engine to use.";
131 };
132
133 host = mkOption {
134 type = types.str;
135 default = "localhost";
136 description = "Database host address.";
137 };
138
139 port = mkOption {
140 type = types.port;
141 default = if cfg.database.type == "mysql" then mysql.port else pgsql.settings.port;
142 defaultText = literalExpression ''
143 if config.${opt.database.type} == "mysql"
144 then config.${options.services.mysql.port}
145 else config.services.postgresql.settings.port
146 '';
147 description = "Database host port.";
148 };
149
150 name = mkOption {
151 type = types.str;
152 default = "zabbix";
153 description = "Database name.";
154 };
155
156 user = mkOption {
157 type = types.str;
158 default = "zabbix";
159 description = "Database user.";
160 };
161
162 passwordFile = mkOption {
163 type = types.nullOr types.path;
164 default = null;
165 example = "/run/keys/zabbix-dbpassword";
166 description = ''
167 A file containing the password corresponding to
168 {option}`database.user`.
169 '';
170 };
171
172 socket = mkOption {
173 type = types.nullOr types.path;
174 default = null;
175 example = "/run/postgresql";
176 description = "Path to the unix socket file to use for authentication.";
177 };
178
179 createLocally = mkOption {
180 type = types.bool;
181 default = true;
182 description = "Whether to create a local database automatically.";
183 };
184 };
185
186 listen = {
187 ip = mkOption {
188 type = types.str;
189 default = "0.0.0.0";
190 description = ''
191 List of comma delimited IP addresses that the trapper should listen on.
192 Trapper will listen on all network interfaces if this parameter is missing.
193 '';
194 };
195
196 port = mkOption {
197 type = types.port;
198 default = 10051;
199 description = ''
200 Listen port for trapper.
201 '';
202 };
203 };
204
205 openFirewall = mkOption {
206 type = types.bool;
207 default = false;
208 description = ''
209 Open ports in the firewall for the Zabbix Server.
210 '';
211 };
212
213 settings = mkOption {
214 type =
215 with types;
216 attrsOf (oneOf [
217 int
218 str
219 (listOf str)
220 ]);
221 default = { };
222 description = ''
223 Zabbix Server configuration. Refer to
224 <https://www.zabbix.com/documentation/current/manual/appendix/config/zabbix_server>
225 for details on supported values.
226 '';
227 example = {
228 CacheSize = "1G";
229 SSHKeyLocation = "/var/lib/zabbix/.ssh";
230 StartPingers = 32;
231 };
232 };
233
234 };
235
236 };
237
238 # implementation
239
240 config = mkIf cfg.enable {
241
242 assertions = [
243 {
244 assertion =
245 cfg.database.createLocally -> cfg.database.user == user && cfg.database.user == cfg.database.name;
246 message = "services.zabbixServer.database.user must be set to ${user} if services.zabbixServer.database.createLocally is set true";
247 }
248 {
249 assertion = cfg.database.createLocally -> cfg.database.passwordFile == null;
250 message = "a password cannot be specified if services.zabbixServer.database.createLocally is set to true";
251 }
252 ];
253
254 services.zabbixServer.settings = mkMerge [
255 {
256 LogType = "console";
257 ListenIP = cfg.listen.ip;
258 ListenPort = cfg.listen.port;
259 # TODO: set to cfg.database.socket if database type is pgsql?
260 DBHost = optionalString (cfg.database.createLocally != true) cfg.database.host;
261 DBName = cfg.database.name;
262 DBUser = cfg.database.user;
263 PidFile = "${runtimeDir}/zabbix_server.pid";
264 SocketDir = runtimeDir;
265 FpingLocation = "/run/wrappers/bin/fping";
266 LoadModule = builtins.attrNames cfg.modules;
267 }
268 (mkIf (cfg.database.createLocally != true) { DBPort = cfg.database.port; })
269 (mkIf (cfg.database.passwordFile != null) { Include = [ "${passwordFile}" ]; })
270 (mkIf (mysqlLocal && cfg.database.socket != null) { DBSocket = cfg.database.socket; })
271 (mkIf (cfg.modules != { }) { LoadModulePath = "${moduleEnv}/lib"; })
272 ];
273
274 networking.firewall = mkIf cfg.openFirewall {
275 allowedTCPPorts = [ cfg.listen.port ];
276 };
277
278 services.mysql = optionalAttrs mysqlLocal {
279 enable = true;
280 package = mkDefault pkgs.mariadb;
281 };
282
283 systemd.services.mysql.postStart = mkAfter (
284 optionalString mysqlLocal ''
285 ( echo "CREATE DATABASE IF NOT EXISTS \`${cfg.database.name}\` CHARACTER SET utf8 COLLATE utf8_bin;"
286 echo "CREATE USER IF NOT EXISTS '${cfg.database.user}'@'localhost' IDENTIFIED WITH ${
287 if (getName config.services.mysql.package == getName pkgs.mariadb) then
288 "unix_socket"
289 else
290 "auth_socket"
291 };"
292 echo "GRANT ALL PRIVILEGES ON \`${cfg.database.name}\`.* TO '${cfg.database.user}'@'localhost';"
293 ) | ${config.services.mysql.package}/bin/mysql -N
294 ''
295 );
296
297 services.postgresql = optionalAttrs pgsqlLocal {
298 enable = true;
299 ensureDatabases = [ cfg.database.name ];
300 ensureUsers = [
301 {
302 name = cfg.database.user;
303 ensureDBOwnership = true;
304 }
305 ];
306 };
307
308 users.users.${user} = {
309 description = "Zabbix daemon user";
310 uid = config.ids.uids.zabbix;
311 inherit group;
312 };
313
314 users.groups.${group} = {
315 gid = config.ids.gids.zabbix;
316 };
317
318 security.wrappers = {
319 fping = {
320 setuid = true;
321 owner = "root";
322 group = "root";
323 source = "${pkgs.fping}/bin/fping";
324 };
325 };
326
327 systemd.services.zabbix-server = {
328 description = "Zabbix Server";
329
330 wantedBy = [ "multi-user.target" ];
331 after = optional mysqlLocal "mysql.service" ++ optional pgsqlLocal "postgresql.service";
332
333 path = [ "/run/wrappers" ] ++ cfg.extraPackages;
334 preStart =
335 ''
336 # pre 19.09 compatibility
337 if test -e "${runtimeDir}/db-created"; then
338 mv "${runtimeDir}/db-created" "${stateDir}/"
339 fi
340 ''
341 + optionalString pgsqlLocal ''
342 if ! test -e "${stateDir}/db-created"; then
343 cat ${cfg.package}/share/zabbix/database/postgresql/schema.sql | ${pgsql.package}/bin/psql ${cfg.database.name}
344 cat ${cfg.package}/share/zabbix/database/postgresql/images.sql | ${pgsql.package}/bin/psql ${cfg.database.name}
345 cat ${cfg.package}/share/zabbix/database/postgresql/data.sql | ${pgsql.package}/bin/psql ${cfg.database.name}
346 touch "${stateDir}/db-created"
347 fi
348 ''
349 + optionalString mysqlLocal ''
350 if ! test -e "${stateDir}/db-created"; then
351 cat ${cfg.package}/share/zabbix/database/mysql/schema.sql | ${mysql.package}/bin/mysql ${cfg.database.name}
352 cat ${cfg.package}/share/zabbix/database/mysql/images.sql | ${mysql.package}/bin/mysql ${cfg.database.name}
353 cat ${cfg.package}/share/zabbix/database/mysql/data.sql | ${mysql.package}/bin/mysql ${cfg.database.name}
354 touch "${stateDir}/db-created"
355 fi
356 ''
357 + optionalString (cfg.database.passwordFile != null) ''
358 # create a copy of the supplied password file in a format zabbix can consume
359 install -m 0600 <(echo "DBPassword = $(cat ${cfg.database.passwordFile})") ${passwordFile}
360 '';
361
362 serviceConfig = {
363 ExecStart = "@${cfg.package}/sbin/zabbix_server zabbix_server -f --config ${configFile}";
364 Restart = "always";
365 RestartSec = 2;
366
367 User = user;
368 Group = group;
369 RuntimeDirectory = "zabbix";
370 StateDirectory = "zabbix";
371 PrivateTmp = true;
372 };
373 };
374
375 systemd.services.httpd.after =
376 optional (config.services.zabbixWeb.enable && mysqlLocal) "mysql.service"
377 ++ optional (config.services.zabbixWeb.enable && pgsqlLocal) "postgresql.service";
378
379 };
380
381}