From 658e96a0299cb10ec484ace5872d23b3d56d835c Mon Sep 17 00:00:00 2001 From: Robert Butts Date: Tue, 25 Jun 2019 20:23:42 -0600 Subject: [PATCH 1/2] Add tm testcaches http commands to change caches This is with the intention of adding an integration test framework for Traffic Monitor, which can use this tool and send commands to change the fake caches to test various scenarios. --- .../tools/testcaches/fakesrvr/cmd.go | 220 ++++++++++++++++++ .../tools/testcaches/fakesrvr/server.go | 35 +++ .../tools/testcaches/fakesrvrdata/run.go | 96 +++++--- .../tools/testcaches/fakesrvrdata/ths.go | 27 ++- 4 files changed, 348 insertions(+), 30 deletions(-) create mode 100644 traffic_monitor/tools/testcaches/fakesrvr/cmd.go diff --git a/traffic_monitor/tools/testcaches/fakesrvr/cmd.go b/traffic_monitor/tools/testcaches/fakesrvr/cmd.go new file mode 100644 index 0000000000..c29c9281b1 --- /dev/null +++ b/traffic_monitor/tools/testcaches/fakesrvr/cmd.go @@ -0,0 +1,220 @@ +package fakesrvr + +/* + * 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 ( + "net/http" + "strconv" + "strings" + "sync/atomic" + "unsafe" + + "github.com/apache/trafficcontrol/traffic_monitor/tools/testcaches/fakesrvrdata" +) + +type CmdFunc = func(http.ResponseWriter, *http.Request, fakesrvrdata.Ths) + +var cmds = map[string]CmdFunc{ + "setstat": cmdSetStat, + "setdelay": cmdSetDelay, + "setsystem": cmdSetSystem, +} + +// cmdSetStat sets the rate of the given stat increase for the given remap. +// +// query parameters: +// remap: string; required; the full name of the remap whose kbps to set. +// stat: string; required; the stat to set (in_bytes, out_bytes, status_2xx, status_3xx, status_4xx, status_5xx). +// min: unsigned integer; required; new minimum of kbps increase of InBytes stat for the given remap. +// max: unsigned integer; required; new maximum of kbps increase of InBytes stat for the given remap. +// +func cmdSetStat(w http.ResponseWriter, r *http.Request, fakeSrvrDataThs fakesrvrdata.Ths) { + urlQry := r.URL.Query() + + newMinStr := urlQry.Get("min") + newMin, err := strconv.ParseUint(newMinStr, 10, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("error parsing query parameter 'min': must be a positive integer: " + err.Error() + "\n")) + return + } + + newMaxStr := urlQry.Get("max") + newMax, err := strconv.ParseUint(newMaxStr, 10, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("error parsing query parameter 'max': must be a positive integer: " + err.Error() + "\n")) + return + } + + remap := urlQry.Get("remap") + if remap == "" { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("missing query parameter 'remap': must specify a remap to set\n")) + return + } + + stat := urlQry.Get("stat") + + validStats := map[string]struct{}{ + "in_bytes": {}, + "out_bytes": {}, + "status_2xx": {}, + "status_3xx": {}, + "status_4xx": {}, + "status_5xx": {}, + } + + if _, ok := validStats[stat]; !ok { + w.WriteHeader(http.StatusBadRequest) + statNames := []string{} + for statName, _ := range validStats { + statNames = append(statNames, statName) + } + w.Write([]byte("error with query parameter 'stat' '" + stat + "': not found. Valid stats are: [" + strings.Join(statNames, ",") + "\n")) + return + } + + srvr := (*fakesrvrdata.FakeServerData)(fakeSrvrDataThs.Get()) + if _, ok := srvr.ATS.Remaps[remap]; !ok { + w.WriteHeader(http.StatusBadRequest) + remapNames := []string{} + for remapName, _ := range srvr.ATS.Remaps { + remapNames = append(remapNames, remapName) + } + w.Write([]byte("error with query parameter 'remap' '" + remap + "': not found. Valid remaps are: [" + strings.Join(remapNames, ",") + "\n")) + return + } + + incs := <-fakeSrvrDataThs.GetIncrementsChan + inc := incs[remap] + + switch stat { + case "in_bytes": + inc.Min.InBytes = newMin + inc.Max.InBytes = newMax + case "out_bytes": + inc.Min.OutBytes = newMin + inc.Max.OutBytes = newMax + case "status_2xx": + inc.Min.Status2xx = newMin + inc.Max.Status2xx = newMax + case "status_3xx": + inc.Min.Status3xx = newMin + inc.Max.Status3xx = newMax + case "status_4xx": + inc.Min.Status4xx = newMin + inc.Max.Status4xx = newMax + case "status_5xx": + inc.Min.Status5xx = newMin + inc.Max.Status5xx = newMax + default: + panic("unknown stat; should never happen") + } + + fakeSrvrDataThs.IncrementChan <- fakesrvrdata.IncrementChanT{RemapName: remap, BytesPerSec: inc} + + w.WriteHeader(http.StatusNoContent) +} + +func cmdSetDelay(w http.ResponseWriter, r *http.Request, fakeSrvrDataThs fakesrvrdata.Ths) { + urlQry := r.URL.Query() + + newMinStr := urlQry.Get("min") + newMin, err := strconv.ParseUint(newMinStr, 10, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("error parsing query parameter 'min': must be a non-negative integer: " + err.Error() + "\n")) + return + } + + newMaxStr := urlQry.Get("max") + newMax, err := strconv.ParseUint(newMaxStr, 10, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("error parsing query parameter 'max': must be a non-negative integer: " + err.Error() + "\n")) + return + } + + newMinMax := fakesrvrdata.MinMaxUint64{Min: newMin, Max: newMax} + newMinMaxPtr := &newMinMax + + p := (unsafe.Pointer)(newMinMaxPtr) + atomic.StorePointer(fakeSrvrDataThs.DelayMS, p) + w.WriteHeader(http.StatusNoContent) +} + +func cmdSetSystem(w http.ResponseWriter, r *http.Request, fakeSrvrDataThs fakesrvrdata.Ths) { + urlQry := r.URL.Query() + + if newSpeedStr := urlQry.Get("speed"); newSpeedStr != "" { + newSpeed, err := strconv.ParseUint(newSpeedStr, 10, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("error parsing query parameter 'speed': must be a non-negative integer: " + err.Error() + "\n")) + return + } + + srvr := (*fakesrvrdata.FakeServerData)(fakeSrvrDataThs.Get()) + srvr.System.Speed = int(newSpeed) + fakeSrvrDataThs.Set(fakesrvrdata.ThsT(srvr)) + } + + if newLoadAvg1MStr := urlQry.Get("loadavg1m"); newLoadAvg1MStr != "" { + newLoadAvg1M, err := strconv.ParseFloat(newLoadAvg1MStr, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("error parsing query parameter 'loadavg1m': must be a number: " + err.Error() + "\n")) + return + } + + srvr := (*fakesrvrdata.FakeServerData)(fakeSrvrDataThs.Get()) + srvr.System.ProcLoadAvg.CPU1m = newLoadAvg1M + fakeSrvrDataThs.Set(fakesrvrdata.ThsT(srvr)) + } + + if newLoadAvg5MStr := urlQry.Get("loadavg5m"); newLoadAvg5MStr != "" { + newLoadAvg5M, err := strconv.ParseFloat(newLoadAvg5MStr, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("error parsing query parameter 'loadavg5m': must be a number: " + err.Error() + "\n")) + return + } + + srvr := (*fakesrvrdata.FakeServerData)(fakeSrvrDataThs.Get()) + srvr.System.ProcLoadAvg.CPU5m = newLoadAvg5M + fakeSrvrDataThs.Set(fakesrvrdata.ThsT(srvr)) + } + + if newLoadAvg10MStr := urlQry.Get("loadavg10m"); newLoadAvg10MStr != "" { + newLoadAvg10M, err := strconv.ParseFloat(newLoadAvg10MStr, 64) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("error parsing query parameter 'loadavg10m': must be a non-negative integer: " + err.Error() + "\n")) + return + } + + srvr := (*fakesrvrdata.FakeServerData)(fakeSrvrDataThs.Get()) + srvr.System.ProcLoadAvg.CPU10m = newLoadAvg10M + fakeSrvrDataThs.Set(fakesrvrdata.ThsT(srvr)) + } + + w.WriteHeader(http.StatusNoContent) +} diff --git a/traffic_monitor/tools/testcaches/fakesrvr/server.go b/traffic_monitor/tools/testcaches/fakesrvr/server.go index acfbbc7b8c..40fe4dd71d 100644 --- a/traffic_monitor/tools/testcaches/fakesrvr/server.go +++ b/traffic_monitor/tools/testcaches/fakesrvr/server.go @@ -22,8 +22,11 @@ package fakesrvr import ( "encoding/json" "fmt" + "math/rand" "net/http" "strconv" + "strings" + "sync/atomic" "time" "github.com/apache/trafficcontrol/traffic_monitor/tools/testcaches/fakesrvrdata" @@ -40,6 +43,20 @@ func reqIsApplicationSystem(r *http.Request) bool { func astatsHandler(fakeSrvrDataThs fakesrvrdata.Ths) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { srvr := (*fakesrvrdata.FakeServerData)(fakeSrvrDataThs.Get()) + + delayMSPtr := (*fakesrvrdata.MinMaxUint64)(atomic.LoadPointer(fakeSrvrDataThs.DelayMS)) + minDelayMS := delayMSPtr.Min + maxDelayMS := delayMSPtr.Max + + if maxDelayMS != 0 { + delayMS := minDelayMS + if minDelayMS != maxDelayMS { + delayMS += uint64(rand.Int63n(int64((maxDelayMS - minDelayMS)))) + } + delay := time.Duration(delayMS) * time.Millisecond + time.Sleep(delay) + } + // TODO cast to System, if query string `application=system` b := []byte{} err := error(nil) @@ -56,9 +73,27 @@ func astatsHandler(fakeSrvrDataThs fakesrvrdata.Ths) http.HandlerFunc { } } +func cmdHandler(fakeSrvrDataThs fakesrvrdata.Ths) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + path = strings.ToLower(path) + path = strings.TrimLeft(path, "/cmd") + for cmd, cmdF := range cmds { + if strings.HasPrefix(path, cmd) { + cmdF(w, r, fakeSrvrDataThs) + return + } + } + w.WriteHeader(http.StatusNotFound) + w.Write([]byte("command '" + path + "' not found\n")) + } +} + func Serve(port int, fakeSrvrData fakesrvrdata.Ths) *http.Server { mux := http.NewServeMux() mux.HandleFunc("/_astats", astatsHandler(fakeSrvrData)) + mux.HandleFunc("/cmd", cmdHandler(fakeSrvrData)) + mux.HandleFunc("/cmd/", cmdHandler(fakeSrvrData)) server := &http.Server{ Addr: ":" + strconv.Itoa(port), Handler: mux, diff --git a/traffic_monitor/tools/testcaches/fakesrvrdata/run.go b/traffic_monitor/tools/testcaches/fakesrvrdata/run.go index 4a1956c6c8..d2c1a6b782 100644 --- a/traffic_monitor/tools/testcaches/fakesrvrdata/run.go +++ b/traffic_monitor/tools/testcaches/fakesrvrdata/run.go @@ -72,44 +72,82 @@ func Run(s FakeServerData, remapIncrements map[string]BytesPerSec) (Ths, error) } ths := NewThs() ths.Set(&s) + go run(ths, remapIncrements) return ths, nil } +type IncrementChanT struct { + RemapName string + BytesPerSec BytesPerSec +} + // run starts a goroutine incrementing the FakeServerData's values according to the remapIncrements. Never returns. func run(srvrThs Ths, remapIncrements map[string]BytesPerSec) { tickSecs := uint64(1) // adjustable for performance (i.e. a higher number is less CPU work) + + ticker := time.NewTicker(time.Second * time.Duration(tickSecs)) + for { - time.Sleep(time.Second * time.Duration(tickSecs)) - srvr := srvrThs.Get() - newRemaps := copyRemaps(srvr.ATS.Remaps) - for remap, increments := range remapIncrements { - srvrRemap := newRemaps[remap] - if increments.Min.InBytes != increments.Min.InBytes { - i := uint64(rand.Int63n(int64((increments.Max.InBytes-increments.Min.InBytes)*tickSecs))) + (increments.Min.InBytes * tickSecs) - srvrRemap.InBytes += i - srvr.System.ProcNetDev.RcvBytes += i - } - if increments.Min.OutBytes != increments.Max.OutBytes { - i := uint64(rand.Int63n(int64((increments.Max.OutBytes-increments.Min.OutBytes)*tickSecs))) + (increments.Min.OutBytes * tickSecs) - srvrRemap.OutBytes += i - srvr.System.ProcNetDev.SndBytes += i - } - if increments.Min.Status2xx != increments.Max.Status2xx { - srvrRemap.Status2xx += uint64(rand.Int63n(int64((increments.Max.Status2xx-increments.Min.Status2xx)*tickSecs))) + (increments.Min.Status2xx * tickSecs) - } - if increments.Min.Status3xx != increments.Max.Status3xx { - srvrRemap.Status3xx += uint64(rand.Int63n(int64((increments.Max.Status3xx-increments.Min.Status3xx)*tickSecs))) + (increments.Min.Status3xx * tickSecs) - } - if increments.Min.Status4xx != increments.Max.Status4xx { - srvrRemap.Status4xx += uint64(rand.Int63n(int64((increments.Max.Status4xx-increments.Min.Status4xx)*tickSecs))) + (increments.Min.Status4xx * tickSecs) - } - if increments.Min.Status5xx != increments.Max.Status5xx { - srvrRemap.Status5xx += uint64(rand.Int63n(int64((increments.Max.Status5xx-increments.Min.Status5xx)*tickSecs))) + (increments.Min.Status5xx * tickSecs) + select { + case srvrThs.GetIncrementsChan <- remapIncrements: + case newIncrement := <-srvrThs.IncrementChan: + remapIncrements[newIncrement.RemapName] = newIncrement.BytesPerSec + case <-ticker.C: + srvr := srvrThs.Get() + newRemaps := copyRemaps(srvr.ATS.Remaps) + for remap, increments := range remapIncrements { + srvrRemap := newRemaps[remap] + + addInBytes := increments.Min.InBytes * tickSecs + if increments.Min.InBytes != increments.Max.InBytes { + addInBytes += uint64(rand.Int63n(int64((increments.Max.InBytes - increments.Min.InBytes) * tickSecs))) + } + srvrRemap.InBytes += addInBytes + srvr.System.ProcNetDev.RcvBytes += addInBytes + + addOutBytes := increments.Min.OutBytes * tickSecs + if increments.Min.OutBytes != increments.Max.OutBytes { + addOutBytes += uint64(rand.Int63n(int64((increments.Max.OutBytes - increments.Min.OutBytes) * tickSecs))) + } + srvrRemap.OutBytes += addOutBytes + srvr.System.ProcNetDev.SndBytes += addOutBytes + + srvrRemap.Status2xx += increments.Min.Status2xx * tickSecs + if increments.Min.Status2xx != increments.Max.Status2xx { + srvrRemap.Status2xx += uint64(rand.Int63n(int64((increments.Max.Status2xx - increments.Min.Status2xx) * tickSecs))) + } + + srvrRemap.Status3xx += increments.Min.Status3xx * tickSecs + if increments.Min.Status3xx != increments.Max.Status3xx { + srvrRemap.Status3xx += uint64(rand.Int63n(int64((increments.Max.Status3xx - increments.Min.Status3xx) * tickSecs))) + } + + srvrRemap.Status4xx += increments.Min.Status4xx * tickSecs + if increments.Min.Status4xx != increments.Max.Status4xx { + srvrRemap.Status4xx += uint64(rand.Int63n(int64((increments.Max.Status4xx - increments.Min.Status4xx) * tickSecs))) + } + + srvrRemap.Status5xx += increments.Min.Status5xx * tickSecs + if increments.Min.Status5xx != increments.Max.Status5xx { + srvrRemap.Status5xx += uint64(rand.Int63n(int64((increments.Max.Status5xx - increments.Min.Status5xx) * tickSecs))) + } + + newRemaps[remap] = srvrRemap } - newRemaps[remap] = srvrRemap + srvr.ATS.Remaps = newRemaps + srvrThs.Set(srvr) } - srvr.ATS.Remaps = newRemaps - srvrThs.Set(srvr) + } +} + +// tryReceiveIncrement asynchronously tries to recieve from incrementChan if it has a value. +// Returns the value if it was read, or returns false if no value was waiting. +func tryReceiveIncrement(incrementChan <-chan IncrementChanT) (IncrementChanT, bool) { + select { + case inc := <-incrementChan: + return inc, true + default: + return IncrementChanT{}, false } } diff --git a/traffic_monitor/tools/testcaches/fakesrvrdata/ths.go b/traffic_monitor/tools/testcaches/fakesrvrdata/ths.go index e0befa2436..a08566a1d5 100644 --- a/traffic_monitor/tools/testcaches/fakesrvrdata/ths.go +++ b/traffic_monitor/tools/testcaches/fakesrvrdata/ths.go @@ -21,17 +21,42 @@ package fakesrvrdata import ( "sync" + "unsafe" ) +type MinMaxUint64 struct { + Min uint64 + Max uint64 +} + // Ths provides threadsafe access to a ThsT pointer. Note the object itself is not safe for multiple access, and must not be mutated, either by the original owner after calling Set, or by future users who call Get. If you need to mutate, perform a deep copy. type Ths struct { v *ThsT m *sync.RWMutex + // IncrementChan may be used to set the increments for a particular remap. + // Note this is not synchronized with GetIncrementChan, so multiple writers calling GetIncrementChan and IncrmeentChan to get and set will race, unless they are externally synchronized. + IncrementChan chan IncrementChanT + // GetIncrementsChan may be used to get the current increments for all remaps. + // The returned map must not be modified. + // Note this is not synchronized with GetIncrementChan, so multiple writers calling GetIncrementChan and IncrmeentChan to get and set will race, unless they are externally synchronized. + GetIncrementsChan chan map[string]BytesPerSec + + // DelayMS is the minimum and maximum delay to serve requests, in milliseconds. + // Atomic - MUST be accessed with sync/atomic.LoadUintptr and sync/atomic.StoreUintptr. + DelayMS *unsafe.Pointer } func NewThs() Ths { v := ThsT(nil) - return Ths{m: &sync.RWMutex{}, v: &v} + delayMSPtr := &MinMaxUint64{} + delayMSUnsafePtr := unsafe.Pointer(delayMSPtr) + return Ths{ + m: &sync.RWMutex{}, + v: &v, + IncrementChan: make(chan IncrementChanT, 10), // arbitrarily allow 10 writes before blocking. TODO document? config? + GetIncrementsChan: make(chan map[string]BytesPerSec), + DelayMS: &delayMSUnsafePtr, + } } func (t Ths) Set(v ThsT) { From bdfd9f3bf12394904aa806a1d93ec7684d90a0fa Mon Sep 17 00:00:00 2001 From: Robert Butts Date: Sun, 30 Jun 2019 23:33:15 -0600 Subject: [PATCH 2/2] Add TM client, integration test framework --- traffic_monitor/Dockerfile | 41 ++ traffic_monitor/Dockerfile_run.sh | 81 ++++ traffic_monitor/health/event.go | 11 + traffic_monitor/tests/integration/Dockerfile | 48 +++ .../tests/integration/Dockerfile_run.sh | 394 ++++++++++++++++++ traffic_monitor/tests/integration/README.md | 12 + .../tests/integration/client_test.go | 112 +++++ .../tests/integration/config/config.go | 157 +++++++ .../tests/integration/docker-compose.yml | 102 +++++ .../tests/integration/kbps_test.go | 75 ++++ .../integration/traffic-monitor-test.conf | 17 + .../tests/integration/traffic_monitor_test.go | 125 ++++++ traffic_monitor/tmclient/tmclient.go | 252 +++++++++++ traffic_monitor/tools/testcaches/Dockerfile | 29 ++ .../tools/testcaches/Dockerfile_run.sh | 47 +++ traffic_monitor/tools/testcaches/README.md | 65 +++ .../tools/testcaches/fakesrvr/server.go | 2 + traffic_monitor/tools/testto/Dockerfile | 29 ++ .../tools/testto/Dockerfile_run.sh | 41 ++ traffic_monitor/tools/testto/README.md | 36 ++ traffic_monitor/tools/testto/testto.go | 246 +++++++++++ traffic_monitor/variables.env | 22 + 22 files changed, 1944 insertions(+) create mode 100644 traffic_monitor/Dockerfile create mode 100755 traffic_monitor/Dockerfile_run.sh create mode 100644 traffic_monitor/tests/integration/Dockerfile create mode 100755 traffic_monitor/tests/integration/Dockerfile_run.sh create mode 100644 traffic_monitor/tests/integration/README.md create mode 100644 traffic_monitor/tests/integration/client_test.go create mode 100644 traffic_monitor/tests/integration/config/config.go create mode 100644 traffic_monitor/tests/integration/docker-compose.yml create mode 100644 traffic_monitor/tests/integration/kbps_test.go create mode 100644 traffic_monitor/tests/integration/traffic-monitor-test.conf create mode 100644 traffic_monitor/tests/integration/traffic_monitor_test.go create mode 100644 traffic_monitor/tmclient/tmclient.go create mode 100644 traffic_monitor/tools/testcaches/Dockerfile create mode 100755 traffic_monitor/tools/testcaches/Dockerfile_run.sh create mode 100644 traffic_monitor/tools/testto/Dockerfile create mode 100755 traffic_monitor/tools/testto/Dockerfile_run.sh create mode 100644 traffic_monitor/tools/testto/README.md create mode 100644 traffic_monitor/tools/testto/testto.go create mode 100644 traffic_monitor/variables.env diff --git a/traffic_monitor/Dockerfile b/traffic_monitor/Dockerfile new file mode 100644 index 0000000000..9c70c8a62f --- /dev/null +++ b/traffic_monitor/Dockerfile @@ -0,0 +1,41 @@ +# 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 a very simple Dockerfile. +# All it does is install and start the Traffic Monitor, given a Traffic Ops to point it to. +# It doesn't do any of the complex things the Dockerfiles in infrastructure/docker or infrastructure/cdn-in-a-box do, like inserting itself into Traffic Ops. +# It is designed for a very simple use case, where the complex orchestration of other Traffic Control components is done elsewhere (or manually). + +# Example Build and Run: +# docker build --build-arg RPM=traffic_monitor.rpm --tag traffic_monitor:latest . +# +# docker run --detach --name tm --hostname tm --net=tmi --env TO_URI=http://to.invalid:3000 --env TO_USER=user --env TO_PASS=pass traffic_monitor:latest + +FROM centos/systemd +MAINTAINER dev@trafficcontrol.apache.org + +ARG RPM=traffic_monitor.rpm +ADD $RPM / + +RUN yum install -y initscripts +RUN yum install -y /$(basename $RPM) +RUN rm /$(basename $RPM) + +RUN curl -L jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 > /usr/sbin/jq && chmod +x /usr/sbin/jq + +ADD Dockerfile_run.sh / +ENTRYPOINT /Dockerfile_run.sh diff --git a/traffic_monitor/Dockerfile_run.sh b/traffic_monitor/Dockerfile_run.sh new file mode 100755 index 0000000000..f6cffad73f --- /dev/null +++ b/traffic_monitor/Dockerfile_run.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env 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. + +# The following environment variables must be set (ordinarily by `docker run -e` arguments): +# TO_URI +# TO_USER +# TO_PASS +# CDN + +# Check that env vars are set +envvars=( TO_URI TO_USER TO_PASS CDN PORT ) +for v in $envvars +do + if [[ -z $$v ]]; then echo "$v is unset"; exit 1; fi +done + +start() { + service traffic_monitor start + touch /opt/traffic_monitor/var/log/traffic_monitor.log + exec tail -f /opt/traffic_monitor/var/log/traffic_monitor.log +} + +init() { + mkdir -p /opt/traffic_monitor/conf + cat > /opt/traffic_monitor/conf/traffic_monitor.cfg <<- EOF + { + "cache_health_polling_interval_ms": 6000, + "cache_stat_polling_interval_ms": 6000, + "monitor_config_polling_interval_ms": 15000, + "http_timeout_ms": 2000, + "peer_polling_interval_ms": 5000, + "peer_optimistic": true, + "max_events": 200, + "max_stat_history": 5, + "max_health_history": 5, + "health_flush_interval_ms": 20, + "stat_flush_interval_ms": 20, + "log_location_event": "/opt/traffic_monitor/var/log/event.log", + "log_location_error": "/opt/traffic_monitor/var/log/traffic_monitor.log", + "log_location_warning": "/opt/traffic_monitor/var/log/traffic_monitor.log", + "log_location_info": "null", + "log_location_debug": "null", + "serve_read_timeout_ms": 10000, + "serve_write_timeout_ms": 10000, + "http_poll_no_sleep": false, + "static_file_dir": "/opt/traffic_monitor/static/" + } +EOF + + cat > /opt/traffic_monitor/conf/traffic_ops.cfg <<- EOF + { + "username": "$TO_USER", + "password": "$TO_PASS", + "url": "$TO_URI", + "insecure": true, + "cdnName": "$CDN", + "httpListener": ":$PORT" + } + EOF + + echo "INITIALIZED=1" >> /etc/environment +} + +source /etc/environment +if [ -z "$INITIALIZED" ]; then init; fi +start diff --git a/traffic_monitor/health/event.go b/traffic_monitor/health/event.go index 80de3b2c1a..903adf55d7 100644 --- a/traffic_monitor/health/event.go +++ b/traffic_monitor/health/event.go @@ -20,7 +20,9 @@ package health */ import ( + "errors" "fmt" + "strconv" "sync" "time" @@ -33,6 +35,15 @@ func (t Time) MarshalJSON() ([]byte, error) { return []byte(fmt.Sprintf("%d", time.Time(t).Unix())), nil } +func (t *Time) UnmarshalJSON(data []byte) error { + i, err := strconv.ParseInt(string(data), 10, 64) + if err != nil { + return errors.New("health.Time (" + string(data) + ") must be a unix epoch integer: " + err.Error()) + } + *t = Time(time.Unix(i, 0)) + return nil +} + // Event represents an event change in aggregated data. For example, a cache being marked as unavailable. type Event struct { Time Time `json:"time"` diff --git a/traffic_monitor/tests/integration/Dockerfile b/traffic_monitor/tests/integration/Dockerfile new file mode 100644 index 0000000000..97e2d1820e --- /dev/null +++ b/traffic_monitor/tests/integration/Dockerfile @@ -0,0 +1,48 @@ +# 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 a very simple Dockerfile. +# All it does is install and start the Traffic Monitor, given a Traffic Ops to point it to. +# It doesn't do any of the complex things the Dockerfiles in infrastructure/docker or infrastructure/cdn-in-a-box do, like inserting itself into Traffic Ops. +# It is designed for a very simple use case, where the complex orchestration of other Traffic Control components is done elsewhere (or manually). + +# Example Build and Run: +# docker build --build-arg RPM=traffic_monitor.rpm --tag traffic_monitor:latest . +# +# docker run --detach --name tm --hostname tm --net=tmi --env TO_URI=http://to.invalid:3000 --env TO_USER=user --env TO_PASS=pass traffic_monitor:latest + +FROM centos/systemd +MAINTAINER dev@trafficcontrol.apache.org + +# ARG RPM=traffic_monitor.rpm +# ADD $RPM / + +RUN yum install -y initscripts +RUN yum install -y epel-release +RUN yum install -y golang +# RUN yum install -y /$(basename $RPM) +# RUN rm /$(basename $RPM) + +COPY traffic_monitor_integration_test / + +# TODO build go test here, instead of requiring it manually be built beforehand +# See /infrastructure/cdn-in-a-box/traffic_ops_integration_test/Dockerfile + +RUN curl -L jq https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64 > /usr/sbin/jq && chmod +x /usr/sbin/jq + +COPY Dockerfile_run.sh / +ENTRYPOINT /Dockerfile_run.sh diff --git a/traffic_monitor/tests/integration/Dockerfile_run.sh b/traffic_monitor/tests/integration/Dockerfile_run.sh new file mode 100755 index 0000000000..0316a91097 --- /dev/null +++ b/traffic_monitor/tests/integration/Dockerfile_run.sh @@ -0,0 +1,394 @@ +#!/usr/bin/env 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. + +# The following environment variables must be set (ordinarily by `docker run -e` arguments): +# TO_URI +# TO_USER +# TO_PASS +# CDN + +# Check that env vars are set +envvars=( TESTTO_URI TESTTO_PORT TESTCACHES_URI TESTCACHES_PORT_START TM_URI ) +for v in $envvars +do + if [[ -z $$v ]]; then echo "$v is unset"; exit 1; fi +done + + +CFG_FILE=/traffic-monitor-integration-test.cfg + +start() { + printf "DEBUG traffic_monitor_integration starting\n" + + exec /traffic_monitor_integration_test -test.v -cfg $CFG_FILE +} + +init() { + wait_for_to + + curl -Lvsk ${TESTTO_URI}/api/1.2/cdns/fake/snapshot -X POST -d ' +{ + "config": { + "api.cache-control.max_age": "30", + "consistent.dns.routing": "true", + "coveragezone.polling.interval": "30", + "coveragezone.polling.url": "30", + "dnssec.dynamic.response.expiration": "60", + "dnssec.enabled": "false", + "domain_name": "monitor-integration.test", + "federationmapping.polling.interval": "60", + "federationmapping.polling.url": "foo", + "geolocation.polling.interval": "30", + "geolocation.polling.url": "foo", + "keystore.maintenance.interval": "30", + "neustar.polling.interval": "30", + "neustar.polling.url": "foo", + "soa": { + + }, + "dnssec.inception": "0", + "ttls": { + "admin": "30", + "expire": "30", + "minimum": "30", + "refresh": "30", + "retry": "30" + }, + "weight": "1", + "zonemanager.cache.maintenance.interval": "30", + "zonemanager.threadpool.scale": "1" + }, + "contentServers": { + "server0": { + "cacheGroup": "cg0", + "profile": "Edge0", + "fqdn": "server0.monitor-integration.test", + "hashCount": 1, + "hashId": "server0", + "httpsPort" : null, + "ip": "testcaches", + "ip6": null, + "locationId": "", + "port" : 30000, + "status": "REPORTED", + "type": "EDGE", + "deliveryServices": {"ds0":["ds0.monitor-integration.test"]}, + "routingDisabled": 0 + }, + "server1": { + "cacheGroup": "cg0", + "profile": "Edge0", + "fqdn": "server1.monitor-integration.test", + "hashCount": 1, + "hashId": "server1", + "httpsPort" : null, + "ip": "testcaches", + "ip6": null, + "locationId": "", + "port" : 30001, + "status": "REPORTED", + "type": "EDGE", + "deliveryServices": {"ds0":["ds0.monitor-integration.test"]}, + "routingDisabled": 0 + } + }, + "deliveryServices": { + "ds0": { + "anonymousBlockingEnabled": false, + "consistentHashQueryParams": [], + "consistentHashRegex": "", + "coverageZoneOnly": false, + "dispersion": { + "limit": 1, + "shuffled": false + }, + "domains": ["ds0.monitor-integration.test"], + "geolocationProvider": null, + "matchsets": [ + { + "protocol": "HTTP", + "matchlist": [ + { + "regex": "\\.*ds0\\.*", + "match-type": "regex" + } + ] + } + ], + "missLocation": {"lat": 0, "lon": 0}, + "protocol": { + "acceptHttp": true, + "acceptHttps": false, + "redirectToHttps": false + }, + "regionalGeoBlocking": "false", + "responseHeaders": {}, + "requestHeaders": [], + "soa": { + "admin": "60", + "expire": "60", + "minimum": "60", + "refresh": "60", + "retry": "60" + }, + "sslEnabled": false, + "ttl": 60, + "ttls": { + "A": "60", + "AAAA": "60", + "DNSKEY": "60", + "DS": "60", + "NS": "60", + "SOA": "60" + }, + "maxDnsIpsForLocation": 3, + "ip6RoutingEnabled": false, + "routingName": "ccr", + "bypassDestination": null, + "deepCachingType": null, + "geoEnabled": false, + "geoLimitRedirectURL": null, + "staticDnsEntries": [] + } + }, + "edgeLocations": { + "cg0": {"latitude":0, "longitude":0} + }, + "trafficRouterLocations": { + "tr0": {"latitude":0, "longitude":0} + }, + "monitors": { + "trafficmonitor": { + "fqdn": "trafficmonitor.monitor-integration.test", + "httpsPort": null, + "ip": "trafficmonitor", + "ip6": null, + "location": "cg0", + "port": 80, + "profile": "Monitor0", + "status": "REPORTED" + } + }, + "stats": { + "CDN_name": "fake", + "date": 1561000000, + "tm_host": "testto", + "tm_path": "/fake", + "tm_user": "fake", + "tm_version": "integrationtest/0.fake" + } +} +' + + curl -Lvsk ${TESTTO_URI}/api/1.2/cdns/fake/configs/monitoring.json -X POST -d ' +{ + "trafficServers": [ + { + "profile": "Edge0", + "ip": "testcaches", + "status": "REPORTED", + "cacheGroup": "cg0", + "ip6": null, + "port": 30000, + "httpsPort": null, + "hostName": "server0", + "fqdn": "server0.monitor-integration.test", + "interfaceName": "bond0", + "type": "EDGE", + "hashId": "server0", + "deliveryServices": {"ds0":["ds0.monitor-integration.test"]} + }, + { + "profile": "Edge0", + "ip": "testcaches", + "status": "REPORTED", + "cacheGroup": "cg0", + "ip6": null, + "port": 30001, + "httpsPort": null, + "hostName": "server1", + "fqdn": "server1.monitor-integration.test", + "interfaceName": "bond0", + "type": "EDGE", + "hashId": "server1", + "deliveryServices": {"ds0":["ds0.monitor-integration.test"]} + } + ], + "cacheGroups": [ + { + "cg0": { + "name": "cg0", + "coordinates": {"latitude": 0, "longitude": 0} + } + } + ], + "config": { + "peers.polling.interval": 30, + "health.polling.interval": 2000, + "heartbeat.polling.interval": 2000, + "tm.polling.interval": 30 + }, + "trafficMonitors": [ + { + "port": 80, + "ip6": "", + "ip": "trafficmonitor", + "hostName": "trafficmonitor", + "fqdn": "trafficmonitor.traffic-monitor-integration.test", + "profile": "Monitor0", + "location": "cg0", + "status": "REPORTED" + } + ], + "deliveryServices": [ + { + "xmlId": "ds0", + "TotalTpsThreshold": 1000000, + "status": "Available", + "TotalKbpsThreshold": 10000000 + } + ], + "profiles": [ + { + "parameters": { + "health.connection.timeout": 10, + "health.polling.url": "http://${hostname}/_astats?application=plugin.remap", + "health.polling.format": "", + "health.polling.type": "", + "history.count": 0, + "MinFreeKbps": 20000, + "health_threshold": {} + }, + "name": "Edge0", + "type": "EDGE" + }, + { + "parameters": { + "health.connection.timeout": 10, + "health.polling.url": "", + "health.polling.format": "", + "health.polling.type": "", + "history.count": 5, + "MinFreeKbps": 20000, + "health_threshold": {} + }, + "name": "Monitor0", + "type": "RASCAL" + } + ] +} +' + + curl -Lvsk ${TESTTO_URI}/api/1.2/servers -X POST -d ' +[ + { + "cachegroup": "foo", + "cachegroupId": 0, + "cdnId": 1, + "cdnName": "fake", + "deliveryServices": null, + "fqdn": "trafficmonitor.traffic-monitor-integration.test", + "guid": "foo", + "hostName": "trafficmonitor", + "httpsPort": null, + "id": 1, + "iloIpAddress": null, + "iloIpGateway": null, + "iloIpNetmask": null, + "iloPassword": null, + "iloUsername": null, + "interfaceMtu": null, + "interfaceName": "bond0", + "ip6Address": null, + "ip6Gateway": null, + "ipAddress": "trafficmonitor", + "ipGateway": "192.0.0.1", + "ipNetmask": "255.255.255.0", + "lastUpdated": "2019", + "mgmtIpAddress": null, + "mgmtIpGateway": null, + "mgmtIpNetmask": null, + "offlineReason": "none", + "physLocation": "", + "physLocationId": 0, + "profile": "Monitor0", + "profileDesc": "nodesc", + "profileId": 0, + "rack": "", + "revalPending": false, + "routerHostName": "", + "routerPortName": "", + "status": "REPORTED", + "statusId": 0, + "tcpPort": 80, + "type": "RASCAL", + "typeId": 0, + "updPending": false, + "xmppId": "", + "xmppPasswd": "" + } +] +' + + # DEBUG + printf "\n\ntestto:\n" + curl -Lk ${TESTTO_URI}/api/1.2/cdns/foo/snapshot | head -5 + printf "\n\ntestcaches:\n" + curl -Lk ${TESTCACHES_URI}:${TESTCACHES_PORT_START}/_astats | head -5 + printf "\n\ntraffic_monitor:\n" + curl -Lk http://trafficmonitor | head -5 + + + cat > $CFG_FILE <<- EOF +{ + "trafficMonitor": { + "url": "$TM_URI" + }, + "default": { + "session": { + "timeoutInSecs": 30 + }, + "log": { + "debug": "stdout", + "event": "stdout", + "info": "stdout", + "error": "stdout", + "warning": "stdout" + } + } +} +EOF + + echo "INITIALIZED=1" >> /etc/environment +} + +wait_for_to() { + while true; do + curl -Lvsk ${TESTTO_URI}/api/1.2/servers 2>&1 1> /dev/null | grep -E '< HTTP/[0-9]\.?[0-9]* 200' + RC=$?; + if [[ $RC -eq 0 ]] ; then + break; + fi + printf "Waiting for Traffic Ops to return a 200 OK\n"; + sleep 2; + done +} + +source /etc/environment +if [ -z "$INITIALIZED" ]; then init; fi +start diff --git a/traffic_monitor/tests/integration/README.md b/traffic_monitor/tests/integration/README.md new file mode 100644 index 0000000000..2368cb6954 --- /dev/null +++ b/traffic_monitor/tests/integration/README.md @@ -0,0 +1,12 @@ +# Traffic Monitor Integration Test Framework + +## Running + +From the `trafficcontrol/traffic_monitor` directory: + +``` +(cd tools/testto && go build) +(cd tools/testcaches && go build) +(cd tests/integration && go test -c -o traffic_monitor_integration_test) +sudo docker-compose -p tmi --project-directory . -f tests/integration/docker-compose.yml run tmintegrationtest +``` diff --git a/traffic_monitor/tests/integration/client_test.go b/traffic_monitor/tests/integration/client_test.go new file mode 100644 index 0000000000..658275d987 --- /dev/null +++ b/traffic_monitor/tests/integration/client_test.go @@ -0,0 +1,112 @@ +package integration + +/* + + Licensed 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 ( + "testing" +) + +func TestClient(t *testing.T) { + if actual, err := TMClient.CacheCount(); err != nil { + t.Errorf("client CacheCount error expected nil, actual %v\n", err) + } else if actual <= 0 { + t.Errorf("client CacheCount expected > 0 actual %v\n", actual) + } + + if actual, err := TMClient.CacheAvailableCount(); err != nil { + t.Errorf("client CacheAvailableCount error expected nil, actual %v\n", err) + } else if actual <= 0 { + t.Errorf("client CacheAvailableCount expected > 0 actual %v\n", actual) + } + + if actual, err := TMClient.CacheDownCount(); err != nil { + t.Errorf("client CacheDownCount error expected nil, actual %v\n", err) + } else if actual < 0 { + t.Errorf("client CacheDownCount expected >= 0 actual %v\n", actual) + } + + if actual, err := TMClient.Version(); err != nil { + t.Errorf("client Version error expected nil, actual %v\n", err) + } else if actual == "" { + t.Errorf("client Version expected not empty, actual empty\n") + } + + if actual, err := TMClient.TrafficOpsURI(); err != nil { + t.Errorf("client TrafficOpsURI error expected nil, actual %v\n", err) + } else if actual == "" { + t.Errorf("client TrafficOpsURI expected not empty, actual empty\n") + } + + if actual, err := TMClient.BandwidthKBPS(); err != nil { + t.Errorf("client BandwidthKBPS error expected nil, actual %v\n", err) + } else if actual < 0 { + t.Errorf("client BandwidthKBPS expected >=0 , actual %v\n", actual) + } + + if actual, err := TMClient.BandwidthCapacityKBPS(); err != nil { + t.Errorf("client BandwidthCapacityKBPS error expected nil, actual %v\n", err) + } else if actual <= 0 { + t.Errorf("client BandwidthCapacityKBPS expected >0 , actual %v\n", actual) + } + + if actual, err := TMClient.CacheStatuses(); err != nil { + t.Errorf("client CacheStatuses error expected nil, actual %v\n", err) + } else if len(actual) == 0 { + t.Errorf("client len(CacheStatuses) expected >0 , actual %v\n", actual) + } + + if actual, err := TMClient.MonitorConfig(); err != nil { + t.Errorf("client MonitorConfig error expected nil, actual %v\n", err) + } else if len(actual.TrafficServer) == 0 { + t.Errorf("client len(TrafficMonitorConfig.TrafficServers) expected not empty, actual %v\n", actual) + } + + if actual, err := TMClient.CRConfigHistory(); err != nil { + t.Errorf("client CRConfigHistory error expected nil, actual %v\n", err) + } else if len(actual) == 0 { + t.Errorf("client len(CRConfigHistory) expected !=0, actual %v\n", actual) + } + + if actual, err := TMClient.EventLog(); err != nil { + t.Errorf("client EventLog error expected nil, actual %v\n", err) + } else if len(actual.Events) == 0 { + t.Errorf("client len(EventLog.Events) expected !=0, actual %v\n", actual) + } + + if actual, err := TMClient.CacheStats(); err != nil { + t.Errorf("client CacheStats error expected nil, actual %v\n", err) + } else if len(actual.Caches) == 0 { + t.Errorf("client len(CacheStats.Caches) expected !=0, actual %v\n", actual) + } + + if actual, err := TMClient.DSStats(); err != nil { + t.Errorf("client DSStats error expected nil, actual %v\n", err) + } else if len(actual.DeliveryService) == 0 { + t.Errorf("client len(DSStats.DeliveryService) expected !=0, actual %v\n", actual) + } + + if actual, err := TMClient.CRStates(false); err != nil { + t.Errorf("client CRStates error expected nil, actual %v\n", err) + } else if len(actual.Caches) == 0 { + t.Errorf("client len(CRStates.Caches) expected !=0, actual %v\n", actual) + } + + if actual, err := TMClient.CRConfig(); err != nil { + t.Errorf("client CRConfig error expected nil, actual %v\n", err) + } else if len(actual.ContentServers) == 0 { + t.Errorf("client len(CRConfig.ContentServers) expected !=0, actual %v\n", actual) + } +} diff --git a/traffic_monitor/tests/integration/config/config.go b/traffic_monitor/tests/integration/config/config.go new file mode 100644 index 0000000000..dc4917faab --- /dev/null +++ b/traffic_monitor/tests/integration/config/config.go @@ -0,0 +1,157 @@ +/* + * 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. + */ + +package config + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + "reflect" + + "github.com/apache/trafficcontrol/lib/go-log" + "github.com/kelseyhightower/envconfig" +) + +// Config reflects the structure of the test-to-api.conf file +type Config struct { + TrafficMonitor TrafficMonitor `json:"trafficMonitor"` + Default Default `json:"default"` +} + +// TrafficMonitor is the monitor config section. +type TrafficMonitor struct { + // URL points to the Traffic Monitor instance being tested + URL string `json:"url" envconfig:"TM_URL"` +} + +// Default - config section +type Default struct { + Session Session `json:"session"` + Log Locations `json:"logLocations"` +} + +// Session - config section +type Session struct { + TimeoutInSecs int `json:"timeoutInSecs" envconfig:"SESSION_TIMEOUT_IN_SECS"` +} + +// Locations - reflects the structure of the database.conf file +type Locations struct { + Debug string `json:"debug"` + Event string `json:"event"` + Error string `json:"error"` + Info string `json:"info"` + Warning string `json:"warning"` +} + +// LoadConfig - reads the config file into the Config struct +func LoadConfig(confPath string) (Config, error) { + var cfg Config + + if _, err := os.Stat(confPath); !os.IsNotExist(err) { + confBytes, err := ioutil.ReadFile(confPath) + if err != nil { + return Config{}, fmt.Errorf("Reading CDN conf '%s': %v", confPath, err) + } + + err = json.Unmarshal(confBytes, &cfg) + if err != nil { + return Config{}, fmt.Errorf("unmarshalling '%s': %v", confPath, err) + } + } + errs := validate(confPath, cfg) + if len(errs) > 0 { + fmt.Printf("configuration error:\n") + for _, e := range errs { + fmt.Printf("%v\n", e) + } + os.Exit(0) + } + err := envconfig.Process("traffic-ops-client-tests", &cfg) + if err != nil { + fmt.Errorf("cannot parse config: %v\n", err) + os.Exit(0) + } + + return cfg, err +} + +// validate all required fields in the config. +func validate(confPath string, config Config) []error { + + errs := []error{} + + var f string + f = "TrafficMonitor" + toTag, ok := getStructTag(config, f) + if !ok { + errs = append(errs, fmt.Errorf("'%s' must be configured in %s", toTag, confPath)) + } + + if config.TrafficMonitor.URL == "" { + f = "URL" + tag, ok := getStructTag(config.TrafficMonitor, f) + if !ok { + errs = append(errs, fmt.Errorf("cannot lookup structTag: %s", f)) + } + errs = append(errs, fmt.Errorf("'%s.%s' must be configured in %s", toTag, tag, confPath)) + } + + return errs +} + +func getStructTag(thing interface{}, fieldName string) (string, bool) { + var tag string + var ok bool + t := reflect.TypeOf(thing) + if t != nil { + if f, ok := t.FieldByName(fieldName); ok { + tag = f.Tag.Get("json") + return tag, ok + } + } + return tag, ok +} + +// ErrorLog - critical messages +func (c Config) ErrorLog() log.LogLocation { + return log.LogLocation(c.Default.Log.Error) +} + +// WarningLog - warning messages +func (c Config) WarningLog() log.LogLocation { + return log.LogLocation(c.Default.Log.Warning) +} + +// InfoLog - information messages +func (c Config) InfoLog() log.LogLocation { + return log.LogLocation(c.Default.Log.Info) +} + +// DebugLog - troubleshooting messages +func (c Config) DebugLog() log.LogLocation { + return log.LogLocation(c.Default.Log.Debug) +} + +// EventLog - access.log high level transactions +func (c Config) EventLog() log.LogLocation { + return log.LogLocation(c.Default.Log.Event) +} diff --git a/traffic_monitor/tests/integration/docker-compose.yml b/traffic_monitor/tests/integration/docker-compose.yml new file mode 100644 index 0000000000..ae36e1d020 --- /dev/null +++ b/traffic_monitor/tests/integration/docker-compose.yml @@ -0,0 +1,102 @@ +# 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. +# +# To run the integration test: +# 1. build Traffic Monitor and then copy the RPM to this directory as traffic_monitor.rpm. +# 2. build traffic_monitor/tools/testcaches and place the binary in this directory. +# 3. build traffic_monitor/tools/testto and place the binary in this directory. +# 4. Run this compose +# +# Commands are, from the root trafficcontrol directory: +# ./pkg -v traffic_monitor_build +# cp ./dist/traffic_monitor*.rpm ./traffic_monitor/ +# (cd ./traffic_monitor/tools/testto && go build) +# (cd ./traffic_monitor/tools/testcaches && go build) +# cd ./traffic_monitor +# docker-compose up +# +# To run the integration tests, your current working directory must be trafficcontrol/traffic_monitor. +# This is because Docker doesn't allow accessing files outside the current "context," and the integration tests need +# +# docker-compose up -d +# + +--- +version: '2.1' + +services: + testto: + build: + context: ./tools/testto + dockerfile: ./Dockerfile + domainname: traffic-monitor-integration.test + env_file: + - variables.env + hostname: testto + image: testto + volumes: + - shared:/shared + + testcaches: + build: + context: ./tools/testcaches + dockerfile: ./Dockerfile + domainname: traffic-monitor-integration.test + env_file: + - variables.env + hostname: testcaches + image: testcaches + volumes: + - shared:/shared + + trafficmonitor: + build: + context: . + dockerfile: ./Dockerfile + args: + RPM: ./traffic_monitor.rpm + depends_on: + - testto + domainname: traffic-monitor-integration.test + env_file: + - variables.env + hostname: trafficmonitor + image: trafficmonitor + ports: + - "80:80" + volumes: + - shared:/shared + + tmintegrationtest: + build: + context: ./tests/integration + dockerfile: ./Dockerfile + depends_on: + - testto + - testcaches + - trafficmonitor + domainname: traffic-monitor-integration.test + env_file: + - variables.env + hostname: tmintegrationtest + image: tmintegrationtest + volumes: + - shared:/shared + +volumes: + shared: + external: false diff --git a/traffic_monitor/tests/integration/kbps_test.go b/traffic_monitor/tests/integration/kbps_test.go new file mode 100644 index 0000000000..3d1e6cda43 --- /dev/null +++ b/traffic_monitor/tests/integration/kbps_test.go @@ -0,0 +1,75 @@ +package integration + +/* + + Licensed 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 ( + "fmt" + "net/http" + "testing" + "time" + + "github.com/apache/trafficcontrol/lib/go-tc" +) + +func TestKBPS(t *testing.T) { + crc, err := TMClient.CRConfig() + if err != nil { + t.Fatalf("client CRConfig error expected nil, actual %v\n", err) + } + + if len(crc.ContentServers) == 0 { + t.Fatalf("Monitor CRConfig has no servers, cannot test KBPS") + } + + serverName := "" + server := tc.CRConfigTrafficOpsServer{} + for crcServerName, crcServer := range crc.ContentServers { + server = crcServer + serverName = crcServerName + break + } + if server.Ip == nil { + t.Fatalf("Monitor CRConfig server '" + serverName + "' has no Ip, cannot test KBPS") + } + if server.Port == nil { + t.Fatalf("Monitor CRConfig server '" + serverName + "' has no Port, cannot test KBPS") + } + + const bytesPerKilobit = 125 + + expectedKbps := 10000 + + httpClient := http.Client{Timeout: time.Duration(Config.Default.Session.TimeoutInSecs) * time.Second} + + kbps10 := bytesPerKilobit * expectedKbps + uri := fmt.Sprintf(`http://%v:%v/cmd/setstat?remap=num1.example.net&stat=out_bytes&min=%v&max=%v`, *server.Ip, *server.Port, kbps10, kbps10) + resp, err := httpClient.Get(uri) + if err != nil { + t.Fatalf("Error posting fake cache command '" + uri + "': " + err.Error()) + } + resp.Body.Close() + + time.Sleep(time.Second * 5) // TODO determine if there's a faster or more precise way to wait for polled data? + + kbps, err := TMClient.BandwidthKBPS() + if err != nil { + t.Fatalf("getting monitor bandwidth kbps: %v\n", err) + } + + if kbps < float64(expectedKbps)*0.9 || kbps > float64(expectedKbps)*1.1 { + t.Errorf("monitor bandwidth kbps expected %v actual %v\n", expectedKbps, kbps) + } +} diff --git a/traffic_monitor/tests/integration/traffic-monitor-test.conf b/traffic_monitor/tests/integration/traffic-monitor-test.conf new file mode 100644 index 0000000000..24956daef1 --- /dev/null +++ b/traffic_monitor/tests/integration/traffic-monitor-test.conf @@ -0,0 +1,17 @@ +{ + "default": { + "logLocations": { + "debug": "stdout", + "error": "stdout", + "event": "stdout", + "info": "stdout", + "warning": "stdout" + }, + "session": { + "timeoutInSecs": 30 + } + }, + "trafficMonitor": { + "URL": "http://localhost:80" + } +} diff --git a/traffic_monitor/tests/integration/traffic_monitor_test.go b/traffic_monitor/tests/integration/traffic_monitor_test.go new file mode 100644 index 0000000000..c4dbee1385 --- /dev/null +++ b/traffic_monitor/tests/integration/traffic_monitor_test.go @@ -0,0 +1,125 @@ +package integration + +/* + + Licensed 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 ( + // "database/sql" + "flag" + "fmt" + "net/http" + "os" + "strings" + "testing" + "time" + + "github.com/apache/trafficcontrol/lib/go-log" + "github.com/apache/trafficcontrol/traffic_monitor/tests/integration/config" + "github.com/apache/trafficcontrol/traffic_monitor/tmclient" +) + +var Config config.Config +var TMClient *tmclient.TMClient + +func TestMain(m *testing.M) { + var err error + configFileName := flag.String("cfg", "traffic-monitor-test.conf", "The config file path") + flag.Parse() + + if Config, err = config.LoadConfig(*configFileName); err != nil { + fmt.Printf("Error Loading Config %v %v\n", Config, err) + os.Exit(1) + } + + if err = log.InitCfg(Config); err != nil { + fmt.Printf("Error initializing loggers: %v\n", err) + os.Exit(1) + } + + log.Infof(`Using Config values: + TM Config File: %s + TM URL: %s + TM Session Timeout: %ds +`, *configFileName, Config.TrafficMonitor.URL, Config.Default.Session.TimeoutInSecs) + + // //Load the test data + // LoadFixtures(*tcFixturesFileName) + + // var db *sql.DB + // db, err = OpenConnection() + // if err != nil { + // fmt.Printf("\nError opening connection to %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOpsDB.User, err) + // os.Exit(1) + // } + // defer db.Close() + + // err = Teardown(db) + // if err != nil { + // fmt.Printf("\nError tearingdown data %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOpsDB.User, err) + // os.Exit(1) + // } + + // err = SetupTestData(db) + // if err != nil { + // fmt.Printf("\nError setting up data %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOpsDB.User, err) + // os.Exit(1) + // } + + tmReqTimeout := time.Second * time.Duration(Config.Default.Session.TimeoutInSecs) + + // err = SetupSession(toReqTimeout, Config.TrafficOps.URL, Config.TrafficOps.Users.Admin, Config.TrafficOps.UserPassword) + // if err != nil { + // fmt.Printf("\nError creating session to %s - %s, %v\n", Config.TrafficOps.URL, Config.TrafficOpsDB.User, err) + // os.Exit(1) + // } + + monitorWaitSpan := 30 * time.Second // TODO make configurable? + + if !WaitForMonitor(Config.TrafficMonitor.URL, monitorWaitSpan) { + fmt.Printf("\nError communicating with Monitor '%v' - didn't return a 200 OK in %v\n", Config.TrafficMonitor.URL, monitorWaitSpan) + os.Exit(1) + } + + TMClient = tmclient.New(Config.TrafficMonitor.URL, tmReqTimeout) + + // Now run the test case + rc := m.Run() + os.Exit(rc) +} + +// WaitForMonitor waits for the monitor to fully start, and stop serving 5xx codes. +// If the monitor does not return a 200 from an API endpoint by timeout, returns false. +func WaitForMonitor(url string, timeout time.Duration) bool { + httpClient := http.Client{Timeout: timeout} + + tryInterval := time.Second // TODO make configurable? + + start := time.Now() + for { + if time.Now().After(start.Add(timeout)) { + return false + } + time.Sleep(tryInterval) + resp, err := httpClient.Get(strings.TrimSuffix(url, "/") + "/api/version") + if err != nil { + continue + } + resp.Body.Close() + if resp.StatusCode != 200 { + continue + } + return true + } +} diff --git a/traffic_monitor/tmclient/tmclient.go b/traffic_monitor/tmclient/tmclient.go new file mode 100644 index 0000000000..6ce0b0871f --- /dev/null +++ b/traffic_monitor/tmclient/tmclient.go @@ -0,0 +1,252 @@ +package tmclient + +/* + * 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 ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "strconv" + "strings" + "time" + + "github.com/apache/trafficcontrol/lib/go-tc" + "github.com/apache/trafficcontrol/traffic_monitor/cache" + "github.com/apache/trafficcontrol/traffic_monitor/datareq" + "github.com/apache/trafficcontrol/traffic_monitor/dsdata" + "github.com/apache/trafficcontrol/traffic_monitor/handler" + "github.com/apache/trafficcontrol/traffic_monitor/towrap" +) + +type TMClient struct { + url string + timeout time.Duration +} + +func New(url string, timeout time.Duration) *TMClient { + return &TMClient{url: strings.TrimSuffix(url, "/"), timeout: timeout} +} + +func (c *TMClient) CacheCount() (int, error) { return c.getInt("/api/cache-count") } + +func (c *TMClient) CacheAvailableCount() (int, error) { return c.getInt("/api/cache-available-count") } + +func (c *TMClient) CacheDownCount() (int, error) { return c.getInt("/api/cache-down-count") } + +func (c *TMClient) Version() (string, error) { return c.getStr("/api/version") } + +func (c *TMClient) TrafficOpsURI() (string, error) { return c.getStr("/api/traffic-ops-uri") } + +func (c *TMClient) BandwidthKBPS() (float64, error) { return c.getFloat("/api/bandwidth-kbps") } + +func (c *TMClient) BandwidthCapacityKBPS() (float64, error) { + return c.getFloat("/api/bandwidth-capacity-kbps") +} + +func (c *TMClient) CacheStatuses() (map[tc.CacheName]datareq.CacheStatus, error) { + path := "/api/cache-statuses" + obj := map[tc.CacheName]datareq.CacheStatus{} + if err := c.GetJSON(path, &obj); err != nil { + return nil, err // GetJSON adds context + } + return obj, nil +} + +func (c *TMClient) MonitorConfig() (tc.TrafficMonitorConfigMap, error) { + path := "/api/monitor-config" + obj := tc.TrafficMonitorConfigMap{} + if err := c.GetJSON(path, &obj); err != nil { + return tc.TrafficMonitorConfigMap{}, err // GetJSON adds context + } + return obj, nil +} + +func (c *TMClient) CRConfigHistory() ([]towrap.CRConfigStat, error) { + path := "/api/crconfig-history" + obj := []towrap.CRConfigStat{} + if err := c.GetJSON(path, &obj); err != nil { + return nil, err // GetJSON adds context + } + return obj, nil +} + +func (c *TMClient) EventLog() (datareq.JSONEvents, error) { + path := "/publish/EventLog" + obj := datareq.JSONEvents{} + if err := c.GetJSON(path, &obj); err != nil { + return datareq.JSONEvents{}, err // GetJSON adds context + } + return obj, nil +} + +func (c *TMClient) CacheStats() (cache.Stats, error) { + path := "/publish/CacheStats" + obj := cache.Stats{} + if err := c.GetJSON(path, &obj); err != nil { + return cache.Stats{}, err // GetJSON adds context + } + return obj, nil +} + +func (c *TMClient) DSStats() (dsdata.Stats, error) { + path := "/publish/DsStats" + obj := dsdata.Stats{} + if err := c.GetJSON(path, &obj); err != nil { + return dsdata.Stats{}, err // GetJSON adds context + } + return obj, nil +} + +func (c *TMClient) CRStates(raw bool) (tc.CRStates, error) { + path := "/publish/CrStates" + if raw { + path += "?raw" + } + obj := tc.CRStates{} + if err := c.GetJSON(path, &obj); err != nil { + return tc.CRStates{}, err // GetJSON adds context + } + return obj, nil +} + +func (c *TMClient) CRConfig() (tc.CRConfig, error) { + path := "/publish/CrConfig" + obj := tc.CRConfig{} + if err := c.GetJSON(path, &obj); err != nil { + return tc.CRConfig{}, err // GetJSON adds context + } + return obj, nil +} + +// CRConfigBytes returns the raw bytes of the Monitor's CRConfig. +// +// If you need a deserialized object, use TMClient.CRConfig() instead. +// +// This function exists because the Monitor very intentionally serves the CRConfig bytes as +// published by Traffic Ops, without deserializing or reserializing it. +// +// This can be useful to check for serialization or versioning issues, in case the Go object +// is missing values sent by Traffic Ops, or has other serialization issues. +// +// +func (c *TMClient) CRConfigBytes() ([]byte, error) { return c.getBytes("publish/CrConfig") } + +func (c *TMClient) PeerStates() (datareq.APIPeerStates, error) { + path := "/publish/PeerStates" + obj := datareq.APIPeerStates{} + if err := c.GetJSON(path, &obj); err != nil { + return datareq.APIPeerStates{}, err // GetJSON adds context + } + return obj, nil +} + +func (c *TMClient) Stats() (datareq.Stats, error) { + path := "/publish/Stats" + obj := datareq.Stats{} + if err := c.GetJSON(path, &obj); err != nil { + return datareq.Stats{}, err // GetJSON adds context + } + return obj, nil +} + +func (c *TMClient) StatSummary() (datareq.StatSummary, error) { + path := "/publish/StatSummary" + obj := datareq.StatSummary{} + if err := c.GetJSON(path, &obj); err != nil { + return datareq.StatSummary{}, err // GetJSON adds context + } + return obj, nil +} + +func (c *TMClient) ConfigDoc() (handler.OpsConfig, error) { + path := "/publish/ConfigDoc" + obj := handler.OpsConfig{} + if err := c.GetJSON(path, &obj); err != nil { + return handler.OpsConfig{}, err // GetJSON adds context + } + return obj, nil +} + +func (c *TMClient) getBytes(path string) ([]byte, error) { + url := c.url + path + httpClient := http.Client{Timeout: c.timeout} + resp, err := httpClient.Get(url) + if err != nil { + return nil, errors.New("getting from '" + url + "': " + err.Error()) + } + defer resp.Body.Close() + + if resp.StatusCode < 200 || resp.StatusCode > 299 { + return nil, fmt.Errorf("Monitor '"+url+"' returned bad status %v", resp.StatusCode) + } + + respBts, err := ioutil.ReadAll(resp.Body) + if err != nil { + return nil, errors.New("reading body from '" + url + "': " + err.Error()) + } + return respBts, nil +} + +func (c *TMClient) GetJSON(path string, obj interface{}) error { + bts, err := c.getBytes(path) + if err != nil { + return err // getBytes already adds context + } + if err := json.Unmarshal(bts, obj); err != nil { + return errors.New("unmarshalling response '" + string(bts) + "' json: " + err.Error()) + } + return nil +} + +func (c *TMClient) getStr(path string) (string, error) { + respBts, err := c.getBytes(path) + if err != nil { + return "", err // getBytes already adds context + } + return string(respBts), nil +} + +func (c *TMClient) getInt(path string) (int, error) { + respStr, err := c.getStr(path) + if err != nil { + return 0, err // getStr already adds context + } + + respInt, err := strconv.Atoi(respStr) + if err != nil { + return 0, errors.New("parsing response '" + respStr + "': " + err.Error()) + } + return respInt, nil +} + +func (c *TMClient) getFloat(path string) (float64, error) { + respStr, err := c.getStr(path) + if err != nil { + return 0, err // getStr already adds context + } + + respFloat, err := strconv.ParseFloat(respStr, 64) + if err != nil { + return 0, errors.New("parsing response '" + respStr + "': " + err.Error()) + } + return respFloat, nil +} diff --git a/traffic_monitor/tools/testcaches/Dockerfile b/traffic_monitor/tools/testcaches/Dockerfile new file mode 100644 index 0000000000..e83c1f2c04 --- /dev/null +++ b/traffic_monitor/tools/testcaches/Dockerfile @@ -0,0 +1,29 @@ +# 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 centos/systemd +MAINTAINER dev@trafficcontrol.apache.org + +# RUN rpm --import https://mirror.go-repo.io/centos/RPM-GPG-KEY-GO-REPO +# RUN curl -s https://mirror.go-repo.io/centos/go-repo.repo | tee /etc/yum.repos.d/go-repo.repo +# RUN yum install -y golang + +ARG APP_BIN=testcaches +ADD $APP_BIN /usr/sbin/ + +ADD Dockerfile_run.sh / +CMD /Dockerfile_run.sh diff --git a/traffic_monitor/tools/testcaches/Dockerfile_run.sh b/traffic_monitor/tools/testcaches/Dockerfile_run.sh new file mode 100755 index 0000000000..298068eb0b --- /dev/null +++ b/traffic_monitor/tools/testcaches/Dockerfile_run.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env 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. + + +# envvars=( PORT ) +# for v in $envvars +# do +# if [[ -z $$v ]]; then echo "$v is unset"; exit 1; fi +# done + +start() { + ARGS= + if [[ -n "${NUM_PORTS}" ]]; then + ARGS="$ARGS -numPorts ${NUM_PORTS}" + fi + if [[ -n "${NUM_REMAPS}" ]]; then + ARGS="$ARGS -numRemaps ${NUM_REMAPS}" + fi + if [[ -n "${PORT_START}" ]]; then + ARGS="$ARGS -portStart ${PORT_START}" + fi + testcaches ${ARGS} +} + +init() { + + echo "INITIALIZED=1" >> /etc/environment +} + +source /etc/environment +if [ -z "$INITIALIZED" ]; then init; fi +start diff --git a/traffic_monitor/tools/testcaches/README.md b/traffic_monitor/tools/testcaches/README.md index 2617a93cda..77dc65a8cc 100644 --- a/traffic_monitor/tools/testcaches/README.md +++ b/traffic_monitor/tools/testcaches/README.md @@ -30,3 +30,68 @@ Each port is a unique fake server, with distinct incrementing stats. When run with no parameters, it defaults to ports 40000-40999 and 1000 remaps. Stats are served at the regular ATS `stats_over_http` endpoint, `_astats`. For example, if it's serving on port 40000, it can be reached via `curl http://localhost:40000/_astats`. It also respects the `?application=system` query parameter, and will serve only system stats (the Monitor "health check" [as opposed to the "stat check"]). For example, `curl http://localhost:40000/_astats?application=system`. + +## Commands + +The `testcaches` app accepts a number of commands, which manipulate the data it serves. These command are all available via HTTP requests. + +Each HTTP request is made to a fake cache at a specific port. Thus, you can modify data served by each fake cache independently. + +The commands are: + +### `/cmd/setstat` + +Sets how much a stat increments by every interval (currently, an interval is hard-coded to 1 second). Accepts a min and max, and will increment by a random number between them. The min may equal the max, if a constant increment is desired. + +Query Parameters: +`remap` - the remap rule to set +`stat` - the stat to set +`min` - the minimum number to increment by +`max` - the minimum to increment by + +Example: +`curl -Lvsk http://localhost:4242/cmd/setstat?remap=num1.example.net&stat=out_bytes&min=10&max=25` + +### `setsystem` + +Sets system stats to constant values. Multiple stats may be set with a single request. + +Query Parameters: +`loadavg1m` - the 1m loadavg in the `system` object. +`loadavg5m` - the 5m loadavg in the `system` object. +`loadavg10m` - the 10m loadavg in the `system` object. +`speed` - the network interface speed in the `system` object. This number is in kilobits. I.e. 20000 means 20Gbps. + +Example: +`curl -sk 'http://localhost:4242/cmd/setsystem?loadavg1m=10.1&loadavg5m=27.92&loadavg10m=3.4&speed=20000' ` + +### `setdelay` + +Sets the delay for serving all _astats requests to this fake cache. Accepts a minimum and maximum, which may be qual, and delays the request by a random interval between them. When a delay is set, the server immediately accepts client requests, reads headers and sets up the connection, and then delays writing out the body. + +Query Parameters: +`min` - the minimum delay time, in milliseconds +`max` - the maximum delay time, in milliseconds + +Example: +`curl -Lvsk 'http://localhost:4242/cmd/setdelay?min=200&max=600'` + +## Docker + +Build environment variables: none + +Run environment variables: +- `NUM_PORTS` - app `numPorts` argument +- `NUM_REMAPS` - app `numRemaps` argument +- `PORT_START` - app `portStart` argument + +### Building + +`docker build . --tag testcaches` + +### Running + +``` +docker network create tmi +docker run --detach --network tmi --name testcaches --hostname testcaches --publish 30000-30099:30000-30099 --env NUM_PORTS=100 --env NUM_REMAPS=100 --env PORT_START=30000 testcaches +``` diff --git a/traffic_monitor/tools/testcaches/fakesrvr/server.go b/traffic_monitor/tools/testcaches/fakesrvr/server.go index 40fe4dd71d..2c17bca4f1 100644 --- a/traffic_monitor/tools/testcaches/fakesrvr/server.go +++ b/traffic_monitor/tools/testcaches/fakesrvr/server.go @@ -67,7 +67,9 @@ func astatsHandler(fakeSrvrDataThs fakesrvrdata.Ths) http.HandlerFunc { b, err = json.MarshalIndent(&srvr, "", " ") // TODO debug, change to Marshal } if err != nil { + w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(`{"error": "marshalling: ` + err.Error() + `"}`)) // TODO escape error for JSON + return } w.Write(b) } diff --git a/traffic_monitor/tools/testto/Dockerfile b/traffic_monitor/tools/testto/Dockerfile new file mode 100644 index 0000000000..e3e7983c7f --- /dev/null +++ b/traffic_monitor/tools/testto/Dockerfile @@ -0,0 +1,29 @@ +# 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 centos/systemd +MAINTAINER dev@trafficcontrol.apache.org + +# RUN rpm --import https://mirror.go-repo.io/centos/RPM-GPG-KEY-GO-REPO +# RUN curl -s https://mirror.go-repo.io/centos/go-repo.repo | tee /etc/yum.repos.d/go-repo.repo +# RUN yum install -y golang + +ARG APP_BIN=testto +ADD $APP_BIN /usr/sbin/ + +ADD Dockerfile_run.sh / +CMD /Dockerfile_run.sh diff --git a/traffic_monitor/tools/testto/Dockerfile_run.sh b/traffic_monitor/tools/testto/Dockerfile_run.sh new file mode 100755 index 0000000000..9ef0ce900f --- /dev/null +++ b/traffic_monitor/tools/testto/Dockerfile_run.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env 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. + + +# envvars=( PORT ) +# for v in $envvars +# do +# if [[ -z $$v ]]; then echo "$v is unset"; exit 1; fi +# done + +start() { + ARGS= + if [[ -n "${PORT}" ]]; then + ARGS="$ARGS -port ${PORT}" + fi + testto ${ARGS} +} + +init() { + + echo "INITIALIZED=1" >> /etc/environment +} + +source /etc/environment +if [ -z "$INITIALIZED" ]; then init; fi +start diff --git a/traffic_monitor/tools/testto/README.md b/traffic_monitor/tools/testto/README.md new file mode 100644 index 0000000000..957b4aa862 --- /dev/null +++ b/traffic_monitor/tools/testto/README.md @@ -0,0 +1,36 @@ + + +## Docker + +Build environment variables: none + +Run environment variables: +- `PORT` - the port to serve on. If none is specified, no port argument will be passed to the app, and it will use its default. + +### Building + +`docker build --tag testto .` + +### Running + +``` +docker network create tmi +docker run --detach --network tmi --name testto --hostname testto --publish 27080:80 --env PORT=80 testto +``` diff --git a/traffic_monitor/tools/testto/testto.go b/traffic_monitor/tools/testto/testto.go new file mode 100644 index 0000000000..df06ec9341 --- /dev/null +++ b/traffic_monitor/tools/testto/testto.go @@ -0,0 +1,246 @@ +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 ( + "encoding/json" + "flag" + "fmt" + "net/http" + "regexp" + "strconv" + "sync" + "time" + + "github.com/apache/trafficcontrol/lib/go-tc" +) + +func main() { + port := flag.Int("port", 8000, "Port to serve on") + flag.Parse() + if *port < 0 || *port > 65535 { + fmt.Println("port must be 0-65535") + return + } + + toDataThs := NewThs() + toDataThs.Set(ThsT(&FakeTOData{Servers: []tc.Server{}})) + + server := Serve(*port, toDataThs) + fmt.Printf("Serving on %v\n", *port) + server = server // debug + for { + // TODO handle sighup to die + time.Sleep(time.Hour) + } +} + +type FakeTOData struct { + Monitoring tc.TrafficMonitorConfig + CRConfig tc.CRConfig + Servers []tc.Server +} + +// TODO make timeouts configurable? + +const readTimeout = time.Second * 10 +const writeTimeout = time.Second * 10 + +func Serve(port int, fakeTOData Ths) *http.Server { + // TODO add HTTPS + server := &http.Server{ + Addr: ":" + strconv.Itoa(port), + Handler: RouteHandler(fakeTOData), + ReadTimeout: readTimeout, + WriteTimeout: writeTimeout, + MaxHeaderBytes: 1 << 20, + } + go func() { + if err := server.ListenAndServe(); err != nil { + fmt.Println("Error serving on port " + strconv.Itoa(port) + ": " + err.Error()) + } + }() + return server +} + +type Route struct { + Regex *regexp.Regexp + Handler http.HandlerFunc +} + +func GetRoutes(fakeTOData Ths) []Route { + routes := []Route{} + for route, makeHandler := range Routes { + routeRegex := regexp.MustCompile(route) + routes = append(routes, Route{Regex: routeRegex, Handler: makeHandler(fakeTOData)}) + } + return routes +} + +type MakeHandlerFunc func(fakeTOData Ths) http.HandlerFunc + +var Routes = map[string]MakeHandlerFunc{ + `/api/(.*)/user/login/?(\.json)?$`: loginHandler, + `/api/(.*)/cdns/(.*)/configs/monitoring/?(\.json)?$`: monitoringHandler, + `/api/(.*)/servers/?(\.json)?$`: serversHandler, + `/api/(.*)/cdns/(.*)/snapshot/?(\.json)?$`: crConfigHandler, +} + +func RouteHandler(fakeTOData Ths) http.HandlerFunc { + routes := GetRoutes(fakeTOData) + return func(w http.ResponseWriter, r *http.Request) { + for _, route := range routes { + if route.Regex.MatchString(r.URL.Path) { + route.Handler(w, r) + return + } + } + w.WriteHeader(http.StatusNotFound) + } +} + +func monitoringHandler(fakeTOData Ths) http.HandlerFunc { + return makeJSONGetPostHandler(fakeTOData, monitoringHandlerGet, monitoringHandlerPost) +} + +func serversHandler(fakeTOData Ths) http.HandlerFunc { + return makeJSONGetPostHandler(fakeTOData, serversHandlerGet, serversHandlerPost) +} + +func crConfigHandler(fakeTOData Ths) http.HandlerFunc { + return makeJSONGetPostHandler(fakeTOData, crConfigHandlerGet, crConfigHandlerPost) +} + +func makeJSONGetPostHandler( + fakeTOData Ths, + makeGetHandler func(fakeTOData Ths) http.HandlerFunc, + makePostHandler func(fakeTOData Ths) http.HandlerFunc, +) http.HandlerFunc { + getHandler := makeGetHandler(fakeTOData) + postHandler := makePostHandler(fakeTOData) + return func(w http.ResponseWriter, r *http.Request) { + switch r.Method { + case http.MethodPost: + postHandler(w, r) + case http.MethodGet: + getHandler(w, r) + default: + w.WriteHeader(http.StatusMethodNotAllowed) + } + } +} + +func monitoringHandlerPost(fakeTOData Ths) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + obj := (*FakeTOData)(fakeTOData.Get()) + postJSONObj(w, r, &obj.Monitoring, obj, fakeTOData) + } +} + +func monitoringHandlerGet(fakeTOData Ths) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + writeJSONObj(w, ((*FakeTOData)(fakeTOData.Get())).Monitoring) + } +} + +func serversHandlerGet(fakeTOData Ths) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + writeJSONObj(w, ((*FakeTOData)(fakeTOData.Get())).Servers) + } +} + +func serversHandlerPost(fakeTOData Ths) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + obj := (*FakeTOData)(fakeTOData.Get()) + postJSONObj(w, r, &obj.Servers, obj, fakeTOData) + } +} + +func crConfigHandlerGet(fakeTOData Ths) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + writeJSONObj(w, ((*FakeTOData)(fakeTOData.Get())).CRConfig) + } +} + +func crConfigHandlerPost(fakeTOData Ths) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + obj := (*FakeTOData)(fakeTOData.Get()) + postJSONObj(w, r, &obj.CRConfig, obj, fakeTOData) + } +} + +func writeJSONObj(w http.ResponseWriter, obj interface{}) { + bts, err := json.MarshalIndent(&obj, "", " ") + if err != nil { + w.WriteHeader(http.StatusInternalServerError) + w.Write([]byte(`{"error": "marshalling: ` + err.Error() + `"}`)) + return + } + // TODO write content type + w.Write([]byte(`{"response":`)) + w.Write(bts) + w.Write([]byte(`}`)) + w.Write([]byte("\n")) +} + +func postJSONObj(w http.ResponseWriter, r *http.Request, obj interface{}, fakeTOData ThsT, fakeTODataThs Ths) { + if err := json.NewDecoder(r.Body).Decode(obj); err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(`{"error": "unmarshalling posted body: ` + err.Error() + `"}`)) + return + } + fakeTODataThs.Set(fakeTOData) + w.WriteHeader(http.StatusNoContent) +} + +type ThsT *FakeTOData + +type Ths struct { + v *ThsT + m *sync.RWMutex +} + +func NewThs() Ths { + v := ThsT(nil) + return Ths{ + m: &sync.RWMutex{}, + v: &v, + } +} + +func (t Ths) Set(v ThsT) { + t.m.Lock() + defer t.m.Unlock() + *t.v = v +} + +func (t Ths) Get() ThsT { + t.m.RLock() + defer t.m.RUnlock() + return *t.v +} + +func loginHandler(Ths) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Set-Cookie", `mojolicious=fake; Path=/; Expires=Thu, 13 Dec 2018 21:21:33 GMT; HttpOnly`) + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{"alerts":[{"text": "Successfully logged in.","level": "success"}]}`)) + } +} diff --git a/traffic_monitor/variables.env b/traffic_monitor/variables.env new file mode 100644 index 0000000000..86e6d5daed --- /dev/null +++ b/traffic_monitor/variables.env @@ -0,0 +1,22 @@ +# environment variables for the Traffic Monitor Docker Compose files + +TO_URI=http://testto +TO_USER=nouser +TO_PASS=nopass +CDN=nocdn +PORT=80 + +# testcaches +NUM_PORTS=100 +NUM_REMAPS=100 +PORT_START=30000 + +# testto + +# tmintegrationtest +TESTTO_URI=testto +TESTTO_PORT=80 +TESTCACHES_URI=testcaches +TESTCACHES_PORT_START=30000 + +TM_URI=http://trafficmonitor