diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a1acf2b8c..78c1b611a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/). ## [unreleased] +### Added +- [#6033](https://github.com/apache/trafficcontrol/issues/6033) Added ability to assign multiple server capabilities to a server. + ### Changed - Traffic Portal now obscures sensitive text in Delivery Service "Raw Remap" fields, private SSL keys, "Header Rewrite" rules, and ILO interface passwords by default. diff --git a/docs/source/api/v4/multiple_server_capabilities.rst b/docs/source/api/v4/multiple_server_capabilities.rst new file mode 100644 index 0000000000..37f56b1549 --- /dev/null +++ b/docs/source/api/v4/multiple_server_capabilities.rst @@ -0,0 +1,83 @@ +.. +.. +.. Licensed 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. +.. + +.. _to-api-multiple_server_capabilities: + +******************************** +``multiple_server_capabilities`` +******************************** + +.. versionadded:: 4.1 + +``PUT`` +======== +Associates a list of :term:`Server Capability` to a server. The API call replaces all the server capabilities assigned to a server with the ones specified in the serverCapabilities field. + +:Auth. Required: Yes +:Roles Required: "admin" or "operations" +:Permissions Required: SERVER:UPDATE, SERVER:READ, SERVER-CAPABILITY:READ +:Response Type: Object + +Request Structure +----------------- +:serverId: The integral, unique identifier of a server to be associated with a :term:`Server Capability` +:serverCapabilities: List of :term:`Server Capability`'s name to associate + +.. code-block:: http + :caption: Request Example + + PUT /api/4.1/multiple_server_capabilities/ HTTP/1.1 + Host: trafficops.infra.ciab.test + User-Agent: curl/7.47.0 + Accept: */* + Cookie: mojolicious=... + Content-Length: 84 + Content-Type: application/json + + { + "serverId": 1, + "serverCapabilities": ["test", "disk"] + } + +Response Structure +------------------ +:serverId: The integral, unique identifier of the newly associated server +:serverCapabilities: List of :term:`Server Capability`'s name + +.. code-block:: http + :caption: Response Example + + HTTP/1.1 200 OK + Access-Control-Allow-Credentials: true + Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie + Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE + Access-Control-Allow-Origin: * + Content-Type: application/json + Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 8 Aug 2022 22:40:54 GMT; Max-Age=3600; HttpOnly + Whole-Content-Sha512: eQrl48zWids0kDpfCYmmtYMpegjnFxfOVvlBYxxLSfp7P7p6oWX4uiC+/Cfh2X9i3G+MQ36eH95gukJqOBOGbQ== + X-Server-Name: traffic_ops_golang/ + Date: Mon, 08 Aug 2022 16:15:11 GMT + Content-Length: 157 + + { + "alerts": [{ + "text": "Multiple Server Capabilities assigned to a server", + "level": "success" + }], + "response": { + "serverId": 1, + "serverCapabilities": ["test", "disk"] + } + } diff --git a/lib/go-tc/server_server_capability.go b/lib/go-tc/server_server_capability.go index 6c406bfa2f..535a95da7f 100644 --- a/lib/go-tc/server_server_capability.go +++ b/lib/go-tc/server_server_capability.go @@ -27,6 +27,12 @@ type ServerServerCapability struct { ServerCapability *string `json:"serverCapability" db:"server_capability"` } +// MultipleServerCapabilities represents an association between a server and list of server capabilities. +type MultipleServerCapabilities struct { + ServerID int `json:"serverId" db:"server"` + ServerCapabilities []string `json:"serverCapabilities" db:"server_capability"` +} + // ServerServerCapabilitiesResponse is the type of a response from Traffic // Ops to a request made to its /server_server_capabilities. type ServerServerCapabilitiesResponse struct { diff --git a/traffic_ops/testing/api/v4/server_server_capabilities_test.go b/traffic_ops/testing/api/v4/server_server_capabilities_test.go index 71ca9dc9be..b7519355c4 100644 --- a/traffic_ops/testing/api/v4/server_server_capabilities_test.go +++ b/traffic_ops/testing/api/v4/server_server_capabilities_test.go @@ -37,6 +37,7 @@ func TestServerServerCapabilities(t *testing.T) { currentTime := time.Now().UTC().Add(-15 * time.Second) tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123) + var multipleSCs []string methodTests := utils.V4TestCase{ "GET": { @@ -147,6 +148,16 @@ func TestServerServerCapabilities(t *testing.T) { Expectations: utils.CkRequest(utils.HasError(), utils.HasStatus(http.StatusBadRequest)), }, }, + "PUT": { + "OK When Assigned Multiple Server Capabilities": { + ClientSession: TOSession, + RequestBody: map[string]interface{}{ + "serverId": GetServerID(t, "dtrc-mid-04")(), + "serverCapabilities": append(multipleSCs, "disk", "blah"), + }, + Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK)), + }, + }, "DELETE": { "OK when NOT the LAST SERVER of CACHE GROUP of TOPOLOGY DS which has REQUIRED CAPABILITIES": { ClientSession: TOSession, @@ -180,13 +191,18 @@ func TestServerServerCapabilities(t *testing.T) { t.Run(method, func(t *testing.T) { for name, testCase := range testCases { ssc := tc.ServerServerCapability{} + msc := tc.MultipleServerCapabilities{} var serverId int var serverCapability string if testCase.RequestBody != nil { dat, err := json.Marshal(testCase.RequestBody) assert.NoError(t, err, "Error occurred when marshalling request body: %v", err) - err = json.Unmarshal(dat, &ssc) + if method == "PUT" { + err = json.Unmarshal(dat, &msc) + } else { + err = json.Unmarshal(dat, &ssc) + } assert.NoError(t, err, "Error occurred when unmarshalling request body: %v", err) } @@ -205,6 +221,13 @@ func TestServerServerCapabilities(t *testing.T) { check(t, reqInf, nil, alerts, err) } }) + case "PUT": + t.Run(name, func(t *testing.T) { + alerts, reqInf, err := testCase.ClientSession.AssignMultipleServerCapability(msc, testCase.RequestOpts, serverId) + for _, check := range testCase.Expectations { + check(t, reqInf, nil, alerts, err) + } + }) case "DELETE": t.Run(name, func(t *testing.T) { if val, ok := testCase.RequestOpts.QueryParameters["serverId"]; ok { diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go index 2c02779b60..7089c120bf 100644 --- a/traffic_ops/traffic_ops_golang/routing/routes.go +++ b/traffic_ops/traffic_ops_golang/routing/routes.go @@ -131,6 +131,9 @@ func Routes(d ServerData) ([]Route, http.Handler, error) { * 4.x API */ + // Assign Multiple Server Capabilities + {Version: api.Version{Major: 4, Minor: 1}, Method: http.MethodPut, Path: `multiple_server_capabilities/?$`, Handler: server.AssignMultipleServerCapabilities, RequiredPrivLevel: auth.PrivLevelOperations, RequiredPermissions: []string{"SERVER:UPDATE", "SERVER:READ", "SERVER-CAPABILITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 40792419258}, + // CDNI integration {Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodGet, Path: `OC/FCI/advertisement/?$`, Handler: cdni.GetCapabilities, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDNI-CAPACITY:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729077}, {Version: api.Version{Major: 4, Minor: 0}, Method: http.MethodPut, Path: `OC/CI/configuration/?$`, Handler: cdni.PutConfiguration, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"CDNI-CAPACITY:UPDATE"}, Authenticated: Authenticated, Middlewares: nil, ID: 541357729078}, diff --git a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go index 64291f21cd..21ae31db2a 100644 --- a/traffic_ops/traffic_ops_golang/server/servers_server_capability.go +++ b/traffic_ops/traffic_ops_golang/server/servers_server_capability.go @@ -20,6 +20,7 @@ package server */ import ( + "encoding/json" "errors" "fmt" "net/http" @@ -441,3 +442,88 @@ func getDSTenantIDsByIDs(tx *sqlx.Tx, dsIDs []int64) ([]DSTenant, error) { return dsTenantIDs, nil } + +// AssignMultipleServerCapabilities helps assign multiple server capabilities to a given server. +func AssignMultipleServerCapabilities(w http.ResponseWriter, r *http.Request) { + inf, userErr, sysErr, errCode := api.NewInfo(r, nil, nil) + tx := inf.Tx.Tx + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, inf.Tx.Tx, errCode, userErr, sysErr) + return + } + defer inf.Close() + + var msc tc.MultipleServerCapabilities + if err := json.NewDecoder(r.Body).Decode(&msc); err != nil { + api.HandleErr(w, r, tx, http.StatusBadRequest, err, nil) + return + } + + // Check existence prior to checking type + _, exists, err := dbhelpers.GetServerNameFromID(tx, int64(msc.ServerID)) + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) + } + if !exists { + userErr := fmt.Errorf("server %d does not exist", msc.ServerID) + api.HandleErr(w, r, tx, http.StatusNotFound, userErr, nil) + return + } + + // Ensure type is correct + correctType := true + if err := tx.QueryRow(scCheckServerTypeQuery(), msc.ServerID).Scan(&correctType); err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, fmt.Errorf("checking server type: %w", err)) + return + } + if !correctType { + userErr := fmt.Errorf("server %d has an incorrect server type. Server capabilities can only be assigned to EDGE or MID servers", msc.ServerID) + api.HandleErr(w, r, tx, http.StatusBadRequest, userErr, nil) + return + } + + cdnName, err := dbhelpers.GetCDNNameFromServerID(tx, int64(msc.ServerID)) + if err != nil { + api.HandleErr(w, r, tx, http.StatusInternalServerError, nil, err) + return + } + + userErr, sysErr, errCode = dbhelpers.CheckIfCurrentUserCanModifyCDN(tx, string(cdnName), inf.User.UserName) + if userErr != nil || sysErr != nil { + api.HandleErr(w, r, tx, errCode, userErr, sysErr) + return + } + + //Delete existing rows from server_server_capability for a given server + _, err = tx.Exec("DELETE FROM server_server_capability ssc WHERE ssc.server=$1", msc.ServerID) + if err != nil { + useErr, sysErr, statusCode := api.ParseDBError(err) + api.HandleErr(w, r, tx, statusCode, useErr, sysErr) + return + } + + multipleServerCapabilities := make([]string, 0, len(msc.ServerCapabilities)) + + mscQuery := `WITH inserted AS ( + INSERT INTO server_server_capability + SELECT "server_capability", $2 + FROM UNNEST($1::text[]) AS tmp("server_capability") + RETURNING server_capability + ) + SELECT ARRAY_AGG(server_capability) + FROM ( + SELECT server_capability + FROM inserted + ) AS returned(server_capability)` + + err = tx.QueryRow(mscQuery, pq.Array(msc.ServerCapabilities), msc.ServerID).Scan(pq.Array(&multipleServerCapabilities)) + if err != nil { + useErr, sysErr, statusCode := api.ParseDBError(err) + api.HandleErr(w, r, tx, statusCode, useErr, sysErr) + return + } + msc.ServerCapabilities = multipleServerCapabilities + alerts := tc.CreateAlerts(tc.SuccessLevel, "Multiple Server Capabilities assigned to a server") + api.WriteAlertsObj(w, r, http.StatusOK, alerts, msc) + return +} diff --git a/traffic_ops/v4-client/endpoints.go b/traffic_ops/v4-client/endpoints.go index fd9478cd1c..470afe26da 100644 --- a/traffic_ops/v4-client/endpoints.go +++ b/traffic_ops/v4-client/endpoints.go @@ -22,6 +22,7 @@ package client // Versions are ordered latest-first. func apiVersions() []string { return []string{ + "4.1", "4.0", } } diff --git a/traffic_ops/v4-client/server_server_capabilities.go b/traffic_ops/v4-client/server_server_capabilities.go index 9cd03733a7..dd17238517 100644 --- a/traffic_ops/v4-client/server_server_capabilities.go +++ b/traffic_ops/v4-client/server_server_capabilities.go @@ -27,6 +27,9 @@ import ( // /server_server_capabilities API endpoint. const apiServerServerCapabilities = "/server_server_capabilities" +// apiMultipleServerCapabilities is the API version-relative path to the /multiple_server_capabilities API endpoint. +const apiMultipleServerCapabilities = "/multiple_server_capabilities" + // CreateServerServerCapability assigns a Server Capability to a Server. func (to *Session) CreateServerServerCapability(ssc tc.ServerServerCapability, opts RequestOptions) (tc.Alerts, toclientlib.ReqInf, error) { var alerts tc.Alerts @@ -53,3 +56,10 @@ func (to *Session) GetServerServerCapabilities(opts RequestOptions) (tc.ServerSe reqInf, err := to.get(apiServerServerCapabilities, opts, &resp) return resp, reqInf, err } + +// AssignMultipleServerCapability assigns multiple server capabilities to a server. +func (to *Session) AssignMultipleServerCapability(msc tc.MultipleServerCapabilities, opts RequestOptions, id int) (tc.Alerts, toclientlib.ReqInf, error) { + var alerts tc.Alerts + reqInf, err := to.put(apiMultipleServerCapabilities, opts, msc, &alerts) + return alerts, reqInf, err +}