From 3e8cdb6924288b7ac4734666dc68243b8a41b588 Mon Sep 17 00:00:00 2001 From: Robert Butts Date: Sat, 19 May 2018 17:43:08 -0600 Subject: [PATCH] Add TO Go cdns/health, cdns/name/health --- lib/go-tc/traffic_monitor.go | 12 ++ lib/go-util/num.go | 9 ++ .../cachesstats/cachesstats.go | 51 +----- .../traffic_ops_golang/cdn/capacity.go | 75 ++------- traffic_ops/traffic_ops_golang/cdn/health.go | 147 ++++++++++++++++++ .../dbhelpers/db_helpers.go | 12 ++ .../traffic_ops_golang/routing/routes.go | 3 + .../traffic_ops_golang/tenant/tenancy.go | 12 ++ .../util/monitorhlp/monitorhlp.go | 122 +++++++++++++++ 9 files changed, 337 insertions(+), 106 deletions(-) create mode 100644 traffic_ops/traffic_ops_golang/cdn/health.go create mode 100644 traffic_ops/traffic_ops_golang/util/monitorhlp/monitorhlp.go diff --git a/lib/go-tc/traffic_monitor.go b/lib/go-tc/traffic_monitor.go index f789214a3a..fcb6b20a5d 100644 --- a/lib/go-tc/traffic_monitor.go +++ b/lib/go-tc/traffic_monitor.go @@ -229,3 +229,15 @@ func TrafficMonitorTransformToMap(tmConfig *TrafficMonitorConfig) (*TrafficMonit return &tm, nil } + +type HealthData struct { + TotalOffline uint64 `json:"totalOffline"` + TotalOnline uint64 `json:"totalOnline"` + CacheGroups []HealthDataCacheGroup `json:"cachegroups"` +} + +type HealthDataCacheGroup struct { + Offline int64 `json:"offline"` + Online int64 `json:"online"` + Name CacheGroupName `json:"name"` +} diff --git a/lib/go-util/num.go b/lib/go-util/num.go index 7a12a7fee6..7f336e0e0e 100644 --- a/lib/go-util/num.go +++ b/lib/go-util/num.go @@ -177,3 +177,12 @@ func HashInts(ints []int, sortIntsBeforeHashing bool) []byte { bts := sha512.Sum512(buf) return bts[:] } + +// IntSliceToMap creates an int set from an array. +func IntSliceToMap(s []int) map[int]struct{} { + m := map[int]struct{}{} + for _, v := range s { + m[v] = struct{}{} + } + return m +} diff --git a/traffic_ops/traffic_ops_golang/cachesstats/cachesstats.go b/traffic_ops/traffic_ops_golang/cachesstats/cachesstats.go index d0645dfb4b..9157f9feb1 100644 --- a/traffic_ops/traffic_ops_golang/cachesstats/cachesstats.go +++ b/traffic_ops/traffic_ops_golang/cachesstats/cachesstats.go @@ -20,12 +20,10 @@ package cachesstats */ import ( - "crypto/tls" "database/sql" "encoding/json" "errors" "net/http" - "net/url" "strconv" "time" @@ -33,6 +31,7 @@ import ( "github.com/apache/trafficcontrol/lib/go-tc" "github.com/apache/trafficcontrol/lib/go-util" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api" + "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/monitorhlp" ) func Get(w http.ResponseWriter, r *http.Request) { @@ -56,25 +55,9 @@ func getCachesStats(tx *sql.Tx) ([]CacheData, error) { return nil, errors.New("getting monitors: " + err.Error()) } - monitorForwardProxy, monitorForwardProxyExists, err := getGlobalParam(tx, MonitorProxyParameter) + client, err := monitorhlp.GetClient(tx) if err != nil { - return nil, errors.New("getting global monitor proxy parameter: " + err.Error()) - } - - client := &http.Client{Timeout: MonitorRequestTimeout} - if monitorForwardProxyExists { - proxyURI, err := url.Parse(monitorForwardProxy) - if err != nil { - return nil, errors.New("monitor forward proxy '" + monitorForwardProxy + "' in parameter '" + MonitorProxyParameter + "' not a URI: " + err.Error()) - } - clientTransport := &http.Transport{Proxy: http.ProxyURL(proxyURI)} - if proxyURI.Scheme == "https" { - // TM does not support HTTP/2 and golang when connecting to https will use HTTP/2 by default causing a conflict - // The result will be an unsupported scheme error - // Setting TLSNextProto to any empty map will disable using HTTP/2 per https://golang.org/src/net/http/doc.go - clientTransport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) - } - client = &http.Client{Timeout: MonitorRequestTimeout, Transport: clientTransport} + return nil, errors.New("getting monitor client: " + err.Error()) } cacheData, err := getCacheData(tx) @@ -91,7 +74,7 @@ func getCachesStats(tx *sql.Tx) ([]CacheData, error) { success := false errs := []error{} for _, monitorFQDN := range monitorFQDNs { - crStates, err := getCRStates(monitorFQDN, client) + crStates, err := monitorhlp.GetCRStates(monitorFQDN, client) if err != nil { errs = append(errs, errors.New("getting CRStates for CDN '"+string(cdn)+"' monitor '"+monitorFQDN+"': "+err.Error())) continue @@ -197,31 +180,7 @@ func addStats(cacheData []CacheData, stats CacheStats) []CacheData { return cacheData } -// CRStates contains the Monitor CRStates needed by Cachedata. It is NOT the full object served by the Monitor, but only the data required by the caches stats endpoint. -type CRStates struct { - Caches map[tc.CacheName]Available `json:"caches"` -} - -type Available struct { - IsAvailable bool `json:"isAvailable"` -} - -func getCRStates(monitorFQDN string, client *http.Client) (CRStates, error) { - path := `/publish/CrStates` - resp, err := client.Get("http://" + monitorFQDN + path) - if err != nil { - return CRStates{}, errors.New("getting CRStates from Monitor '" + monitorFQDN + "': " + err.Error()) - } - defer resp.Body.Close() - - crs := CRStates{} - if err := json.NewDecoder(resp.Body).Decode(&crs); err != nil { - return CRStates{}, errors.New("decoding CRStates from monitor '" + monitorFQDN + "': " + err.Error()) - } - return crs, nil -} - -func addHealth(cacheData []CacheData, crStates CRStates) []CacheData { +func addHealth(cacheData []CacheData, crStates tc.CRStates) []CacheData { if crStates.Caches == nil { return cacheData // TODO warn? } diff --git a/traffic_ops/traffic_ops_golang/cdn/capacity.go b/traffic_ops/traffic_ops_golang/cdn/capacity.go index 02d8e28089..0a33d20e4c 100644 --- a/traffic_ops/traffic_ops_golang/cdn/capacity.go +++ b/traffic_ops/traffic_ops_golang/cdn/capacity.go @@ -33,6 +33,7 @@ import ( "github.com/apache/trafficcontrol/lib/go-log" "github.com/apache/trafficcontrol/lib/go-tc" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api" + "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/monitorhlp" ) func GetCapacity(w http.ResponseWriter, r *http.Request) { @@ -50,27 +51,6 @@ const MonitorProxyParameter = "tm.traffic_mon_fwd_proxy" const MonitorRequestTimeout = time.Second * 10 const MonitorOnlineStatus = "ONLINE" -// CRStates contains the Monitor CRStates members needed for health. It is NOT the full object served by the Monitor, but only the data required by this endpoint. -type CRStates struct { - Caches map[tc.CacheName]Available `json:"caches"` -} - -type Available struct { - IsAvailable bool `json:"isAvailable"` -} - -// CRConfig contains the Monitor CRConfig members needed for health. It is NOT the full object served by the Monitor, but only the data required by this endpoint. -type CRConfig struct { - ContentServers map[tc.CacheName]CRConfigServer `json:"contentServers"` -} - -type CRConfigServer struct { - CacheGroup tc.CacheGroupName `json:"locationId"` - Status tc.CacheStatus `json:"status"` - Type tc.CacheType `json:"type"` - Profile string `json:"profile"` -} - func getCapacity(tx *sql.Tx) (CapacityResp, error) { monitors, err := getCDNMonitorFQDNs(tx) if err != nil { @@ -144,15 +124,15 @@ func getCapacityData(monitors map[tc.CDNName][]string, thresholds map[string]flo for cdn, monitorFQDNs := range monitors { err := error(nil) for _, monitorFQDN := range monitorFQDNs { - crStates := CRStates{} - crConfig := CRConfig{} + crStates := tc.CRStates{} + crConfig := tc.CRConfig{} cacheStats := CacheStats{} - if crStates, err = getCRStates(monitorFQDN, client); err != nil { + if crStates, err = monitorhlp.GetCRStates(monitorFQDN, client); err != nil { err = errors.New("getting CRStates for CDN '" + string(cdn) + "' monitor '" + monitorFQDN + "': " + err.Error()) log.Warnln("getCapacity failed to get CRStates from cdn '" + string(cdn) + " monitor '" + monitorFQDN + "', trying next monitor: " + err.Error()) continue } - if crConfig, err = getCRConfig(monitorFQDN, client); err != nil { + if crConfig, err = monitorhlp.GetCRConfig(monitorFQDN, client); err != nil { err = errors.New("getting CRConfig for CDN '" + string(cdn) + "' monitor '" + monitorFQDN + "': " + err.Error()) log.Warnln("getCapacity failed to get CRConfig from cdn '" + string(cdn) + " monitor '" + monitorFQDN + "', trying next monitor: " + err.Error()) continue @@ -172,30 +152,34 @@ func getCapacityData(monitors map[tc.CDNName][]string, thresholds map[string]flo return cap, nil } -func addCapacity(cap CapData, cacheStats CacheStats, crStates CRStates, crConfig CRConfig, thresholds map[string]float64) CapData { +func addCapacity(cap CapData, cacheStats CacheStats, crStates tc.CRStates, crConfig tc.CRConfig, thresholds map[string]float64) CapData { for cacheName, stats := range cacheStats.Caches { - cache, ok := crConfig.ContentServers[cacheName] + cache, ok := crConfig.ContentServers[string(cacheName)] if !ok { continue } - if !strings.HasPrefix(string(cache.Type), string(tc.CacheTypeEdge)) { + if cache.ServerType == nil || cache.ServerStatus == nil || cache.Profile == nil { + log.Warnln("addCapacity got cache with nil values! Skipping!") + continue + } + if !strings.HasPrefix(*cache.ServerType, string(tc.CacheTypeEdge)) { continue } if len(stats.KBPS) < 1 || len(stats.MaxKBPS) < 1 { continue } - if cache.Status == "REPORTED" || cache.Status == "ONLINE" { + if string(*cache.ServerStatus) == string(tc.CacheStatusReported) || string(*cache.ServerStatus) == string(tc.CacheStatusOnline) { if crStates.Caches[cacheName].IsAvailable { cap.Available += float64(stats.KBPS[0].Value) } else { cap.Unavailable += float64(stats.KBPS[0].Value) } - } else if cache.Status == "ADMIN_DOWN" { + } else if string(*cache.ServerStatus) == string(tc.CacheStatusAdminDown) { cap.Maintenance += float64(stats.KBPS[0].Value) } else { continue // don't add capacity for OFFLINE or other statuses } - cap.Capacity += float64(stats.MaxKBPS[0].Value) - thresholds[cache.Profile] + cap.Capacity += float64(stats.MaxKBPS[0].Value) - thresholds[*cache.Profile] } return cap } @@ -234,35 +218,6 @@ AND pa.name = 'health.threshold.availableBandwidthInKbps' return profileThresholds, nil } -func getCRStates(monitorFQDN string, client *http.Client) (CRStates, error) { - path := `/publish/CrStates` - resp, err := client.Get("http://" + monitorFQDN + path) - if err != nil { - return CRStates{}, errors.New("getting CRStates from Monitor '" + monitorFQDN + "': " + err.Error()) - } - defer resp.Body.Close() - - crs := CRStates{} - if err := json.NewDecoder(resp.Body).Decode(&crs); err != nil { - return CRStates{}, errors.New("decoding CRStates from monitor '" + monitorFQDN + "': " + err.Error()) - } - return crs, nil -} - -func getCRConfig(monitorFQDN string, client *http.Client) (CRConfig, error) { - path := `/publish/CrConfig` - resp, err := client.Get("http://" + monitorFQDN + path) - if err != nil { - return CRConfig{}, errors.New("getting CRConfig from Monitor '" + monitorFQDN + "': " + err.Error()) - } - defer resp.Body.Close() - crs := CRConfig{} - if err := json.NewDecoder(resp.Body).Decode(&crs); err != nil { - return CRConfig{}, errors.New("decoding CRConfig from monitor '" + monitorFQDN + "': " + err.Error()) - } - return crs, nil -} - // CacheStats contains the Monitor CacheStats needed by Cachedata. It is NOT the full object served by the Monitor, but only the data required by the caches stats endpoint. type CacheStats struct { Caches map[tc.CacheName]CacheStat `json:"caches"` diff --git a/traffic_ops/traffic_ops_golang/cdn/health.go b/traffic_ops/traffic_ops_golang/cdn/health.go new file mode 100644 index 0000000000..6faba132ae --- /dev/null +++ b/traffic_ops/traffic_ops_golang/cdn/health.go @@ -0,0 +1,147 @@ +package cdn + +/* + * 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 ( + "database/sql" + "errors" + "net/http" + "strings" + + "github.com/apache/trafficcontrol/lib/go-tc" + "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api" + "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/monitorhlp" +) + +func GetHealth(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + health, err := getHealth(inf.Tx.Tx) + if err != nil { + api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting cdn health: "+err.Error())) + return + } + + api.WriteResp(w, r, health) +} + +func GetNameHealth(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"name"}, nil) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + health, err := getNameHealth(inf.Tx.Tx, tc.CDNName(inf.Params["name"])) + if err != nil { + api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting cdn name health: "+err.Error())) + return + } + + api.WriteResp(w, r, health) +} + +func getHealth(tx *sql.Tx) (tc.HealthData, error) { + monitors, err := monitorhlp.GetURLs(tx) + if err != nil { + return tc.HealthData{}, errors.New("getting monitors: " + err.Error()) + } + return getMonitorsHealth(tx, monitors) +} + +func getNameHealth(tx *sql.Tx, name tc.CDNName) (tc.HealthData, error) { + monitors, err := monitorhlp.GetURLs(tx) + if err != nil { + return tc.HealthData{}, errors.New("getting monitors: " + err.Error()) + } + monitor, ok := monitors[name] + monitors = nil + if ok { + monitors = map[tc.CDNName]string{name: monitor} + } + return getMonitorsHealth(tx, monitors) +} + +func getMonitorsHealth(tx *sql.Tx, monitors map[tc.CDNName]string) (tc.HealthData, error) { + client, err := monitorhlp.GetClient(tx) + if err != nil { + return tc.HealthData{}, errors.New("getting monitor client: " + err.Error()) + } + + totalOnline := uint64(0) + totalOffline := uint64(0) + cgData := map[tc.CacheGroupName]tc.HealthDataCacheGroup{} + for cdn, monitorFQDN := range monitors { + crStates, err := monitorhlp.GetCRStates(monitorFQDN, client) + // TODO on err, try another online monitor + if err != nil { + return tc.HealthData{}, errors.New("getting CRStates for CDN '" + string(cdn) + "' monitor '" + monitorFQDN + "': " + err.Error()) + } + crConfig, err := monitorhlp.GetCRConfig(monitorFQDN, client) + // TODO on err, try another online monitor + if err != nil { + return tc.HealthData{}, errors.New("getting CRConfig for CDN '" + string(cdn) + "' monitor '" + monitorFQDN + "': " + err.Error()) + } + cgData, totalOnline, totalOffline = addHealth(cgData, totalOnline, totalOffline, crStates, crConfig) + } + + healthData := tc.HealthData{TotalOffline: totalOffline, TotalOnline: totalOnline} + for _, health := range cgData { + healthData.CacheGroups = append(healthData.CacheGroups, health) + } + return healthData, nil +} + +// addHealth adds the given cache states to the given data and totals, and returns the new data and totals +func addHealth(data map[tc.CacheGroupName]tc.HealthDataCacheGroup, totalOnline uint64, totalOffline uint64, crStates tc.CRStates, crConfig tc.CRConfig) (map[tc.CacheGroupName]tc.HealthDataCacheGroup, uint64, uint64) { + for cacheName, avail := range crStates.Caches { + cache, ok := crConfig.ContentServers[string(cacheName)] + if !ok { + continue + } + if cache.ServerStatus == nil || *cache.ServerStatus != tc.CRConfigServerStatus(tc.CacheStatusReported) { + continue + } + if cache.ServerType == nil || !strings.HasPrefix(string(*cache.ServerType), string(tc.CacheTypeEdge)) { + continue + } + if cache.CacheGroup == nil { + continue // TODO warn? + } + + cgHealth := data[tc.CacheGroupName(*cache.CacheGroup)] + cgHealth.Name = tc.CacheGroupName(*cache.CacheGroup) + if avail.IsAvailable { + cgHealth.Online++ + totalOnline++ + } else { + cgHealth.Offline++ + totalOffline++ + } + data[tc.CacheGroupName(*cache.CacheGroup)] = cgHealth + } + return data, totalOnline, totalOffline +} diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go index 208decb07d..82ec8c1a1a 100644 --- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go +++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go @@ -558,3 +558,15 @@ func GetUserByEmail(tx *sqlx.Tx, email string) (tc.User, bool, error) { } return u, true, nil } + +// GetGlobalParam returns the global parameter with the requested name, whether it existed, and any error +func GetGlobalParam(tx *sql.Tx, name string) (string, bool, error) { + val := "" + if err := tx.QueryRow(`SELECT value FROM parameter WHERE config_file = 'global' and name = $1`, name).Scan(&val); err != nil { + if err == sql.ErrNoRows { + return "", false, nil + } + return "", false, errors.New("querying global parameter '" + name + "': " + err.Error()) + } + return val, true, nil +} diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go index ddbebb9003..1189721b2c 100644 --- a/traffic_ops/traffic_ops_golang/routing/routes.go +++ b/traffic_ops/traffic_ops_golang/routing/routes.go @@ -198,6 +198,9 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) { //Database dumps {1.1, http.MethodGet, `dbdump/?`, dbdump.DBDump, auth.PrivLevelAdmin, Authenticated, nil, 274016647, perlBypass}, + {1.1, http.MethodGet, `cdns/{name}/health/?(\.json)?$`, cdn.GetNameHealth, auth.PrivLevelReadOnly, Authenticated, nil, 1135348194, perlBypass}, + {1.1, http.MethodGet, `cdns/health/?(\.json)?$`, cdn.GetHealth, auth.PrivLevelReadOnly, Authenticated, nil, 1085381134, perlBypass}, + //Division: CRUD {1.1, http.MethodGet, `divisions/?(\.json)?$`, api.ReadHandler(&division.TODivision{}), auth.PrivLevelReadOnly, Authenticated, nil, 1085181534, noPerlBypass}, {1.1, http.MethodGet, `divisions/{id}$`, api.ReadHandler(&division.TODivision{}), auth.PrivLevelReadOnly, Authenticated, nil, 1241497902, noPerlBypass}, diff --git a/traffic_ops/traffic_ops_golang/tenant/tenancy.go b/traffic_ops/traffic_ops_golang/tenant/tenancy.go index c40643c326..15c6843580 100644 --- a/traffic_ops/traffic_ops_golang/tenant/tenancy.go +++ b/traffic_ops/traffic_ops_golang/tenant/tenancy.go @@ -58,6 +58,18 @@ func GetDeliveryServiceTenantInfo(xmlID string, tx *sql.Tx) (*DeliveryServiceTen return &ds, nil } +func GetDeliveryServiceTenantInfoID(tx *sql.Tx, dsID int) (*DeliveryServiceTenantInfo, error) { + ds := DeliveryServiceTenantInfo{} + ds.ID = util.IntPtr(dsID) + if err := tx.QueryRow(`SELECT tenant_id FROM deliveryservice where id = $1`, &ds.ID).Scan(&ds.TenantID); err != nil { + if err == sql.ErrNoRows { + return &ds, errors.New("a deliveryservice with id '" + strconv.Itoa(dsID) + "' was not found") + } + return nil, errors.New("querying tenant id from delivery service: " + err.Error()) + } + return &ds, nil +} + // Check checks that the given user has access to the given XMLID. Returns a user error, system error, // and the HTTP status code to be returned to the user if an error occurred. On success, the user error // and system error will both be nil, and the error code should be ignored. diff --git a/traffic_ops/traffic_ops_golang/util/monitorhlp/monitorhlp.go b/traffic_ops/traffic_ops_golang/util/monitorhlp/monitorhlp.go new file mode 100644 index 0000000000..2679e67a2e --- /dev/null +++ b/traffic_ops/traffic_ops_golang/util/monitorhlp/monitorhlp.go @@ -0,0 +1,122 @@ +package monitorhlp + +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import ( + "crypto/tls" + "database/sql" + "encoding/json" + "errors" + "net/http" + "net/url" + "strconv" + "time" + + "github.com/apache/trafficcontrol/lib/go-tc" + "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers" +) + +const MonitorProxyParameter = "tm.traffic_mon_fwd_proxy" +const MonitorRequestTimeout = time.Second * 10 +const MonitorOnlineStatus = "ONLINE" + +// GetClient returns the http.Client for making requests to the Traffic Monitor. This should always be used, rather than creating a default http.Client, to ensure any monitor forward proxy parameter is used correctly. +func GetClient(tx *sql.Tx) (*http.Client, error) { + monitorForwardProxy, monitorForwardProxyExists, err := dbhelpers.GetGlobalParam(tx, MonitorProxyParameter) + if err != nil { + return nil, errors.New("getting global monitor proxy parameter: " + err.Error()) + } + client := &http.Client{Timeout: MonitorRequestTimeout} + if monitorForwardProxyExists { + proxyURI, err := url.Parse(monitorForwardProxy) + if err != nil { + return nil, errors.New("monitor forward proxy '" + monitorForwardProxy + "' in parameter '" + MonitorProxyParameter + "' not a URI: " + err.Error()) + } + + clientTransport := &http.Transport{Proxy: http.ProxyURL(proxyURI)} + // Disable HTTP/2. Go Transport Proxy does not support H2 Servers, and if the server does support it, the client will fail. + // See https://github.com/golang/go/issues/26479 "We only support http1 proxies currently." + clientTransport.TLSNextProto = make(map[string]func(authority string, c *tls.Conn) http.RoundTripper) + client = &http.Client{Timeout: MonitorRequestTimeout, Transport: clientTransport} + } + return client, nil +} + +// GetURLs returns an FQDN, including port, of an online monitor for each CDN. If a CDN has no online monitors, that CDN will not have an entry in the map. If a CDN has multiple online monitors, an arbitrary one will be returned. +func GetURLs(tx *sql.Tx) (map[tc.CDNName]string, error) { + rows, err := tx.Query(` +SELECT s.host_name, s.domain_name, s.tcp_port, c.name as cdn +FROM server as s +JOIN type as t ON s.type = t.id +JOIN status as st ON st.id = s.status +JOIN cdn as c ON c.id = s.cdn_id +WHERE t.name = '` + tc.MonitorTypeName + `' +AND st.name = '` + MonitorOnlineStatus + `' +`) + if err != nil { + return nil, errors.New("querying monitors: " + err.Error()) + } + defer rows.Close() + monitors := map[tc.CDNName]string{} + for rows.Next() { + host := "" + domain := "" + port := sql.NullInt64{} + cdn := "" + if err := rows.Scan(&host, &domain, &port, &cdn); err != nil { + return nil, errors.New("scanning monitors: " + err.Error()) + } + fqdn := host + "." + domain + if port.Valid { + fqdn += ":" + strconv.FormatInt(port.Int64, 10) + } + monitors[tc.CDNName(cdn)] = fqdn + } + return monitors, nil +} + +func GetCRStates(monitorFQDN string, client *http.Client) (tc.CRStates, error) { + path := `/publish/CrStates` + resp, err := client.Get("http://" + monitorFQDN + path) + if err != nil { + return tc.CRStates{}, errors.New("getting CRStates from Monitor '" + monitorFQDN + "': " + err.Error()) + } + defer resp.Body.Close() + + crs := tc.CRStates{} + if err := json.NewDecoder(resp.Body).Decode(&crs); err != nil { + return tc.CRStates{}, errors.New("decoding CRStates from monitor '" + monitorFQDN + "': " + err.Error()) + } + return crs, nil +} + +func GetCRConfig(monitorFQDN string, client *http.Client) (tc.CRConfig, error) { + path := `/publish/CrConfig` + resp, err := client.Get("http://" + monitorFQDN + path) + if err != nil { + return tc.CRConfig{}, errors.New("getting CRConfig from Monitor '" + monitorFQDN + "': " + err.Error()) + } + defer resp.Body.Close() + crs := tc.CRConfig{} + if err := json.NewDecoder(resp.Body).Decode(&crs); err != nil { + return tc.CRConfig{}, errors.New("decoding CRConfig from monitor '" + monitorFQDN + "': " + err.Error()) + } + return crs, nil +}