This is an automated email from the ASF dual-hosted git repository.
rawlin pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git
The following commit(s) were added to refs/heads/master by this push:
new cf08c90 TR Ultimate Test Harness - HTTP Load Tests (#6520)
cf08c90 is described below
commit cf08c901a36d5225e1d4cf8f1897dc005a8cc875
Author: Zach Hoffman <[email protected]>
AuthorDate: Mon Jan 24 07:53:46 2022 -0800
TR Ultimate Test Harness - HTTP Load Tests (#6520)
* TR Ultimate Test Harness - HTTP Load Tests
* CDN in a Box configuration for the TRU Test Harness
* Fix whitespace and remove semicolon line endings
* Rename BenchmarkTime to BenchmarkSeconds
* parameters -> parameter
* Only check for HTTP status code 302
* Remove unused function
* Handle errors before asserting string type
* Time out requests after 10 seconds
* Use lib/go-log only instead of golang log library directly
* Match test name with direectory name
* Import CDN in a Box CA certificate
* Allow insecure connection to Traffic Ops if TO_INSECURE is set
* Use Traffic Ops timeout from TO_TIMEOUT
* Initialize logging
* Warn the user when a Traffic Router's Server Interface is skipped
* ServerInterfaceInfo.GetDefaultAddressOrCIDR(): Accept IP addresses too,
not just CIDRs
* ServerInterfaceInfo.GetDefaultAddress() is now guaranteed to not include
subnets
---
.../cdn-in-a-box/docker-compose.tr-load-tests.yml | 51 +++
.../cdn-in-a-box/traffic_ops/to-access.sh | 5 +-
.../traffic_router_load_test/Dockerfile | 65 +++
.../cdn-in-a-box/traffic_router_load_test/run.sh | 45 ++
lib/go-tc/servers.go | 30 +-
lib/go-tc/traffic_router.go | 35 +-
traffic_router/core/src/test/resources/czmap.json | 8 +-
traffic_router/ultimate-test-harness/README.md | 20 +
traffic_router/ultimate-test-harness/http_test.go | 495 +++++++++++++++++++++
9 files changed, 744 insertions(+), 10 deletions(-)
diff --git a/infrastructure/cdn-in-a-box/docker-compose.tr-load-tests.yml
b/infrastructure/cdn-in-a-box/docker-compose.tr-load-tests.yml
new file mode 100644
index 0000000..aeff4a8
--- /dev/null
+++ b/infrastructure/cdn-in-a-box/docker-compose.tr-load-tests.yml
@@ -0,0 +1,51 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+#
+# This compose file runs the traffic ops integration tests assuming
+# there is already a trafficops docker instance. When using docker,
+# make sure any container rpms you need are updated. Below is an
+# example of how to run the main compose with this file:
+#
+# docker-compose -f docker-compose.yml -f docker-compose.traffic-ops-test.yml
up -d edge enroller dns db smtp trafficops trafficvault integration
+# docker-compose -f docker-compose.traffic-ops-test.yml logs -f integration
+
+---
+version: '3.8'
+
+services:
+ load-tests:
+ build:
+ context: ../..
+ dockerfile:
infrastructure/cdn-in-a-box/traffic_router_load_test/Dockerfile
+ hostname: load-tests
+ env_file:
+ - variables.env
+ volumes:
+ - shared:/shared
+ domainname: infra.ciab.test
+
+volumes:
+ schemas:
+ external: false
+ shared:
+ external: false
+ traffic_ops_data:
+ external: false
+ content:
+ external: false
+ ca:
+ external: false
diff --git a/infrastructure/cdn-in-a-box/traffic_ops/to-access.sh
b/infrastructure/cdn-in-a-box/traffic_ops/to-access.sh
index 0eab594..cfd539d 100755
--- a/infrastructure/cdn-in-a-box/traffic_ops/to-access.sh
+++ b/infrastructure/cdn-in-a-box/traffic_ops/to-access.sh
@@ -183,11 +183,14 @@ to-enroll() {
export MY_NET_INTERFACE='eth0'
export MY_DOMAINNAME="$(dnsdomainname)"
MY_IP="$(ifconfig $MY_NET_INTERFACE | grep 'inet ' | tr -s ' ' | cut -d
' ' -f 3)"
- export MY_IP=${MY_IP#"addr:"}
+ export MY_IP="${MY_IP#"addr:"}/24"
export MY_GATEWAY="$(route -n | grep $MY_NET_INTERFACE | grep -E
'^0\.0\.0\.0' | tr -s ' ' | cut -d ' ' -f2)"
MY_NETMASK="$(ifconfig $MY_NET_INTERFACE | grep 'inet ' | tr -s ' ' |
cut -d ' ' -f 5)"
export MY_NETMASK=${MY_NETMASK#"Mask:"}
export MY_IP6_ADDRESS="$(ifconfig $MY_NET_INTERFACE | grep inet6 | grep
-i global | sed 's/addr://' | awk '{ print $2 }')"
+ if [[ "$MY_IP6_ADDRESS" != */64 ]]; then
+ MY_IP6_ADDRESS="${MY_IP6_ADDRESS}/64"
+ fi
export MY_IP6_GATEWAY="$(route -n6 | grep UG | awk '{print $2}')"
case "$serverType" in
diff --git a/infrastructure/cdn-in-a-box/traffic_router_load_test/Dockerfile
b/infrastructure/cdn-in-a-box/traffic_router_load_test/Dockerfile
new file mode 100644
index 0000000..e6e418c
--- /dev/null
+++ b/infrastructure/cdn-in-a-box/traffic_router_load_test/Dockerfile
@@ -0,0 +1,65 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+FROM alpine:3.14 AS load-test-builder
+
+COPY GO_VERSION /
+RUN set -o errexit; \
+ go_version=$(cat /GO_VERSION); \
+ wget -O go.tar.gz
https://dl.google.com/go/go${go_version}.linux-amd64.tar.gz; \
+ tar -C /usr/local -xvzf go.tar.gz; \
+ ln -s /usr/local/go/bin/go /usr/bin/go; \
+ rm go.tar.gz; \
+ architecture=$(uname -m); \
+ mkdir lib64; \
+ # Use musl libc where the go binary expects glibc
+ # Less-generalized: ln -s /lib/ld-musl-x86_64.so.1
/lib64/ld-linux-x86-64.so.2
+ ln -s /lib/ld-musl-${architecture}.so.[0-9]
/lib64/ld-linux-${architecture//_/-}.so.2; \
+ # Test the go binary
+ go version
+ENV GOPATH=/go CGO_ENABLED=0
+ENV PATH="${PATH}:${GOPATH}/bin"
+ARG TC_DIR=${GOPATH}/src/github.com/apache/trafficcontrol
+
+COPY ./lib/ ${TC_DIR}/lib/
+COPY ./traffic_ops/toclientlib/ ${TC_DIR}/traffic_ops/toclientlib/
+COPY ./traffic_ops/v4-client/ ${TC_DIR}/traffic_ops/v4-client/
+COPY ./go.mod ./go.sum ${TC_DIR}/
+COPY ./traffic_router/ultimate-test-harness
${TC_DIR}/traffic_router/ultimate-test-harness
+
+RUN cd ${TC_DIR}/traffic_router/ultimate-test-harness && \
+ go test -c
+
+FROM alpine:3.14 AS load-test
+
+RUN apk add --no-cache \
+ # for to-access.sh
+ bash \
+ # contains dig, required by to-access.sh
+ bind-tools \
+ # to recognize the CDN in a Box CA certificate
+ ca-certificates
+
+WORKDIR /opt/load-test/app
+COPY --from=load-test-builder
/go/src/github.com/apache/trafficcontrol/traffic_router/ultimate-test-harness .
+
+COPY ./infrastructure/cdn-in-a-box/traffic_ops/to-access.sh /
+COPY infrastructure/cdn-in-a-box/dns/set-dns.sh \
+ infrastructure/cdn-in-a-box/dns/insert-self-into-dns.sh \
+ infrastructure/cdn-in-a-box/traffic_router_load_test/run.sh
/usr/local/bin/
+
+CMD run.sh
diff --git a/infrastructure/cdn-in-a-box/traffic_router_load_test/run.sh
b/infrastructure/cdn-in-a-box/traffic_router_load_test/run.sh
new file mode 100755
index 0000000..0d297ef
--- /dev/null
+++ b/infrastructure/cdn-in-a-box/traffic_router_load_test/run.sh
@@ -0,0 +1,45 @@
+#!/bin/bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+# Check that env vars are set
+envvars=( DB_SERVER DB_PORT DB_ROOT_PASS DB_USER DB_USER_PASS ADMIN_USER
ADMIN_PASS)
+set -ex
+for v in $envvars
+do
+ if [[ -z "${!v}" ]]; then echo "$v is unset"; exit 1; fi
+done
+
+set-dns.sh
+insert-self-into-dns.sh
+source /to-access.sh
+
+# Source the CIAB-CA shared SSL environment
+until [[ -v 'X509_GENERATION_COMPLETE' ]]; do
+ echo 'Waiting on X509 vars to be defined'
+ sleep 1
+ if [[ ! -e "$X509_CA_ENV_FILE" ]]; then
+ continue
+ fi
+ source "$X509_CA_ENV_FILE"
+done
+
+# Copy the CIAB-CA certificate to the ca-certificates directory so it can be
added to the trust store
+cp "$X509_CA_CERT_FULL_CHAIN_FILE" /usr/local/share/ca-certificates/
+update-ca-certificates
+
+./ultimate-test-harness.test
diff --git a/lib/go-tc/servers.go b/lib/go-tc/servers.go
index 557ca89..edd2d5d 100644
--- a/lib/go-tc/servers.go
+++ b/lib/go-tc/servers.go
@@ -132,15 +132,33 @@ type ServerInterfaceInfoV40 struct {
RouterPortName string `json:"routerPortName" db:"router_port_name"`
}
-// GetDefaultAddress returns the IPv4 and IPv6 service addresses of the
interface.
+// GetDefaultAddressOrCIDR returns the IPv4 and IPv6 service addresses of the
interface.
func (i *ServerInterfaceInfo) GetDefaultAddress() (string, string) {
- var ipv4 string
- var ipv6 string
+ ipv4, ipv6 := i.GetDefaultAddressOrCIDR()
+ address, _, err := net.ParseCIDR(ipv4)
+ if address != nil && err == nil {
+ ipv4 = address.String()
+ }
+ address, _, err = net.ParseCIDR(ipv6)
+ if address != nil && err == nil {
+ ipv6 = address.String()
+ }
+ return ipv4, ipv6
+}
+
+// GetDefaultAddressOrCIDR returns the IPv4 and IPv6 service addresses of the
interface,
+// including a subnet, if one exists.
+func (i *ServerInterfaceInfo) GetDefaultAddressOrCIDR() (string, string) {
+ var ipv4, ipv6 string
+ var err error
for _, ip := range i.IPAddresses {
if ip.ServiceAddress {
- address, _, err := net.ParseCIDR(ip.Address)
- if err != nil || address == nil {
- continue
+ address := net.ParseIP(ip.Address)
+ if address == nil {
+ address, _, err = net.ParseCIDR(ip.Address)
+ if err != nil || address == nil {
+ continue
+ }
}
if address.To4() != nil {
ipv4 = ip.Address
diff --git a/lib/go-tc/traffic_router.go b/lib/go-tc/traffic_router.go
index d0667cb..5421644 100644
--- a/lib/go-tc/traffic_router.go
+++ b/lib/go-tc/traffic_router.go
@@ -23,6 +23,39 @@ import (
"time"
)
+// CoverageZonePollingPrefix is the prefix of all Names of Parameters used to
define
+// coverage zone polling parameters.
+const CoverageZonePollingPrefix = "coveragezone.polling."
+
+const CoverageZonePollingURL = CoverageZonePollingPrefix + "url"
+
+type CoverageZoneLocation struct {
+ Network []string `json:"network,omitempty"`
+ Network6 []string `json:"network6,omitempty"`
+}
+
+func (c *CoverageZoneLocation) GetFirstIPAddressOfType(isIPv4 bool) string {
+ var network []string
+ if isIPv4 {
+ network = c.Network
+ } else {
+ network = c.Network6
+ }
+ if len(network) < 1 {
+ return ""
+ }
+ return network[0]
+}
+
+// CoverageZoneFile is used for unmarshalling a Coverage Zone File.
+type CoverageZoneFile struct {
+ CoverageZones map[string]CoverageZoneLocation
`json:"coverageZones,omitempty"`
+}
+
+// X_MM_CLIENT_IP is an optional HTTP header that causes Traffic Router to use
its value
+// as the client IP address.
+const X_MM_CLIENT_IP = "X-MM-Client-IP"
+
// SOA (Start of Authority record) defines the SOA record for the CDN's
// top-level domain.
type SOA struct {
@@ -244,7 +277,7 @@ func GetVIPInterface(ts TrafficServer) ServerInterfaceInfo {
// Deprecated: LegacyTrafficServer is deprecated.
func (ts *TrafficServer) ToLegacyServer() LegacyTrafficServer {
vipInterface := GetVIPInterface(*ts)
- ipv4, ipv6 := vipInterface.GetDefaultAddress()
+ ipv4, ipv6 := vipInterface.GetDefaultAddressOrCIDR()
return LegacyTrafficServer{
Profile: ts.Profile,
diff --git a/traffic_router/core/src/test/resources/czmap.json
b/traffic_router/core/src/test/resources/czmap.json
index 840511a..e550c4e 100644
--- a/traffic_router/core/src/test/resources/czmap.json
+++ b/traffic_router/core/src/test/resources/czmap.json
@@ -1,7 +1,7 @@
{
"coverageZones":
{
- "cache-group-01":
+ "CDN_in_a_Box_Edge":
{
"network6":
[
@@ -12,7 +12,11 @@
[
"192.168.8.0/24",
"192.168.9.0/24"
- ]
+ ],
+ "coordinates": {
+ "latitude": 38.897663,
+ "longitude": -77.036574
+ }
}
}
}
diff --git a/traffic_router/ultimate-test-harness/README.md
b/traffic_router/ultimate-test-harness/README.md
new file mode 100644
index 0000000..b751c67
--- /dev/null
+++ b/traffic_router/ultimate-test-harness/README.md
@@ -0,0 +1,20 @@
+<!--
+ Licensed to the Apache Software Foundation (ASF) under one
+ or more contributor license agreements. See the NOTICE file
+ distributed with this work for additional information
+ regarding copyright ownership. The ASF licenses this file
+ to you under the Apache License, Version 2.0 (the
+ "License"); you may not use this file except in compliance
+ with the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing,
+ software distributed under the License is distributed on an
+ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ KIND, either express or implied. See the License for the
+ specific language governing permissions and limitations
+ under the License.
+-->
+
+This is the Traffic Router Ultimate Test Harness, a test to verify Traffic
Router performance.
diff --git a/traffic_router/ultimate-test-harness/http_test.go
b/traffic_router/ultimate-test-harness/http_test.go
new file mode 100644
index 0000000..820473e
--- /dev/null
+++ b/traffic_router/ultimate-test-harness/http_test.go
@@ -0,0 +1,495 @@
+package main
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+ "crypto/tls"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "math/rand"
+ "net"
+ "net/http"
+ "net/url"
+ "os"
+ "strconv"
+ "strings"
+ "testing"
+ "text/tabwriter"
+ "time"
+
+ "github.com/apache/trafficcontrol/lib/go-log"
+ "github.com/apache/trafficcontrol/lib/go-tc"
+ "github.com/apache/trafficcontrol/lib/go-util"
+ client "github.com/apache/trafficcontrol/traffic_ops/v4-client"
+
+ "github.com/kelseyhightower/envconfig"
+)
+
+const (
+ UserAgent = "Traffic Router Load Tests"
+)
+
+type TOConfig struct {
+ TOURL string `required:"true" envconfig:"TO_URL"`
+ TOUser string `required:"true" envconfig:"TO_USER"`
+ TOPassword string `required:"true" envconfig:"TO_PASSWORD"`
+ TOInsecure bool `default:"true" envconfig:"TO_INSECURE"`
+ TOTimeout int `default:"30" envconfig:"TO_TIMEOUT"`
+}
+
+type TRDetails struct {
+ Hostname string
+ IPAddresses []string
+ ClientIP string
+ ClientIPAddressMap IPAddressMap
+ Port int
+ DSHost string
+ CDNName tc.CDNName
+}
+
+type IPAddressMap struct {
+ Zones []string
+ Map map[string]tc.CoverageZoneLocation
+}
+
+type Benchmark struct {
+ RequestsPerSecondThreshold int
+ BenchmarkSeconds int
+ ThreadCount int
+ ClientIP *string
+ PathCount int
+ MaxPathLength int
+ DSType tc.Type
+ TrafficRouters []TRDetails
+ CoverageZoneLocation string
+}
+
+var (
+ toConfig TOConfig
+ toSession *client.Session
+ count int
+)
+
+func getTOConfig(t *testing.T) {
+ err := envconfig.Process("", &toConfig)
+ if err != nil {
+ t.Fatalf("reading configuration from the environment: %s",
err.Error())
+ }
+}
+
+var ipv4Only *bool
+var ipv6Only *bool
+var cdnName *string
+var deliveryServiceName *string
+var trafficRouterName *string
+var clientIPAddress *string
+var useCoverageZone *bool
+var coverageZoneLocation *string
+var requestsPerSecondThreshold *int
+var benchmarkTime *int
+var threadCount *int
+var pathCount *int
+var maxPathLength *int
+
+func init() {
+ rand.Seed(time.Now().UnixNano())
+ ipv4Only = flag.Bool("4", false, "test IPv4 addresses only")
+ ipv6Only = flag.Bool("6", false, "test IPv4 addresses only")
+ cdnName = flag.String("cdn", "", "the name of a CDN to search for
Delivery Services")
+ deliveryServiceName = flag.String("ds", "", "the name (XMLID) of a
Delivery Service to use for tests")
+ trafficRouterName = flag.String("hostname", "", "the hostname of a
Traffic Router to use")
+ clientIPAddress = flag.String("ip_address", "", "spoof your client IP
address to Traffic Router's geolocation")
+ useCoverageZone = flag.Bool("coverage_zone", false, "whether to use an
IP address from the Traffic Router's Coverage Zone File")
+ coverageZoneLocation = flag.String("coverage_zone_location", "", "the
Coverage Zone location to use (implies coverage_zone=true)")
+ requestsPerSecondThreshold = flag.Int("requests_threshold", 8000, "the
minimum number of requests per second a Traffic Router must successfully
respond to")
+ benchmarkTime = flag.Int("benchmark_time", 10, "the duration of each
load test in seconds")
+ threadCount = flag.Int("thread_count", 12, "the number of threads to
use for each test")
+ pathCount = flag.Int("path_count", 10000, "the number of paths to
generate for use in requests to Delivery Services")
+ maxPathLength = flag.Int("max_path_length", 100, "the maximum length
for each generated path")
+
+ log.Init(os.Stderr, os.Stderr, os.Stderr, os.Stderr, os.Stderr)
+}
+
+func getCoverageZoneURL(cdnName tc.CDNName) (string, error) {
+ snapshot, _, err := toSession.GetCRConfig(string(cdnName),
client.RequestOptions{})
+ if err != nil {
+ return "", fmt.Errorf("getting the Snapshot of CDN '%s': %s",
cdnName, err.Error())
+ }
+ czPollingURLInterface, ok :=
snapshot.Response.Config[tc.CoverageZonePollingURL]
+ if !ok {
+ return "", fmt.Errorf("parameter %s was not found in the
Snapshot of CDN '%s'", tc.CoverageZonePollingURL, cdnName)
+ }
+ czPollingURL := czPollingURLInterface.(string)
+ return czPollingURL, nil
+}
+
+func getCoverageZoneFile(czPollingURL string) (tc.CoverageZoneFile, error) {
+ czMap := tc.CoverageZoneFile{}
+ czMapRequest, err := http.NewRequest("GET", czPollingURL, nil)
+ if err != nil {
+ return czMap, fmt.Errorf("creating HTTP request for URL %s:
%s", czPollingURL, err.Error())
+ }
+ czMapRequest.Header.Set("User-Agent", UserAgent)
+ httpClient := http.Client{Timeout: time.Duration(toConfig.TOTimeout) *
time.Second, Transport: &http.Transport{TLSClientConfig:
&tls.Config{InsecureSkipVerify: toConfig.TOInsecure}}}
+ czMapResponse, err := httpClient.Do(czMapRequest)
+ if err != nil {
+ return czMap, fmt.Errorf("getting Coverage Zone File from URL
%s: %s", czPollingURL, err.Error())
+ }
+ defer log.Close(czMapResponse.Body, "closing the Coverage Zone File
response")
+ czMapBytes, err := ioutil.ReadAll(czMapResponse.Body)
+ if err != nil {
+ return czMap, fmt.Errorf("reading Coverage Zone File bytes:
%s", err.Error())
+ }
+ if err = json.Unmarshal(czMapBytes, &czMap); err != nil {
+ return czMap, fmt.Errorf("unmarshalling Coverage Zone Map
bytes: %s", err.Error())
+ }
+ return czMap, nil
+}
+
+func (i *IPAddressMap) buildFromCoverageZoneMap(czMap tc.CoverageZoneFile)
error {
+ i.Zones = make([]string, len(czMap.CoverageZones))
+ i.Map = map[string]tc.CoverageZoneLocation{}
+ zoneIndex := 0
+ for location, networks := range czMap.CoverageZones {
+ coverageZoneLocation := tc.CoverageZoneLocation{
+ Network: make([]string, 2*len(networks.Network)),
+ Network6: make([]string, 2*len(networks.Network6)),
+ }
+ for index, ipAddress := range networks.Network {
+ _, ipNet, err := net.ParseCIDR(ipAddress)
+ if err != nil {
+ return fmt.Errorf("parsing IP address %s in
CIDR notation: %s", ipAddress, err.Error())
+ }
+ coverageZoneLocation.Network[index*2] =
util.FirstIP(ipNet).To4().String()
+ coverageZoneLocation.Network[index*2+1] =
util.LastIP(ipNet).To4().String()
+ }
+ for index, ipAddress6 := range networks.Network6 {
+ _, ipNet, err := net.ParseCIDR(ipAddress6)
+ if err != nil {
+ return fmt.Errorf("parsing IP address %s in
CIDR notation: %s", ipAddress6, err.Error())
+ }
+ coverageZoneLocation.Network6[index*2] =
util.FirstIP(ipNet).To16().String()
+ coverageZoneLocation.Network6[index*2+1] =
util.LastIP(ipNet).To16().String()
+ }
+ i.Map[location] = coverageZoneLocation
+ i.Zones[zoneIndex] = location
+ zoneIndex++
+ }
+ return nil
+}
+
+func buildIPAddressMap(cdnName tc.CDNName) (IPAddressMap, error) {
+ ipAddressMap := IPAddressMap{}
+ czPollingURL, err := getCoverageZoneURL(cdnName)
+ if err != nil {
+ return ipAddressMap, fmt.Errorf("getting Coverage Zone Polling
URL from the Snapshot of CDN '%s': %s", cdnName, err.Error())
+ }
+ czMap, err := getCoverageZoneFile(czPollingURL)
+ if err != nil {
+ return ipAddressMap, fmt.Errorf("getting Coverage Zone File:
%s", err.Error())
+ }
+ if err = ipAddressMap.buildFromCoverageZoneMap(czMap); err != nil {
+ return ipAddressMap, fmt.Errorf("building IP Address Map from
Coverage Zone File: %s", err.Error())
+ }
+
+ return ipAddressMap, nil
+}
+
+func TestLoad(t *testing.T) {
+ var err error
+ if err = flag.Set("test.v", "true"); err != nil {
+ t.Errorf("settings flags 'test.v': %s", err.Error())
+ }
+ flag.Parse()
+ getTOConfig(t)
+
+ if *coverageZoneLocation != "" {
+ *useCoverageZone = true
+ }
+ ipAddressMaps := map[tc.CDNName]IPAddressMap{}
+
+ toSession, _, err = client.LoginWithAgent(toConfig.TOURL,
toConfig.TOUser, toConfig.TOPassword, toConfig.TOInsecure, UserAgent, true,
time.Second*time.Duration(toConfig.TOTimeout))
+ if err != nil {
+ t.Fatalf("logging into Traffic Ops server %s: %s",
toConfig.TOURL, err.Error())
+ }
+
+ trafficRouters, err := getTrafficRouters(*trafficRouterName,
tc.CDNName(*cdnName))
+ if err != nil {
+ t.Fatalf("could not get Traffic Routers: %s", err.Error())
+ }
+
+ var trafficRouterDetailsList []TRDetails
+ for _, trafficRouter := range trafficRouters {
+ var ipAddresses []string
+ for _, serverInterface := range trafficRouter.Interfaces {
+ if !serverInterface.Monitor {
+ log.Warnf("skipping server interface %s of
Traffic Router %s because it is unmonitored", serverInterface.Name,
*trafficRouter.HostName)
+ continue
+ }
+ ipv4, ipv6 := serverInterface.GetDefaultAddress()
+ if ipv4 != "" && !*ipv6Only {
+ ipAddresses = append(ipAddresses, ipv4)
+ }
+ if ipv6 != "" && !*ipv4Only {
+ ipAddresses = append(ipAddresses, "["+ipv6+"]")
+ }
+ }
+ if len(ipAddresses) < 1 {
+ log.Warnf("need at least 1 monitored service address on
an interface of Traffic Router '%s' to use it for benchmarks, but %d such
addresses were found", *trafficRouter.HostName, len(ipAddresses))
+ continue
+ }
+ dsTypeName := tc.DSTypeHTTP
+ httpDSes := getDSes(t, *trafficRouter.CDNID, dsTypeName,
tc.DeliveryServiceName(*deliveryServiceName))
+ if len(httpDSes) < 1 {
+ log.Warnf("at least 1 Delivery Service with type '%s'
is required to run HTTP load tests on Traffic Router '%s', but %d were found",
dsTypeName, *trafficRouter.HostName, len(httpDSes))
+ }
+ dsURL, err := url.Parse(httpDSes[0].ExampleURLs[0])
+ if err != nil {
+ t.Fatalf("parsing Delivery Service URL %s: %s", dsURL,
err.Error())
+ }
+ cdnName := tc.CDNName(*trafficRouter.CDNName)
+
+ trafficRouterDetails := TRDetails{
+ Hostname: *trafficRouter.HostName,
+ IPAddresses: ipAddresses,
+ ClientIP: *clientIPAddress,
+ Port: *trafficRouter.TCPPort,
+ DSHost: dsURL.Host,
+ CDNName: cdnName,
+ }
+ if *useCoverageZone {
+ _, ok := ipAddressMaps[cdnName]
+ if !ok {
+ ipAddressMaps[cdnName], err =
buildIPAddressMap(cdnName)
+ if err != nil {
+ t.Fatalf("building IP Address map for
CDN '%s': %s", cdnName, err.Error())
+ }
+ }
+ trafficRouterDetails.ClientIPAddressMap =
ipAddressMaps[cdnName]
+ }
+ trafficRouterDetailsList = append(trafficRouterDetailsList,
trafficRouterDetails)
+ }
+ if len(trafficRouterDetailsList) < 1 {
+ t.Fatalf("no Traffic Router with at least 1 HTTP Delivery
Service and at least 1 monitored service address was found")
+ }
+
+ benchmark := Benchmark{
+ RequestsPerSecondThreshold: *requestsPerSecondThreshold,
+ BenchmarkSeconds: *benchmarkTime,
+ ThreadCount: *threadCount,
+ PathCount: *pathCount,
+ MaxPathLength: *maxPathLength,
+ TrafficRouters: trafficRouterDetailsList,
+ CoverageZoneLocation: *coverageZoneLocation,
+ }
+
+ passedTests := 0
+ failedTests := 0
+
+ fmt.Printf("Passing criteria: Routing at least %d requests per
second\n", benchmark.RequestsPerSecondThreshold)
+ writer := tabwriter.NewWriter(os.Stdout, 20, 8, 1, '\t',
tabwriter.AlignRight)
+ fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", "Traffic Router",
"Protocol", "Delivery Service", "Passed?", "Requests Per Second", "Redirects",
"Failures")
+ for trafficRouterIndex, trafficRouter := range benchmark.TrafficRouters
{
+ for ipAddressIndex, ipAddress := range
trafficRouter.IPAddresses {
+ trafficRouterURL := fmt.Sprintf("http://%s:%d/",
ipAddress, trafficRouter.Port)
+
+ isIPv4 := strings.Contains(ipAddress, ".")
+ if trafficRouter.ClientIP == "" &&
trafficRouter.ClientIPAddressMap.Zones != nil {
+ if benchmark.CoverageZoneLocation != "" {
+ location :=
trafficRouter.ClientIPAddressMap.Map[benchmark.CoverageZoneLocation]
+ trafficRouter.ClientIP =
location.GetFirstIPAddressOfType(isIPv4)
+ }
+ if trafficRouter.ClientIP == "" {
+ for _, location := range
trafficRouter.ClientIPAddressMap.Map {
+ trafficRouter.ClientIP =
location.GetFirstIPAddressOfType(isIPv4)
+ if trafficRouter.ClientIP != ""
{
+ break
+ }
+ }
+ }
+ }
+
+ redirects, failures := 0, 0
+ redirectsChannels := make([]chan int,
benchmark.ThreadCount)
+ failuresChannels := make([]chan int,
benchmark.ThreadCount)
+ for threadIndex := 0; threadIndex <
benchmark.ThreadCount; threadIndex++ {
+ redirectsChannels[threadIndex] = make(chan int)
+ failuresChannels[threadIndex] = make(chan int)
+ go benchmark.Run(t,
redirectsChannels[threadIndex], failuresChannels[threadIndex],
trafficRouterIndex, trafficRouterURL, ipAddressIndex)
+ }
+
+ for threadIndex := 0; threadIndex <
benchmark.ThreadCount; threadIndex++ {
+ redirects += <-redirectsChannels[threadIndex]
+ failures += <-failuresChannels[threadIndex]
+ }
+ protocol := "IPv6"
+ if isIPv4 {
+ protocol = "IPv4"
+ }
+ var passed string
+ requestsPerSecond := redirects /
benchmark.BenchmarkSeconds
+ if requestsPerSecond >
benchmark.RequestsPerSecondThreshold {
+ passedTests++
+ passed = "Yes"
+ } else {
+ failedTests++
+ passed = "No"
+ }
+ fmt.Fprintf(writer, "%s\t%s\t%s\t%s\t%d\t%d\t%d\n",
trafficRouter.Hostname, protocol, trafficRouter.DSHost, passed,
requestsPerSecond, redirects, failures)
+ writer.Flush()
+ }
+ }
+ summary := fmt.Sprintf("%d out of %d load tests passed", passedTests,
passedTests+failedTests)
+ if failedTests < 1 {
+ t.Logf(summary)
+ } else {
+ t.Fatal(summary)
+ }
+}
+
+func (b *Benchmark) Run(t *testing.T, redirectsChannel chan int,
failuresChannel chan int, trafficRouterIndex int, trafficRouterURL string,
ipAddressIndex int) {
+ paths := generatePaths(b.PathCount, b.MaxPathLength)
+ stopTime := time.Now().Add(time.Duration(b.BenchmarkSeconds) *
time.Second)
+ redirects, failures := 0, 0
+ var req *http.Request
+ var resp *http.Response
+ var err error
+ httpClient := http.Client{
+ CheckRedirect: func(req *http.Request, via []*http.Request)
error {
+ return http.ErrUseLastResponse
+ },
+ Timeout: 10 * time.Second,
+ }
+ trafficRouter := b.TrafficRouters[trafficRouterIndex]
+ for time.Now().Before(stopTime) {
+ requestURL := trafficRouterURL + paths[rand.Intn(len(paths))]
+ if req, err = http.NewRequest("GET", requestURL, nil); err !=
nil {
+ t.Fatalf("creating GET request to Traffic Router '%s'
(IP address %s): %s",
+ trafficRouter.Hostname,
trafficRouter.IPAddresses[ipAddressIndex], err.Error())
+ }
+ req.Header.Set("User-Agent", UserAgent)
+ if trafficRouter.ClientIP != "" {
+ req.Header.Set(tc.X_MM_CLIENT_IP,
trafficRouter.ClientIP)
+ }
+ req.Host = trafficRouter.DSHost
+ resp, err = httpClient.Do(req)
+ if err == nil && resp.StatusCode == http.StatusFound {
+ redirects++
+ } else {
+ failures++
+ }
+ }
+ redirectsChannel <- redirects
+ failuresChannel <- failures
+}
+
+func generatePaths(pathCount, maxPathLength int) []string {
+ const alphabetSize = 26 + 26 + 10
+ alphabet := make([]rune, alphabetSize)
+ index := 0
+ for char := 'A'; char <= 'Z'; char++ {
+ alphabet[index] = char
+ index++
+ }
+ for char := 'a'; char <= 'z'; char++ {
+ alphabet[index] = char
+ index++
+ }
+ for char := '0'; char <= '9'; char++ {
+ alphabet[index] = char
+ index++
+ }
+ paths := make([]string, pathCount)
+ for index = 0; index < pathCount; index++ {
+ pathLength := rand.Intn(maxPathLength)
+ generatedURL := make([]rune, pathLength)
+ for runeIndex := 0; runeIndex < pathLength; runeIndex++ {
+ generatedURL[runeIndex] =
alphabet[rand.Intn(alphabetSize)]
+ }
+ paths[index] = string(generatedURL)
+ }
+ return paths
+}
+
+func getTrafficRouters(trafficRouterName string, cdnName tc.CDNName)
([]tc.ServerV40, error) {
+ requestOptions := client.RequestOptions{QueryParameters: url.Values{
+ "type": {tc.RouterTypeName},
+ "status": {tc.CacheStatusOnline.String()},
+ }}
+ if trafficRouterName != "" {
+ requestOptions.QueryParameters.Set("hostName",
trafficRouterName)
+ }
+ if cdnName != "" {
+ cdnRequestOptions := client.RequestOptions{QueryParameters:
url.Values{
+ "name": {string(cdnName)},
+ }}
+ cdnResponse, _, err := toSession.GetCDNs(cdnRequestOptions)
+ if err != nil {
+ return nil, fmt.Errorf("requesting a CDN named '%s':
%s", cdnName, err.Error())
+ }
+ cdns := cdnResponse.Response
+ if len(cdns) != 1 {
+ return nil, fmt.Errorf("did not find exactly 1 CDN with
name '%s'", cdnName)
+ }
+ requestOptions.QueryParameters.Set("cdn", string(cdnName))
+ }
+ response, _, err := toSession.GetServers(requestOptions)
+ if err != nil {
+ return nil, fmt.Errorf("requesting %s-status Traffic Routers:
%s", requestOptions.QueryParameters["status"], err.Error())
+ }
+ trafficRouters := response.Response
+ if len(trafficRouters) < 1 {
+ return trafficRouters, fmt.Errorf("no Traffic Routers were
found with these criteria: %v", requestOptions.QueryParameters)
+ }
+ return trafficRouters, nil
+}
+
+func getDSes(t *testing.T, cdnId int, dsTypeName tc.DSType, dsName
tc.DeliveryServiceName) []tc.DeliveryServiceV40 {
+ requestOptions := client.RequestOptions{QueryParameters:
url.Values{"name": {dsTypeName.String()}}}
+ var dsType tc.Type
+ {
+ response, _, err := toSession.GetTypes(requestOptions)
+ if err != nil {
+ t.Fatalf("getting type %s: %s",
requestOptions.QueryParameters["name"], err.Error())
+ }
+ types := response.Response
+ if len(types) != 1 {
+ t.Fatalf("did not find exactly 1 type with name '%s'",
requestOptions.QueryParameters["name"])
+ }
+ dsType = types[0]
+ }
+
+ requestOptions = client.RequestOptions{QueryParameters: url.Values{
+ "cdn": {strconv.Itoa(cdnId)},
+ "type": {strconv.Itoa(dsType.ID)},
+ "status": {tc.CacheStatusOnline.String()},
+ }}
+ if dsName != "" {
+ requestOptions.QueryParameters.Set("xmlId", dsName.String())
+ }
+ response, _, err := toSession.GetDeliveryServices(requestOptions)
+ if err != nil {
+ t.Fatalf("getting Delivery Services with type '%s' (type ID
%d): %s", dsType.Name, dsType.ID, err.Error())
+ }
+ httpDSes := response.Response
+ return httpDSes
+}