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