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