Repository: apache/trafficcontrol
Pull request #7023 (2022-08-16 16:04:47)

Add asn field to server
<!--
Thank you for contributing! Please be sure to read our contribution guidelines: https://github.com/apache/trafficcontrol/blob/master/CONTRIBUTING.md
If this closes or relates to an existing issue, please reference it using one of the following:

Closes: #ISSUE
Related: #ISSUE

If this PR fixes a security vulnerability, DO NOT submit! Instead, contact
the Apache Traffic Control Security Team at security@trafficcontrol.apache.org and follow the
guidelines at https://apache.org/security regarding vulnerability disclosure.
-->
This PR is not related to any issue. It adds the `ASN` field to the server struct, so that users can use `asn` as a query param to `GET` servers.

<!-- **^ Add meaningful description above** --><hr/>

## Which Traffic Control components are affected by this PR?
<!-- Please delete all components from this list that are NOT affected by this PR.
Feel free to add the name of a tool or script that is affected but not on the list.
-->
- Documentation
- Traffic Ops
- Traffic Portal

## What is the best way to verify this PR?
<!-- Please include here ALL the steps necessary to test your PR.
If your PR has tests (and most should), provide the steps needed to run the tests.
If not, please provide step-by-step instructions to test the PR manually and explain why your PR does not need tests. -->

Run TO and TP locally.
Hit the `4.1 GET /servers` route and make sure that the `asns` field shows up in the response. 
Hit the same endpoint and add `asn=<>` as a query param.
Make sure you see the correct servers in the response.
Make sure the other routes DO NOT return this new field.
Make sure all other CRUD routes for servers work fine.
Test TP to verify that you can show/ hide the new field in servers.

## If this is a bugfix, which Traffic Control versions contained the bug?
<!-- Delete this section if the PR is not a bugfix, or if the bug is only in the master branch.
Examples:
- 5.1.2
- 5.1.3 (RC1)
 -->
- master

