blob: d66060a49f87b8f5dc3fa394f8882333b50b75a2 [file] [log] [blame]
package server
/*
* 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/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/dbhelpers"
)
// InvalidStatusForDeliveryServicesAlertText returns a string describing that
// setting a server to 'status' invalidates the Active delivery services
// identified in 'dsIDs'.
//
// If 'dsIDs' is empty/nil, returns an empty string.
func InvalidStatusForDeliveryServicesAlertText(prefix, serverType string, dsIDs []int) string {
if len(dsIDs) < 1 {
return ""
}
alertText := prefix
if len(dsIDs) == 1 {
alertText += fmt.Sprintf(" #%d", dsIDs[0])
} else if len(dsIDs) == 2 {
alertText += fmt.Sprintf("s #%d and #%d", dsIDs[0], dsIDs[1])
} else {
dsNums := make([]string, 0, len(dsIDs)-1)
for _, dsID := range dsIDs[:len(dsIDs)-1] {
dsNums = append(dsNums, "#"+strconv.Itoa(dsID))
}
alertText += fmt.Sprintf("s %s, and #%d", strings.Join(dsNums, ", "), dsIDs[len(dsIDs)-1])
}
typeMsg := tc.CacheTypeEdge.String()
if strings.HasPrefix(serverType, tc.OriginTypeName) {
typeMsg = tc.OriginTypeName
}
alertText += fmt.Sprintf(" with no '%s' or '%s' %s servers", tc.CacheStatusOnline, tc.CacheStatusReported, typeMsg)
return alertText
}
// UpdateStatusHandler is the handler for PUT requests to the /servers/{{ID}}/status API endpoint.
func UpdateStatusHandler(w http.ResponseWriter, r *http.Request) {
inf, userErr, sysErr, errCode := api.NewInfo(r, []string{"id"}, []string{"id"})
tx := inf.Tx.Tx
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, tx, errCode, userErr, sysErr)
return
}
defer inf.Close()
reqObj := tc.ServerPutStatus{}
if err := json.NewDecoder(r.Body).Decode(&reqObj); err != nil {
api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("malformed JSON: "+err.Error()), nil)
return
}
id := inf.IntParams["id"]
serverInfo, exists, err := dbhelpers.GetServerInfo(id, tx)
if err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
return
}
if !exists {
api.HandleErr(w, r, tx, http.StatusNotFound, fmt.Errorf("server ID %d not found", id), nil)
return
}
cdnName, err := dbhelpers.GetCDNNameFromServerID(inf.Tx.Tx, int64(id))
if err != nil {
api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
return
}
userErr, sysErr, statusCode := dbhelpers.CheckIfCurrentUserHasCdnLock(inf.Tx.Tx, string(cdnName), inf.User.UserName)
if statusCode == http.StatusForbidden {
userErr = fmt.Errorf("this action will result in server updates being queued and %v", userErr)
}
if userErr != nil || sysErr != nil {
api.HandleErr(w, r, inf.Tx.Tx, statusCode, userErr, sysErr)
return
}
status := tc.StatusNullable{}
statusExists := false
if reqObj.Status.Name != nil {
status, statusExists, err = dbhelpers.GetStatusByName(*reqObj.Status.Name, tx)
} else if reqObj.Status.ID != nil {
status, statusExists, err = dbhelpers.GetStatusByID(*reqObj.Status.ID, tx)
} else {
api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("status is required"), nil)
return
}
if err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
return
}
if !statusExists {
api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("invalid status (does not exist)"), nil)
return
}
if *status.Name == tc.CacheStatusAdminDown.String() || *status.Name == tc.CacheStatusOffline.String() {
if reqObj.OfflineReason == nil {
api.HandleErr(w, r, tx, http.StatusBadRequest, errors.New("offlineReason is required for "+tc.CacheStatusAdminDown.String()+" or "+tc.CacheStatusOffline.String()+" status"), nil)
return
}
*reqObj.OfflineReason = inf.User.UserName + ": " + *reqObj.OfflineReason
} else {
reqObj.OfflineReason = nil
}
existingStatus, existingStatusUpdatedTime := checkExistingStatusInfo(id, tx)
if *status.Name != string(tc.CacheStatusOnline) && *status.Name != string(tc.CacheStatusReported) && *status.ID != existingStatus {
dsIDs, err := getActiveDeliveryServicesThatOnlyHaveThisServerAssigned(id, serverInfo.Type, tx)
if err != nil {
sysErr = fmt.Errorf("getting Delivery Services to which server #%d is assigned that have no other servers: %v", id, err)
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, sysErr)
return
}
if len(dsIDs) > 0 {
prefix := fmt.Sprintf("setting server status to '%s' would leave Active Delivery Service", *status.Name)
alertText := InvalidStatusForDeliveryServicesAlertText(prefix, serverInfo.Type, dsIDs)
api.WriteAlerts(w, r, http.StatusConflict, tc.CreateAlerts(tc.ErrorLevel, alertText))
return
}
}
if err := updateServerStatusAndOfflineReason(existingStatus, *status.ID, id, existingStatusUpdatedTime, reqObj.OfflineReason, tx); err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
return
}
offlineReason := ""
if reqObj.OfflineReason != nil {
offlineReason = *reqObj.OfflineReason
}
msg := "Updated status [ " + *status.Name + " ] for " + serverInfo.HostName + "." + serverInfo.DomainName + " [ " + offlineReason + " ]"
// queue updates on child servers if server is ^EDGE or ^MID
if strings.HasPrefix(serverInfo.Type, tc.CacheTypeEdge.String()) || strings.HasPrefix(serverInfo.Type, tc.CacheTypeMid.String()) {
if err := queueUpdatesOnChildCaches(tx, serverInfo.CDNID, serverInfo.CachegroupID); err != nil {
api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err)
return
}
msg += " and queued updates on all child caches"
}
api.CreateChangeLogRawTx(api.ApiChange, msg, inf.User, tx)
api.WriteRespAlert(w, r, tc.SuccessLevel, msg)
}
// queueUpdatesOnChildCaches queues updates on child caches of the given cdnID and parentCachegroupID and returns an error (if one occurs).
func queueUpdatesOnChildCaches(tx *sql.Tx, cdnID, parentCachegroupID int) error {
q := `
/* topology_descendants finds the descendant topology nodes of the topology node
* for the cachegroup containing server $2.
*/
WITH RECURSIVE topology_descendants AS (
/* This is the base case of the recursive CTE, the topology node for the
* cachegroup containing cachegroup $2.
*/
SELECT tcp.parent child, NULL cachegroup
FROM cachegroup c
JOIN topology_cachegroup tc ON c."name" = tc.cachegroup
JOIN topology_cachegroup_parents tcp ON tc.id = tcp.parent
WHERE c.id = $2
UNION ALL
/* Find all direct topology child nodes tc of a given topology descendant td. */
SELECT tcp.child, tc.cachegroup
FROM topology_descendants td, topology_cachegroup_parents tcp
JOIN topology_cachegroup tc ON tcp.child = tc.id
WHERE td.child = tcp.parent
/* server_topology_descendants is the set of every server whose cachegroup is a
* descendant topology node found by topology_descendants.
*/
), server_topology_descendants AS (
SELECT c.id
FROM cachegroup c
JOIN topology_descendants td ON c."name" = td.cachegroup
)
UPDATE public.server
SET config_update_time = now()
WHERE server.cdn_id = $1
AND (server.cachegroup IN (
SELECT id
FROM cachegroup
WHERE parent_cachegroup_id = $2
OR secondary_parent_cachegroup_id = $2
)
OR server.cachegroup IN (SELECT stc.id FROM server_topology_descendants stc));
`
if _, err := tx.Exec(q, cdnID, parentCachegroupID); err != nil {
return errors.New("queueing updates on child caches: " + err.Error())
}
return nil
}
// checkExistingStatusInfo returns the existing status and status_last_updated values for the server in question
func checkExistingStatusInfo(serverID int, tx *sql.Tx) (int, time.Time) {
status := 0
var statusLastUpdated time.Time
q := `SELECT status,
status_last_updated
FROM server
WHERE id = $1`
response, err := tx.Query(q, serverID)
if err != nil {
log.Errorf("couldn't get status/ status_last_updated for server with id %v", serverID)
return status, statusLastUpdated
}
defer response.Close()
for response.Next() {
if err := response.Scan(&status, &statusLastUpdated); err != nil {
log.Errorf("couldn't get status/ status_last_updated of server with id %v, err: %v", serverID, err.Error())
}
}
return status, statusLastUpdated
}
// updateServerStatusAndOfflineReason updates a server's status and offline_reason and returns an error (if one occurs).
func updateServerStatusAndOfflineReason(existingStatus, statusID, serverID int, existingStatusUpdatedTime time.Time, offlineReason *string, tx *sql.Tx) error {
newStatusUpdatedTime := time.Now()
// Set the status_last_updated time to the current time ONLY IF the new status is different from the old one
if existingStatus == statusID {
newStatusUpdatedTime = existingStatusUpdatedTime
}
q := `
UPDATE server
SET status = $1,
offline_reason = $2,
status_last_updated = $3
WHERE id = $4
`
if _, err := tx.Exec(q, statusID, offlineReason, &newStatusUpdatedTime, serverID); err != nil {
return errors.New("updating server status and offline_reason: " + err.Error())
}
return nil
}