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