blob: 64291f21cdb00419f18c2e53edcaecd2df28c138 [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 (
"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/lib/go-tc/tovalidate"
"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"
validation "github.com/go-ozzo/ozzo-validation"
"github.com/jmoiron/sqlx"
"github.com/lib/pq"
)
const (
ServerCapabilityQueryParam = "serverCapability"
ServerQueryParam = "serverId"
ServerHostNameQueryParam = "serverHostName"
)
type (
TOServerServerCapability struct {
api.APIInfoImpl `json:"-"`
tc.ServerServerCapability
}
DSTenant struct {
TenantID int `db:"tenant_id"`
ID int `db:"id"`
}
)
func (ssc *TOServerServerCapability) SetLastUpdated(t tc.TimeNoMod) { ssc.LastUpdated = &t }
func (ssc *TOServerServerCapability) NewReadObj() interface{} {
return &tc.ServerServerCapability{}
}
func (ssc *TOServerServerCapability) SelectQuery() string { return scSelectQuery() }
func (ssc *TOServerServerCapability) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
return map[string]dbhelpers.WhereColumnInfo{
ServerCapabilityQueryParam: dbhelpers.WhereColumnInfo{Column: "sc.server_capability"},
ServerQueryParam: dbhelpers.WhereColumnInfo{Column: "s.id", Checker: api.IsInt},
ServerHostNameQueryParam: dbhelpers.WhereColumnInfo{Column: "s.host_name"},
}
}
func (ssc *TOServerServerCapability) DeleteQuery() string { return scDeleteQuery() }
func (ssc TOServerServerCapability) GetKeyFieldsInfo() []api.KeyFieldInfo {
return []api.KeyFieldInfo{
{Field: ServerQueryParam, Func: api.GetIntKey},
{Field: ServerCapabilityQueryParam, Func: api.GetStringKey},
}
}
// Need to satisfy Identifier interface but is a no-op as path does not have Update
func (ssc TOServerServerCapability) GetKeys() (map[string]interface{}, bool) {
if ssc.ServerID == nil {
return map[string]interface{}{ServerQueryParam: 0}, false
}
if ssc.ServerCapability == nil {
return map[string]interface{}{ServerCapabilityQueryParam: 0}, false
}
return map[string]interface{}{
ServerQueryParam: *ssc.ServerID,
ServerCapabilityQueryParam: *ssc.ServerCapability,
}, true
}
func (ssc *TOServerServerCapability) SetKeys(keys map[string]interface{}) {
sID, _ := keys[ServerQueryParam].(int)
ssc.ServerID = &sID
sc, _ := keys[ServerCapabilityQueryParam].(string)
ssc.ServerCapability = &sc
}
func (ssc *TOServerServerCapability) GetAuditName() string {
if ssc.ServerCapability != nil {
return *ssc.ServerCapability
}
return "unknown"
}
func (ssc *TOServerServerCapability) GetType() string {
return "server server_capability"
}
// Validate fulfills the api.Validator interface.
func (ssc TOServerServerCapability) Validate() (error, error) {
errs := validation.Errors{
ServerQueryParam: validation.Validate(ssc.ServerID, validation.Required),
ServerCapabilityQueryParam: validation.Validate(ssc.ServerCapability, validation.Required),
}
return util.JoinErrs(tovalidate.ToErrors(errs)), nil
}
func (ssc *TOServerServerCapability) Read(h http.Header, useIMS bool) ([]interface{}, error, error, int, *time.Time) {
api.DefaultSort(ssc.APIInfo(), "serverHostName")
return api.GenericRead(h, ssc, useIMS)
}
func (v *TOServerServerCapability) SelectMaxLastUpdatedQuery(where, orderBy, pagination, tableName string) string {
return `SELECT max(t) from (
SELECT max(sc.last_updated) as t from server_server_capability sc
JOIN server s ON sc.server = s.id ` + where + orderBy + pagination +
` UNION ALL
select max(last_updated) as t from last_deleted l where l.table_name='server_server_capability') as res`
}
func (ssc *TOServerServerCapability) Delete() (error, error, int) {
tenantIDs, err := tenant.GetUserTenantIDListTx(ssc.APIInfo().Tx.Tx, ssc.APIInfo().User.TenantID)
if err != nil {
return nil, fmt.Errorf("deleting servers_server_capability: %v", err), http.StatusInternalServerError
}
accessibleTenants := make(map[int]struct{}, len(tenantIDs))
for _, id := range tenantIDs {
accessibleTenants[id] = struct{}{}
}
userErr, sysErr, status := checkTopologyBasedDSRequiredCapabilities(ssc, accessibleTenants)
if userErr != nil || sysErr != nil {
return userErr, sysErr, status
}
userErr, sysErr, status = checkDSRequiredCapabilities(ssc, accessibleTenants)
if userErr != nil || sysErr != nil {
return userErr, sysErr, status
}
if ssc.ServerID != nil {
cdnName, err := dbhelpers.GetCDNNameFromServerID(ssc.APIInfo().Tx.Tx, int64(*ssc.ServerID))
if err != nil {
return nil, err, http.StatusInternalServerError
}
userErr, sysErr, errCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(ssc.APIInfo().Tx.Tx, string(cdnName), ssc.APIInfo().User.UserName)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
}
return api.GenericDelete(ssc)
}
func checkTopologyBasedDSRequiredCapabilities(ssc *TOServerServerCapability, accessibleTenants map[int]struct{}) (error, error, int) {
dsRows, err := ssc.APIInfo().Tx.Tx.Query(getTopologyBasedDSesReqCapQuery(), ssc.ServerID, ssc.ServerCapability)
if err != nil {
return nil, fmt.Errorf("querying topology-based DSes with the required capability %s: %v", *ssc.ServerCapability, err), http.StatusInternalServerError
}
defer log.Close(dsRows, "closing dsRows in checkTopologyBasedDSRequiredCapabilities")
xmlidToTopology := make(map[string]string)
xmlidToTenantID := make(map[string]int)
xmlidToReqCaps := make(map[string][]string)
for dsRows.Next() {
xmlID := ""
topology := ""
tenantID := 0
reqCaps := []string{}
if err := dsRows.Scan(&xmlID, &topology, &tenantID, pq.Array(&reqCaps)); err != nil {
return nil, fmt.Errorf("scanning dsRows in checkTopologyBasedDSRequiredCapabilities: %v", err), http.StatusInternalServerError
}
xmlidToTenantID[xmlID] = tenantID
xmlidToTopology[xmlID] = topology
xmlidToReqCaps[xmlID] = reqCaps
}
if len(xmlidToTopology) == 0 {
return nil, nil, http.StatusOK
}
serverRows, err := ssc.APIInfo().Tx.Tx.Query(getServerCapabilitiesOfCachegoupQuery(), ssc.ServerID, ssc.ServerCapability)
if err != nil {
return nil, fmt.Errorf("querying server capabilitites of server %d's cachegroup: %v", *ssc.ServerID, err), http.StatusInternalServerError
}
defer log.Close(serverRows, "closing serverRows in checkTopologyBasedDSRequiredCapabilities")
serverIDToCapabilities := make(map[int]map[string]struct{})
for serverRows.Next() {
serverID := 0
capabilities := []string{}
if err := serverRows.Scan(&serverID, pq.Array(&capabilities)); err != nil {
return nil, fmt.Errorf("scanning serverRows in checkTopologyBasedDSRequiredCapabilities: %v", err), http.StatusInternalServerError
}
serverIDToCapabilities[serverID] = make(map[string]struct{})
for _, c := range capabilities {
serverIDToCapabilities[serverID][c] = struct{}{}
}
}
unsatisfiedDSes := []string{}
for ds, dsReqCaps := range xmlidToReqCaps {
dsIsSatisfied := false
for _, serverCaps := range serverIDToCapabilities {
serverHasCapabilities := true
for _, dsReqCap := range dsReqCaps {
if _, ok := serverCaps[dsReqCap]; !ok {
serverHasCapabilities = false
break
}
}
if serverHasCapabilities {
dsIsSatisfied = true
break
}
}
if !dsIsSatisfied {
unsatisfiedDSes = append(unsatisfiedDSes, ds)
}
}
if len(unsatisfiedDSes) == 0 {
return nil, nil, http.StatusOK
}
dsStrings := make([]string, 0, len(unsatisfiedDSes))
for _, ds := range unsatisfiedDSes {
if _, ok := accessibleTenants[xmlidToTenantID[ds]]; ok {
dsStrings = append(dsStrings, "(xml_id = "+ds+", topology = "+xmlidToTopology[ds]+")")
}
}
return fmt.Errorf("this capability is required by delivery services, but there are no other servers in this server's cachegroup to satisfy them %s", strings.Join(dsStrings, ", ")), nil, http.StatusBadRequest
}
func checkDSRequiredCapabilities(ssc *TOServerServerCapability, accessibleTenants map[int]struct{}) (error, error, int) {
// Ensure that the user is not removing a server capability from the server
// that is required by the delivery services the server is assigned to (if applicable)
dsIDs := []int64{}
if err := ssc.APIInfo().Tx.Tx.QueryRow(checkDSReqCapQuery(), ssc.ServerID, ssc.ServerCapability).Scan(pq.Array(&dsIDs)); err != nil {
return nil, fmt.Errorf("checking removing server server capability would still suffice delivery service requried capabilites: %v", err), http.StatusInternalServerError
}
if len(dsIDs) > 0 {
return ssc.buildDSReqCapError(dsIDs, accessibleTenants)
}
return nil, nil, http.StatusOK
}
func (ssc *TOServerServerCapability) buildDSReqCapError(dsIDs []int64, accessibleTenants map[int]struct{}) (error, error, int) {
dsTenantIDs, err := getDSTenantIDsByIDs(ssc.APIInfo().Tx, dsIDs)
if err != nil {
return nil, err, http.StatusInternalServerError
}
authDSIDs := []string{}
for _, dsTenantID := range dsTenantIDs {
if _, ok := accessibleTenants[dsTenantID.TenantID]; ok {
if ok {
authDSIDs = append(authDSIDs, strconv.Itoa(dsTenantID.ID))
}
continue
}
}
dsStr := "delivery services"
if len(authDSIDs) > 0 {
dsStr = fmt.Sprintf("the delivery services %v", strings.Join(authDSIDs, ","))
}
return fmt.Errorf("cannot remove the capability %v from the server %v as the server is assigned to %v that require it", *ssc.ServerCapability, *ssc.ServerID, dsStr), nil, http.StatusBadRequest
}
func (ssc *TOServerServerCapability) Create() (error, error, int) {
tx := ssc.APIInfo().Tx
// Check existence prior to checking type
_, exists, err := dbhelpers.GetServerNameFromID(tx.Tx, int64(*ssc.ServerID))
if err != nil {
return nil, err, http.StatusInternalServerError
}
if !exists {
return fmt.Errorf("server %v does not exist", *ssc.ServerID), nil, http.StatusNotFound
}
// Ensure type is correct
correctType := true
if err := tx.Tx.QueryRow(scCheckServerTypeQuery(), ssc.ServerID).Scan(&correctType); err != nil {
return nil, fmt.Errorf("checking server type: %v", err), http.StatusInternalServerError
}
if !correctType {
return fmt.Errorf("server %v has an incorrect server type. Server capabilities can only be assigned to EDGE or MID servers", *ssc.ServerID), nil, http.StatusBadRequest
}
cdnName, err := dbhelpers.GetCDNNameFromServerID(tx.Tx, int64(*ssc.ServerID))
if err != nil {
return nil, err, http.StatusInternalServerError
}
userErr, sysErr, errCode := dbhelpers.CheckIfCurrentUserCanModifyCDN(tx.Tx, string(cdnName), ssc.APIInfo().User.UserName)
if userErr != nil || sysErr != nil {
return userErr, sysErr, errCode
}
resultRows, err := tx.NamedQuery(scInsertQuery(), ssc)
if err != nil {
return api.ParseDBError(err)
}
defer resultRows.Close()
rowsAffected := 0
for resultRows.Next() {
rowsAffected++
if err := resultRows.StructScan(&ssc); err != nil {
return nil, errors.New(ssc.GetType() + " create scanning: " + err.Error()), http.StatusInternalServerError
}
}
if rowsAffected == 0 {
return nil, errors.New(ssc.GetType() + " create: no " + ssc.GetType() + " was inserted, no rows was returned"), http.StatusInternalServerError
} else if rowsAffected > 1 {
return nil, errors.New("too many rows returned from " + ssc.GetType() + " insert"), http.StatusInternalServerError
}
return nil, nil, http.StatusOK
}
func scSelectQuery() string {
return `SELECT
sc.server_capability,
sc.server,
sc.last_updated,
s.host_name as host_name
FROM server_server_capability sc
JOIN server s ON sc.server = s.id`
}
func scDeleteQuery() string {
return `DELETE FROM server_server_capability
WHERE server = :server AND server_capability = :server_capability`
}
func scInsertQuery() string {
return `INSERT INTO server_server_capability (
server_capability,
server) VALUES (
:server_capability,
:server) RETURNING server, server_capability, last_updated`
}
func scCheckServerTypeQuery() string {
return `
SELECT EXISTS (
SELECT s.id
FROM server s
JOIN type t ON s.type = t.id
WHERE s.id = $1
AND t.use_in_table = 'server'
AND (t.name LIKE 'MID%' OR t.name LIKE 'EDGE%'))`
}
func checkDSReqCapQuery() string {
return `
SELECT ARRAY(
SELECT dsrc.deliveryservice_id
FROM deliveryservices_required_capability as dsrc
WHERE deliveryservice_id IN (
SELECT deliveryservice
FROM deliveryservice_server
WHERE server = $1)
AND dsrc.required_capability = $2)`
}
// get the topology-based DSes (with all their required capabilities) that a given
// server is assigned to, filtered by the given capability
func getTopologyBasedDSesReqCapQuery() string {
return `
SELECT
ds.xml_id,
ds.topology,
ds.tenant_id,
ARRAY_AGG(dsrc.required_capability) AS req_caps
FROM server s
JOIN cachegroup c ON s.cachegroup = c.id
JOIN topology_cachegroup tc ON c.name = tc.cachegroup
JOIN deliveryservice ds ON ds.topology = tc.topology
JOIN deliveryservices_required_capability dsrc ON dsrc.deliveryservice_id = ds.id
WHERE s.id = $1
GROUP BY ds.xml_id, ds.tenant_id, ds.topology
HAVING $2 = ANY(ARRAY_AGG(dsrc.required_capability))
`
}
// get all the capabilities of the servers in a given server's cachegroup
// that have a given capability
func getServerCapabilitiesOfCachegoupQuery() string {
return `
SELECT s.id, ARRAY_AGG(ssc.server_capability) AS capabilities
FROM server s
JOIN cachegroup c ON c.id = s.cachegroup AND c.id = (SELECT cachegroup FROM server WHERE server.id = $1)
JOIN server_server_capability ssc ON ssc.server = s.id
WHERE
s.cdn_id = (SELECT cdn_id FROM server WHERE server.id = $1)
AND s.id != $1
GROUP BY s.id
HAVING $2 = ANY(ARRAY_AGG(ssc.server_capability));
`
}
func getDSTenantIDsByIDs(tx *sqlx.Tx, dsIDs []int64) ([]DSTenant, error) {
dsTenantIDs := []DSTenant{}
query, args, err := sqlx.In("SELECT id, tenant_id FROM deliveryservice where id IN (?);", dsIDs)
if err != nil {
return nil, fmt.Errorf("building query for getting delivery services' tenants: %v", err)
}
query = tx.Rebind(query)
resultRows, err := tx.Queryx(query, args...)
if err != nil {
return nil, fmt.Errorf("querying tenant IDs for delivery service IDs: %v", err)
}
defer log.Close(resultRows, "closing resultRows in getDSTenantIDsByIDs")
for resultRows.Next() {
dsTenantID := DSTenant{}
if err := resultRows.StructScan(&dsTenantID); err != nil {
return nil, errors.New("scanning delivery service tenant ID: " + err.Error())
}
dsTenantIDs = append(dsTenantIDs, dsTenantID)
}
return dsTenantIDs, nil
}