Merge pull request #119172 from midchildan/package/trafficserver

nixos/trafficserver: init
This commit is contained in:
Luke Granger-Brown 2021-05-03 09:48:07 +01:00 committed by GitHub
commit d922cad4d6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 753 additions and 0 deletions

View file

@ -4747,6 +4747,12 @@
githubId = 1102396;
name = "Jussi Maki";
};
joaquinito2051 = {
email = "joaquinito2051@gmail.com";
github = "heroku-miraheze";
githubId = 61781343;
name = "Joaquín Rufo Gutierrez";
};
jobojeha = {
email = "jobojeha@jeppener.de";
github = "jobojeha";

View file

@ -977,6 +977,7 @@
./services/web-servers/shellinabox.nix
./services/web-servers/tomcat.nix
./services/web-servers/traefik.nix
./services/web-servers/trafficserver.nix
./services/web-servers/ttyd.nix
./services/web-servers/uwsgi.nix
./services/web-servers/varnish/default.nix

View file

@ -0,0 +1,318 @@
{ config, lib, pkgs, ... }:
with lib;
let
cfg = config.services.trafficserver;
user = config.users.users.trafficserver.name;
group = config.users.groups.trafficserver.name;
getManualUrl = name: "https://docs.trafficserver.apache.org/en/latest/admin-guide/files/${name}.en.html";
getConfPath = name: "${pkgs.trafficserver}/etc/trafficserver/${name}";
yaml = pkgs.formats.yaml { };
fromYAML = f:
let
jsonFile = pkgs.runCommand "in.json"
{
nativeBuildInputs = [ pkgs.remarshal ];
} ''
yaml2json < "${f}" > "$out"
'';
in
builtins.fromJSON (builtins.readFile jsonFile);
mkYamlConf = name: cfg:
if cfg != null then {
"trafficserver/${name}.yaml".source = yaml.generate "${name}.yaml" cfg;
} else {
"trafficserver/${name}.yaml".text = "";
};
mkRecordLines = path: value:
if isAttrs value then
lib.mapAttrsToList (n: v: mkRecordLines (path ++ [ n ]) v) value
else if isInt value then
"CONFIG ${concatStringsSep "." path} INT ${toString value}"
else if isFloat value then
"CONFIG ${concatStringsSep "." path} FLOAT ${toString value}"
else
"CONFIG ${concatStringsSep "." path} STRING ${toString value}";
mkRecordsConfig = cfg: concatStringsSep "\n" (flatten (mkRecordLines [ ] cfg));
mkPluginConfig = cfg: concatStringsSep "\n" (map (p: "${p.path} ${p.arg}") cfg);
in
{
options.services.trafficserver = {
enable = mkEnableOption "Apache Traffic Server";
cache = mkOption {
type = types.lines;
default = "";
example = "dest_domain=example.com suffix=js action=never-cache";
description = ''
Caching rules that overrule the origin's caching policy.
Consult the <link xlink:href="${getManualUrl "cache.config"}">upstream
documentation</link> for more details.
'';
};
hosting = mkOption {
type = types.lines;
default = "";
example = "domain=example.com volume=1";
description = ''
Partition the cache according to origin server or domain
Consult the <link xlink:href="${getManualUrl "hosting.config"}">
upstream documentation</link> for more details.
'';
};
ipAllow = mkOption {
type = types.nullOr yaml.type;
default = fromYAML (getConfPath "ip_allow.yaml");
defaultText = "upstream defaults";
example = literalExample {
ip_allow = [{
apply = "in";
ip_addrs = "127.0.0.1";
action = "allow";
methods = "ALL";
}];
};
description = ''
Control client access to Traffic Server and Traffic Server connections
to upstream servers.
Consult the <link xlink:href="${getManualUrl "ip_allow.yaml"}">upstream
documentation</link> for more details.
'';
};
logging = mkOption {
type = types.nullOr yaml.type;
default = fromYAML (getConfPath "logging.yaml");
defaultText = "upstream defaults";
example = literalExample { };
description = ''
Configure logs.
Consult the <link xlink:href="${getManualUrl "logging.yaml"}">upstream
documentation</link> for more details.
'';
};
parent = mkOption {
type = types.lines;
default = "";
example = ''
dest_domain=. method=get parent="p1.example:8080; p2.example:8080" round_robin=true
'';
description = ''
Identify the parent proxies used in an cache hierarchy.
Consult the <link xlink:href="${getManualUrl "parent.config"}">upstream
documentation</link> for more details.
'';
};
plugins = mkOption {
default = [ ];
description = ''
Controls run-time loadable plugins available to Traffic Server, as
well as their configuration.
Consult the <link xlink:href="${getManualUrl "plugin.config"}">upstream
documentation</link> for more details.
'';
type = with types;
listOf (submodule {
options.path = mkOption {
type = str;
example = "xdebug.so";
description = ''
Path to plugin. The path can either be absolute, or relative to
the plugin directory.
'';
};
options.arg = mkOption {
type = str;
default = "";
example = "--header=ATS-My-Debug";
description = "arguments to pass to the plugin";
};
});
};
records = mkOption {
type = with types;
let valueType = (attrsOf (oneOf [ int float str valueType ])) // {
description = "Traffic Server records value";
};
in
valueType;
default = { };
example = literalExample { proxy.config.proxy_name = "my_server"; };
description = ''
List of configurable variables used by Traffic Server.
Consult the <link xlink:href="${getManualUrl "records.config"}">
upstream documentation</link> for more details.
'';
};
remap = mkOption {
type = types.lines;
default = "";
example = "map http://from.example http://origin.example";
description = ''
URL remapping rules used by Traffic Server.
Consult the <link xlink:href="${getManualUrl "remap.config"}">
upstream documentation</link> for more details.
'';
};
splitDns = mkOption {
type = types.lines;
default = "";
example = ''
dest_domain=internal.corp.example named="255.255.255.255:212 255.255.255.254" def_domain=corp.example search_list="corp.example corp1.example"
dest_domain=!internal.corp.example named=255.255.255.253
'';
description = ''
Specify the DNS server that Traffic Server should use under specific
conditions.
Consult the <link xlink:href="${getManualUrl "splitdns.config"}">
upstream documentation</link> for more details.
'';
};
sslMulticert = mkOption {
type = types.lines;
default = "";
example = "dest_ip=* ssl_cert_name=default.pem";
description = ''
Configure SSL server certificates to terminate the SSL sessions.
Consult the <link xlink:href="${getManualUrl "ssl_multicert.config"}">
upstream documentation</link> for more details.
'';
};
sni = mkOption {
type = types.nullOr yaml.type;
default = null;
example = literalExample {
sni = [{
fqdn = "no-http2.example.com";
https = "off";
}];
};
description = ''
Configure aspects of TLS connection handling for both inbound and
outbound connections.
Consult the <link xlink:href="${getManualUrl "sni.yaml"}">upstream
documentation</link> for more details.
'';
};
storage = mkOption {
type = types.lines;
default = "/var/cache/trafficserver 256M";
example = "/dev/disk/by-id/XXXXX volume=1";
description = ''
List all the storage that make up the Traffic Server cache.
Consult the <link xlink:href="${getManualUrl "storage.config"}">
upstream documentation</link> for more details.
'';
};
strategies = mkOption {
type = types.nullOr yaml.type;
default = null;
description = ''
Specify the next hop proxies used in an cache hierarchy and the
algorithms used to select the next proxy.
Consult the <link xlink:href="${getManualUrl "strategies.yaml"}">
upstream documentation</link> for more details.
'';
};
volume = mkOption {
type = types.nullOr yaml.type;
default = "";
example = "volume=1 scheme=http size=20%";
description = ''
Manage cache space more efficiently and restrict disk usage by
creating cache volumes of different sizes.
Consult the <link xlink:href="${getManualUrl "volume.config"}">
upstream documentation</link> for more details.
'';
};
};
config = mkIf cfg.enable {
environment.etc = {
"trafficserver/cache.config".text = cfg.cache;
"trafficserver/hosting.config".text = cfg.hosting;
"trafficserver/parent.config".text = cfg.parent;
"trafficserver/plugin.config".text = mkPluginConfig cfg.plugins;
"trafficserver/records.config".text = mkRecordsConfig cfg.records;
"trafficserver/remap.config".text = cfg.remap;
"trafficserver/splitdns.config".text = cfg.splitDns;
"trafficserver/ssl_multicert.config".text = cfg.sslMulticert;
"trafficserver/storage.config".text = cfg.storage;
"trafficserver/volume.config".text = cfg.volume;
} // (mkYamlConf "ip_allow" cfg.ipAllow)
// (mkYamlConf "logging" cfg.logging)
// (mkYamlConf "sni" cfg.sni)
// (mkYamlConf "strategies" cfg.strategies);
environment.systemPackages = [ pkgs.trafficserver ];
systemd.packages = [ pkgs.trafficserver ];
# Traffic Server does privilege handling independently of systemd, and
# therefore should be started as root
systemd.services.trafficserver = {
enable = true;
wantedBy = [ "multi-user.target" ];
};
# These directories can't be created by systemd because:
#
# 1. Traffic Servers starts as root and switches to an unprivileged user
# afterwards. The runtime directories defined below are assumed to be
# owned by that user.
# 2. The bin/trafficserver script assumes these directories exist.
systemd.tmpfiles.rules = [
"d '/run/trafficserver' - ${user} ${group} - -"
"d '/var/cache/trafficserver' - ${user} ${group} - -"
"d '/var/lib/trafficserver' - ${user} ${group} - -"
"d '/var/log/trafficserver' - ${user} ${group} - -"
];
services.trafficserver = {
records.proxy.config.admin.user_id = user;
records.proxy.config.body_factory.template_sets_dir =
"${pkgs.trafficserver}/etc/trafficserver/body_factory";
};
users.users.trafficserver = {
description = "Apache Traffic Server";
isSystemUser = true;
inherit group;
};
users.groups.trafficserver = { };
};
}

