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
+}

Reply via email to