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