zrhoffman commented on a change in pull request #3707:
URL: https://github.com/apache/trafficcontrol/pull/3707#discussion_r495219828



##########
File path: traffic_monitor/variables.env
##########
@@ -0,0 +1,22 @@
+# environment variables for the Traffic Monitor Docker Compose files

Review comment:
       This file needs an Apache license header

##########
File path: 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)
+       // }

Review comment:
       This is all commented out because it's still *todo*, right? Could we get 
a `TODO` comment about this code block?

##########
File path: 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))

Review comment:
       This too can be
   
   ```go
   fakeSrvrDataThs.Set(srvr)
   ```

##########
File path: 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",

Review comment:
       After #4700, this part looks like
   
   ```json
         "interfaces": [
           {
             "ipAddresses": [
               {
                 "address": "172.16.239.6",
                 "gateway": "172.16.239.1",
                 "serviceAddress": true
               },
               {
                 "address": "fc01:9400:1000:8::6",
                 "gateway": "fc01:9400:1000:8::1",
                 "serviceAddress": true
               }
             ],
             "maxBandwidth": null,
             "monitor": true,
             "mtu": 1500,
             "name": "eth0"
           }
         ],
   ```

##########
File path: 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))

Review comment:
       This can be just
   
   ```go
   fakeSrvrDataThs.Set(srvr)
   ```

##########
File path: 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)

Review comment:
       Is this error meant to be printed?

##########
File path: traffic_monitor/tools/testcaches/fakesrvr/server.go
##########
@@ -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))))

Review comment:
       Redundant parenthesis here

##########
File path: 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)

Review comment:
       Instead of `tests/integration`, could you make the directory name 
`tests/_integration` so that go testing `./...` does not test this directory?

##########
File path: 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)

Review comment:
       *Reading* should not be capitalized

##########
File path: 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)

Review comment:
       When I run the docker-compose command to start the test,
   
   ```shell
   docker-compose -p tmi --project-directory . -f 
tests/integration/docker-compose.yml run tmintegrationtest
   ```
   
   I indefinitely get
   
   ```
   Waiting for Traffic Ops to return a 200 OK
   ```
   
   Looking at the `testto` container, it says
   
   ```logs
   [user@computer traffic_monitor]$ 3 tmi --project-directory . -f 
tests/integration/docker-compose.yml logs -f testto
   Attaching to tmi_testto_1
   testto_1             | testto: /lib64/libc.so.6: version `GLIBC_2.32' not 
