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