| 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" |
| "fmt" |
| "net/http" |
| "strconv" |
| "strings" |
| |
| "github.com/apache/trafficcontrol/lib/go-log" |
| "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/dbhelpers" |
| "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant" |
| "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/monitorhlp" |
| ) |
| |
| func GetCapacity(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) |
| return |
| } |
| |
| capacity, err := getCapacity(inf.Tx.Tx, ds, cdn) |
| if err != nil { |
| api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("getting delivery service capacity: "+err.Error())) |
| return |
| } |
| |
| api.WriteResp(w, r, capacity) |
| } |
| |
| type CapacityResp struct { |
| AvailablePercent float64 `json:"availablePercent"` |
| UnavailablePercent float64 `json:"unavailablePercent"` |
| UtilizedPercent float64 `json:"utilizedPercent"` |
| MaintenancePercent float64 `json:"maintenancePercent"` |
| } |
| |
| type CapData struct { |
| Available float64 |
| Unavailable float64 |
| Maintenance float64 |
| Capacity float64 |
| } |
| |
| func getCapacity(tx *sql.Tx, ds tc.DeliveryServiceName, cdn tc.CDNName) (CapacityResp, error) { |
| monitors, err := monitorhlp.GetURLs(tx) |
| if err != nil { |
| return CapacityResp{}, errors.New("getting monitor URLs: " + err.Error()) |
| } |
| client, err := monitorhlp.GetClient(tx) |
| if err != nil { |
| return CapacityResp{}, errors.New("getting monitor client: " + err.Error()) |
| } |
| |
| thresholds, err := getEdgeProfileHealthThresholdBandwidth(tx) |
| if err != nil { |
| return CapacityResp{}, errors.New("getting profile thresholds: " + err.Error()) |
| } |
| |
| monitorFQDNs, ok := monitors[cdn] |
| if !ok { |
| return CapacityResp{}, nil // TODO emulates perl; change to error? |
| } |
| |
| errs := []error{} |
| for _, monitorFQDN := range monitorFQDNs { |
| crStates, err := monitorhlp.GetCRStates(monitorFQDN, client) |
| if err != nil { |
| errs = append(errs, errors.New("getting CRStates for delivery service '"+string(ds)+"' monitor '"+monitorFQDN+"': "+err.Error())) |
| continue |
| } |
| crConfig, err := monitorhlp.GetCRConfig(monitorFQDN, client) |
| if err != nil { |
| errs = append(errs, errors.New("getting CRConfig for delivery service '"+string(ds)+"' monitor '"+monitorFQDN+"': "+err.Error())) |
| continue |
| } |
| statsToFetch := []string{tc.StatNameMaxKBPS, tc.StatNameKBPS} |
| cacheStats, _, err := monitorhlp.GetCacheStats(monitorFQDN, client, statsToFetch) |
| if err != nil { |
| legacyCacheStats, _, err := monitorhlp.GetLegacyCacheStats(monitorFQDN, client, statsToFetch) |
| if err != nil { |
| errs = append(errs, errors.New("getting CacheStats for delivery service '"+string(ds)+"' monitor '"+monitorFQDN+"': "+err.Error())) |
| continue |
| } |
| cacheStats = monitorhlp.UpgradeLegacyStats(legacyCacheStats) |
| } |
| cap := addCapacity(CapData{}, ds, cacheStats, crStates, crConfig, thresholds) |
| if cap.Capacity == 0 { |
| if dsHasServer(ds, crConfig) { |
| return CapacityResp{}, errors.New("Delivery service '" + string(ds) + "' has servers, but capacity was zero!'") |
| } |
| log.Warnf("Delivery service '" + string(ds) + "' has no servers. Returning 0 capacity'") |
| return CapacityResp{}, nil // avoid divide-by-zero below. |
| } |
| return CapacityResp{ |
| UtilizedPercent: (cap.Available * 100) / cap.Capacity, |
| UnavailablePercent: (cap.Unavailable * 100) / cap.Capacity, |
| MaintenancePercent: (cap.Maintenance * 100) / cap.Capacity, |
| AvailablePercent: ((cap.Capacity - cap.Unavailable - cap.Maintenance - cap.Available) * 100) / cap.Capacity, |
| }, nil |
| } |
| return CapacityResp{}, errors.New("getting capacity: " + util.JoinErrs(errs).Error()) |
| } |
| |
| // dsHasServer checks whether a given DS has servers. |
| func dsHasServer(ds tc.DeliveryServiceName, crConfig tc.CRConfig) bool { |
| for _, server := range crConfig.ContentServers { |
| if _, ok := server.DeliveryServices[string(ds)]; ok { |
| return true |
| } |
| } |
| return false |
| |
| } |
| |
| func addCapacity( |
| cap CapData, |
| ds tc.DeliveryServiceName, |
| cacheStats tc.Stats, |
| crStates tc.CRStates, |
| crConfig tc.CRConfig, |
| thresholds map[string]float64, |
| ) CapData { |
| for cacheName, statsCache := range cacheStats.Caches { |
| cache, ok := crConfig.ContentServers[string(cacheName)] |
| if !ok { |
| log.Warnln("Getting delivery service capacity: delivery service '" + string(ds) + "' cache '" + string(cacheName) + "' in CacheStats but not CRConfig, skipping") |
| continue |
| } |
| |
| if _, ok := cache.DeliveryServices[string(ds)]; !ok { |
| continue |
| } |
| if cache.ServerType == nil || !strings.HasPrefix(string(*cache.ServerType), string(tc.CacheTypeEdge)) { |
| continue |
| } |
| |
| stat := statsCache.Stats |
| if len(stat[tc.StatNameKBPS]) < 1 || len(stat[tc.StatNameMaxKBPS]) < 1 { |
| log.Warnln("Getting delivery service capacity: delivery service '" + string(ds) + "' cache '" + string(cacheName) + "' CacheStats has no kbps or maxKbps, skipping") |
| continue |
| } |
| |
| kbps, err := statToFloat(stat[tc.StatNameKBPS][0].Val) |
| if err != nil { |
| log.Warnln("Getting delivery service capacity: delivery service '" + string(ds) + "' cache '" + string(cacheName) + "' CacheStats kbps is not a number, skipping") |
| continue |
| } |
| maxKBPS, err := statToFloat(stat[tc.StatNameMaxKBPS][0].Val) |
| if err != nil { |
| log.Warnln("Getting delivery service capacity: delivery service '" + string(ds) + "' cache '" + string(cacheName) + "' CacheStats maxKps is not a number, skipping") |
| continue |
| } |
| if cache.ServerStatus == nil { |
| log.Warnln("Getting delivery service capacity: delivery service '" + string(ds) + "' cache '" + string(cacheName) + "' CRConfig Status is nil, skipping") |
| continue |
| } |
| if cache.Profile == nil { |
| log.Warnln("Getting delivery service capacity: delivery service '" + string(ds) + "' cache '" + string(cacheName) + "' CRConfig Profile is nil, skipping") |
| continue |
| } |
| if tc.CacheStatus(*cache.ServerStatus) == tc.CacheStatusReported || tc.CacheStatus(*cache.ServerStatus) == tc.CacheStatusOnline { |
| if crStates.Caches[tc.CacheName(cacheName)].IsAvailable { |
| cap.Available += kbps |
| } else { |
| cap.Unavailable += kbps |
| } |
| } else if tc.CacheStatus(*cache.ServerStatus) == tc.CacheStatusAdminDown { |
| cap.Maintenance += kbps |
| } else { |
| continue // don't add capacity for OFFLINE or other statuses |
| } |
| cap.Capacity += maxKBPS - thresholds[*cache.Profile] |
| |
| } |
| return cap |
| } |
| |
| // statToFloat converts a CacheStats stat interface{} to a float64 |
| func statToFloat(s interface{}) (float64, error) { |
| switch v := s.(type) { |
| case int: |
| return float64(v), nil |
| case int64: |
| return float64(v), nil |
| case float64: |
| return v, nil |
| case string: |
| iv, err := strconv.ParseFloat(v, 64) |
| if err != nil { |
| return 0.0, errors.New("stat is a string which is not a number: " + err.Error()) |
| } |
| return iv, nil |
| default: |
| return 0.0, fmt.Errorf("unknown stat type: %T", s) |
| } |
| } |
| |
| func getEdgeProfileHealthThresholdBandwidth(tx *sql.Tx) (map[string]float64, error) { |
| rows, err := tx.Query(` |
| SELECT pr.name as profile, pa.name, pa.config_file, pa.value |
| FROM parameter as pa |
| JOIN profile_parameter as pp ON pp.parameter = pa.id |
| JOIN profile as pr ON pp.profile = pr.id |
| JOIN server as s ON s.profile = pr.id |
| JOIN cdn as c ON c.id = s.cdn_id |
| JOIN type as t ON s.type = t.id |
| WHERE t.name LIKE 'EDGE%' |
| AND pa.config_file = 'rascal-config.txt' |
| AND pa.name = 'health.threshold.availableBandwidthInKbps' |
| `) |
| if err != nil { |
| return nil, errors.New("querying thresholds: " + err.Error()) |
| } |
| defer rows.Close() |
| profileThresholds := map[string]float64{} |
| for rows.Next() { |
| profile := "" |
| threshStr := "" |
| if err := rows.Scan(&profile, &threshStr); err != nil { |
| return nil, errors.New("scanning thresholds: " + err.Error()) |
| } |
| threshStr = strings.TrimPrefix(threshStr, ">") |
| thresh, err := strconv.ParseFloat(threshStr, 64) |
| if err != nil { |
| return nil, errors.New("profile '" + profile + "' health.threshold.availableBandwidthInKbps is not a number") |
| } |
| profileThresholds[profile] = thresh |
| } |
| return profileThresholds, nil |
| } |