View file

@ -413,6 +413,7 @@ in
# traefik test relies on docker-containers
trac = handleTest ./trac.nix {};
traefik = handleTestOn ["x86_64-linux"] ./traefik.nix {};
trafficserver = handleTest ./trafficserver.nix {};
transmission = handleTest ./transmission.nix {};
trezord = handleTest ./trezord.nix {};
trickster = handleTest ./trickster.nix {};

View file

@ -0,0 +1,176 @@
# verifies:
# 1. Traffic Server is able to start
# 2. Traffic Server spawns traffic_crashlog upon startup
# 3. Traffic Server proxies HTTP requests according to URL remapping rules
# in 'services.trafficserver.remap'
# 4. Traffic Server applies per-map settings specified with the conf_remap
# plugin
# 5. Traffic Server caches HTTP responses
# 6. Traffic Server processes HTTP PUSH requests
# 7. Traffic Server can load the healthchecks plugin
# 8. Traffic Server logs HTTP traffic as configured
#
# uses:
# - bin/traffic_manager
# - bin/traffic_server
# - bin/traffic_crashlog
# - bin/traffic_cache_tool
# - bin/traffic_ctl
# - bin/traffic_logcat
# - bin/traffic_logstats
# - bin/tspush
import ./make-test-python.nix ({ pkgs, ... }: {
name = "trafficserver";
meta = with pkgs.lib.maintainers; {
maintainers = [ midchildan ];
};
nodes = {
ats = { pkgs, lib, config, ... }: let
user = config.users.users.trafficserver.name;
group = config.users.groups.trafficserver.name;
healthchecks = pkgs.writeText "healthchecks.conf" ''
/status /tmp/ats.status text/plain 200 500
'';
in {
services.trafficserver.enable = true;
services.trafficserver.records = {
proxy.config.http.server_ports = "80 80:ipv6";
proxy.config.hostdb.host_file.path = "/etc/hosts";
proxy.config.log.max_space_mb_headroom = 0;
proxy.config.http.push_method_enabled = 1;
# check that cache storage is usable before accepting traffic
proxy.config.http.wait_for_cache = 2;
};
services.trafficserver.plugins = [
{ path = "healthchecks.so"; arg = toString healthchecks; }
{ path = "xdebug.so"; }
];
services.trafficserver.remap = ''
map http://httpbin.test http://httpbin
map http://pristine-host-hdr.test http://httpbin \
@plugin=conf_remap.so \
@pparam=proxy.config.url_remap.pristine_host_hdr=1
map http://ats/tspush http://httpbin/cache \
@plugin=conf_remap.so \
@pparam=proxy.config.http.cache.required_headers=0
'';
services.trafficserver.storage = ''
/dev/vdb volume=1
'';
networking.firewall.allowedTCPPorts = [ 80 ];
virtualisation.emptyDiskImages = [ 256 ];
services.udev.extraRules = ''
KERNEL=="vdb", OWNER="${user}", GROUP="${group}"
'';
};
httpbin = { pkgs, lib, ... }: let
python = pkgs.python3.withPackages
(ps: with ps; [ httpbin gunicorn gevent ]);
in {
systemd.services.httpbin = {
enable = true;
after = [ "network.target" ];
wantedBy = [ "multi-user.target" ];
serviceConfig = {
ExecStart = "${python}/bin/gunicorn -b 0.0.0.0:80 httpbin:app -k gevent";
};
};
networking.firewall.allowedTCPPorts = [ 80 ];
};
client = { pkgs, lib, ... }: {
environment.systemPackages = with pkgs; [ curl ];
};
};
testScript = { nodes, ... }: let
sampleFile = pkgs.writeText "sample.txt" ''
It's the season of White Album.
'';
in ''
import json
import re
ats.wait_for_unit("trafficserver")
ats.wait_for_open_port(80)
httpbin.wait_for_unit("httpbin")
httpbin.wait_for_open_port(80)
with subtest("Traffic Server is running"):
out = ats.succeed("traffic_ctl server status")
assert out.strip() == "Proxy -- on"
with subtest("traffic_crashlog is running"):
ats.succeed("pgrep -f traffic_crashlog")
with subtest("basic remapping works"):
out = client.succeed("curl -vv -H 'Host: httpbin.test' http://ats/headers")
assert json.loads(out)["headers"]["Host"] == "httpbin"
with subtest("conf_remap plugin works"):
out = client.succeed(
"curl -vv -H 'Host: pristine-host-hdr.test' http://ats/headers"
)
assert json.loads(out)["headers"]["Host"] == "pristine-host-hdr.test"
with subtest("caching works"):
out = client.succeed(
"curl -vv -D - -H 'Host: httpbin.test' -H 'X-Debug: X-Cache' http://ats/cache/60 -o /dev/null"
)
assert "X-Cache: miss" in out
out = client.succeed(
"curl -vv -D - -H 'Host: httpbin.test' -H 'X-Debug: X-Cache' http://ats/cache/60 -o /dev/null"
)
assert "X-Cache: hit-fresh" in out
with subtest("pushing to cache works"):
url = "http://ats/tspush"
ats.succeed(f"echo {url} > /tmp/urls.txt")
out = ats.succeed(
f"tspush -f '${sampleFile}' -u {url}"
)
assert "HTTP/1.0 201 Created" in out, "cache push failed"
out = ats.succeed(
"traffic_cache_tool --spans /etc/trafficserver/storage.config find --input /tmp/urls.txt"
)
assert "Span: /dev/vdb" in out, "cache not stored on disk"
out = client.succeed(f"curl {url}").strip()
expected = (
open("${sampleFile}").read().strip()
)
assert out == expected, "cache content mismatch"
with subtest("healthcheck plugin works"):
out = client.succeed("curl -vv http://ats/status -o /dev/null -w '%{http_code}'")
assert out.strip() == "500"
ats.succeed("touch /tmp/ats.status")
out = client.succeed("curl -vv http://ats/status -o /dev/null -w '%{http_code}'")
assert out.strip() == "200"
with subtest("logging works"):
access_log_path = "/var/log/trafficserver/squid.blog"
ats.wait_for_file(access_log_path)
out = ats.succeed(f"traffic_logcat {access_log_path}").split("\n")[0]
expected = "^\S+ \S+ \S+ TCP_MISS/200 \S+ GET http://httpbin/headers - DIRECT/httpbin application/json$"
assert re.fullmatch(expected, out) is not None, "no matching logs"
out = json.loads(ats.succeed(f"traffic_logstats -jf {access_log_path}"))
assert out["total"]["error.total"]["req"] == "0", "unexpected log stat"
'';
})