found (required by testto)
   tmi_testto_1 exited with code 1
   ```
   
   So the instructions should have you build with `CGO_ENABLED=0` (and 
`GOOS=linux`).
   

##########
File path: 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))

Review comment:
       Another `fakeSrvrDataThs.Set(srvr)` candidate

##########
File path: 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))

Review comment:
       `fakeSrvrDataThs.Set(srvr)` can be here

##########
File path: 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

Review comment:
       After resolving the above linking error, rebuilding the images, and 
re-running, I get unmarshalling errors like
   
   ```json
   {"error": "unmarshalling posted body: json: cannot unmarshal bool into Go 
struct field CRConfigDeliveryService.deliveryServices.anonymousBlockingEnabled 
of type string"}
   ```
   
   It eventually says
   
   ```
   Error communicating with Monitor 'http://trafficmonitor' - didn't return a 
200 OK in 30s
   ```
   
   <details><summary>Full <code>tmintegrationtest</code> logs are here (click 
to expand)</summary>
   
   ```
   < HTTP/1.1 200 OK
   * About to connect() to testto port 80 (#0)
   *   Trying 172.23.0.3...
   * Connected to testto (172.23.0.3) port 80 (#0)
   > POST /api/1.2/cdns/fake/snapshot HTTP/1.1
   > User-Agent: curl/7.29.0
   > Host: testto
   > Accept: */*
   > Content-Length: 3756
   > Content-Type: application/x-www-form-urlencoded
   > Expect: 100-continue
   >
   < HTTP/1.1 100 Continue
   < HTTP/1.1 400 Bad Request
   < Date: Fri, 25 Sep 2020 21:32:19 GMT
   < Content-Length: 169
   < Content-Type: text/plain; charset=utf-8
   * HTTP error before end of send, stop sending
   <
   * Closing connection 0
   {"error": "unmarshalling posted body: json: cannot unmarshal bool into Go 
struct field CRConfigDeliveryService.deliveryServices.anonymousBlockingEnabled 
of type string"}* About to connect() to testto port 80 (#0)
   *   Trying 172.23.0.3...
   * Connected to testto (172.23.0.3) port 80 (#0)
   > POST /api/1.2/cdns/fake/configs/monitoring.json HTTP/1.1
   > User-Agent: curl/7.29.0
   > Host: testto
   > Accept: */*
   > Content-Length: 2308
   > Content-Type: application/x-www-form-urlencoded
   > Expect: 100-continue
   >
   < HTTP/1.1 100 Continue
   < HTTP/1.1 400 Bad Request
   < Date: Fri, 25 Sep 2020 21:32:19 GMT
   < Content-Length: 167
   < Content-Type: text/plain; charset=utf-8
   * HTTP error before end of send, stop sending
   <
   * Closing connection 0
   {"error": "unmarshalling posted body: json: cannot unmarshal object into Go 
struct field TrafficServer.trafficServers.deliveryServices of type 
[]tc.tsdeliveryService"}* About to connect() to testto port 80 (#0)
   *   Trying 172.23.0.3...
   * Connected to testto (172.23.0.3) port 80 (#0)
   > POST /api/1.2/servers HTTP/1.1
   > User-Agent: curl/7.29.0
   > Host: testto
   > Accept: */*
   > Content-Length: 1135
   > Content-Type: application/x-www-form-urlencoded
   > Expect: 100-continue
   >
   < HTTP/1.1 100 Continue
   < HTTP/1.1 204 No Content
   < Date: Fri, 25 Sep 2020 21:32:19 GMT
   <
   * Connection #0 to host testto left intact
   
   
   testto:
     % Total    % Received % Xferd  Average Speed   Time    Time     Time  
Current
                                    Dload  Upload   Total   Spent    Left  Speed
   100  3877    0  3877    0     0   657k      0 --:--:-- --:--:-- --:--:--  
757k
   {"response":{
     "config": {
       "api.cache-control.max_age": "30",
       "consistent.dns.routing": "true",
       "coveragezone.polling.interval": "30",
   
   
   testcaches:
     % Total    % Received % Xferd  Average Speed   Time    Time     Time  
Current
                                    Dload  Upload   Total   Spent    Left  Speed
   { 0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
   1  "ats": {
   0    "plugin.remap_stats.num7.example.net.in_bytes": 2,
   0    "plugin.remap_stats.num7.example.net.out_bytes": 2,
        "plugin.remap_stats.num7.example.net.status_2xx": 1,
   34955    0 34955    0     0  4648k      0 --:--:-- --:--:-- --:--:-- 4876k
   (23) Failed writing body
   
   
   traffic_monitor:
     % Total    % Received % Xferd  Average Speed   Time    Time     Time  
Current
                                    Dload  Upload   Total   Spent    Left  Speed
   100  6522<!DOCTYPE html>0     0      0      0 --:--:-- --:--:-- --:--:--     0
   
    <!--
    Licensed to the Apache Software Foundation (ASF) under one
    or more contributor license agreements.  See the NOTICE file
   0  6522    0     0  1159k      0 --:--:-- --:--:-- --:--:-- 1273k
   (23) Failed writing body
   DEBUG traffic_monitor_integration starting
   
   Error communicating with Monitor 'http://trafficmonitor' - didn't return a 
200 OK in 30s
   ```
   </details>
   
   Looking at the logs of the `trafficmonitor` container, it doen't seem to get 
past
   
   ```go
   ERROR: datareq.go:152: 2020-09-25T21:32:38.143874758Z: Request Error: 
/api/version: service still starting, some caches unpolled: map[]
   ```
   
   although there are other errors in there too, like
   
   ```go
   trafficmonitor_1     | ERROR: monitorconfig.go:346: 
2020-09-25T21:32:33.530687712Z: Failed to parse polling strings for cache 
server 'server0': no service addresses found
   trafficmonitor_1     | ERROR: monitorconfig.go:346: 
2020-09-25T21:32:33.530798803Z: Failed to parse polling strings for cache 
server 'server1': no service addresses found
   ```
   
   <details><summary>Full <code>trafficmonitor</code> container logs are here 
(click to expand)</summary>
   
   ```go
   Attaching to tmi_trafficmonitor_1
   trafficmonitor_1     | Failed to get D-Bus connection: Operation not 
permitted
   trafficmonitor_1     | /etc/init.d/traffic_monitor: line 41: 
/etc/sysconfig/network: No such file or directory
   trafficmonitor_1     | Starting traffic_monitor: 
   trafficmonitor_1     | ERROR: opsconfig.go:77: 
2020-09-25T21:32:18.530467624Z: OpsConfigManager: getting CDN name from Traffic 
Ops, using config CDN 'nocdn': getting monitor CDN: no server 'trafficmonitor' 
found in Traffic Ops
   trafficmonitor_1     | 
   trafficmonitor_1     | ERROR: opsconfig.go:77: 
2020-09-25T21:32:18.538113399Z: OpsConfigManager: Error getting Traffic Ops 
data: Error getting CRconfig from Traffic Ops: invalid CRConfig: 
CRConfig.Stats.CDN missing
   trafficmonitor_1     | 
   trafficmonitor_1     | ERROR: opsconfig.go:205: 
2020-09-25T21:32:18.538157142Z: retrying in 100ms
   trafficmonitor_1     | ERROR: opsconfig.go:77: 
2020-09-25T21:32:18.638750564Z: OpsConfigManager: Error getting Traffic Ops 
data: Error getting CRconfig from Traffic Ops: invalid CRConfig: 
CRConfig.Stats.CDN missing
   trafficmonitor_1     | 
   trafficmonitor_1     | ERROR: opsconfig.go:205: 
2020-09-25T21:32:18.638781915Z: retrying in 248.612746ms
   trafficmonitor_1     | ERROR: opsconfig.go:77: 
2020-09-25T21:32:18.888066827Z: OpsConfigManager: Error getting Traffic Ops 
data: Error getting CRconfig from Traffic Ops: invalid CRConfig: 
CRConfig.Stats.CDN missing
   trafficmonitor_1     | 
   trafficmonitor_1     | ERROR: opsconfig.go:205: 
2020-09-25T21:32:18.888119514Z: retrying in 642.263625ms
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:20.102734056Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 2020-09-25T21:32:21.1049107Z: 
Request Error: /api/version: service still starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:22.107247084Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:23.109295834Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:24.111672602Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:25.113719758Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:26.115813436Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:27.118294393Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:28.120237324Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:29.122398443Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:30.123841491Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:31.125970238Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:32.128329372Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:33.130950123Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: monitorconfig.go:346: 
2020-09-25T21:32:33.530687712Z: Failed to parse polling strings for cache 
server 'server0': no service addresses found
   trafficmonitor_1     | ERROR: monitorconfig.go:346: 
2020-09-25T21:32:33.530798803Z: Failed to parse polling strings for cache 
server 'server1': no service addresses found
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:34.135402114Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:35.136748588Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:36.139031306Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:37.141564084Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:38.143874758Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:39.146134866Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 2020-09-25T21:32:40.14928317Z: 
Request Error: /api/version: service still starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:41.151503741Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:42.154148808Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:43.156421926Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:44.158672414Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:45.159938856Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:46.161990084Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:47.163412929Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:48.164121248Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   trafficmonitor_1     | ERROR: datareq.go:152: 
2020-09-25T21:32:49.165983499Z: Request Error: /api/version: service still 
starting, some caches unpolled: map[]
   ```
   </details>

##########
File path: 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 '

Review comment:
       As with the rest of the API v2.0 routes, this has become 
`cdns/fake/configs/monitoring`, `json` suffix no longer supported




----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

For queries about this service, please contact Infrastructure at:
[email protected]


Reply via email to