traffic_ops/traffic_ops_golang/deliveryservice/servers/servers.go (968 lines of code) (raw):

package servers /* * 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" "encoding/json" "errors" "fmt" "net/http" "strconv" "strings" "time" "github.com/apache/trafficcontrol/v8/lib/go-log" "github.com/apache/trafficcontrol/v8/lib/go-rfc" "github.com/apache/trafficcontrol/v8/lib/go-tc" "github.com/apache/trafficcontrol/v8/lib/go-tc/tovalidate" "github.com/apache/trafficcontrol/v8/lib/go-util" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/api" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/auth" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/dbhelpers" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/deliveryservice" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/tenant" "github.com/apache/trafficcontrol/v8/traffic_ops/traffic_ops_golang/util/ims" validation "github.com/go-ozzo/ozzo-validation" "github.com/jmoiron/sqlx" "github.com/lib/pq" ) // TODeliveryServiceRequest provides a type alias to define functions on type TODeliveryServiceServer struct { api.APIInfoImpl `json:"-"` tc.DeliveryServiceServer TenantIDs pq.Int64Array `json:"-" db:"accessibleTenants"` DeliveryServiceIDs pq.Int64Array `json:"-" db:"dsids"` ServerIDs pq.Int64Array `json:"-" db:"serverids"` CDN string `json:"-" db:"cdn"` } func (dss TODeliveryServiceServer) GetKeyFieldsInfo() []api.KeyFieldInfo { return []api.KeyFieldInfo{{Field: "deliveryservice", Func: api.GetIntKey}, {Field: "server", Func: api.GetIntKey}} } // Implementation of the Identifier, Validator interface functions func (dss TODeliveryServiceServer) GetKeys() (map[string]interface{}, bool) { if dss.DeliveryService == nil { return map[string]interface{}{"deliveryservice": 0}, false } if dss.Server == nil { return map[string]interface{}{"server": 0}, false } keys := make(map[string]interface{}) ds_id := *dss.DeliveryService server_id := *dss.Server keys["deliveryservice"] = ds_id keys["server"] = server_id return keys, true } func (dss *TODeliveryServiceServer) GetAuditName() string { if dss.DeliveryService != nil && dss.Server != nil { return strconv.Itoa(*dss.DeliveryService) + "-" + strconv.Itoa(*dss.Server) } return "unknown" } func (dss *TODeliveryServiceServer) GetType() string { return "deliveryserviceServers" } func (dss *TODeliveryServiceServer) SetKeys(keys map[string]interface{}) { ds_id, _ := keys["deliveryservice"].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here. dss.DeliveryService = &ds_id server_id, _ := keys["server"].(int) //this utilizes the non panicking type assertion, if the thrown away ok variable is false i will be the zero of the type, 0 here. dss.Server = &server_id } // Validate fulfills the api.Validator interface func (dss *TODeliveryServiceServer) Validate(tx *sql.Tx) error { errs := validation.Errors{ "deliveryservice": validation.Validate(dss.DeliveryService, validation.Required), "server": validation.Validate(dss.Server, validation.Required), } return util.JoinErrs(tovalidate.ToErrors(errs)) } // ReadDSSHandler is the handler for GET requests to /deliveryserviceserver. func ReadDSSHandler(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"limit", "page"}) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } defer inf.Close() dsIDs := []int64{} dsIDStrs := strings.Split(inf.Params["deliveryserviceids"], ",") for _, dsIDStr := range dsIDStrs { dsIDStr = strings.TrimSpace(dsIDStr) if dsIDStr == "" { continue } dsID, err := strconv.Atoi(dsIDStr) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, 400, errors.New("deliveryserviceids query parameter must be a comma-delimited list of integers, got '"+inf.Params["deliveryserviceids"]+"'"), nil) return } dsIDs = append(dsIDs, int64(dsID)) } serverIDs := []int64{} serverIDStrs := strings.Split(inf.Params["serverids"], ",") for _, serverIDStr := range serverIDStrs { serverIDStr = strings.TrimSpace(serverIDStr) if serverIDStr == "" { continue } serverID, err := strconv.Atoi(serverIDStr) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, 400, errors.New("serverids query parameter must be a comma-delimited list of integers, got '"+inf.Params["serverids"]+"'"), nil) return } serverIDs = append(serverIDs, int64(serverID)) } dss := TODeliveryServiceServer{} dss.SetInfo(inf) cfg, e := api.GetConfig(r.Context()) useIMS := false if e == nil && cfg != nil { useIMS = cfg.UseIMS } else { log.Warnf("Couldn't get config %v", e) } results, err, maxTime := dss.readDSS(r.Header, inf.Tx, inf.User, inf.Params, inf.IntParams, dsIDs, serverIDs, useIMS) if maxTime != nil && api.SetLastModifiedHeader(r, useIMS) { // RFC1123 date := maxTime.Format("Mon, 02 Jan 2006 15:04:05 MST") w.Header().Add(rfc.LastModified, date) } if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err) return } // statusnotmodified if err == nil && results == nil { w.WriteHeader(http.StatusNotModified) } if inf.Version.GreaterThanOrEqualTo(&api.Version{ Major: 5, Minor: 0, }) { var resultsV5 tc.DeliveryServiceServerResponseV5 resultsV5.Limit = results.Limit resultsV5.Orderby = results.Orderby resultsV5.Size = results.Size resultsV5.Alerts = results.Alerts resultsV5.Response = *upgrade(results.Response) api.WriteRespRaw(w, r, resultsV5) } else { api.WriteRespRaw(w, r, results) } } func upgrade(dsServers []tc.DeliveryServiceServer) *[]tc.DeliveryServiceServerV5 { var err error dsServersV5 := make([]tc.DeliveryServiceServerV5, len(dsServers)) for i, s := range dsServers { dsServersV5[i].Server = s.Server dsServersV5[i].DeliveryService = s.DeliveryService if dsServersV5[i].LastUpdated, err = util.ConvertTimeFormat(s.LastUpdated.Time, time.RFC3339); err != nil { dsServersV5[i].LastUpdated = &s.LastUpdated.Time } } return &dsServersV5 } func (dss *TODeliveryServiceServer) readDSS(h http.Header, tx *sqlx.Tx, user *auth.CurrentUser, params map[string]string, intParams map[string]int, dsIDs []int64, serverIDs []int64, useIMS bool) (*tc.DeliveryServiceServerResponse, error, *time.Time) { var maxTime time.Time var runSecond bool // NOTE: if the 'orderby' query param exists but has an empty value, that means no ordering should be done. // If the 'orderby' query param does not exist, order by "deliveryService" by default. This allows clients to // specifically skip sorting if it's unnecessary, reducing load on the DB. orderby, ok := params["orderby"] if !ok { orderby = "deliveryService" } limit := 20 offset := 0 page := 0 err := error(nil) if plimit, ok := intParams["limit"]; ok { limit = plimit } if ppage, ok := intParams["page"]; ok { page = ppage offset = page if offset > 0 { offset -= 1 } offset *= limit } tenantIDs, err := tenant.GetUserTenantIDListTx(tx.Tx, user.TenantID) if err != nil { return nil, errors.New("getting user tenant ID list: " + err.Error()), nil } for _, id := range tenantIDs { dss.TenantIDs = append(dss.TenantIDs, int64(id)) } dss.ServerIDs = serverIDs dss.DeliveryServiceIDs = dsIDs queryValues := map[string]interface{}{} cdn := "" if cdnName, ok := params["cdn"]; ok { cdn = cdnName queryValues["cdn"] = cdnName } dss.CDN = cdn query1, err := selectQuery(orderby, strconv.Itoa(limit), strconv.Itoa(offset), dsIDs, serverIDs, true, cdn) if err != nil { log.Warnf("Error getting the max last updated query %v", err) } if useIMS { queryValues["accessibleTenants"] = pq.Array(tenantIDs) runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, h, queryValues, query1) if !runSecond { log.Debugln("IMS HIT") return nil, nil, &maxTime } log.Debugln("IMS MISS") } else { log.Debugln("Non IMS request") } query, err := selectQuery(orderby, strconv.Itoa(limit), strconv.Itoa(offset), dsIDs, serverIDs, false, cdn) if err != nil { return nil, errors.New("creating query for DeliveryserviceServers: " + err.Error()), nil } log.Debugln("Query is ", query) rows, err := tx.NamedQuery(query, dss) if err != nil { return nil, errors.New("Error querying DeliveryserviceServers: " + err.Error()), nil } defer rows.Close() servers := []tc.DeliveryServiceServer{} for rows.Next() { s := tc.DeliveryServiceServer{} if err = rows.StructScan(&s); err != nil { return nil, errors.New("error parsing dss rows: " + err.Error()), nil } servers = append(servers, s) } return &tc.DeliveryServiceServerResponse{Orderby: orderby, Response: servers, Size: page, Limit: limit}, nil, &maxTime } func selectQuery(orderBy string, limit string, offset string, dsIDs []int64, serverIDs []int64, getMaxQuery bool, cdn string) (string, error) { selectStmt := `SELECT s.deliveryService, s.server, s.last_updated FROM deliveryservice_server s` if getMaxQuery { selectStmt = `SELECT max(t) from ( ( SELECT max(s.last_updated) as t FROM deliveryservice_server s` } allowedOrderByCols := map[string]string{ "": "", "deliveryservice": "s.deliveryService", "server": "s.server", "lastupdated": "s.last_updated", "deliveryService": "s.deliveryService", "lastUpdated": "s.last_updated", "last_updated": "s.last_updated", } orderBy, ok := allowedOrderByCols[orderBy] if !ok { return "", errors.New("orderBy '" + orderBy + "' not permitted") } // TODO refactor to use dbhelpers.AddTenancyCheck selectStmt += ` JOIN deliveryservice d on s.deliveryservice = d.id WHERE d.tenant_id = ANY(CAST(:accessibleTenants AS bigint[])) ` if len(dsIDs) > 0 { selectStmt += ` AND s.deliveryservice = ANY(:dsids) ` } if len(serverIDs) > 0 { selectStmt += ` AND s.server = ANY(:serverids) ` } if len(cdn) > 0 { selectStmt += ` AND d.cdn_id = (SELECT id FROM cdn WHERE name = :cdn) ` } if getMaxQuery { selectStmt += ` GROUP BY s.deliveryservice` } if orderBy != "" { selectStmt += ` ORDER BY ` + orderBy } selectStmt += ` LIMIT ` + limit + ` OFFSET ` + offset + ` ROWS ` if getMaxQuery { return selectStmt + ` ) UNION ALL select max(last_updated) as t from last_deleted l where l.table_name='deliveryservice_server') as res`, nil } return selectStmt, nil } type DSServerIds struct { DsId *int `json:"dsId" db:"deliveryservice"` Servers []int `json:"servers"` Replace *bool `json:"replace"` } type TODSServerIds DSServerIds const checkPreExistingEdgeServersQuery = ` SELECT t.name AS name FROM type t JOIN server s ON t.id = s.type JOIN status st ON s.status = st.id JOIN deliveryservice_server dss ON dss.server = s.id WHERE s.id = ANY(ARRAY(SELECT server FROM deliveryservice_server WHERE deliveryservice=$1)) AND (st.name = '` + string(tc.CacheStatusOnline) + `' OR st.name = '` + string(tc.CacheStatusReported) + `') AND t.name like '` + string(tc.EdgeTypePrefix) + `%' AND dss.deliveryservice=$1 LIMIT 1 ` func hasAvailableEdgesCurrentlyAssigned(tx *sql.Tx, dsID int) (bool, error) { t := "" if err := tx.QueryRow(checkPreExistingEdgeServersQuery, dsID).Scan(&t); err != nil { if err == sql.ErrNoRows { return false, nil } return false, fmt.Errorf("couldn't query for pre existing edge servers for this DS: %s", err.Error()) } return true, nil } // GetReplaceHandler is the handler for POST requests to the /deliveryserviceserver API endpoint. func GetReplaceHandler(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, nil, []string{"limit", "page"}) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } defer inf.Close() payload := DSServerIds{} if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("malformed JSON"), nil) return } servers := payload.Servers dsId := payload.DsId if servers == nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("servers must exist in post"), nil) return } if dsId == nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("dsid must exist in post"), nil) return } if payload.Replace == nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("replace must exist in post"), nil) return } ds, ok, err := GetDSInfo(inf.Tx.Tx, *dsId) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("deliveryserviceserver getting delivery service info for ID %d: %v", *dsId, err)) return } if !ok { api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("no delivery service with that ID exists"), nil) return } if userErr, sysErr, errCode := tenant.Check(inf.User, ds.Name, inf.Tx.Tx); userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } if ds.CDNID != nil { cdn, ok, err := dbhelpers.GetCDNNameFromID(inf.Tx.Tx, int64(*ds.CDNID)) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err) return } else if !ok { api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, nil, nil) return } userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(inf.Tx.Tx, string(cdn), inf.User.UserName) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr) return } } serverInfos, err := dbhelpers.GetServerInfosFromIDs(inf.Tx.Tx, servers) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err) return } userErr, sysErr, status := validateDSSAssignments(inf.Tx.Tx, ds, serverInfos, *payload.Replace) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr) return } if *payload.Replace { // delete existing _, err := inf.Tx.Tx.Exec("DELETE FROM deliveryservice_server WHERE deliveryservice = $1", *dsId) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("unable to remove the existing servers assigned to the delivery service: "+err.Error())) return } } respServers := []int{} for _, server := range servers { dtos := map[string]interface{}{"id": dsId, "server": server} if _, err := inf.Tx.NamedExec(insertIdsQuery(), dtos); err != nil { usrErr, sysErr, code := api.ParseDBError(err) api.HandleErr(w, r, inf.Tx.Tx, code, usrErr, sysErr) return } respServers = append(respServers, server) } if err := deliveryservice.EnsureParams(inf.Tx.Tx, *dsId, ds.Name, ds.EdgeHeaderRewrite, ds.MidHeaderRewrite, ds.RegexRemap, ds.SigningAlgorithm, ds.Type, ds.MaxOriginConnections); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deliveryservice_server replace ensuring ds parameters: "+err.Error())) return } if err := deliveryservice.EnsureCacheURLParams(inf.Tx.Tx, ds.ID, ds.Name, ds.CacheURL); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deliveryservice_server replace ensuring ds parameters: "+err.Error())) return } api.CreateChangeLogRawTx(api.ApiChange, "DS: "+ds.Name+", ID: "+strconv.Itoa(*dsId)+", ACTION: Replace existing servers assigned to delivery service", inf.User, inf.Tx.Tx) api.WriteRespAlertObj(w, r, tc.SuccessLevel, "server assignments complete", tc.DSSMapResponse{DsId: *dsId, Replace: *payload.Replace, Servers: respServers}) } type TODeliveryServiceServers tc.DeliveryServiceServers // GetCreateHandler is the handler for POST requests to /deliveryservices/{{XMLID}}/servers. func GetCreateHandler(w http.ResponseWriter, r *http.Request) { inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"xml_id"}, nil) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } defer inf.Close() dsName := inf.Params["xml_id"] if userErr, sysErr, errCode := tenant.Check(inf.User, dsName, inf.Tx.Tx); userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) return } ds, ok, err := GetDSInfoByName(inf.Tx.Tx, dsName) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, fmt.Errorf("ds servers getting delivery service info for xmlID %s: %v", dsName, err)) return } else if !ok { api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, nil, errors.New("delivery service not found")) return } if ds.CDNID != nil { cdn, ok, err := dbhelpers.GetCDNNameFromID(inf.Tx.Tx, int64(*ds.CDNID)) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err) return } else if !ok { api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, nil, nil) return } userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(inf.Tx.Tx, string(cdn), inf.User.UserName) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr) return } } // get list of server Ids to insert payload := tc.DeliveryServiceServers{} if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusBadRequest, errors.New("malformed JSON"), nil) return } payload.XmlId = dsName serverNames := payload.ServerNames serverInfos, err := dbhelpers.GetServerInfosFromHostNames(inf.Tx.Tx, serverNames) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err) return } userErr, sysErr, status := validateDSSAssignments(inf.Tx.Tx, ds, serverInfos, false) if userErr != nil || sysErr != nil { api.HandleErr(w, r, inf.Tx.Tx, status, userErr, sysErr) return } res, err := inf.Tx.Tx.Exec(`INSERT INTO deliveryservice_server (deliveryservice, server) SELECT $1, id FROM server WHERE host_name = ANY($2::text[])`, ds.ID, pq.Array(serverNames)) if err != nil { usrErr, sysErr, code := api.ParseDBError(err) api.HandleErr(w, r, inf.Tx.Tx, code, usrErr, sysErr) return } if rowsAffected, err := res.RowsAffected(); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("ds servers inserting for create delivery service servers: getting rows affected: "+err.Error())) return } else if int(rowsAffected) != len(serverNames) { // this happens when the names they gave don't exist api.HandleErr(w, r, inf.Tx.Tx, http.StatusNotFound, errors.New("servers not found"), nil) return } if err := deliveryservice.EnsureParams(inf.Tx.Tx, ds.ID, ds.Name, ds.EdgeHeaderRewrite, ds.MidHeaderRewrite, ds.RegexRemap, ds.SigningAlgorithm, ds.Type, ds.MaxOriginConnections); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deliveryservice_server replace ensuring ds parameters: "+err.Error())) return } if err := deliveryservice.EnsureCacheURLParams(inf.Tx.Tx, ds.ID, ds.Name, ds.CacheURL); err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("deliveryservice_server replace ensuring ds parameters: "+err.Error())) return } api.CreateChangeLogRawTx(api.ApiChange, "DS: "+dsName+", ID: "+strconv.Itoa(ds.ID)+", ACTION: Assigned servers "+strings.Join(serverNames, ", ")+" to delivery service", inf.User, inf.Tx.Tx) api.WriteResp(w, r, tc.DeliveryServiceServers{ServerNames: payload.ServerNames, XmlId: payload.XmlId}) } // validateDSSAssignments returns an error if the given servers cannot be assigned to the given delivery service. func validateDSSAssignments(tx *sql.Tx, ds DSInfo, serverInfos []tc.ServerInfo, replace bool) (error, error, int) { anyAvailableServers := false userErr, sysErr, status := validateDSS(tx, ds, serverInfos) if userErr != nil || sysErr != nil { return userErr, sysErr, status } if ds.Active && replace { ids := make([]int, 0, len(serverInfos)) newOrgCount := 0 newAvailableEdgeCount := 0 for _, inf := range serverInfos { ids = append(ids, inf.ID) // We dont check for the cache type to be = EDGE here because if this is a new DS, and we want to assign an online/ reported ORG to it, // we should be able to do that. if inf.Status == string(tc.CacheStatusOnline) || inf.Status == string(tc.CacheStatusReported) { anyAvailableServers = true if inf.Type == tc.OriginTypeName { newOrgCount++ } else if strings.HasPrefix(inf.Type, tc.CacheTypeEdge.String()) { newAvailableEdgeCount++ } } } // Prevent the user from deleting all the servers in an active, non-topology-based DS if len(ids) == 0 && ds.Topology == nil { return fmt.Errorf("this server assignment leaves Active Delivery Service #%d without any '%s' or '%s' servers", ds.ID, tc.CacheStatusOnline, tc.CacheStatusReported), nil, http.StatusConflict } // prevent the user from deleting all ORG servers from an active, MSO-enabled DS if ds.UseMultiSiteOrigin && newOrgCount < 1 { return fmt.Errorf("this server assignment leaves Active, MSO-enabled Delivery Service #%d without any '%s' or '%s' %s servers", ds.ID, tc.CacheStatusOnline, tc.CacheStatusReported, tc.OriginTypeName), nil, http.StatusConflict } if ds.Topology == nil { // The following check is necessary because of the following: // Consider a brand new active DS that has no server assignments. // Now, you wish to assign an online/ reported ORG server to it. // Since this is a new DS and it didnt have any "pre existing" online/ reported EDGEs, this should be possible. // However, if that DS had a couple of online/ reported EDGEs assigned to it, and now if you wanted to "replace" // that assignment with the new assignment of an online/ reported ORG, this should be prohibited by TO. currentlyHasAvailableEdgesAssigned, err := hasAvailableEdgesCurrentlyAssigned(tx, ds.ID) if err != nil { return nil, fmt.Errorf("checking for pre existing ONLINE/ REPORTED EDGES: %v", err), http.StatusInternalServerError } if (currentlyHasAvailableEdgesAssigned && newAvailableEdgeCount < 1) || !anyAvailableServers { return fmt.Errorf("this server assignment leaves Active Delivery Service #%d without any '%s' or '%s' servers", ds.ID, tc.CacheStatusOnline, tc.CacheStatusReported), nil, http.StatusConflict } } } userErr, sysErr, status = ValidateServerCapabilities(tx, ds.ID, serverInfos) if userErr != nil || sysErr != nil { return userErr, sysErr, status } return nil, nil, http.StatusOK } func validateDSS(tx *sql.Tx, ds DSInfo, servers []tc.ServerInfo) (error, error, int) { if ds.Topology == nil { for _, s := range servers { if ds.CDNID != nil && s.CDNID != *ds.CDNID { return errors.New("server and delivery service CDNs do not match"), nil, http.StatusBadRequest } } return nil, nil, http.StatusOK } for _, s := range servers { if s.Type != tc.OriginTypeName { return fmt.Errorf("only servers of type %s may be assigned to topology-based delivery services", tc.OriginTypeName), nil, http.StatusBadRequest } } _, cachegroups, sysErr := dbhelpers.GetTopologyCachegroups(tx, *ds.Topology) if sysErr != nil { return nil, fmt.Errorf("validating %s servers in topology %s: %v", tc.OriginTypeName, *ds.Topology, sysErr), http.StatusInternalServerError } userErr := CheckServersInCachegroups(servers, cachegroups) if userErr != nil { return fmt.Errorf("validating %s servers in topology %s: %v", tc.OriginTypeName, *ds.Topology, userErr), nil, http.StatusBadRequest } return nil, nil, http.StatusOK } // CheckServersInCachegroups checks whether or not all the given server cachegroups belong to the topology // and returns a user error (if any). func CheckServersInCachegroups(servers []tc.ServerInfo, cachegroups []string) error { cgSet := make(map[string]struct{}, len(cachegroups)) for _, c := range cachegroups { cgSet[c] = struct{}{} } invalid := []string{} for _, s := range servers { if _, ok := cgSet[s.Cachegroup]; !ok { invalid = append(invalid, s.HostName+" ("+s.Cachegroup+")") } } if len(invalid) > 0 { return fmt.Errorf("the following servers are not in any of the given cachegroups (%s): %s", strings.Join(cachegroups, ", "), strings.Join(invalid, ", ")) } return nil } // ValidateServerCapabilities checks that the delivery service's requirements are met by each server to be assigned. func ValidateServerCapabilities(tx *sql.Tx, dsID int, serverNamesAndTypes []tc.ServerInfo) (error, error, int) { nonOriginServerNames := []string{} for _, s := range serverNamesAndTypes { if strings.HasPrefix(s.Type, tc.EdgeTypePrefix) { nonOriginServerNames = append(nonOriginServerNames, s.HostName) } } dsCaps, err := dbhelpers.GetDSRequiredCapabilitiesFromID(dsID, tx) if err != nil { return nil, fmt.Errorf("validating server capabilities: %v", err), http.StatusInternalServerError } serverCaps, err := dbhelpers.GetServerCapabilitiesOfServers(nonOriginServerNames, tx) if err != nil { return nil, err, http.StatusInternalServerError } for hostname, caps := range serverCaps { for _, dsc := range dsCaps { if !util.ContainsStr(caps, dsc) { return fmt.Errorf("the cache %s cannot be assigned to this delivery service without having the required delivery service capabilities: %v", hostname, dsCaps), nil, http.StatusBadRequest } } } return nil, nil, 0 } func insertIdsQuery() string { query := `INSERT INTO deliveryservice_server (deliveryservice, server) VALUES (:id, :server )` return query } // GetReadAssigned is the handler for GET requests to /deliveryservices/{id}/servers. func GetReadAssigned(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() alerts := tc.Alerts{} servers, err := read(inf) if err != nil { alerts.AddNewAlert(tc.ErrorLevel, err.Error()) api.WriteAlerts(w, r, http.StatusInternalServerError, alerts) return } if inf.Version.Major == 3 { v3ServerList := []tc.DSServer{} for _, srv := range servers { routerHostName := "" routerPort := "" interfaces := *srv.ServerInterfaces // All interfaces should have the same router name/port when they were upgraded from v1/2/3 to v4, so we can just choose any of them if len(interfaces) != 0 { routerHostName = interfaces[0].RouterHostName routerPort = interfaces[0].RouterPortName } v3Interfaces, err := tc.V4InterfaceInfoToV3Interfaces(interfaces) if err != nil { api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, errors.New("converting to server detail v11: "+err.Error())) return } v3server := tc.DSServer{} pid, pdesc := dbhelpers.GetProfileIDDesc(inf.Tx.Tx, srv.ProfileNames[0]) v3server.DSServerBase = srv.DSServerBaseV4.ToDSServerBase(&routerHostName, &routerPort, &pdesc, &pid) v3server.ServerInterfaces = &v3Interfaces v3ServerList = append(v3ServerList, v3server) } api.WriteAlertsObj(w, r, http.StatusOK, alerts, v3ServerList) return } // Based on version we load Delivery Service Server - for version 5 and above we use DSServerV5 if inf.Version.GreaterThanOrEqualTo(&api.Version{Major: 5, Minor: 0}) { newServerList := make([]tc.DSServerV5, len(servers)) for i, server := range servers { newServerList[i] = server.Upgrade() } api.WriteAlertsObj(w, r, http.StatusOK, alerts, newServerList) return } api.WriteAlertsObj(w, r, http.StatusOK, alerts, servers) } func read(inf *api.Info) ([]tc.DSServerV4, error) { queryDataString := `, cg.name as cachegroup, s.cachegroup as cachegroup_id, s.cdn_id, cdn.name as cdn_name, s.domain_name, s.guid, s.host_name, s.https_port, s.ilo_ip_address, s.ilo_ip_gateway, s.ilo_ip_netmask, s.ilo_password, s.ilo_username, s.last_updated, s.mgmt_ip_address, s.mgmt_ip_gateway, s.mgmt_ip_netmask, s.offline_reason, pl.name as phys_location, s.phys_location as phys_location_id, (SELECT ARRAY_AGG(profile_name) FROM server_profile WHERE server_profile.server=s.id) as profile_name, s.rack, st.name as status, s.status as status_id, s.tcp_port, t.name as server_type, s.type as server_type_id, s.config_update_time > s.config_apply_time AS upd_pending ` queryFormatString := ` SELECT s.id %v FROM server s JOIN cachegroup cg ON s.cachegroup = cg.id JOIN cdn cdn ON s.cdn_id = cdn.id JOIN phys_location pl ON s.phys_location = pl.id JOIN profile p ON s.profile = p.id JOIN status st ON s.status = st.id JOIN type t ON s.type = t.id WHERE s.id in (select server from deliveryservice_server where deliveryservice = $1)` dsID := inf.IntParams["id"] idRows, err := inf.Tx.Queryx(fmt.Sprintf(queryFormatString, ""), dsID) if err != nil { return nil, errors.New("error querying dss ids: " + err.Error()) } var serverIDs []int for idRows.Next() { var serverID int err := idRows.Scan(&serverID) if err != nil { return nil, errors.New("error scanning dss id rows: " + err.Error()) } serverIDs = append(serverIDs, serverID) } serversMap, err := dbhelpers.GetServersInterfaces(serverIDs, inf.Tx.Tx) if err != nil { return nil, errors.New("unable to get server interfaces: " + err.Error()) } rows, err := inf.Tx.Queryx(fmt.Sprintf(queryFormatString, queryDataString), dsID) if err != nil { return nil, errors.New("error querying dss rows: " + err.Error()) } defer rows.Close() servers := []tc.DSServerV4{} for rows.Next() { s := tc.DSServerV4{} err := rows.Scan( &s.ID, &s.Cachegroup, &s.CachegroupID, &s.CDNID, &s.CDNName, &s.DomainName, &s.GUID, &s.HostName, &s.HTTPSPort, &s.ILOIPAddress, &s.ILOIPGateway, &s.ILOIPNetmask, &s.ILOPassword, &s.ILOUsername, &s.LastUpdated, &s.MgmtIPAddress, &s.MgmtIPGateway, &s.MgmtIPNetmask, &s.OfflineReason, &s.PhysLocation, &s.PhysLocationID, pq.Array(&s.ProfileNames), &s.Rack, &s.Status, &s.StatusID, &s.TCPPort, &s.Type, &s.TypeID, &s.UpdPending, ) if err != nil { return nil, errors.New("error scanning dss rows: " + err.Error()) } s.ServerInterfaces = &[]tc.ServerInterfaceInfoV40{} if interfacesMap, ok := serversMap[*s.ID]; ok { for _, interfaceInfo := range interfacesMap { *s.ServerInterfaces = append(*s.ServerInterfaces, interfaceInfo) } } canViewILOPswd := false if (inf.Version.GreaterThanOrEqualTo(&api.Version{Major: 4}) && inf.Config.RoleBasedPermissions) || inf.Version.GreaterThanOrEqualTo(&api.Version{Major: 5}) { canViewILOPswd = inf.User.Can(tc.PermSecureServerRead) } else { canViewILOPswd = inf.User.PrivLevel == auth.PrivLevelAdmin } if !canViewILOPswd { s.ILOPassword = util.StrPtr("") } servers = append(servers, s) } return servers, nil } type TODSSDeliveryService struct { api.APIInfoImpl `json:"-"` tc.DeliveryServiceNullable } // Read shows all of the delivery services associated with the specified server. func (dss *TODSSDeliveryService) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) { version := dss.APIInfo().Version if version == nil { return nil, nil, errors.New("TODSSDeliveryService.Read called with nil API version"), http.StatusInternalServerError, nil } if version.Major == 1 && version.Minor < 1 { return nil, nil, fmt.Errorf("TODSSDeliveryService.Read called with invalid API version: %d.%d", version.Major, version.Minor), http.StatusInternalServerError, nil } var maxTime time.Time var runSecond bool params := dss.APIInfo().Params tx := dss.APIInfo().Tx.Tx user := dss.APIInfo().User if err := api.IsInt(params["id"]); err != nil { return nil, err, nil, http.StatusBadRequest, nil } if _, ok := params["orderby"]; !ok { params["orderby"] = "xml_id" } // Query Parameters to Database Query column mappings // see the fields mapped in the SQL query queryParamsToSQLCols := map[string]dbhelpers.WhereColumnInfo{ "xml_id": dbhelpers.WhereColumnInfo{Column: "ds.xml_id"}, "xmlId": dbhelpers.WhereColumnInfo{Column: "ds.xml_id"}, } where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(params, queryParamsToSQLCols) if len(errs) > 0 { return nil, nil, errors.New("reading server dses: " + util.JoinErrsStr(errs)), http.StatusInternalServerError, nil } if where != "" { where = where + " AND " } else { where = "WHERE " } serverID, _ := strconv.Atoi(params["id"]) serverInfo, exists, err := dbhelpers.GetServerInfo(serverID, tx) if err != nil { return nil, nil, err, http.StatusInternalServerError, nil } if !exists { return nil, fmt.Errorf("server with ID %d doesn't exist", serverID), nil, http.StatusNotFound, nil } if serverInfo.Type == tc.OriginTypeName { where += `ds.id in (SELECT deliveryservice FROM deliveryservice_server WHERE server = :server)` } else { where += ` (ds.id in ( SELECT deliveryservice FROM deliveryservice_server WHERE server = :server ) OR ds.id in ( SELECT d.id FROM deliveryservice d JOIN cdn c ON d.cdn_id = c.id WHERE d.topology in ( SELECT topology FROM topology_cachegroup WHERE cachegroup = ( SELECT name FROM cachegroup WHERE id = ( SELECT cachegroup FROM server WHERE id = :server ))) AND d.cdn_id = (SELECT cdn_id FROM server WHERE id = :server))) AND (( (SELECT COALESCE(ARRAY_AGG(ssc.server_capability), '{}') FROM server_server_capability ssc WHERE ssc."server" = :server) @> ( SELECT COALESCE(ds.required_capabilities, '{}') ))) ` } tenantIDs, err := tenant.GetUserTenantIDListTx(tx, user.TenantID) if err != nil { log.Errorln("received error querying for user's tenants: " + err.Error()) return nil, nil, err, http.StatusInternalServerError, nil } where, queryValues = dbhelpers.AddTenancyCheck(where, queryValues, "ds.tenant_id", tenantIDs) query := deliveryservice.SelectDeliveryServicesQuery + where + orderBy + pagination queryValues["server"] = dss.APIInfo().Params["id"] if useIMS { runSecond, maxTime = ims.TryIfModifiedSinceQuery(dss.APIInfo().Tx, h, queryValues, selectMaxLastUpdatedQuery(where)) if !runSecond { log.Debugln("IMS HIT") return nil, nil, nil, http.StatusNotModified, &maxTime } log.Debugln("IMS MISS") } else { log.Debugln("Non IMS request") } log.Debugln("generated deliveryServices query: " + query) log.Debugf("executing with values: %++v\n", queryValues) dses, userErr, sysErr, _ := deliveryservice.GetDeliveryServices(query, queryValues, dss.APIInfo().Tx) if sysErr != nil { sysErr = fmt.Errorf("reading server dses: %v ", sysErr) } if userErr != nil || sysErr != nil { return nil, userErr, sysErr, http.StatusInternalServerError, nil } returnable := make([]interface{}, 0, len(dses)) for _, d := range dses { ds := d.DS if version.Major > 4 { returnable = append(returnable, ds) } else if version.Major > 3 && version.Minor >= 0 { returnable = append(returnable, ds.Downgrade()) } else { legacyDS := ds.Downgrade() if version.Minor > 0 { dsV31 := legacyDS.DowngradeToV31() returnable = append(returnable, dsV31) } else { dsV30 := legacyDS.DowngradeToV31().DeliveryServiceV30 returnable = append(returnable, dsV30) } } } return returnable, nil, nil, http.StatusOK, &maxTime } func selectMaxLastUpdatedQuery(where string) string { return `SELECT max(t) from ( SELECT max(dss.last_updated) as t from deliveryservice_server dss JOIN deliveryservice ds ON ds.id = dss.deliveryservice ` + where + ` UNION ALL select max(last_updated) as t from last_deleted l where l.table_name='deliveryservice_server') as res` } type DSInfo struct { Active bool ID int Name string Type tc.DSType EdgeHeaderRewrite *string MidHeaderRewrite *string RegexRemap *string SigningAlgorithm *string CacheURL *string MaxOriginConnections *int Topology *string CDNID *int UseMultiSiteOrigin bool } // language=sql const getDSInfoBaseQuery = ` SELECT ds.active, ds.id, ds.xml_id, tp.name as type, ds.edge_header_rewrite, ds.mid_header_rewrite, ds.regex_remap, ds.signing_algorithm, ds.cacheurl, ds.max_origin_connections, ds.topology, ds.cdn_id, ds.multi_site_origin FROM deliveryservice ds JOIN type tp ON ds.type = tp.id ` func scanDSInfoRow(row *sql.Row) (DSInfo, bool, error) { di := DSInfo{} var useMSO *bool var active tc.DeliveryServiceActiveState if err := row.Scan( &active, &di.ID, &di.Name, &di.Type, &di.EdgeHeaderRewrite, &di.MidHeaderRewrite, &di.RegexRemap, &di.SigningAlgorithm, &di.CacheURL, &di.MaxOriginConnections, &di.Topology, &di.CDNID, &useMSO, ); err != nil { if err == sql.ErrNoRows { return DSInfo{}, false, nil } return DSInfo{}, false, fmt.Errorf("querying delivery service server ds info: %v", err) } di.Active = active == tc.DSActiveStateActive di.Type = tc.DSTypeFromString(string(di.Type)) if useMSO != nil { di.UseMultiSiteOrigin = *useMSO } return di, true, nil } // GetDSInfo loads the DeliveryService fields needed by Delivery Service Servers from the database, from the ID. Returns the data, whether the delivery service was found, and any error. func GetDSInfo(tx *sql.Tx, id int) (DSInfo, bool, error) { qry := getDSInfoBaseQuery + ` WHERE ds.id = $1 ` row := tx.QueryRow(qry, id) return scanDSInfoRow(row) } // GetDSInfoByName loads the DeliveryService fields needed by Delivery Service Servers from the database, from the name (xml_id). Returns the data, whether the delivery service was found, and any error. func GetDSInfoByName(tx *sql.Tx, dsName string) (DSInfo, bool, error) { qry := getDSInfoBaseQuery + ` WHERE ds.xml_id = $1 ` row := tx.QueryRow(qry, dsName) return scanDSInfoRow(row) }