From 4c257dfd742d686ec84332348f3e0e5a3deef30b Mon Sep 17 00:00:00 2001 From: Robert Butts Date: Sat, 23 Jun 2018 12:10:42 -0600 Subject: [PATCH] Add TO Go deliveryservices/id/health --- .../dbhelpers/db_helpers.go | 45 +++--- .../deliveryservice/health.go | 142 ++++++++++++++++++ .../traffic_ops_golang/routing/routes.go | 2 + 3 files changed, 172 insertions(+), 17 deletions(-) create mode 100644 traffic_ops/traffic_ops_golang/deliveryservice/health.go diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go index 85a0a2e587..9294843448 100644 --- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go +++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go @@ -306,16 +306,22 @@ func GetDSTenantIDFromXMLID(tx *sql.Tx, xmlid string) (int, bool, error) { return id, true, nil } +// returns returns the delivery service name and cdn, whether it existed, and any error. func GetDSNameAndCDNFromID(tx *sql.Tx, id int) (tc.DeliveryServiceName, tc.CDNName, bool, error) { - dsName := tc.DeliveryServiceName("") - cdnName := tc.CDNName("") - if err := tx.QueryRow(`SELECT ds.xml_id, cdn.name FROM deliveryservice ds JOIN cdn ON cdn.id = ds.cdn_id WHERE ds.id = $1`, id).Scan(&dsName, &cdnName); err != nil { + name := tc.DeliveryServiceName("") + cdn := tc.CDNName("") + if err := tx.QueryRow(` +SELECT ds.xml_id, cdn.name +FROM deliveryservice as ds +JOIN cdn on cdn.id = ds.cdn_id +WHERE ds.id = $1 +`, id).Scan(&name, &cdn); err != nil { if err == sql.ErrNoRows { return tc.DeliveryServiceName(""), tc.CDNName(""), false, nil } - return tc.DeliveryServiceName(""), tc.CDNName(""), false, errors.New("querying: " + err.Error()) + return tc.DeliveryServiceName(""), tc.CDNName(""), false, errors.New("querying delivery service name: " + err.Error()) } - return dsName, cdnName, true, nil + return name, cdn, true, nil } // GetProfileNameFromID returns the profile's name, whether a profile with ID exists, or any error. @@ -560,6 +566,23 @@ func GetCDNs(tx *sql.Tx) (map[tc.CDNName]struct{}, error) { return cdns, nil } +// GetGlobalParams returns the value of the global param, whether it existed, or any error +func GetGlobalParam(tx *sql.Tx, name string) (string, bool, error) { + return GetParam(tx, name, "global") +} + +// GetParam returns the value of the param, whether it existed, or any error. +func GetParam(tx *sql.Tx, name string, configFile string) (string, bool, error) { + val := "" + if err := tx.QueryRow(`select value from parameter where name = $1 and config_file = $2`, name, configFile).Scan(&val); err != nil { + if err == sql.ErrNoRows { + return "", false, nil + } + return "", false, errors.New("Error querying global paramter '" + name + "': " + err.Error()) + } + return val, true, nil +} + // GetCacheGroupNameFromID Get Cache Group name from a given ID func GetCacheGroupNameFromID(tx *sql.Tx, id int64) (tc.CacheGroupName, bool, error) { name := "" @@ -599,18 +622,6 @@ 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 -} - // UsernameExists reports whether or not the the given username exists as a user in the database to // which the passed transaction refers. If anything goes wrong when checking the existence of said // user, the error is directly returned to the caller. Note that in that case, no real meaning diff --git a/traffic_ops/traffic_ops_golang/deliveryservice/health.go b/traffic_ops/traffic_ops_golang/deliveryservice/health.go new file mode 100644 index 0000000000..af0f527a71 --- /dev/null +++ b/traffic_ops/traffic_ops_golang/deliveryservice/health.go @@ -0,0 +1,142 @@ +package deliveryservice + +/* + * 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/dbhelpers" + "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant" + "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, []string{"id"}, []string{"id"}) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + dsID := inf.IntParams["id"] + + userErr, sysErr, errCode = tenant.CheckID(inf.Tx.Tx, inf.User, dsID) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + + ds, cdn, ok, err := dbhelpers.GetDSNameAndCDNFromID(inf.Tx.Tx, dsID) + if err != nil { + api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting delivery service name from ID: "+err.Error())) + return + } + if !ok { + api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, nil, nil) + } + + health, err := getHealth(inf.Tx.Tx, ds, cdn) + if err != nil { + api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting delivery service health: "+err.Error())) + return + } + + api.WriteResp(w, r, health) +} + +func getHealth(tx *sql.Tx, ds tc.DeliveryServiceName, cdn 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[cdn] + if !ok { + return tc.HealthData{}, nil // TODO emulates old Perl behavior; change to return error? + } + return getMonitorHealth(tx, ds, monitor) +} + +func getMonitorHealth(tx *sql.Tx, ds tc.DeliveryServiceName, monitorFQDN 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{} + + crStates, err := monitorhlp.GetCRStates(monitorFQDN, client) + // TODO on err, try another online monitor + if err != nil { + return tc.HealthData{}, errors.New("getting CRStates for delivery service '" + string(ds) + "' 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 delivery service '" + string(ds) + "' monitor '" + monitorFQDN + "': " + err.Error()) + } + cgData, totalOnline, totalOffline = addHealth(ds, cgData, totalOnline, totalOffline, crStates, crConfig) + + healthData := tc.HealthData{TotalOffline: totalOffline, TotalOnline: totalOnline, CacheGroups: []tc.HealthDataCacheGroup{}} + 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(ds tc.DeliveryServiceName, 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 // TODO warn? + } + if _, ok := cache.DeliveryServices[string(ds)]; !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/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go index c5669d234a..7822a97e11 100644 --- a/traffic_ops/traffic_ops_golang/routing/routes.go +++ b/traffic_ops/traffic_ops_golang/routing/routes.go @@ -563,6 +563,8 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) { {1.1, http.MethodGet, `deliveryservices/{id}/urlkeys/?(\.json)?$`, deliveryservice.GetURLKeysByID, auth.PrivLevelReadOnly, Authenticated, nil, 393197114, noPerlBypass}, {1.1, http.MethodGet, `riak/bucket/{bucket}/key/{key}/values/?(\.json)?$`, apiriak.GetBucketKey, auth.PrivLevelAdmin, Authenticated, nil, 2020510801, noPerlBypass}, + {1.1, http.MethodGet, `deliveryservices/{id}/health/?(\.json)?$`, deliveryservice.GetHealth, auth.PrivLevelReadOnly, Authenticated, nil, 2034590101, perlBypass}, + {1.1, http.MethodGet, `steering/{deliveryservice}/targets/?(\.json)?$`, api.ReadHandler(&steeringtargets.TOSteeringTargetV11{}), auth.PrivLevelReadOnly, Authenticated, nil, 1569607824, noPerlBypass}, {1.1, http.MethodGet, `steering/{deliveryservice}/targets/{target}$`, api.ReadHandler(&steeringtargets.TOSteeringTargetV11{}), auth.PrivLevelReadOnly, Authenticated, nil, 105995848, noPerlBypass}, {1.1, http.MethodPost, `steering/{deliveryservice}/targets/?(\.json)?$`, api.CreateHandler(&steeringtargets.TOSteeringTargetV11{}), auth.PrivLevelSteering, Authenticated, nil, 1338216397, noPerlBypass},