Merge pull request #16132 from zohl/tt-rss

tt-rss service: init at 16.3

authored by

Rok Garbas and committed by
GitHub
d73c115a 82f08794

+647 -22
+2 -1
nixos/modules/module-list.nix
··· 462 462 ./services/ttys/gpm.nix 463 463 ./services/ttys/kmscon.nix 464 464 ./services/web-apps/pump.io.nix 465 + ./services/web-apps/tt-rss.nix 465 466 ./services/web-servers/apache-httpd/default.nix 466 467 ./services/web-servers/caddy.nix 467 468 ./services/web-servers/fcgiwrap.nix ··· 471 472 ./services/web-servers/lighttpd/gitweb.nix 472 473 ./services/web-servers/lighttpd/inginious.nix 473 474 ./services/web-servers/nginx/default.nix 474 - ./services/web-servers/phpfpm.nix 475 + ./services/web-servers/phpfpm/default.nix 475 476 ./services/web-servers/shellinabox.nix 476 477 ./services/web-servers/tomcat.nix 477 478 ./services/web-servers/uwsgi.nix
+567
nixos/modules/services/web-apps/tt-rss.nix
··· 1 + { config, lib, pkgs, ... }: 2 + 3 + with lib; 4 + let 5 + cfg = config.services.tt-rss; 6 + 7 + configVersion = 26; 8 + 9 + boolToString = b: if b then "true" else "false"; 10 + 11 + cacheDir = "cache"; 12 + lockDir = "lock"; 13 + feedIconsDir = "feed-icons"; 14 + 15 + dbPort = if cfg.database.port == null 16 + then (if cfg.database.type == "pgsql" then 5432 else 3306) 17 + else cfg.database.port; 18 + 19 + poolName = "tt-rss"; 20 + virtualHostName = "tt-rss"; 21 + 22 + tt-rss-config = pkgs.writeText "config.php" '' 23 + <?php 24 + 25 + define('PHP_EXECUTABLE', '${pkgs.php}/bin/php'); 26 + 27 + define('LOCK_DIRECTORY', '${lockDir}'); 28 + define('CACHE_DIR', '${cacheDir}'); 29 + define('ICONS_DIR', '${feedIconsDir}'); 30 + define('ICONS_URL', '${feedIconsDir}'); 31 + define('SELF_URL_PATH', '${cfg.selfUrlPath}'); 32 + 33 + define('MYSQL_CHARSET', 'UTF8'); 34 + 35 + define('DB_TYPE', '${cfg.database.type}'); 36 + define('DB_HOST', '${cfg.database.host}'); 37 + define('DB_USER', '${cfg.database.user}'); 38 + define('DB_NAME', '${cfg.database.name}'); 39 + define('DB_PASS', '${escape ["'" "\\"] cfg.database.password}'); 40 + define('DB_PORT', '${toString dbPort}'); 41 + 42 + define('AUTH_AUTO_CREATE', ${boolToString cfg.auth.autoCreate}); 43 + define('AUTH_AUTO_LOGIN', ${boolToString cfg.auth.autoLogin}); 44 + 45 + define('FEED_CRYPT_KEY', '${escape ["'" "\\"] cfg.feedCryptKey}'); 46 + 47 + 48 + define('SINGLE_USER_MODE', ${boolToString cfg.singleUserMode}); 49 + 50 + define('SIMPLE_UPDATE_MODE', ${boolToString cfg.simpleUpdateMode}); 51 + define('CHECK_FOR_UPDATES', ${boolToString cfg.checkForUpdates}); 52 + 53 + define('FORCE_ARTICLE_PURGE', ${toString cfg.forceArticlePurge}); 54 + define('SESSION_COOKIE_LIFETIME', ${toString cfg.sessionCookieLifetime}); 55 + define('ENABLE_GZIP_OUTPUT', ${boolToString cfg.enableGZipOutput}); 56 + 57 + define('PLUGINS', '${builtins.concatStringsSep "," cfg.plugins}'); 58 + 59 + define('LOG_DESTINATION', '${cfg.logDestination}'); 60 + define('CONFIG_VERSION', ${toString configVersion}); 61 + 62 + 63 + define('PUBSUBHUBBUB_ENABLED', ${boolToString cfg.pubSubHubbub.enable}); 64 + define('PUBSUBHUBBUB_HUB', '${cfg.pubSubHubbub.hub}'); 65 + 66 + define('SPHINX_SERVER', '${cfg.sphinx.server}'); 67 + define('SPHINX_INDEX', '${builtins.concatStringsSep "," cfg.sphinx.index}'); 68 + 69 + define('ENABLE_REGISTRATION', ${boolToString cfg.registration.enable}); 70 + define('REG_NOTIFY_ADDRESS', '${cfg.registration.notifyAddress}'); 71 + define('REG_MAX_USERS', ${toString cfg.registration.maxUsers}); 72 + 73 + define('SMTP_SERVER', '${cfg.email.server}'); 74 + define('SMTP_LOGIN', '${cfg.email.login}'); 75 + define('SMTP_PASSWORD', '${escape ["'" "\\"] cfg.email.password}'); 76 + define('SMTP_SECURE', '${cfg.email.security}'); 77 + 78 + define('SMTP_FROM_NAME', '${escape ["'" "\\"] cfg.email.fromName}'); 79 + define('SMTP_FROM_ADDRESS', '${escape ["'" "\\"] cfg.email.fromAddress}'); 80 + define('DIGEST_SUBJECT', '${escape ["'" "\\"] cfg.email.digestSubject}'); 81 + ''; 82 + 83 + in { 84 + 85 + ###### interface 86 + 87 + options = { 88 + 89 + services.tt-rss = { 90 + 91 + enable = mkEnableOption "tt-rss"; 92 + 93 + user = mkOption { 94 + type = types.str; 95 + default = "nginx"; 96 + example = "nginx"; 97 + description = '' 98 + User account under which both the service and the web-application run. 99 + ''; 100 + }; 101 + 102 + pool = mkOption { 103 + type = types.str; 104 + default = "${poolName}"; 105 + description = '' 106 + Name of existing phpfpm pool that is used to run web-application. 107 + If not specified a pool will be created automatically with 108 + default values. 109 + ''; 110 + }; 111 + 112 + virtualHost = mkOption { 113 + type = types.str; 114 + default = "${virtualHostName}"; 115 + description = '' 116 + Name of existing nginx virtual host that is used to run web-application. 117 + If not specified a host will be created automatically with 118 + default values. 119 + ''; 120 + }; 121 + 122 + database = { 123 + type = mkOption { 124 + type = types.enum ["pgsql" "mysql"]; 125 + default = "pgsql"; 126 + description = '' 127 + Database to store feeds. Supported are pgsql and mysql. 128 + ''; 129 + }; 130 + 131 + host = mkOption { 132 + type = types.str; 133 + default = "localhost"; 134 + description = '' 135 + Host of the database. 136 + ''; 137 + }; 138 + 139 + name = mkOption { 140 + type = types.str; 141 + default = "tt_rss"; 142 + description = '' 143 + Name of the existing database. 144 + ''; 145 + }; 146 + 147 + user = mkOption { 148 + type = types.str; 149 + default = "tt_rss"; 150 + description = '' 151 + The database user. The user must exist and has access to 152 + the specified database. 153 + ''; 154 + }; 155 + 156 + password = mkOption { 157 + type = types.nullOr types.str; 158 + default = null; 159 + description = '' 160 + The database user's password. 161 + ''; 162 + }; 163 + 164 + port = mkOption { 165 + type = types.nullOr types.int; 166 + default = null; 167 + description = '' 168 + The database's port. If not set, the default ports will be provided (5432 169 + and 3306 for pgsql and mysql respectively). 170 + ''; 171 + }; 172 + }; 173 + 174 + auth = { 175 + autoCreate = mkOption { 176 + type = types.bool; 177 + default = true; 178 + description = '' 179 + Allow authentication modules to auto-create users in tt-rss internal 180 + database when authenticated successfully. 181 + ''; 182 + }; 183 + 184 + autoLogin = mkOption { 185 + type = types.bool; 186 + default = true; 187 + description = '' 188 + Automatically login user on remote or other kind of externally supplied 189 + authentication, otherwise redirect to login form as normal. 190 + If set to true, users won't be able to set application language 191 + and settings profile. 192 + ''; 193 + }; 194 + }; 195 + 196 + pubSubHubbub = { 197 + hub = mkOption { 198 + type = types.str; 199 + default = ""; 200 + description = '' 201 + URL to a PubSubHubbub-compatible hub server. If defined, "Published 202 + articles" generated feed would automatically become PUSH-enabled. 203 + ''; 204 + }; 205 + 206 + enable = mkOption { 207 + type = types.bool; 208 + default = false; 209 + description = '' 210 + Enable client PubSubHubbub support in tt-rss. When disabled, tt-rss 211 + won't try to subscribe to PUSH feed updates. 212 + ''; 213 + }; 214 + }; 215 + 216 + sphinx = { 217 + server = mkOption { 218 + type = types.str; 219 + default = "localhost:9312"; 220 + description = '' 221 + Hostname:port combination for the Sphinx server. 222 + ''; 223 + }; 224 + 225 + index = mkOption { 226 + type = types.listOf types.str; 227 + default = ["ttrss" "delta"]; 228 + description = '' 229 + Index names in Sphinx configuration. Example configuration 230 + files are available on tt-rss wiki. 231 + ''; 232 + }; 233 + }; 234 + 235 + registration = { 236 + enable = mkOption { 237 + type = types.bool; 238 + default = false; 239 + description = '' 240 + Allow users to register themselves. Please be aware that allowing 241 + random people to access your tt-rss installation is a security risk 242 + and potentially might lead to data loss or server exploit. Disabled 243 + by default. 244 + ''; 245 + }; 246 + 247 + notifyAddress = mkOption { 248 + type = types.str; 249 + default = ""; 250 + description = '' 251 + Email address to send new user notifications to. 252 + ''; 253 + }; 254 + 255 + maxUsers = mkOption { 256 + type = types.int; 257 + default = 0; 258 + description = '' 259 + Maximum amount of users which will be allowed to register on this 260 + system. 0 - no limit. 261 + ''; 262 + }; 263 + }; 264 + 265 + email = { 266 + server = mkOption { 267 + type = types.str; 268 + default = ""; 269 + example = "localhost:25"; 270 + description = '' 271 + Hostname:port combination to send outgoing mail. Blank - use system 272 + MTA. 273 + ''; 274 + }; 275 + 276 + login = mkOption { 277 + type = types.str; 278 + default = ""; 279 + description = '' 280 + SMTP authentication login used when sending outgoing mail. 281 + ''; 282 + }; 283 + 284 + password = mkOption { 285 + type = types.str; 286 + default = ""; 287 + description = '' 288 + SMTP authentication password used when sending outgoing mail. 289 + ''; 290 + }; 291 + 292 + security = mkOption { 293 + type = types.enum ["" "ssl" "tls"]; 294 + default = ""; 295 + description = '' 296 + Used to select a secure SMTP connection. Allowed values: ssl, tls, 297 + or empty. 298 + ''; 299 + }; 300 + 301 + fromName = mkOption { 302 + type = types.str; 303 + default = "Tiny Tiny RSS"; 304 + description = '' 305 + Name for sending outgoing mail. This applies to password reset 306 + notifications, digest emails and any other mail. 307 + ''; 308 + }; 309 + 310 + fromAddress = mkOption { 311 + type = types.str; 312 + default = ""; 313 + description = '' 314 + Address for sending outgoing mail. This applies to password reset 315 + notifications, digest emails and any other mail. 316 + ''; 317 + }; 318 + 319 + digestSubject = mkOption { 320 + type = types.str; 321 + default = "[tt-rss] New headlines for last 24 hours"; 322 + description = '' 323 + Subject line for email digests. 324 + ''; 325 + }; 326 + }; 327 + 328 + sessionCookieLifetime = mkOption { 329 + type = types.int; 330 + default = 86400; 331 + description = '' 332 + Default lifetime of a session (e.g. login) cookie. In seconds, 333 + 0 means cookie will be deleted when browser closes. 334 + ''; 335 + }; 336 + 337 + selfUrlPath = mkOption { 338 + type = types.str; 339 + description = '' 340 + Full URL of your tt-rss installation. This should be set to the 341 + location of tt-rss directory, e.g. http://example.org/tt-rss/ 342 + You need to set this option correctly otherwise several features 343 + including PUSH, bookmarklets and browser integration will not work properly. 344 + ''; 345 + example = "http://localhost"; 346 + }; 347 + 348 + feedCryptKey = mkOption { 349 + type = types.str; 350 + default = ""; 351 + description = '' 352 + Key used for encryption of passwords for password-protected feeds 353 + in the database. A string of 24 random characters. If left blank, encryption 354 + is not used. Requires mcrypt functions. 355 + Warning: changing this key will make your stored feed passwords impossible 356 + to decrypt. 357 + ''; 358 + }; 359 + 360 + singleUserMode = mkOption { 361 + type = types.bool; 362 + default = true; 363 + 364 + description = '' 365 + Operate in single user mode, disables all functionality related to 366 + multiple users and authentication. Enabling this assumes you have 367 + your tt-rss directory protected by other means (e.g. http auth). 368 + ''; 369 + }; 370 + 371 + simpleUpdateMode = mkOption { 372 + type = types.bool; 373 + default = false; 374 + description = '' 375 + Enables fallback update mode where tt-rss tries to update feeds in 376 + background while tt-rss is open in your browser. 377 + If you don't have a lot of feeds and don't want to or can't run 378 + background processes while not running tt-rss, this method is generally 379 + viable to keep your feeds up to date. 380 + Still, there are more robust (and recommended) updating methods 381 + available, you can read about them here: http://tt-rss.org/wiki/UpdatingFeeds 382 + ''; 383 + }; 384 + 385 + forceArticlePurge = mkOption { 386 + type = types.int; 387 + default = 0; 388 + description = '' 389 + When this option is not 0, users ability to control feed purging 390 + intervals is disabled and all articles (which are not starred) 391 + older than this amount of days are purged. 392 + ''; 393 + }; 394 + 395 + checkForUpdates = mkOption { 396 + type = types.bool; 397 + default = true; 398 + description = '' 399 + Check for updates automatically if running Git version 400 + ''; 401 + }; 402 + 403 + enableGZipOutput = mkOption { 404 + type = types.bool; 405 + default = true; 406 + description = '' 407 + Selectively gzip output to improve wire performance. This requires 408 + PHP Zlib extension on the server. 409 + Enabling this can break tt-rss in several httpd/php configurations, 410 + if you experience weird errors and tt-rss failing to start, blank pages 411 + after login, or content encoding errors, disable it. 412 + ''; 413 + }; 414 + 415 + plugins = mkOption { 416 + type = types.listOf types.str; 417 + default = ["auth_internal" "note"]; 418 + description = '' 419 + List of plugins to load automatically for all users. 420 + System plugins have to be specified here. Please enable at least one 421 + authentication plugin here (auth_*). 422 + Users may enable other user plugins from Preferences/Plugins but may not 423 + disable plugins specified in this list. 424 + Disabling auth_internal in this list would automatically disable 425 + reset password link on the login form. 426 + ''; 427 + }; 428 + 429 + logDestination = mkOption { 430 + type = types.enum ["" "sql" "syslog"]; 431 + default = "sql"; 432 + description = '' 433 + Log destination to use. Possible values: sql (uses internal logging 434 + you can read in Preferences -> System), syslog - logs to system log. 435 + Setting this to blank uses PHP logging (usually to http server 436 + error.log). 437 + ''; 438 + }; 439 + }; 440 + }; 441 + 442 + 443 + ###### implementation 444 + 445 + config = let 446 + root = "/var/lib/tt-rss"; 447 + in mkIf cfg.enable { 448 + 449 + services.phpfpm.pools = if cfg.pool == "${poolName}" then { 450 + "${poolName}" = { 451 + listen = "/var/run/phpfpm/${poolName}.sock"; 452 + extraConfig = '' 453 + listen.owner = nginx 454 + listen.group = nginx 455 + listen.mode = 0600 456 + user = nginx 457 + pm = dynamic 458 + pm.max_children = 75 459 + pm.start_servers = 10 460 + pm.min_spare_servers = 5 461 + pm.max_spare_servers = 20 462 + pm.max_requests = 500 463 + catch_workers_output = 1 464 + ''; 465 + }; 466 + } else {}; 467 + 468 + 469 + services.nginx.virtualHosts = if cfg.virtualHost == "${virtualHostName}" then { 470 + "${virtualHostName}" = { 471 + root = "${root}"; 472 + extraConfig = '' 473 + access_log /var/log/nginx-${virtualHostName}-access.log; 474 + error_log /var/log/nginx-${virtualHostName}-error.log; 475 + ''; 476 + 477 + locations."/" = { 478 + extraConfig = '' 479 + index index.php; 480 + ''; 481 + }; 482 + 483 + locations."~ \.php$" = { 484 + extraConfig = '' 485 + fastcgi_split_path_info ^(.+\.php)(/.+)$; 486 + fastcgi_pass unix:${config.services.phpfpm.pools."${cfg.pool}".listen}; 487 + fastcgi_index index.php; 488 + fastcgi_param SCRIPT_FILENAME ${root}/$fastcgi_script_name; 489 + 490 + include ${pkgs.nginx}/conf/fastcgi_params; 491 + ''; 492 + }; 493 + }; 494 + } else {}; 495 + 496 + 497 + systemd.services.tt-rss = let 498 + dbService = if cfg.database.type == "pgsql" then "postgresql.service" else "mysql.service"; 499 + in { 500 + 501 + description = "Tiny Tiny RSS feeds update daemon"; 502 + 503 + preStart = let 504 + callSql = if cfg.database.type == "pgsql" then (e: '' 505 + ${optionalString (cfg.database.password != null) 506 + "PGPASSWORD=${cfg.database.password}"} ${pkgs.postgresql95}/bin/psql \ 507 + -U ${cfg.database.user} \ 508 + -h ${cfg.database.host} \ 509 + --port ${toString dbPort} \ 510 + -c '${e}' \ 511 + ${cfg.database.name}'') 512 + 513 + else if cfg.database.type == "mysql" then (e: '' 514 + echo '${e}' | ${pkgs.mysql}/bin/mysql \ 515 + ${optionalString (cfg.database.password != null) 516 + "-p${cfg.database.password}"} \ 517 + -u ${cfg.database.user} \ 518 + -h ${cfg.database.host} \ 519 + -P ${toString dbPort} \ 520 + ${cfg.database.name}'') 521 + 522 + else ""; 523 + 524 + in '' 525 + rm -rf "${root}/*" 526 + mkdir -m 755 -p "${root}" 527 + cp -r "${pkgs.tt-rss}/"* "${root}" 528 + ln -sf "${tt-rss-config}" "${root}/config.php" 529 + chown -R "${cfg.user}" "${root}" 530 + chmod -R 755 "${root}" 531 + '' + (optionalString (cfg.database.type == "pgsql") '' 532 + 533 + exists=$(${callSql "select count(*) > 0 from pg_tables where tableowner = user"} \ 534 + | tail -n+3 | head -n-2 | sed -e 's/[ \n\t]*//') 535 + 536 + if [ "$exists" == 'f' ]; then 537 + ${callSql "\\i ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"} 538 + else 539 + echo 'The database contains some data. Leaving it as it is.' 540 + fi; 541 + '') + (optionalString (cfg.database.type == "mysql") '' 542 + 543 + exists=$(${callSql "select count(*) > 0 from information_schema.tables where table_schema = schema()"} \ 544 + | tail -n+2 | sed -e 's/[ \n\t]*//') 545 + 546 + if [ "$exists" == '0' ]; then 547 + ${callSql "\\. ${pkgs.tt-rss}/schema/ttrss_schema_${cfg.database.type}.sql"} 548 + else 549 + echo 'The database contains some data. Leaving it as it is.' 550 + fi; 551 + ''); 552 + 553 + serviceConfig = { 554 + User = "${cfg.user}"; 555 + ExecStart = "${pkgs.php}/bin/php /var/lib/tt-rss/update.php --daemon"; 556 + StandardOutput = "syslog"; 557 + StandardError = "syslog"; 558 + PermissionsStartOnly = true; 559 + }; 560 + 561 + wantedBy = [ "multi-user.target" ]; 562 + requires = ["${dbService}"]; 563 + after = ["network.target" "${dbService}"]; 564 + }; 565 + }; 566 + } 567 +
+13 -21
nixos/modules/services/web-servers/phpfpm.nix nixos/modules/services/web-servers/phpfpm/default.nix
··· 9 9 10 10 pidFile = "${stateDir}/phpfpm.pid"; 11 11 12 + mkPool = n: p: '' 13 + [${n}] 14 + listen = ${p.listen} 15 + ${p.extraConfig} 16 + ''; 17 + 12 18 cfgFile = pkgs.writeText "phpfpm.conf" '' 13 19 [global] 14 20 pid = ${pidFile} ··· 16 22 daemonize = yes 17 23 ${cfg.extraConfig} 18 24 19 - ${concatStringsSep "\n" (mapAttrsToList (n: v: "[${n}]\n${v}") cfg.poolConfigs)} 25 + ${concatStringsSep "\n" (mapAttrsToList mkPool cfg.pools)} 20 26 ''; 21 27 22 28 phpIni = pkgs.writeText "php.ini" '' ··· 61 67 "Options appended to the PHP configuration file <filename>php.ini</filename>."; 62 68 }; 63 69 64 - poolConfigs = mkOption { 65 - type = types.attrsOf types.lines; 70 + pools = mkOption { 71 + type = types.attrsOf (types.submodule (import ./pool-options.nix { 72 + inherit lib; 73 + })); 66 74 default = {}; 67 - example = literalExample '' 68 - { mypool = ''' 69 - listen = /run/phpfpm/mypool 70 - user = nobody 71 - pm = dynamic 72 - pm.max_children = 75 73 - pm.start_servers = 10 74 - pm.min_spare_servers = 5 75 - pm.max_spare_servers = 20 76 - pm.max_requests = 500 77 - '''; 78 - } 79 - ''; 80 75 description = '' 81 - A mapping between PHP FPM pool names and their configurations. 82 - See the documentation on <literal>php-fpm.conf</literal> for 83 - details on configuration directives. If no pools are defined, 84 - the phpfpm service is disabled. 76 + If no pools are defined, the phpfpm service is disabled. 85 77 ''; 86 78 }; 87 79 }; 88 80 }; 89 81 90 - config = mkIf (cfg.poolConfigs != {}) { 82 + config = mkIf (cfg.pools != {}) { 91 83 92 84 systemd.services.phpfpm = { 93 85 wantedBy = [ "multi-user.target" ];
+35
nixos/modules/services/web-servers/phpfpm/pool-options.nix
··· 1 + { lib }: 2 + 3 + with lib; { 4 + 5 + options = { 6 + 7 + listen = mkOption { 8 + type = types.str; 9 + example = "/path/to/unix/socket"; 10 + description = '' 11 + The address on which to accept FastCGI requests. 12 + ''; 13 + }; 14 + 15 + extraConfig = mkOption { 16 + type = types.lines; 17 + example = '' 18 + user = nobody 19 + pm = dynamic 20 + pm.max_children = 75 21 + pm.start_servers = 10 22 + pm.min_spare_servers = 5 23 + pm.max_spare_servers = 20 24 + pm.max_requests = 500 25 + ''; 26 + 27 + description = '' 28 + Extra lines that go into the pool configuration. 29 + See the documentation on <literal>php-fpm.conf</literal> for 30 + details on configuration directives. 31 + ''; 32 + }; 33 + }; 34 + } 35 +
+28
pkgs/servers/tt-rss/default.nix
··· 1 + { stdenv, fetchgit }: 2 + 3 + stdenv.mkDerivation rec { 4 + name = "tt-rss-${version}"; 5 + version = "16.3"; 6 + 7 + src = fetchgit { 8 + url = "https://tt-rss.org/gitlab/fox/tt-rss.git"; 9 + rev = "refs/tags/${version}"; 10 + sha256 = "1584lcq6kcy9f8ik5djb9apck9hxvfpl54sn6yhl3pdfrfdj3nw5"; 11 + }; 12 + 13 + buildPhases = ["unpackPhase" "installPhase"]; 14 + 15 + installPhase = '' 16 + mkdir $out 17 + cp -ra * $out/ 18 + ''; 19 + 20 + meta = with stdenv.lib; { 21 + description = "Web-based news feed (RSS/Atom) aggregator"; 22 + license = licenses.gpl2Plus; 23 + homepage = http://tt-rss.org; 24 + maintainers = with maintainers; [ zohl ]; 25 + platforms = platforms.all; 26 + }; 27 + } 28 +
+2
pkgs/top-level/all-packages.nix
··· 10556 10556 10557 10557 torque = callPackage ../servers/computing/torque { }; 10558 10558 10559 + tt-rss = callPackage ../servers/tt-rss { }; 10560 + 10559 10561 axis2 = callPackage ../servers/http/tomcat/axis2 { }; 10560 10562 10561 10563 unifi = callPackage ../servers/unifi { };