View file

@ -0,0 +1,40 @@
{ lib
, stdenv
, fetchFromGitHub
, autoreconfHook
, pkg-config
, doxygen
, check
, jansson
, openssl
}:
stdenv.mkDerivation rec {
pname = "cjose";
version = "0.6.1";
src = fetchFromGitHub {
owner = "cisco";
repo = "cjose";
rev = version;
sha256 = "1msyjwmylb5c7jc16ryx3xb9cdwx682ihsm0ni766y6dfwx8bkhp";
};
nativeBuildInputs = [ autoreconfHook pkg-config doxygen ];
buildInputs = [ jansson openssl ];
checkInputs = [ check ];
configureFlags = [
"--with-jansson=${jansson}"
"--with-openssl=${openssl.dev}"
];
meta = with lib; {
homepage = "https://github.com/cisco/cjose";
changelog = "https://github.com/cisco/cjose/blob/${version}/CHANGELOG.md";
description = "C library for Javascript Object Signing and Encryption";
license = licenses.mit;
maintainers = with maintainers; [ midchildan ];
platforms = platforms.all;
};
}

View file

@ -0,0 +1,207 @@
{ lib
, stdenv
, fetchurl
, fetchpatch
, makeWrapper
, nixosTests
, pkg-config
, file
, linuxHeaders
, openssl
, pcre
, perlPackages
, python3
, xz
, zlib
# recommended dependencies
, withHwloc ? true
, hwloc
, withCurl ? true
, curl
, withCurses ? true
, ncurses
, withCap ? stdenv.isLinux
, libcap
, withUnwind ? stdenv.isLinux
, libunwind
# optional dependencies
, withBrotli ? false
, brotli
, withCjose ? false
, cjose
, withGeoIP ? false
, geoip
, withHiredis ? false
, hiredis
, withImageMagick ? false
, imagemagick
, withJansson ? false
, jansson
, withKyotoCabinet ? false
, kyotocabinet
, withLuaJIT ? false
, luajit
, withMaxmindDB ? false
, libmaxminddb
# optional features
, enableWCCP ? false
}:
stdenv.mkDerivation rec {
pname = "trafficserver";
version = "9.0.1";
src = fetchurl {
url = "mirror://apache/trafficserver/trafficserver-${version}.tar.bz2";
sha256 = "1q164pvfmbqh3gzy3bqy96lwd0fdbhz78r06pd92p7rmkqwx005z";
};
patches = [
# Adds support for NixOS
# https://github.com/apache/trafficserver/pull/7697
(fetchpatch {
url = "https://github.com/apache/trafficserver/commit/19d3af481cf74c91fbf713fc9d2f8b138ed5fbaf.diff";
sha256 = "0z1ikgpp00rzrrcqh97931586yn9wbksgai9xlkcjd5cg8gq0150";
})
# Fixes a bug in tspush which pushes incorrect contents to cache
# https://github.com/apache/trafficserver/pull/7696
(fetchpatch {
url = "https://github.com/apache/trafficserver/commit/b08215272872f452787915cd3a8e0b0ea0b88385.diff";
sha256 = "0axk8x1xvd8wvpgcxgyqqg7kgxyxwfgwmisq3xnk1da0cqv9cx9f";
})
];
# NOTE: The upstream README indicates that flex is needed for some features,
# but it actually seems to be unnecessary as of this commit[1]. The detection
# logic for bison and flex is still present in the build script[2], but no
# other code seems to depend on it. This situation is susceptible to change
# though, so it's a good idea to inspect the build scripts periodically.
#
# [1]: https://github.com/apache/trafficserver/pull/5617
# [2]: https://github.com/apache/trafficserver/blob/3fd2c60/configure.ac#L742-L788
nativeBuildInputs = [ makeWrapper pkg-config file python3 ]
++ (with perlPackages; [ perl ExtUtilsMakeMaker ])
++ lib.optionals stdenv.isLinux [ linuxHeaders ];
buildInputs = [
openssl
pcre
perlPackages.perl
] ++ lib.optional withBrotli brotli
++ lib.optional withCap libcap
++ lib.optional withCjose cjose
++ lib.optional withCurl curl
++ lib.optional withGeoIP geoip
++ lib.optional withHiredis hiredis
++ lib.optional withHwloc hwloc
++ lib.optional withImageMagick imagemagick
++ lib.optional withJansson jansson
++ lib.optional withKyotoCabinet kyotocabinet
++ lib.optional withCurses ncurses
++ lib.optional withLuaJIT luajit
++ lib.optional withUnwind libunwind
++ lib.optional withMaxmindDB libmaxminddb;
outputs = [ "out" "man" ];
postPatch = ''
patchShebangs \
iocore/aio/test_AIO.sample \
src/traffic_via/test_traffic_via \
src/traffic_logstats/tests \
tools/check-unused-dependencies
substituteInPlace configure --replace '/usr/bin/file' '${file}/bin/file'
'' + lib.optionalString stdenv.isLinux ''
substituteInPlace configure \
--replace '/usr/include/linux' '${linuxHeaders}/include/linux'
'' + lib.optionalString stdenv.isDarwin ''
# 'xcrun leaks' probably requires non-free XCode
substituteInPlace iocore/net/test_certlookup.cc \
--replace 'xcrun leaks' 'true'
'';
configureFlags = [
"--enable-layout=NixOS"
"--enable-experimental-plugins"
(lib.enableFeature enableWCCP "wccp")
# the configure script can't auto-locate the following from buildInputs
"--with-lzma=${xz.dev}"
"--with-zlib=${zlib.dev}"
(lib.withFeatureAs withHiredis "hiredis" hiredis)
];
installFlags = [
"pkgsysconfdir=${placeholder "out"}/etc/trafficserver"
# replace runtime directories with an install-time placeholder directory
"pkgcachedir=${placeholder "out"}/.install-trafficserver"
"pkglocalstatedir=${placeholder "out"}/.install-trafficserver"
"pkglogdir=${placeholder "out"}/.install-trafficserver"
"pkgruntimedir=${placeholder "out"}/.install-trafficserver"
];
postInstall = ''
substituteInPlace rc/trafficserver.service --replace "syslog.target" ""
install -Dm644 rc/trafficserver.service $out/lib/systemd/system/trafficserver.service
wrapProgram $out/bin/tspush \
--set PERL5LIB '${with perlPackages; makePerlPath [ URI ]}' \
--prefix PATH : "${lib.makeBinPath [ file ]}"
find "$out" -name '*.la' -delete
# ensure no files actually exist in this directory
rmdir $out/.install-trafficserver
'';
installCheckPhase = let
expected = ''
Via header is [uScMsEf p eC:t cCMp sF], Length is 22
Via Header Details:
Request headers received from client :simple request (not conditional)
Result of Traffic Server cache lookup for URL :miss (a cache "MISS")
Response information received from origin server :error in response
Result of document write-to-cache: :no cache write performed
Proxy operation result :unknown
Error codes (if any) :connection to server failed
Tunnel info :no tunneling
Cache Type :cache
Cache Lookup Result :cache miss (url not in cache)
Parent proxy connection status :no parent proxy or unknown
Origin server connection status :connection open failed
'';
in ''
runHook preInstallCheck
diff -Naur <($out/bin/traffic_via '[uScMsEf p eC:t cCMp sF]') - <<EOF
${lib.removeSuffix "\n" expected}
EOF
runHook postInstallCheck
'';
doCheck = true;
doInstallCheck = true;
enableParallelBuilding = true;
passthru.tests = { inherit (nixosTests) trafficserver; };
meta = with lib; {
homepage = "https://trafficserver.apache.org";
changelog = "https://raw.githubusercontent.com/apache/trafficserver/${version}/CHANGELOG-${version}";
description = "Fast, scalable, and extensible HTTP caching proxy server";
longDescription = ''
Apache Traffic Server is a high-performance web proxy cache that improves
network efficiency and performance by caching frequently-accessed
information at the edge of the network. This brings content physically
closer to end users, while enabling faster delivery and reduced bandwidth
use. Traffic Server is designed to improve content delivery for
enterprises, Internet service providers (ISPs), backbone providers, and
large intranets by maximizing existing and available bandwidth.
'';
license = licenses.asl20;
maintainers = with maintainers; [ midchildan joaquinito2051 ];
platforms = platforms.unix;
};
}

View file

@ -14039,6 +14039,8 @@ in
cimg = callPackage ../development/libraries/cimg { };
cjose = callPackage ../development/libraries/cjose { };
scmccid = callPackage ../development/libraries/scmccid { };
ccrtp = callPackage ../development/libraries/ccrtp { };
@ -19408,6 +19410,8 @@ in
thanos = callPackage ../servers/monitoring/thanos { };
trafficserver = callPackage ../servers/http/trafficserver { };
inherit (callPackages ../servers/http/tomcat { })
tomcat7
tomcat8