1# verifies:
2# 1. Traffic Server is able to start
3# 2. Traffic Server spawns traffic_crashlog upon startup
4# 3. Traffic Server proxies HTTP requests according to URL remapping rules
5# in 'services.trafficserver.remap'
6# 4. Traffic Server applies per-map settings specified with the conf_remap
7# plugin
8# 5. Traffic Server caches HTTP responses
9# 6. Traffic Server processes HTTP PUSH requests
10# 7. Traffic Server can load the healthchecks plugin
11# 8. Traffic Server logs HTTP traffic as configured
12#
13# uses:
14# - bin/traffic_manager
15# - bin/traffic_server
16# - bin/traffic_crashlog
17# - bin/traffic_cache_tool
18# - bin/traffic_ctl
19# - bin/traffic_logcat
20# - bin/traffic_logstats
21# - bin/tspush
22{ pkgs, ... }:
23{
24 name = "trafficserver";
25 meta = with pkgs.lib.maintainers; {
26 maintainers = [ midchildan ];
27 };
28
29 nodes = {
30 ats =
31 {
32 pkgs,
33 lib,
34 config,
35 ...
36 }:
37 let
38 user = config.users.users.trafficserver.name;
39 group = config.users.groups.trafficserver.name;
40 healthchecks = pkgs.writeText "healthchecks.conf" ''
41 /status /tmp/ats.status text/plain 200 500
42 '';
43 in
44 {
45 services.trafficserver.enable = true;
46
47 services.trafficserver.records = {
48 proxy.config.http.server_ports = "80 80:ipv6";
49 proxy.config.hostdb.host_file.path = "/etc/hosts";
50 proxy.config.log.max_space_mb_headroom = 0;
51 proxy.config.http.push_method_enabled = 1;
52
53 # check that cache storage is usable before accepting traffic
54 proxy.config.http.wait_for_cache = 2;
55 };
56
57 services.trafficserver.plugins = [
58 {
59 path = "healthchecks.so";
60 arg = toString healthchecks;
61 }
62 { path = "xdebug.so"; }
63 ];
64
65 services.trafficserver.remap = ''
66 map http://httpbin.test http://httpbin
67 map http://pristine-host-hdr.test http://httpbin \
68 @plugin=conf_remap.so \
69 @pparam=proxy.config.url_remap.pristine_host_hdr=1
70 map http://ats/tspush http://httpbin/cache \
71 @plugin=conf_remap.so \
72 @pparam=proxy.config.http.cache.required_headers=0
73 '';
74
75 services.trafficserver.storage = ''
76 /dev/vdb volume=1
77 '';
78
79 networking.firewall.allowedTCPPorts = [ 80 ];
80 virtualisation.emptyDiskImages = [ 256 ];
81 services.udev.extraRules = ''
82 KERNEL=="vdb", OWNER="${user}", GROUP="${group}"
83 '';
84 };
85
86 httpbin =
87 { pkgs, lib, ... }:
88 let
89 python = pkgs.python3.withPackages (
90 ps: with ps; [
91 httpbin
92 gunicorn
93 gevent
94 ]
95 );
96 in
97 {
98 systemd.services.httpbin = {
99 enable = true;
100 after = [ "network.target" ];
101 wantedBy = [ "multi-user.target" ];
102 serviceConfig = {
103 ExecStart = "${python}/bin/gunicorn -b 0.0.0.0:80 httpbin:app -k gevent";
104 };
105 };
106
107 networking.firewall.allowedTCPPorts = [ 80 ];
108 };
109
110 client =
111 { pkgs, lib, ... }:
112 {
113 environment.systemPackages = with pkgs; [ curl ];
114 };
115 };
116
117 testScript =
118 { nodes, ... }:
119 let
120 sampleFile = pkgs.writeText "sample.txt" ''
121 It's the season of White Album.
122 '';
123 in
124 ''
125 import json
126 import re
127
128 ats.wait_for_unit("trafficserver")
129 ats.wait_for_open_port(80)
130 httpbin.wait_for_unit("httpbin")
131 httpbin.wait_for_open_port(80)
132 client.systemctl("start network-online.target")
133 client.wait_for_unit("network-online.target")
134
135 with subtest("Traffic Server is running"):
136 out = ats.succeed("traffic_ctl server status")
137 assert out.strip() == "Proxy -- on"
138
139 with subtest("traffic_crashlog is running"):
140 ats.succeed("pgrep -f traffic_crashlog")
141
142 with subtest("basic remapping works"):
143 out = client.succeed("curl -vv -H 'Host: httpbin.test' http://ats/headers")
144 assert json.loads(out)["headers"]["Host"] == "httpbin"
145
146 with subtest("conf_remap plugin works"):
147 out = client.succeed(
148 "curl -vv -H 'Host: pristine-host-hdr.test' http://ats/headers"
149 )
150 assert json.loads(out)["headers"]["Host"] == "pristine-host-hdr.test"
151
152 with subtest("caching works"):
153 out = client.succeed(
154 "curl -vv -D - -H 'Host: httpbin.test' -H 'X-Debug: X-Cache' http://ats/cache/60 -o /dev/null"
155 )
156 assert "X-Cache: miss" in out
157
158 out = client.succeed(
159 "curl -vv -D - -H 'Host: httpbin.test' -H 'X-Debug: X-Cache' http://ats/cache/60 -o /dev/null"
160 )
161 assert "X-Cache: hit-fresh" in out
162
163 with subtest("pushing to cache works"):
164 url = "http://ats/tspush"
165
166 ats.succeed(f"echo {url} > /tmp/urls.txt")
167 out = ats.succeed(
168 f"tspush -f '${sampleFile}' -u {url}"
169 )
170 assert "HTTP/1.0 201 Created" in out, "cache push failed"
171
172 out = ats.succeed(
173 "traffic_cache_tool --spans /etc/trafficserver/storage.config find --input /tmp/urls.txt"
174 )
175 assert "Span: /dev/vdb" in out, "cache not stored on disk"
176
177 out = client.succeed(f"curl {url}").strip()
178 expected = (
179 open("${sampleFile}").read().strip()
180 )
181 assert out == expected, "cache content mismatch"
182
183 with subtest("healthcheck plugin works"):
184 out = client.succeed("curl -vv http://ats/status -o /dev/null -w '%{http_code}'")
185 assert out.strip() == "500"
186
187 ats.succeed("touch /tmp/ats.status")
188
189 out = client.succeed("curl -vv http://ats/status -o /dev/null -w '%{http_code}'")
190 assert out.strip() == "200"
191
192 with subtest("logging works"):
193 access_log_path = "/var/log/trafficserver/squid.blog"
194 ats.wait_for_file(access_log_path)
195
196 out = ats.succeed(f"traffic_logcat {access_log_path}").split("\n")[0]
197 expected = "^\S+ \S+ \S+ TCP_MISS/200 \S+ GET http://httpbin/headers - DIRECT/httpbin application/json$"
198 assert re.fullmatch(expected, out) is not None, "no matching logs"
199
200 out = json.loads(ats.succeed(f"traffic_logstats -jf {access_log_path}"))
201 assert isinstance(out, dict)
202 assert out["total"]["error.total"]["req"] == "0", "unexpected log stat"
203 '';
204}