at 23.11-pre 11 kB view raw
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}