## PR submission checklist
- [x] This PR has tests <!-- If not, please delete this text and explain why this PR does not need tests. -->
- [x] This PR has documentation <!-- If not, please delete this text and explain why this PR does not need documentation. -->
- [x] This PR has a CHANGELOG.md entry <!-- A fix for a bug from an ATC release, an improvement, or a new feature should have a changelog entry. -->
- [x] This PR **DOES NOT FIX A SERIOUS SECURITY VULNERABILITY** (see [the Apache Software Foundation's security guidelines](https://apache.org/security) for details)

<!--
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.
-->


srijeet0406: > The `ServerV4` type alias in `lib/go-tc/servers.go` still says that the latest version of a server is `ServerV40`. This means that other API endpoints that show servers - e.g. `/deliveryservices/{{ID}}/servers/eligible` etc. - all now have different representations of a server than `/servers`. This is, in my opinion, bad. An API object should have exactly one representation.

I've updated the `ServerV4` to point to `ServerV41` now. However, if you hit the other endpoints, for example, `/deliveryservices/{{ID}}/servers/eligible`, you'd still get a server representation without the `ASN` field. That field is only added for the `GET` routes of `/servers`. Because there is not 4.1 equivalent route for the other endpoints (for example, `/deliveryservices/{{ID}}/servers/eligible`), they still return the version 4.0 representation of the server object.
srijeet0406: IDK why the `go fmt` GHA keeps failing. It's checking a piece of code that's not even a part of this PR.
ocket8888: > IDK why the `go fmt` GHA keeps failing. It's checking a piece of code that's not even a part of this PR.

That's because #6960 was erroneously merged with a go fmt failure.
ocket8888: A fix is now open #7038 
srijeet0406: > The `ServerV4` type alias in `lib/go-tc/servers.go` still says that the latest version of a server is `ServerV40`. This means that other API endpoints that show servers - e.g. `/deliveryservices/{{ID}}/servers/eligible` etc. - all now have different representations of a server than `/servers`. This is, in my opinion, bad. An API object should have exactly one representation.

This will be addressed in a follow up PR.

Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c2624abe..c74a5b183 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
 - [#7032](https://github.com/apache/trafficcontrol/issues/7032) Add t3c-apply flag to use local ATS version for config generation rather than Server package Parameter, to allow managing the ATS OS package via external tools. See 'man t3c-apply' and 'man t3c-generate' for details.
 
 - [Traffic Monitor] Added logging for `ipv4Availability` and `ipv6Availability` in TM.
+- [Traffic Ops] Added the `ASN` field in TO Server struct, which provides the ability to query servers by `ASN`.
 
 ### 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/cache-config/t3cutil/toreq/clientfuncs.go b/cache-config/t3cutil/toreq/clientfuncs.go
index 9c5e335ce..f7a9ca15a 100644
--- a/cache-config/t3cutil/toreq/clientfuncs.go
+++ b/cache-config/t3cutil/toreq/clientfuncs.go
@@ -169,7 +169,7 @@ func (cl *TOClient) GetServerByHostName(serverHostName string, reqHdr http.Heade
 			if len(toServers.Response) < 1 {
 				return errors.New("getting server name '" + serverHostName + "' from Traffic Ops '" + torequtil.MaybeIPStr(reqInf.RemoteAddr) + "': no servers returned")
 			}
-			asv, err := serverToLatest(&toServers.Response[0])
+			asv, err := serverToLatest(&toServers.Response[0].ServerV40)
 			if err != nil {
 				return errors.New("converting server to latest version: " + err.Error())
 			}
diff --git a/cache-config/t3cutil/toreq/conversions.go b/cache-config/t3cutil/toreq/conversions.go
index ab098a5ea..f480377fb 100644
--- a/cache-config/t3cutil/toreq/conversions.go
+++ b/cache-config/t3cutil/toreq/conversions.go
@@ -34,7 +34,11 @@ import (
 )
 
 func serversToLatest(svs tc.ServersV4Response) ([]atscfg.Server, error) {
-	return atscfg.ToServers(svs.Response), nil
+	serversV40 := make([]tc.ServerV40, 0)
+	for _, srv := range svs.Response {
+		serversV40 = append(serversV40, srv.ServerV40)
+	}
+	return atscfg.ToServers(serversV40), nil
 }
 
 func serverToLatest(oldSv *tc.ServerV40) (*atscfg.Server, error) {
@@ -218,7 +222,7 @@ func (cl *TOClient) GetServersCompat(opts toclient.RequestOptions) (tc.ServersV4
 		if err != nil {
 			return tc.ServersV4Response{}, reqInf, errors.New("converting server from possible legacy format: " + err.Error())
 		}
-		resp.Response = append(resp.Response, newSv)
+		resp.Response = append(resp.Response, tc.ServerV41{ServerV40: newSv})
 	}
 	return resp, reqInf, nil
 }
diff --git a/docs/source/api/v4/servers.rst b/docs/source/api/v4/servers.rst
index 78e5d1f39..1a57951d0 100644
--- a/docs/source/api/v4/servers.rst
+++ b/docs/source/api/v4/servers.rst
@@ -69,6 +69,11 @@ Request Structure
 	|                |          | the first page is 1. If ``offset`` was defined, this query parameter has no effect. ``limit`` must be defined to  |
 	|                |          | make use of ``page``.                                                                                             |
 	+----------------+----------+-------------------------------------------------------------------------------------------------------------------+
+	| asn            | no       | Return only the servers that have a cachegroup matching the provided ASN.                                         |
+	|                |          |                                                                                                                   |
+	|                |          | .. versionadded:: 4.1                                                                                             |
+	+----------------+----------+-------------------------------------------------------------------------------------------------------------------+
+
 
 .. code-block:: http
 	:caption: Request Example
@@ -81,6 +86,9 @@ Request Structure
 
 Response Structure
 ------------------
+:asns:             The :abbr:`ASN (Autonomous System Number)` associated with the cachegroups of the current server.
+
+	.. versionadded:: 4.1
 :cachegroup:       A string that is the :ref:`name of the Cache Group <cache-group-name>` to which the server belongs
 :cachegroupId:     An integer that is the :ref:`ID of the Cache Group <cache-group-id>` to which the server belongs
 :cdnId:            The integral, unique identifier of the CDN to which the server belongs
@@ -225,6 +233,10 @@ Response Structure
 				"routerHostName": "",
 				"routerPortName": ""
 			}
+		],
+		"asns": [
+			1,
+			2
 		]
 	}],
 	"summary": {
diff --git a/infrastructure/cdn-in-a-box/enroller/enroller.go b/infrastructure/cdn-in-a-box/enroller/enroller.go
index 0633ab51b..f8b61407c 100644
--- a/infrastructure/cdn-in-a-box/enroller/enroller.go
+++ b/infrastructure/cdn-in-a-box/enroller/enroller.go
@@ -757,7 +757,7 @@ func enrollServer(toSession *session, r io.Reader) error {
 		return err
 	}
 
-	alerts, _, err := toSession.CreateServer(s, client.RequestOptions{})
+	alerts, _, err := toSession.CreateServer(tc.ServerV4{ServerV40: s}, client.RequestOptions{})
 	if err != nil {
 		err = fmt.Errorf("error creating Server: %v - alerts: %+v", err, alerts.Alerts)
 		log.Infoln(err)
diff --git a/lib/go-tc/servers.go b/lib/go-tc/servers.go
index 2f435cdb3..8ef5f24e9 100644
--- a/lib/go-tc/servers.go
+++ b/lib/go-tc/servers.go
@@ -34,7 +34,7 @@ import (
 
 // ServersV4Response is the format of a response to a GET request for API v4.x /servers.
 type ServersV4Response struct {
-	Response []ServerV40 `json:"response"`
+	Response []ServerV41 `json:"response"`
 	Summary  struct {
 		Count uint64 `json:"count"`
 	} `json:"summary"`
@@ -1019,6 +1019,12 @@ func UpdateServerPropertiesV40(profileNames []string, properties CommonServerPro
 	}
 }
 
+// ServerV41 is the representation of a Server in version 4.1 of the Traffic Ops API.
+type ServerV41 struct {
+	ServerV40
+	ASNs []int64 `json:"asns"`
+}
+
 // ServerV40 is the representation of a Server in version 4.0 of the Traffic Ops API.
 type ServerV40 struct {
 	Cachegroup        *string                  `json:"cachegroup" db:"cachegroup"`
@@ -1066,7 +1072,7 @@ type ServerV40 struct {
 
 // ServerV4 is the representation of a Server in the latest minor version of
 // version 4 of the Traffic Ops API.
-type ServerV4 = ServerV40
+type ServerV4 = ServerV41
 
 // ServerV30 is the representation of a Server in version 3 of the Traffic Ops API.
 type ServerV30 struct {
diff --git a/traffic_monitor/towrap/towrap.go b/traffic_monitor/towrap/towrap.go
index c24ea22fc..48f0f66cd 100644
--- a/traffic_monitor/towrap/towrap.go
+++ b/traffic_monitor/towrap/towrap.go
@@ -587,7 +587,7 @@ func (s TrafficOpsSessionThreadsafe) fetchServerByHostname(hostName string) (tc.
 	for i, srv := range resp.Response {
 		num = i
 		if srv.CDNName != nil && srv.HostName != nil && *srv.HostName == hostName {
-			server = srv
+			server = srv.ServerV40
 			found = true
 			break
 		}
diff --git a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index e70713899..ed078e6ba 100644
--- a/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
+++ b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
@@ -2024,7 +2024,7 @@ WHERE server.id = $2;`
 }
 
 // GetCommonServerPropertiesFromV4 converts ServerV40 to CommonServerProperties struct.
-func GetCommonServerPropertiesFromV4(s tc.ServerV40, tx *sql.Tx) (tc.CommonServerProperties, error) {
+func GetCommonServerPropertiesFromV4(s tc.ServerV41, tx *sql.Tx) (tc.CommonServerProperties, error) {
 	var id int
 	var desc string
 	if len(s.ProfileNames) == 0 {
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index 7089c120b..98e833408 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -131,6 +131,8 @@ func Routes(d ServerData) ([]Route, http.Handler, error) {
 		 * 4.x API
 		 */
 
+		// GET servers
+		{Version: api.Version{Major: 4, Minor: 1}, Method: http.MethodGet, Path: `servers/?$`, Handler: server.Read, RequiredPrivLevel: auth.PrivLevelReadOnly, RequiredPermissions: []string{"SERVER:READ", "DELIVERY-SERVICE:READ", "CDN:READ", "PHYSICAL-LOCATION:READ", "CACHE-GROUP:READ", "TYPE:READ", "PROFILE:READ"}, Authenticated: Authenticated, Middlewares: nil, ID: 47219592853},
 		// 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},
 
diff --git a/traffic_ops/traffic_ops_golang/server/servers.go b/traffic_ops/traffic_ops_golang/server/servers.go
index e9404fa33..e57d16125 100644
--- a/traffic_ops/traffic_ops_golang/server/servers.go
+++ b/traffic_ops/traffic_ops_golang/server/servers.go
@@ -152,7 +152,8 @@ SELECT
 	s.config_apply_time,
 	s.xmpp_id,
 	s.xmpp_passwd,
-	s.status_last_updated
+	s.status_last_updated,
+	(SELECT ARRAY_AGG(asn) AS asns FROM asn a WHERE a.cachegroup = s.cachegroup) AS asns
 ` + serversFromAndJoin
 
 const selectIDQuery = `
@@ -688,7 +689,7 @@ func Read(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	servers := []tc.ServerV40{}
+	servers := []tc.ServerV41{}
 	var serverCount uint64
 	cfg, e := api.GetConfig(r.Context())
 	useIMS := false
@@ -704,16 +705,23 @@ func Read(w http.ResponseWriter, r *http.Request) {
 	}
 	if errCode == http.StatusNotModified {
 		w.WriteHeader(errCode)
-		api.WriteResp(w, r, []tc.ServerV40{})
+		api.WriteResp(w, r, []tc.ServerV41{})
 		return
 	}
 	if userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
 		return
 	}
-
 	if version.Major >= 4 {
-		api.WriteRespWithSummary(w, r, servers, serverCount)
+		if version.Minor >= 1 {
+			api.WriteRespWithSummary(w, r, servers, serverCount)
+			return
+		}
+		v40Servers := make([]tc.ServerV40, 0)
+		for _, server := range servers {
+			v40Servers = append(v40Servers, server.ServerV40)
+		}
+		api.WriteRespWithSummary(w, r, v40Servers, serverCount)
 		return
 	}
 	v3Servers := make([]tc.ServerV30, 0)
@@ -760,7 +768,7 @@ func getServerCount(tx *sqlx.Tx, query string, queryValues map[string]interface{
 	return serverCount, nil
 }
 
-func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth.CurrentUser, useIMS bool, version api.Version) ([]tc.ServerV40, uint64, error, error, int, *time.Time) {
+func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth.CurrentUser, useIMS bool, version api.Version) ([]tc.ServerV41, uint64, error, error, int, *time.Time) {
 	var maxTime time.Time
 	var runSecond bool
 	// Query Parameters to Database Query column mappings
@@ -787,6 +795,12 @@ func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth
 	}
 
 	if version.Major >= 4 {
+		if version.Minor >= 1 {
+			queryParamsToSQLCols["asn"] = dbhelpers.WhereColumnInfo{
+				Column:  "a.asn",
+				Checker: api.IsInt,
+			}
+		}
 		queryParamsToSQLCols["profileName"] = dbhelpers.WhereColumnInfo{
 			Column:  "sp.profile_name",
 			Checker: nil,
@@ -854,12 +868,22 @@ func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth
 	queryString = selectQuery
 	countQueryString = serverCountQuery
 	if version.Major >= 4 {
+		if version.Minor >= 1 {
+			if _, ok := params["asn"]; ok {
+				queryString = selectQuery + `
+JOIN asn a ON s.cachegroup = a.cachegroup`
+				countQueryString = serverCountQuery + `
+JOIN asn a ON s.cachegroup = a.cachegroup`
+			}
+		}
 		if _, ok := params["profileName"]; ok {
-			queryString = selectQuery + `JOIN server_profile sp ON s.id = sp.server`
-			countQueryString = serverCountQuery + `JOIN server_profile sp ON s.id = sp.server`
+			queryString = queryString + `
+JOIN server_profile sp ON s.id = sp.server`
+			countQueryString = countQueryString + `
+JOIN server_profile sp ON s.id = sp.server`
 		} else {
-			queryString = selectQuery + joinProfileV4
-			countQueryString = serverCountQuery + joinProfileV4
+			queryString = queryString + ` ` + joinProfileV4
+			countQueryString = countQueryString + ` ` + joinProfileV4
 		}
 	}
 	countQuery := countQueryString + queryAddition + where
@@ -872,7 +896,7 @@ func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth
 		return nil, 0, nil, fmt.Errorf("failed to get servers count: %v", err), http.StatusInternalServerError, nil
 	}
 
-	serversList := []tc.ServerV40{}
+	serversList := []tc.ServerV41{}
 	if useIMS {
 		runSecond, maxTime = ims.TryIfModifiedSinceQuery(tx, h, queryValues, selectMaxLastUpdatedQuery(queryAddition, where))
 		if !runSecond {
@@ -899,10 +923,10 @@ func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth
 
 	HiddenField := "********"
 
-	servers := make(map[int]tc.ServerV40)
+	servers := make(map[int]tc.ServerV41)
 	ids := []int{}
 	for rows.Next() {
-		s := tc.ServerV40{}
+		s := tc.ServerV41{}
 		err := rows.Scan(&s.Cachegroup,
 			&s.CachegroupID,
 			&s.CDNID,
@@ -939,7 +963,8 @@ func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth
 			&s.ConfigApplyTime,
 			&s.XMPPID,
 			&s.XMPPPasswd,
-			&s.StatusLastUpdated)
+			&s.StatusLastUpdated,
+			pq.Array(&s.ASNs))
 		if err != nil {
 			return nil, serverCount, nil, errors.New("getting servers: " + err.Error()), http.StatusInternalServerError, nil
 		}
@@ -972,7 +997,7 @@ func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth
 	}
 
 	if len(ids) < 1 {
-		return []tc.ServerV40{}, serverCount, nil, nil, http.StatusOK, nil
+		return []tc.ServerV41{}, serverCount, nil, nil, http.StatusOK, nil
 	}
 
 	query, args, err := sqlx.In(`SELECT max_bandwidth, monitor, mtu, name, server, router_host_name, router_port_name FROM interface WHERE server IN (?)`, ids)
@@ -1043,7 +1068,7 @@ func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth
 		}
 	}
 
-	returnable := make([]tc.ServerV40, 0, len(ids))
+	returnable := make([]tc.ServerV41, 0, len(ids))
 
 	for _, id := range ids {
 		server := servers[id]
@@ -1057,7 +1082,7 @@ func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user *auth
 }
 
 // getMidServers gets the mids used by the edges provided with an option to filter for a given cdn
-func getMidServers(edgeIDs []int, servers map[int]tc.ServerV40, dsID int, cdnID int, tx *sqlx.Tx, includeCapabilities bool) ([]int, error, error, int) {
+func getMidServers(edgeIDs []int, servers map[int]tc.ServerV41, dsID int, cdnID int, tx *sqlx.Tx, includeCapabilities bool) ([]int, error, error, int) {
 	if len(edgeIDs) == 0 {
 		return nil, nil, nil, http.StatusOK
 	}
@@ -1123,8 +1148,9 @@ func getMidServers(edgeIDs []int, servers map[int]tc.ServerV40, dsID int, cdnID
 	defer rows.Close()
 
 	ids := []int{}
+
 	for rows.Next() {
-		var s tc.ServerV40
+		var s tc.ServerV41
 		if err := rows.Scan(&s.Cachegroup,
 			&s.CachegroupID,
 			&s.CDNID,
@@ -1161,7 +1187,8 @@ func getMidServers(edgeIDs []int, servers map[int]tc.ServerV40, dsID int, cdnID
 			&s.ConfigApplyTime,
 			&s.XMPPID,
 			&s.XMPPPasswd,
-			&s.StatusLastUpdated); err != nil {
+			&s.StatusLastUpdated,
+			pq.Array(&s.ASNs)); err != nil {
 			log.Errorf("could not scan mid servers: %s\n", err)
 			return nil, nil, err, http.StatusInternalServerError
 		}
@@ -1322,7 +1349,7 @@ func Update(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	original := originals[0]
+	original := originals[0].ServerV40
 	if original.XMPPID == nil || *original.XMPPID == "" {
 		log.Warnf("original server %s had no XMPPID\n", *original.HostName)
 	}
@@ -1545,7 +1572,7 @@ func Update(w http.ResponseWriter, r *http.Request) {
 	} else {
 		selquery = selectQuery + where
 	}
-	var srvr tc.ServerV40
+	var srvr tc.ServerV41
 	err = inf.Tx.QueryRow(selquery, serverID).Scan(&srvr.Cachegroup,
 		&srvr.CachegroupID,
 		&srvr.CDNID,
@@ -1582,7 +1609,8 @@ func Update(w http.ResponseWriter, r *http.Request) {
 		&srvr.ConfigApplyTime,
 		&srvr.XMPPID,
 		&srvr.XMPPPasswd,
-		&srvr.StatusLastUpdated)
+		&srvr.StatusLastUpdated,
+		pq.Array(&srvr.ASNs))
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
@@ -1603,7 +1631,7 @@ func Update(w http.ResponseWriter, r *http.Request) {
 		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
 		return
 	}
-	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server updated", srvr)
+	api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server updated", srvr.ServerV40)
 
 	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: updated", *srvr.HostName, *srvr.DomainName, *srvr.ID)
 	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, tx)
@@ -1769,7 +1797,7 @@ func createV3(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 
 	where := `WHERE s.id = $1`
 	selquery := selectQuery + where
-	var s4 tc.ServerV40
+	var s4 tc.ServerV41
 	err = inf.Tx.QueryRow(selquery, serverID).Scan(&s4.Cachegroup,
 		&s4.CachegroupID,
 		&s4.CDNID,
@@ -1806,7 +1834,8 @@ func createV3(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 		&s4.ConfigApplyTime,
 		&s4.XMPPID,
 		&s4.XMPPPasswd,
-		&s4.StatusLastUpdated)
+		&s4.StatusLastUpdated,
+		pq.Array(&s4.ASNs))
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
@@ -1903,7 +1932,7 @@ func createV4(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 
 	where := `WHERE s.id = $1`
 	selquery := selectQuery + joinProfileV4 + where
-	var srvr tc.ServerV40
+	var srvr tc.ServerV41
 	err = inf.Tx.QueryRow(selquery, serverID).Scan(&srvr.Cachegroup,
 		&srvr.CachegroupID,
 		&srvr.CDNID,
@@ -1940,7 +1969,8 @@ func createV4(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 		&srvr.ConfigApplyTime,
 		&srvr.XMPPID,
 		&srvr.XMPPPasswd,
-		&srvr.StatusLastUpdated)
+		&srvr.StatusLastUpdated,
+		pq.Array(&srvr.ASNs))
 	if err != nil {
 		api.HandleErr(w, r, inf.Tx.Tx, http.StatusInternalServerError, nil, err)
 		return
@@ -1950,7 +1980,7 @@ func createV4(inf *api.APIInfo, w http.ResponseWriter, r *http.Request) {
 	srvr.Interfaces = server.Interfaces
 
 	alerts := tc.CreateAlerts(tc.SuccessLevel, "Server created")
-	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, srvr)
+	api.WriteAlertsObj(w, r, http.StatusCreated, alerts, srvr.ServerV40)
 
 	changeLogMsg := fmt.Sprintf("SERVER: %s.%s, ID: %d, ACTION: created", *srvr.HostName, *srvr.DomainName, *srvr.ID)
 	api.CreateChangeLogRawTx(api.ApiChange, changeLogMsg, inf.User, inf.Tx.Tx)
@@ -2154,7 +2184,7 @@ func Delete(w http.ResponseWriter, r *http.Request) {
 		return
 	}
 
-	var servers []tc.ServerV40
+	var servers []tc.ServerV41
 	servers, _, userErr, sysErr, errCode, _ = getServers(r.Header, map[string]string{"id": inf.Params["id"]}, inf.Tx, inf.User, false, *version)
 	if userErr != nil || sysErr != nil {
 		api.HandleErr(w, r, tx, errCode, userErr, sysErr)
@@ -2213,7 +2243,7 @@ func Delete(w http.ResponseWriter, r *http.Request) {
 	}
 
 	if inf.Version.Major >= 4 {
-		api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server deleted", server)
+		api.WriteRespAlertObj(w, r, tc.SuccessLevel, "Server deleted", server.ServerV40)
 	} else {
 		csp, err := dbhelpers.GetCommonServerPropertiesFromV4(server, tx)
 		if err != nil {
diff --git a/traffic_portal/app/src/common/modules/table/servers/TableServersController.js b/traffic_portal/app/src/common/modules/table/servers/TableServersController.js
index 761f54cb3..7c8e99419 100644
--- a/traffic_portal/app/src/common/modules/table/servers/TableServersController.js
+++ b/traffic_portal/app/src/common/modules/table/servers/TableServersController.js
@@ -155,6 +155,11 @@ var TableServersController = function(tableName, servers, filter, $scope, $state
 			field: "offlineReason",
 			hide: true
 		},
+		{
+			headerName: "ASNs",
+			field: "asns",
+			hide: true
+		},
 		{
 			headerName: "Phys Location",
 			field: "physLocation",


Test patch:
diff --git a/traffic_ops/testing/api/v4/servers_test.go b/traffic_ops/testing/api/v4/servers_test.go
index e9e2ab649..61857b5ec 100644
--- a/traffic_ops/testing/api/v4/servers_test.go
+++ b/traffic_ops/testing/api/v4/servers_test.go
@@ -33,12 +33,12 @@ import (
 )
 
 func TestServers(t *testing.T) {
-	WithObjs(t, []TCObj{CDNs, Types, Tenants, Users, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, CacheGroups, Servers, Topologies, ServiceCategories, DeliveryServices, DeliveryServiceServerAssignments}, func() {
+	WithObjs(t, []TCObj{CDNs, Types, Tenants, Users, Parameters, Profiles, Statuses, Divisions, Regions, PhysLocations, CacheGroups, Servers, Topologies, ServiceCategories, DeliveryServices, DeliveryServiceServerAssignments, ASN}, func() {
 
 		currentTime := time.Now().UTC().Add(-15 * time.Second)
 		currentTimeRFC := currentTime.Format(time.RFC1123)
 		tomorrow := currentTime.AddDate(0, 0, 1).Format(time.RFC1123)
-
+		setupCacheGroupWithASN(t)
 		methodTests := utils.V4TestCase{
 			"GET": {
 				"NOT MODIFIED when NO CHANGES made": {
@@ -46,6 +46,11 @@ func TestServers(t *testing.T) {
 					RequestOpts:   client.RequestOptions{Header: http.Header{rfc.IfModifiedSince: {tomorrow}}},
 					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusNotModified)),
 				},
+				"OK when CORRECT ASN parameter": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"asn": {"1111"}}},
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseLengthGreaterOrEqual(1)),
+				},
 				"OK when VALID HOSTNAME parameter": {
 					ClientSession: TOSession,
 					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"hostName": {"atlanta-edge-01"}}},
@@ -58,6 +63,17 @@ func TestServers(t *testing.T) {
 					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseLengthGreaterOrEqual(1),
 						validateServerFields(map[string]interface{}{"CachegroupID": GetCacheGroupId(t, "cachegroup1")()})),
 				},
+				"OK when VALID ASN parameter": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"asn": {"1111"}}},
+					Expectations: utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseLengthGreaterOrEqual(1),
+						validateServerFields(map[string]interface{}{"Cachegroup": "topology-mid-cg-01"})),
+				},
+				"EMPTY RESPONSE when INVALID ASN parameter": {
+					ClientSession: TOSession,
+					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"asn": {"5555"}}},
+					Expectations:  utils.CkRequest(utils.NoError(), utils.HasStatus(http.StatusOK), utils.ResponseHasLength(0)),
+				},
 				"OK when VALID CACHEGROUPNAME parameter": {
 					ClientSession: TOSession,
 					RequestOpts:   client.RequestOptions{QueryParameters: url.Values{"cachegroupName": {"topology-mid-cg-01"}}},
@@ -384,10 +400,35 @@ func TestServers(t *testing.T) {
 	})
 }
 
+func setupCacheGroupWithASN(t *testing.T) {
+	// Add a new asn for one of the cachegroups
+	cgResp, _, err := TOSession.GetCacheGroups(client.RequestOptions{QueryParameters: url.Values{"name": {"topology-mid-cg-01"}}})
+	if err != nil {
+		t.Fatalf("couldn't get cachegroups: %v", err)
+	}
+	if len(cgResp.Response) != 1 {
+		t.Fatalf("expected 1 cachegroup, but got %d", len(cgResp.Response))
+	}
+	if cgResp.Response[0].ID == nil {
+		t.Fatalf("ID of cachegroup is nil")
+	}
+
+	asn := tc.ASN{
+		ASN:          1111,
+		Cachegroup:   "topology-mid-cg-01",
+		CachegroupID: *cgResp.Response[0].ID,
+	}
+
+	_, _, err = TOSession.CreateASN(asn, client.NewRequestOptions())
+	if err != nil {
+		t.Fatalf("couldn't create ASN: %v", err)
+	}
+}
+
 func validateServerFields(expectedResp map[string]interface{}) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected response to not be nil.")
-		serverResp := resp.([]tc.ServerV40)
+		serverResp := resp.([]tc.ServerV41)
 		for field, expected := range expectedResp {
 			for _, server := range serverResp {
 				switch field {
@@ -460,7 +501,7 @@ func validateServerFieldsForUpdate(hostname string, expectedResp map[string]inte
 func validateExpectedServers(expectedHostnames []string) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected response to not be nil.")
-		serverResp := resp.([]tc.ServerV40)
+		serverResp := resp.([]tc.ServerV41)
 		var notInResponse []string
 		serverMap := make(map[string]struct{})
 		for _, server := range serverResp {
@@ -479,7 +520,7 @@ func validateExpectedServers(expectedHostnames []string) utils.CkReqFunc {
 func validateServerTypeIsNotMid() utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected response to not be nil.")
-		serverResp := resp.([]tc.ServerV40)
+		serverResp := resp.([]tc.ServerV41)
 		for _, server := range serverResp {
 			assert.RequireNotNil(t, server.HostName, "Expected server host name to not be nil.")
 			assert.NotEqual(t, server.Type, tc.CacheTypeMid.String(), "Expected to find no %s-typed servers but found server %s", tc.CacheTypeMid, *server.HostName)
@@ -490,7 +531,7 @@ func validateServerTypeIsNotMid() utils.CkReqFunc {
 func validateServerPagination(paginationParam string) utils.CkReqFunc {
 	return func(t *testing.T, _ toclientlib.ReqInf, resp interface{}, _ tc.Alerts, _ error) {
 		assert.RequireNotNil(t, resp, "Expected response to not be nil.")
-		paginationResp := resp.([]tc.ServerV40)
+		paginationResp := resp.([]tc.ServerV41)
 		opts := client.NewRequestOptions()
 		opts.QueryParameters.Set("orderby", "id")
 		respBase, _, err := TOSession.GetServers(opts)
@@ -556,16 +597,16 @@ func UpdateTestServerStatusLastUpdated(t *testing.T) {
 	assert.RequireNoError(t, err, "Cannot get Server by hostname '%s': %v - alerts %+v", hostName, err, resp.Alerts)
 	assert.RequireGreaterOrEqual(t, len(resp.Response), 1, "Expected at least one server to exist by hostname '%s'", hostName)
 	assert.RequireNotNil(t, resp.Response[0].StatusLastUpdated, "Traffic Ops returned a representation for a server with null or undefined Status Last Updated time")
-	originalServer := resp.Response[0]
+	originalServer := resp.Response[0].ServerV40
 
 	// Perform an update with no changes to status
-	alerts, _, err := TOSession.UpdateServer(*originalServer.ID, originalServer, client.RequestOptions{})
+	alerts, _, err := TOSession.UpdateServer(*originalServer.ID, tc.ServerV4{ServerV40: originalServer}, client.RequestOptions{})
 	assert.RequireNoError(t, err, "Cannot UPDATE Server by ID %d (hostname '%s'): %v - alerts: %+v", *originalServer.ID, hostName, err, alerts)
 
 	resp, _, err = TOSession.GetServers(opts)
 	assert.RequireNoError(t, err, "Cannot get Server by hostname '%s': %v - alerts %+v", hostName, err, resp.Alerts)
 	assert.RequireGreaterOrEqual(t, len(resp.Response), 1, "Expected at least one server to exist by hostname '%s'", hostName)
-	respServer := resp.Response[0]
+	respServer := resp.Response[0].ServerV40
 	assert.RequireNotNil(t, respServer.StatusLastUpdated, "Traffic Ops returned a representation for a server with null or undefined Status Last Updated time")
 	assert.Equal(t, *originalServer.StatusLastUpdated, *respServer.StatusLastUpdated, "Since status didnt change, no change in 'StatusLastUpdated' time was expected. "+
 		"old value: %v, new value: %v", *originalServer.StatusLastUpdated, *respServer.StatusLastUpdated)
@@ -574,13 +615,13 @@ func UpdateTestServerStatusLastUpdated(t *testing.T) {
 	newStatusID := GetStatusID(t, "ONLINE")()
 	originalServer.StatusID = &newStatusID
 
-	alerts, _, err = TOSession.UpdateServer(*originalServer.ID, originalServer, client.RequestOptions{})
+	alerts, _, err = TOSession.UpdateServer(*originalServer.ID, tc.ServerV4{ServerV40: originalServer}, client.RequestOptions{})
 	assert.RequireNoError(t, err, "Cannot UPDATE Server by ID %d (hostname '%s'): %v - alerts: %+v", *originalServer.ID, hostName, err, alerts)
 
 	resp, _, err = TOSession.GetServers(opts)
 	assert.RequireNoError(t, err, "Cannot get Server by hostname '%s': %v - alerts %+v", hostName, err, resp.Alerts)
 	assert.RequireGreaterOrEqual(t, len(resp.Response), 1, "Expected at least one server to exist by hostname '%s'", hostName)
-	respServer = resp.Response[0]
+	respServer = resp.Response[0].ServerV40
 	assert.RequireNotNil(t, respServer.StatusLastUpdated, "Traffic Ops returned a representation for a server with null or undefined Status Last Updated time")
 	assert.NotEqual(t, *originalServer.StatusLastUpdated, *respServer.StatusLastUpdated, "Since status changed, expected 'StatusLastUpdated' to change. "+
 		"old value: %v, new value: %v", *originalServer.StatusLastUpdated, *respServer.StatusLastUpdated)
@@ -628,7 +669,7 @@ func UpdateDSGetServerDSID(t *testing.T) {
 
 func CreateTestServers(t *testing.T) {
 	for _, server := range testData.Servers {
-		resp, _, err := TOSession.CreateServer(server, client.RequestOptions{})
+		resp, _, err := TOSession.CreateServer(tc.ServerV4{ServerV40: server}, client.RequestOptions{})
 		assert.RequireNoError(t, err, "Could not create server '%s': %v - alerts: %+v", *server.HostName, err, resp.Alerts)
 	}
 }
diff --git a/traffic_ops/testing/api/v4/serverupdatestatus_test.go b/traffic_ops/testing/api/v4/serverupdatestatus_test.go
index e4dc57ffd..efb83c63a 100644
--- a/traffic_ops/testing/api/v4/serverupdatestatus_test.go
+++ b/traffic_ops/testing/api/v4/serverupdatestatus_test.go
@@ -103,19 +103,19 @@ func TestServerUpdateStatus(t *testing.T) {
 			}{
 				{
 					"atlanta-edge-01",
-					&edge1cdn1,
+					&edge1cdn1.ServerV40,
 				},
 				{
 					"atlanta-edge-03",
-					&edge2cdn1,
+					&edge2cdn1.ServerV40,
 				},
 				{
 					"atlanta-mid-16",
-					&mid1cdn1,
+					&mid1cdn1.ServerV40,
 				},
 				{
 					"edge1-cdn2",
-					&edge1cdn2,
+					&edge1cdn2.ServerV40,
 				},
 			} {
 				opts.QueryParameters.Set("hostName", s.name)
@@ -130,7 +130,7 @@ func TestServerUpdateStatus(t *testing.T) {
 					t.Errorf("Expected exactly one server named '%s' to exist - actual: %d", s.name, len(resp.Response))
 					t.Logf("Testing will proceed with server: %+v", resp.Response[0])
 				}
-				*s.server = resp.Response[0]
+				*s.server = resp.Response[0].ServerV40
 				if s.server.ID == nil {
 					t.Fatalf("server '%s' was returned with nil ID", s.name)
 				}
diff --git a/traffic_ops/traffic_ops_golang/server/servers_test.go b/traffic_ops/traffic_ops_golang/server/servers_test.go
index 828328414..a7243f38c 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_test.go
@@ -32,17 +32,18 @@ import (
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
 	"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
 	"github.com/jmoiron/sqlx"
+	"github.com/lib/pq"
 	"gopkg.in/DATA-DOG/go-sqlmock.v1"
 )
 
 type ServerAndInterfaces struct {
-	Server    tc.ServerV40
+	Server    tc.ServerV41
 	Interface tc.ServerInterfaceInfoV40
 }
 
 func getTestServers() []ServerAndInterfaces {
 	servers := []ServerAndInterfaces{}
-	testServer := tc.ServerV40{
+	testServerV40 := tc.ServerV40{
 		Cachegroup:        util.StrPtr("Cachegroup"),
 		CachegroupID:      util.IntPtr(1),
 		CDNID:             util.IntPtr(1),
@@ -81,7 +82,10 @@ func getTestServers() []ServerAndInterfaces {
 		RevalUpdateTime:   &(time.Time{}),
 		RevalApplyTime:    &(time.Time{}),
 	}
-
+	testServer := tc.ServerV41{
+		ServerV40: testServerV40,
+		ASNs:      pq.Int64Array{1, 2},
+	}
 	mtu := uint64(9500)
 
 	iface := tc.ServerInterfaceInfoV40{
@@ -179,7 +183,7 @@ func TestGetServersByCachegroup(t *testing.T) {
 		"last_updated", "mgmt_ip_address", "mgmt_ip_gateway", "mgmt_ip_netmask", "offline_reason", "phys_location",
 		"phys_location_id", "profile_name", "rack", "reval_pending", "revalidate_update_time", "revalidate_apply_time",
 		"status", "status_id", "tcp_port", "server_type", "server_type_id", "upd_pending", "config_update_time",
-		"config_apply_time", "xmpp_id", "xmpp_passwd", "status_last_updated"}
+		"config_apply_time", "xmpp_id", "xmpp_passwd", "status_last_updated", "asns"}
 	interfaceCols := []string{"max_bandwidth", "monitor", "mtu", "name", "server", "router_host_name", "router_port_name"}
 	rows := sqlmock.NewRows(cols)
 	interfaceRows := sqlmock.NewRows(interfaceCols)
@@ -229,6 +233,7 @@ func TestGetServersByCachegroup(t *testing.T) {
 			*ts.XMPPID,
 			*ts.XMPPPasswd,
 			*ts.StatusLastUpdated,
+			[]byte(`{1,2}`),
 		)
 		interfaceRows = interfaceRows.AddRow(
 			srv.Interface.MaxBandwidth,
@@ -300,7 +305,7 @@ func TestGetMidServers(t *testing.T) {
 		"last_updated", "mgmt_ip_address", "mgmt_ip_gateway", "mgmt_ip_netmask", "offline_reason", "phys_location",
 		"phys_location_id", "profile_name", "rack", "reval_pending", "revalidate_update_time", "revalidate_apply_time",
 		"status", "status_id", "tcp_port", "server_type", "server_type_id", "upd_pending", "config_update_time",
-		"config_apply_time", "xmpp_id", "xmpp_passwd", "status_last_updated"}
+		"config_apply_time", "xmpp_id", "xmpp_passwd", "status_last_updated", "asns"}
 	interfaceCols := []string{"max_bandwidth", "monitor", "mtu", "name", "server", "router_host_name", "router_port_name"}
 	rows := sqlmock.NewRows(cols)
 	interfaceRows := sqlmock.NewRows(interfaceCols)
@@ -348,6 +353,7 @@ func TestGetMidServers(t *testing.T) {
 			*ts.XMPPID,
 			*ts.XMPPPasswd,
 			*ts.StatusLastUpdated,
+			[]byte(`{1,2}`),
 		)
 		interfaceRows = interfaceRows.AddRow(
 			srv.Interface.MaxBandwidth,
@@ -391,7 +397,7 @@ func TestGetMidServers(t *testing.T) {
 		"last_updated", "mgmt_ip_address", "mgmt_ip_gateway", "mgmt_ip_netmask", "offline_reason", "phys_location",
 		"phys_location_id", "profile_name", "rack", "reval_pending", "revalidate_update_time", "revalidate_apply_time",
 		"status", "status_id", "tcp_port", "server_type", "server_type_id", "upd_pending", "config_update_time",
-		"config_apply_time", "xmpp_id", "xmpp_passwd", "status_last_updated"}
+		"config_apply_time", "xmpp_id", "xmpp_passwd", "status_last_updated", "asns"}
 	rows2 := sqlmock.NewRows(cols2)
 
 	cgs := []tc.CacheGroup{}
@@ -437,7 +443,7 @@ func TestGetMidServers(t *testing.T) {
 	}
 	cgs = append(cgs, testCG2)
 
-	serverMap := make(map[int]tc.ServerV40, len(servers))
+	serverMap := make(map[int]tc.ServerV41, len(servers))
 	serverIDs := make([]int, 0, len(servers))
 	for _, server := range servers {
 		if server.ID == nil {
@@ -447,7 +453,7 @@ func TestGetMidServers(t *testing.T) {
 		serverMap[*server.ID] = server
 	}
 
-	var ts tc.ServerV40
+	var ts tc.ServerV41
 	for _, s := range servers {
 		if s.HostName != nil && *s.HostName == "server2" {
 			ts = s
@@ -493,6 +499,7 @@ func TestGetMidServers(t *testing.T) {
 		*ts.XMPPID,
 		*ts.XMPPPasswd,
 		*ts.StatusLastUpdated,
+		[]byte(`{1,2}`),
 	)
 
 	mock.ExpectBegin()
diff --git a/traffic_router/ultimate-test-harness/http_test.go b/traffic_router/ultimate-test-harness/http_test.go
index 9b9fa8ce3..ed67bc226 100644
--- a/traffic_router/ultimate-test-harness/http_test.go
+++ b/traffic_router/ultimate-test-harness/http_test.go
@@ -337,10 +337,14 @@ func getTrafficRouters(trafficRouterName string, cdnName tc.CDNName) ([]tc.Serve
 		return nil, fmt.Errorf("requesting %s-status Traffic Routers: %s", requestOptions.QueryParameters["status"], err.Error())
 	}
 	trafficRouters := response.Response
+	trafficRoutersV40 := make([]tc.ServerV40, 0)
+	for _, tr := range trafficRouters {
+		trafficRoutersV40 = append(trafficRoutersV40, tr.ServerV40)
+	}
 	if len(trafficRouters) < 1 {
-		return trafficRouters, fmt.Errorf("no Traffic Routers were found with these criteria: %v", requestOptions.QueryParameters)
+		return trafficRoutersV40, fmt.Errorf("no Traffic Routers were found with these criteria: %v", requestOptions.QueryParameters)
 	}
-	return trafficRouters, nil
+	return trafficRoutersV40, nil
 }
 
 func getDSes(t *testing.T, cdnId int, dsTypeName tc.DSType, dsName tc.DeliveryServiceName) []tc.DeliveryServiceV40 {