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