You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by oc...@apache.org on 2022/09/07 18:39:11 UTC

[trafficcontrol] branch master updated: Add asn field to server (#7023)

This is an automated email from the ASF dual-hosted git repository.

ocket8888 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/trafficcontrol.git


The following commit(s) were added to refs/heads/master by this push:
     new 5e8255fee5 Add asn field to server (#7023)
5e8255fee5 is described below

commit 5e8255fee5eef389238fe84e7fbe90877a142563
Author: Srijeet Chatterjee <30...@users.noreply.github.com>
AuthorDate: Wed Sep 7 12:39:05 2022 -0600

    Add asn field to server (#7023)
    
    * Adding ASN to Server struct
    
    * doc changes
    
    * fix typo
    
    * add godoc
    
    * address code review comments
    
    * address code review changes
    
    * address code review comments
    
    * remove extra doc line
---
 CHANGELOG.md                                       |  1 +
 cache-config/t3cutil/toreq/clientfuncs.go          |  2 +-
 cache-config/t3cutil/toreq/conversions.go          |  8 +-
 docs/source/api/v4/servers.rst                     | 12 +++
 infrastructure/cdn-in-a-box/enroller/enroller.go   |  2 +-
 lib/go-tc/servers.go                               | 10 ++-
 traffic_monitor/towrap/towrap.go                   |  2 +-
 traffic_ops/testing/api/v4/servers_test.go         | 65 +++++++++++++---
 .../testing/api/v4/serverupdatestatus_test.go      | 10 +--
 .../traffic_ops_golang/dbhelpers/db_helpers.go     |  2 +-
 traffic_ops/traffic_ops_golang/routing/routes.go   |  2 +
 traffic_ops/traffic_ops_golang/server/servers.go   | 90 ++++++++++++++--------
 .../traffic_ops_golang/server/servers_test.go      | 23 ++++--
 .../table/servers/TableServersController.js        |  5 ++
 traffic_router/ultimate-test-harness/http_test.go  |  8 +-
 15 files changed, 177 insertions(+), 65 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4c2624abe6..c74a5b183a 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 9c5e335ce5..f7a9ca15af 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 ab098a5eaa..f480377fbe 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 78e5d1f397..1a57951d0a 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 0633ab51bb..f8b61407ce 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 2f435cdb3f..8ef5f24e91 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 c24ea22fc3..48f0f66cd3 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/testing/api/v4/servers_test.go b/traffic_ops/testing/api/v4/servers_test.go
index e9e2ab649b..61857b5ec2 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 e4dc57ffd5..efb83c63a2 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/dbhelpers/db_helpers.go b/traffic_ops/traffic_ops_golang/dbhelpers/db_helpers.go
index e70713899a..ed078e6ba2 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 7089c120bf..98e833408d 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 e9404fa333..e57d161258 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_ops/traffic_ops_golang/server/servers_test.go b/traffic_ops/traffic_ops_golang/server/servers_test.go
index 8283284143..a7243f38c5 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_portal/app/src/common/modules/table/servers/TableServersController.js b/traffic_portal/app/src/common/modules/table/servers/TableServersController.js
index 761f54cb36..7c8e994195 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",
diff --git a/traffic_router/ultimate-test-harness/http_test.go b/traffic_router/ultimate-test-harness/http_test.go
index 9b9fa8ce3e..ed67bc2262 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 {