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 9bfdfbd  TO/TP: View servers by topology-based delivery service (#4966)
9bfdfbd is described below

commit 9bfdfbddb7a0c44cb85574fc62a3ee4de54982b3
Author: Zach Hoffman <[email protected]>
AuthorDate: Thu Sep 3 12:01:22 2020 -0600

    TO/TP: View servers by topology-based delivery service (#4966)
    
    * Query servers by the ID of a topology-based delivery service
    
    * Add an API test
    
    * Traffic Ops changelog entry
    
    * [PATCH] Adds topology tests (zrhoffman/trafficcontrol#23)
    
    * adds UI test for creating a topology
    
    * adds UI test to view the servers for a ds that utilizes a topology
    
    * adds changelog entry for TP
    
    * Explain what servers will be returned when specifying dsId for a
    topology-based delivery service
    
    * Only allow querying servers by topology-based DS for API versions 3 and
    up
    
    * Term, not ref
    
    * Use :dfn: text role
    
    * Ensure version is not nil
    
    * Keep using variadic useInTable
    
    * Fix some warnings
      - Name variables timestamp instead of time to avoid identifier collisions
      - Use non-deprecated WithHdr() TO client functions
      - Remove semicolon
      - Remove parenthesis
    
    * Version is not a pointer now
    
    * adds the ability to view servers assigned to a ds that employs a topo… 
(zrhoffman/trafficcontrol#25)
    
    * adds the ability to view servers assigned to a ds that employs a topology
    
    * only edges or orgs can be removed from a ds
    
    * Nil check for ds.XMLID
    
    * Do not rebind variable ds
    
    * Assert that validation fails by looking for HTTP status code 400
    
    * Use cachegroup3 in my-topology because cachegroup1 has lots of non-cache
    servers
    
    * Fix nil check logic
    
    * fixes orderby in servers api and ds servers UI view 
(zrhoffman/trafficcontrol#26)
    
    * maintains the order of the servers as fetched from  database
    
    * orders ds servers by hostName
    
    * Filter delivery service servers by CDN ID
    
    * Filter by the delivery service's required capabilities
    
    * Remove extra space
    
    * adds a visual indicator that you are viewing ds servers as dictated by 
the topology of the ds. (zrhoffman/trafficcontrol#28)
    
    * changes font-size to medium (zrhoffman/trafficcontrol#29)
    
    * Indent SQL
    
    * Add space for RST table formatting
    
    * Move HasRequiredCapabilitiesQuery to deliveryservice package
    
    * Mention that only servers satisfying a DS's required capabilities are
    returned for the dsId query parameter
    
    Co-authored-by: Jeremy Mitchell <[email protected]>
---
 CHANGELOG.md                                       |  2 +
 docs/source/api/v3/servers.rst                     |  6 +-
 docs/source/glossary.rst                           |  8 ++
 traffic_ops/testing/api/v3/servers_test.go         | 89 +++++++++++++-------
 traffic_ops/testing/api/v3/tc-fixtures.json        |  2 +-
 .../deliveryservices_required_capabilities.go      |  9 ++
 traffic_ops/traffic_ops_golang/server/servers.go   | 96 +++++++++++++++++++---
 .../traffic_ops_golang/server/servers_test.go      |  8 +-
 traffic_ops/v3-client/server.go                    |  1 +
 traffic_ops/v3-client/type.go                      |  4 +-
 .../form.deliveryService.DNS.tpl.html              |  2 +-
 .../form.deliveryService.HTTP.tpl.html             |  2 +-
 .../form.deliveryService.anyMap.tpl.html           |  2 +-
 .../modules/form/topology/form.topology.tpl.html   |  2 +-
 .../table.deliveryServiceServers.tpl.html          |  4 +-
 .../table/topologies/table.topologies.tpl.html     |  2 +-
 .../private/deliveryServices/servers/index.js      |  6 +-
 traffic_portal/app/src/styles/main.scss            |  3 +
 traffic_portal/test/end_to_end/conf.json           |  1 +
 .../deliveryServices/delivery-services-spec.js     | 14 +++-
 .../test/end_to_end/deliveryServices/pageData.js   |  1 +
 .../test/end_to_end/topologies/pageData.js         | 27 ++++++
 .../test/end_to_end/topologies/topologies-spec.js  | 71 ++++++++++++++++
 23 files changed, 303 insertions(+), 59 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fee8242..744e76c 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -13,6 +13,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
     - Traffic Ops: Added new `topology` field to the /api/3.0/deliveryservices 
APIs
     - Traffic Ops: Added support for `topology` query parameter to `GET 
/api/3.0/cachegroups` to return all cachegroups used in the given topology.
     - Traffic Ops: Added support for `topology` query parameter to `GET 
/api/3.0/deliveryservices` to return all delivery services that employ a given 
topology.
+    - Traffic Ops: Added support for `dsId` query parameter for `GET 
/api/3.0/servers` for topology-based delivery services.
     - Traffic Ops: Added new topology-based delivery service fields for header 
rewrites: `firstHeaderRewrite`, `innerHeaderRewrite`, `lastHeaderRewrite`
     - Traffic Ops: Added validation to prohibit assigning caches to 
topology-based delivery services
     - Traffic Ops: Consider Topologies parentage when queueing or checking 
server updates
@@ -21,6 +22,7 @@ The format is based on [Keep a 
Changelog](http://keepachangelog.com/en/1.0.0/).
     - Traffic Portal: Added the ability to assign topologies to delivery 
services.
     - Traffic Portal: Added the ability to view all delivery services and 
cache groups associated with a topology.
     - Traffic Portal: Added the ability to define first, inner and last header 
rewrite values for DNS* and HTTP* delivery services that employ a topology.
+    - Traffic Portal: Adds the ability to view all servers utilized by a 
topology-based delivery service.
     - Traffic Portal: Added topology section to cdn snapshot diff.
     - Traffic Router: Added support for topology-based delivery services
     - Traffic Monitor: Added the ability to mark topology-based delivery 
services as available
diff --git a/docs/source/api/v3/servers.rst b/docs/source/api/v3/servers.rst
index 27c9996..ac55750 100644
--- a/docs/source/api/v3/servers.rst
+++ b/docs/source/api/v3/servers.rst
@@ -36,7 +36,11 @@ Request Structure
        
+============+==========+===================================================================================================================+
        | cachegroup | no       | Return only those servers within the 
:term:`Cache Group` that has this :ref:`cache-group-id`                      |
        
+------------+----------+-------------------------------------------------------------------------------------------------------------------+
-       | dsId       | no       | Return only those servers assigned to the 
:term:`Delivery Service` identified by this integral, unique identifier |
+       | dsId       | no       | Return only those servers assigned to the 
:term:`Delivery Service` identified by this integral, unique identifier.|
+       |            |          | If the Delivery Service has a 
:term:`Topology` assigned to it, the :ref:`to-api-servers` endpoint will return 
    |
+       |            |          | each server whose :term:`Cache Group` is 
associated with a :term:`Topology Node` of that Topology and has the     |
+       |            |          | :term:`Server Capabilities` that are          
                                                                    |
+       |            |          | :term:`required by the Delivery Service 
<Delivery Service required capabilities>`.                                |
        
+------------+----------+-------------------------------------------------------------------------------------------------------------------+
        | hostName   | no       | Return only those servers that have this 
(short) hostname                                                         |
        
+------------+----------+-------------------------------------------------------------------------------------------------------------------+
diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst
index 0cf0493..75a8c51 100644
--- a/docs/source/glossary.rst
+++ b/docs/source/glossary.rst
@@ -422,6 +422,14 @@ Glossary
        Tenancies
                Users are grouped into :dfn:`Tenants` (or :dfn:`Tenancies`) to 
segregate ownership of and permissions over :term:`Delivery Services` and their 
resources. To be clear, the notion of :dfn:`Tenancy` **only** applies within 
the context of :term:`Delivery Services` and does **not** apply permissions 
restrictions to any other aspect of Traffic Control.
 
+       Topology Node
+       Topology Nodes
+       Parent Topology Node
+       Parent Topology Nodes
+       Child Topology Node
+       Child Topology Nodes
+               Each :dfn:`Topology Node` is associated with a particular 
:term:`Cache Group`. In addition, the Topology Node has 0, 1, or 2 Parent 
Topology Nodes and has 0, 1, or 2 Child Topology Nodes, according to your 
configuration.
+
        Topology
        Topologies
                A structure composed of :term:`Cache Groups` and parent 
relationships, which is assignable to one or more :term:`Delivery Services`.
diff --git a/traffic_ops/testing/api/v3/servers_test.go 
b/traffic_ops/testing/api/v3/servers_test.go
index a6bbe49..324e743 100644
--- a/traffic_ops/testing/api/v3/servers_test.go
+++ b/traffic_ops/testing/api/v3/servers_test.go
@@ -32,10 +32,10 @@ func TestServers(t *testing.T) {
        WithObjs(t, []TCObj{CDNs, Types, Tenants, Users, Parameters, Profiles, 
Statuses, Divisions, Regions, PhysLocations, CacheGroups, Topologies, 
DeliveryServices, Servers}, func() {
                GetTestServersIMS(t)
                currentTime := time.Now().UTC().Add(-5 * time.Second)
-               time := currentTime.Format(time.RFC1123)
+               timestamp := currentTime.Format(time.RFC1123)
                var header http.Header
                header = make(map[string][]string)
-               header.Set(rfc.IfModifiedSince, time)
+               header.Set(rfc.IfModifiedSince, timestamp)
                UpdateTestServers(t)
                GetTestServersDetails(t)
                GetTestServers(t)
@@ -214,8 +214,8 @@ func GetTestServersIMS(t *testing.T) {
        var header http.Header
        header = make(map[string][]string)
        futureTime := time.Now().AddDate(0, 0, 1)
-       time := futureTime.Format(time.RFC1123)
-       header.Set(rfc.IfModifiedSince, time)
+       timestamp := futureTime.Format(time.RFC1123)
+       header.Set(rfc.IfModifiedSince, timestamp)
        params := url.Values{}
        for _, server := range testData.Servers {
                if server.HostName == nil {
@@ -256,7 +256,7 @@ func GetTestServers(t *testing.T) {
                        continue
                }
                params.Set("hostName", *server.HostName)
-               resp, _, err := TOSession.GetServers(&params)
+               resp, _, err := TOSession.GetServersWithHdr(&params, nil)
                if err != nil {
                        t.Errorf("cannot GET Server by name '%s': %v - %v", 
*server.HostName, err, resp.Alerts)
                } else if resp.Summary.Count != 1 {
@@ -272,7 +272,7 @@ func GetTestServersDetails(t *testing.T) {
                        t.Errorf("found server with nil hostname: %+v", server)
                        continue
                }
-               resp, _, err := 
TOSession.GetServerDetailsByHostName(*server.HostName)
+               resp, _, err := 
TOSession.GetServerDetailsByHostNameWithHdr(*server.HostName, nil)
                if err != nil {
                        t.Errorf("cannot GET Server Details by name: %v - %v", 
err, resp)
                }
@@ -280,7 +280,7 @@ func GetTestServersDetails(t *testing.T) {
 }
 
 func GetTestServersQueryParameters(t *testing.T) {
-       dses, _, err := TOSession.GetDeliveryServicesNullable()
+       dses, _, err := TOSession.GetDeliveryServicesNullableWithHdr(nil)
        if err != nil {
                t.Fatalf("Failed to get Delivery Services: %v", err)
        }
@@ -295,24 +295,55 @@ func GetTestServersQueryParameters(t *testing.T) {
 
        params := url.Values{}
        params.Add("dsId", strconv.Itoa(*ds.ID))
-       _, _, err = TOSession.GetServers(&params)
+       _, _, err = TOSession.GetServersWithHdr(&params, nil)
        if err != nil {
                t.Fatalf("Failed to get server by Delivery Service ID: %v", err)
        }
 
        currentTime := time.Now().UTC().Add(5 * time.Second)
-       time := currentTime.Format(time.RFC1123)
+       timestamp := currentTime.Format(time.RFC1123)
        var header http.Header
        header = make(map[string][]string)
-       header.Set(rfc.IfModifiedSince, time)
+       header.Set(rfc.IfModifiedSince, timestamp)
        _, reqInf, _ := TOSession.GetServersWithHdr(&params, header)
        if reqInf.StatusCode != http.StatusNotModified {
                t.Errorf("Expected a status code of 304, got %v", 
reqInf.StatusCode)
        }
 
+       foundTopDs := false
+       const topDsXmlId = "ds-top"
+       for _, ds = range dses {
+               if ds.XMLID == nil || *ds.XMLID != topDsXmlId {
+                       continue
+               }
+               foundTopDs = true
+               break
+       }
+       if !foundTopDs {
+               t.Fatalf("unable to find deliveryservice %s", topDsXmlId)
+       }
+       params.Set("dsId", strconv.Itoa(*ds.ID))
+       expectedHostnames := map[string]bool {
+               "edge1-cdn1-cg3": true,
+               "edge2-cdn1-cg3": true,
+               "atlanta-mid-16": true,
+       }
+       response, _, err := TOSession.GetServersWithHdr(&params, nil)
+       if err != nil {
+               t.Fatalf("Failed to get servers by Topology-based Delivery 
Service ID with xmlId %s", err)
+       }
+       if len(response.Response) == 0 {
+               t.Fatalf("Did not find any servers for Topology-based Delivery 
Service with xmlId %s", err)
+       }
+       for _, server := range response.Response {
+               if _, exists := expectedHostnames[*server.HostName]; !exists {
+                       t.Fatalf("expected hostnames %v, actual %s actual: ", 
expectedHostnames, *server.HostName)
+               }
+       }
+
        params.Del("dsId")
 
-       resp, _, err := TOSession.GetServers(nil)
+       resp, _, err := TOSession.GetServersWithHdr(nil, nil)
        if err != nil {
                t.Fatalf("Failed to get servers: %v", err)
        }
@@ -324,7 +355,7 @@ func GetTestServersQueryParameters(t *testing.T) {
        s := resp.Response[0]
 
        params.Add("type", s.Type)
-       if _, _, err := TOSession.GetServers(&params); err != nil {
+       if _, _, err := TOSession.GetServersWithHdr(&params, nil); err != nil {
                t.Errorf("Error getting servers by type: %v", err)
        }
        params.Del("type")
@@ -333,7 +364,7 @@ func GetTestServersQueryParameters(t *testing.T) {
                t.Error("Found server with no Cache Group ID")
        } else {
                params.Add("cachegroup", strconv.Itoa(*s.CachegroupID))
-               if _, _, err := TOSession.GetServers(&params); err != nil {
+               if _, _, err := TOSession.GetServersWithHdr(&params, nil); err 
!= nil {
                        t.Errorf("Error getting servers by Cache Group ID: %v", 
err)
                }
                params.Del("cachegroup")
@@ -343,7 +374,7 @@ func GetTestServersQueryParameters(t *testing.T) {
                t.Error("Found server with no status")
        } else {
                params.Add("status", *s.Status)
-               if _, _, err := TOSession.GetServers(&params); err != nil {
+               if _, _, err := TOSession.GetServersWithHdr(&params, nil); err 
!= nil {
                        t.Errorf("Error getting servers by status: %v", err)
                }
                params.Del("status")
@@ -353,13 +384,13 @@ func GetTestServersQueryParameters(t *testing.T) {
                t.Error("Found server with no Profile ID")
        } else {
                params.Add("profileId", strconv.Itoa(*s.ProfileID))
-               if _, _, err := TOSession.GetServers(&params); err != nil {
+               if _, _, err := TOSession.GetServersWithHdr(&params, nil); err 
!= nil {
                        t.Errorf("Error getting servers by Profile ID: %v", err)
                }
                params.Del("profileId")
        }
 
-       cgs, _, err := TOSession.GetCacheGroupsNullable()
+       cgs, _, err := TOSession.GetCacheGroupsNullableWithHdr(nil)
        if err != nil {
                t.Fatalf("Failed to get Cache Groups: %v", err)
        }
@@ -371,14 +402,14 @@ func GetTestServersQueryParameters(t *testing.T) {
        }
 
        params.Add("parentCacheGroup", strconv.Itoa(*cgs[0].ID))
-       if _, _, err = TOSession.GetServers(&params); err != nil {
+       if _, _, err = TOSession.GetServersWithHdr(&params, nil); err != nil {
                t.Errorf("Error getting servers by parentCacheGroup: %v", err)
        }
        params.Del("parentCacheGroup")
 }
 
 func UniqueIPProfileTestServers(t *testing.T) {
-       serversResp, _, err := TOSession.GetServers(nil)
+       serversResp, _, err := TOSession.GetServersWithHdr(nil, nil)
        if err != nil {
                t.Fatal(err)
        }
@@ -415,7 +446,7 @@ func UniqueIPProfileTestServers(t *testing.T) {
                // Cleanup, don't want to break other tests
                pathParams := url.Values{}
                pathParams.Add("xmppid", xmppId)
-               server, _, err := TOSession.GetServers(&pathParams)
+               server, _, err := TOSession.GetServersWithHdr(&pathParams, nil)
                if err != nil {
                        t.Fatal(err)
                }
@@ -460,7 +491,7 @@ func UpdateTestServers(t *testing.T) {
        params.Add("hostName", hostName)
 
        // Retrieve the server by hostname so we can get the id for the Update
-       resp, _, err := TOSession.GetServers(&params)
+       resp, _, err := TOSession.GetServersWithHdr(&params, nil)
        if err != nil {
                t.Fatalf("cannot GET Server by hostname '%s': %v - %v", 
hostName, err, resp.Alerts)
        }
@@ -508,7 +539,7 @@ func UpdateTestServers(t *testing.T) {
        }
 
        // Retrieve the server to check rack, interfaceName, hostName values 
were updated
-       resp, _, err = TOSession.GetServers(&idParam)
+       resp, _, err = TOSession.GetServersWithHdr(&idParam, nil)
        if err != nil {
                t.Errorf("cannot GET Server by ID: %v - %v", 
*remoteServer.HostName, err)
        }
@@ -551,9 +582,9 @@ func UpdateTestServers(t *testing.T) {
 
        //Check to verify XMPPID never gets updated
        remoteServer.XMPPID = &updatedXMPPID
-       al, _, err := TOSession.UpdateServerByID(*remoteServer.ID, remoteServer)
-       if err != nil {
-               t.Logf("cannot UPDATE Server by ID %d (hostname '%s'): %v - 
%v", *remoteServer.ID, hostName, err, al)
+       al, reqInf, err := TOSession.UpdateServerByID(*remoteServer.ID, 
remoteServer)
+       if err != nil && reqInf.StatusCode != http.StatusBadRequest {
+               t.Logf("error making sure that XMPPID does not get updated, %d 
(hostname '%s'): %v - %v", *remoteServer.ID, hostName, err, al)
        }
 
        //Change back hostname and xmppid to its original name for other tests 
to pass
@@ -563,13 +594,13 @@ func UpdateTestServers(t *testing.T) {
        if err != nil {
                t.Fatalf("cannot UPDATE Server by ID %d (hostname '%s'): %v - 
%v", *remoteServer.ID, hostName, err, alert)
        }
-       resp, _, err = TOSession.GetServers(&params)
+       resp, _, err = TOSession.GetServersWithHdr(&params, nil)
        if err != nil {
                t.Errorf("cannot GET Server by hostName: %v - %v", 
originalHostname, err)
        }
 
        // Assign server to DS and then attempt to update to a different type
-       dses, _, err := TOSession.GetDeliveryServicesNullable()
+       dses, _, err := TOSession.GetDeliveryServicesNullableWithHdr(nil)
        if err != nil {
                t.Fatalf("cannot GET DeliveryServices: %v", err)
        }
@@ -577,7 +608,7 @@ func UpdateTestServers(t *testing.T) {
                t.Fatal("GET DeliveryServices returned no dses, must have at 
least 1 to test invalid type server update")
        }
 
-       serverTypes, _, err := TOSession.GetTypes("server")
+       serverTypes, _, err := TOSession.GetTypesWithHdr(nil, "server")
        if err != nil {
                t.Fatalf("cannot GET Server Types: %v", err)
        }
@@ -617,7 +648,7 @@ func DeleteTestServers(t *testing.T) {
 
                params.Set("hostName", *server.HostName)
 
-               resp, _, err := TOSession.GetServers(&params)
+               resp, _, err := TOSession.GetServersWithHdr(&params, nil)
                if err != nil {
                        t.Errorf("cannot GET Server by hostname '%s': %v - %v", 
*server.HostName, err, resp.Alerts)
                        continue
@@ -641,7 +672,7 @@ func DeleteTestServers(t *testing.T) {
                        }
 
                        // Retrieve the Server to see if it got deleted
-                       resp, _, err := TOSession.GetServers(&params)
+                       resp, _, err := TOSession.GetServersWithHdr(&params, 
nil)
                        if err != nil {
                                t.Errorf("error deleting Server hostname '%s': 
%v - %v", *server.HostName, err, resp.Alerts)
                        }
diff --git a/traffic_ops/testing/api/v3/tc-fixtures.json 
b/traffic_ops/testing/api/v3/tc-fixtures.json
index 7aad2ad..132497f 100644
--- a/traffic_ops/testing/api/v3/tc-fixtures.json
+++ b/traffic_ops/testing/api/v3/tc-fixtures.json
@@ -2763,7 +2763,7 @@
                     "parents": []
                 },
                 {
-                    "cachegroup": "cachegroup1",
+                    "cachegroup": "cachegroup3",
                     "parents": [0, 1]
                 }
             ]
diff --git 
a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go
 
b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go
index 9518e27..44333eb 100644
--- 
a/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go
+++ 
b/traffic_ops/traffic_ops_golang/deliveryservice/deliveryservices_required_capabilities.go
@@ -423,3 +423,12 @@ deliveryservice_id) VALUES (
 :required_capability,
 :deliveryservice_id) RETURNING deliveryservice_id, required_capability, 
last_updated`
 }
+
+// language=SQL
+const HasRequiredCapabilitiesQuery = `
+SELECT EXISTS(
+       SELECT drc.required_capability
+       FROM deliveryservices_required_capability drc
+       WHERE drc.deliveryservice_id = $1
+)
+`
diff --git a/traffic_ops/traffic_ops_golang/server/servers.go 
b/traffic_ops/traffic_ops_golang/server/servers.go
index 43bb78f..b33dd06 100644
--- a/traffic_ops/traffic_ops_golang/server/servers.go
+++ b/traffic_ops/traffic_ops_golang/server/servers.go
@@ -40,6 +40,7 @@ import (
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+       
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/deliveryservice"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/routing/middleware"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/tenant"
        
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/util/ims"
@@ -61,6 +62,43 @@ JOIN status st ON s.status = st.id
 JOIN type t ON s.type = t.id
 `
 
+/* language=SQL */
+const dssTopologiesJoinSubquery = `
+SELECT
+       td.id deliveryservice,
+       s.id "server"
+FROM "server" s
+JOIN cachegroup c on s.cachegroup = c.id
+LEFT JOIN topology_cachegroup tc ON c.name = tc.cachegroup
+LEFT JOIN deliveryservice td ON td.topology = tc.topology
+UNION
+`
+
+/* language=SQL */
+const deliveryServiceServersJoin = `
+FULL OUTER JOIN (
+%s
+SELECT
+       dss.deliveryservice,
+       dss."server"
+FROM deliveryservice_server dss
+) dss ON dss.server = s.id
+JOIN deliveryservice d ON cdn.id = d.cdn_id AND dss.deliveryservice = d.id
+`
+
+/* language=SQL */
+const requiredCapabilitiesCondition = `
+AND (
+       SELECT ARRAY_AGG(ssc.server_capability)
+       FROM server_server_capability ssc
+       WHERE ssc."server" = s.id
+) @> (
+       SELECT ARRAY_AGG(drc.required_capability)
+       FROM deliveryservices_required_capability drc
+       WHERE drc.deliveryservice_id = d.id
+)
+`
+
 const serverCountQuery = `
 SELECT COUNT(s.id)
 ` + serversFromAndJoin
@@ -609,7 +647,7 @@ func Read(w http.ResponseWriter, r *http.Request) {
                log.Warnf("Couldn't get config %v", e)
        }
 
-       servers, serverCount, userErr, sysErr, errCode, maxTime = 
getServers(r.Header, inf.Params, inf.Tx, inf.User, useIMS)
+       servers, serverCount, userErr, sysErr, errCode, maxTime = 
getServers(r.Header, inf.Params, inf.Tx, inf.User, useIMS, *version)
        if maxTime != nil {
                // RFC1123
                date := maxTime.Format("Mon, 02 Jan 2006 15:04:05 MST")
@@ -666,6 +704,13 @@ func ReadID(w http.ResponseWriter, r *http.Request) {
        }
        defer inf.Close()
 
+       // Middleware should've already handled this, so idk why this is a 
pointer at all tbh
+       version := inf.Version
+       if version == nil {
+               middleware.NotImplementedHandler().ServeHTTP(w, r)
+               return
+       }
+
        servers := []tc.ServerNullable{}
        cfg, e := api.GetConfig(r.Context())
        useIMS := false
@@ -674,7 +719,7 @@ func ReadID(w http.ResponseWriter, r *http.Request) {
        } else {
                log.Warnf("Couldn't get config %v", e)
        }
-       servers, _, userErr, sysErr, errCode, _ = getServers(r.Header, 
inf.Params, inf.Tx, inf.User, useIMS)
+       servers, _, userErr, sysErr, errCode, _ = getServers(r.Header, 
inf.Params, inf.Tx, inf.User, useIMS, *version)
        if len(servers) > 1 {
                api.HandleDeprecatedErr(w, r, tx, 
http.StatusInternalServerError, nil, fmt.Errorf("ID '%d' matched more than one 
server (%d total)", inf.IntParams["id"], len(servers)), &alternative)
                return
@@ -713,7 +758,7 @@ JOIN type t ON s.type = t.id ` +
        select max(last_updated) as t from last_deleted l where 
l.table_name='server') as res`
 }
 
-func getServers(h http.Header, params map[string]string, tx *sqlx.Tx, user 
*auth.CurrentUser, useIMS bool) ([]tc.ServerNullable, 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.ServerNullable, 
uint64, error, error, int, *time.Time) {
        var maxTime time.Time
        var runSecond bool
        // Query Parameters to Database Query column mappings
@@ -733,6 +778,7 @@ func getServers(h http.Header, params map[string]string, tx 
*sqlx.Tx, user *auth
 
        usesMids := false
        queryAddition := ""
+       dsHasRequiredCapabilities := false
        if dsIDStr, ok := params[`dsId`]; ok {
                // don't allow query on ds outside user's tenant
                dsID, err := strconv.Atoi(dsIDStr)
@@ -743,10 +789,20 @@ func getServers(h http.Header, params map[string]string, 
tx *sqlx.Tx, user *auth
                if userErr != nil || sysErr != nil {
                        return nil, 0, errors.New("Forbidden"), sysErr, 
http.StatusForbidden, nil
                }
+
+               var joinSubQuery string
+               if version.Major >= 3 {
+                       if err = 
tx.QueryRow(deliveryservice.HasRequiredCapabilitiesQuery, 
dsID).Scan(&dsHasRequiredCapabilities); err != nil {
+                               err = fmt.Errorf("unable to get required 
capabilities for deliveryservice %d: %s", dsID, err)
+                               return nil, 0, nil, err, 
http.StatusInternalServerError, nil
+                       }
+                       joinSubQuery = dssTopologiesJoinSubquery
+               } else {
+                       joinSubQuery = ""
+               }
                // only if dsId is part of params: add join on 
deliveryservice_server table
-               queryAddition = `
-                       FULL OUTER JOIN deliveryservice_server dss ON 
dss.server = s.id
-               `
+               queryAddition = fmt.Sprintf(deliveryServiceServersJoin, 
joinSubQuery)
+
                // depending on ds type, also need to add mids
                dsType, exists, err := dbhelpers.GetDeliveryServiceType(dsID, 
tx.Tx)
                if err != nil {
@@ -760,6 +816,9 @@ func getServers(h http.Header, params map[string]string, tx 
*sqlx.Tx, user *auth
        }
 
        where, orderBy, pagination, queryValues, errs := 
dbhelpers.BuildWhereAndOrderByAndPagination(params, queryParamsToSQLCols)
+       if dsHasRequiredCapabilities {
+               where += requiredCapabilitiesCondition
+       }
        if len(errs) > 0 {
                return nil, 0, util.JoinErrs(errs), nil, http.StatusBadRequest, 
nil
        }
@@ -906,9 +965,10 @@ func getServers(h http.Header, params map[string]string, 
tx *sqlx.Tx, user *auth
                }
        }
 
-       returnable := make([]tc.ServerNullable, 0, len(servers))
-       for _, server := range servers {
-               for _, iface := range interfaces[*server.ID] {
+       returnable := make([]tc.ServerNullable, 0, len(ids))
+       for _, id := range ids {
+               server := servers[id]
+               for _, iface := range interfaces[id] {
                        server.Interfaces = append(server.Interfaces, iface)
                }
                returnable = append(returnable, server)
@@ -1089,8 +1149,15 @@ func Update(w http.ResponseWriter, r *http.Request) {
        }
        defer inf.Close()
 
+       // Middleware should've already handled this, so idk why this is a 
pointer at all tbh
+       version := inf.Version
+       if version == nil {
+               middleware.NotImplementedHandler().ServeHTTP(w, r)
+               return
+       }
+
        //Get original xmppid
-       origSer, _, userErr, sysErr, _, _ := getServers(r.Header, inf.Params, 
inf.Tx, inf.User, false)
+       origSer, _, userErr, sysErr, _, _ := getServers(r.Header, inf.Params, 
inf.Tx, inf.User, false, *version)
        if userErr != nil || sysErr != nil {
                api.HandleErr(w, r, tx, errCode, userErr, sysErr)
                return
@@ -1452,10 +1519,17 @@ func Delete(w http.ResponseWriter, r *http.Request) {
        }
        defer inf.Close()
 
+       // Middleware should've already handled this, so idk why this is a 
pointer at all tbh
+       version := inf.Version
+       if version == nil {
+               middleware.NotImplementedHandler().ServeHTTP(w, r)
+               return
+       }
+
        id := inf.IntParams["id"]
 
        var servers []tc.ServerNullable
-       servers, _, userErr, sysErr, errCode, _ = getServers(r.Header, 
map[string]string{"id": inf.Params["id"]}, inf.Tx, inf.User, false)
+       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)
                return
diff --git a/traffic_ops/traffic_ops_golang/server/servers_test.go 
b/traffic_ops/traffic_ops_golang/server/servers_test.go
index aed94a7..5b00e20 100644
--- a/traffic_ops/traffic_ops_golang/server/servers_test.go
+++ b/traffic_ops/traffic_ops_golang/server/servers_test.go
@@ -27,6 +27,7 @@ import (
        "github.com/apache/trafficcontrol/lib/go-tc"
        "github.com/apache/trafficcontrol/lib/go-util"
 
+       "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/auth"
        "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/test"
 
@@ -256,7 +257,9 @@ func TestGetServersByCachegroup(t *testing.T) {
 
        user := auth.CurrentUser{}
 
-       servers, _, userErr, sysErr, errCode, _ := getServers(nil, v, 
db.MustBegin(), &user, false)
+       version := api.Version{Major: 3, Minor: 0}
+
+       servers, _, userErr, sysErr, errCode, _ := getServers(nil, v, 
db.MustBegin(), &user, false, version)
        if userErr != nil || sysErr != nil {
                t.Errorf("getServers expected: no errors, actual: %v %v with 
status: %s", userErr, sysErr, http.StatusText(errCode))
        }
@@ -360,7 +363,8 @@ func TestGetMidServers(t *testing.T) {
        v := map[string]string{}
 
        user := auth.CurrentUser{}
-       servers, _, userErr, sysErr, errCode, _ := getServers(nil, v, 
db.MustBegin(), &user, false)
+       version := api.Version{Major: 3, Minor: 0}
+       servers, _, userErr, sysErr, errCode, _ := getServers(nil, v, 
db.MustBegin(), &user, false, version)
 
        if userErr != nil || sysErr != nil {
                t.Errorf("getServers expected: no errors, actual: %v %v with 
status: %s", userErr, sysErr, http.StatusText(errCode))
diff --git a/traffic_ops/v3-client/server.go b/traffic_ops/v3-client/server.go
index 2e2b8d8..dc71b1b 100644
--- a/traffic_ops/v3-client/server.go
+++ b/traffic_ops/v3-client/server.go
@@ -139,6 +139,7 @@ func (to *Session) UpdateServerByID(id int, server 
tc.ServerNullable) (tc.Alerts
        route := fmt.Sprintf("%s/%d", API_SERVERS, id)
        resp, remoteAddr, err := to.request(http.MethodPut, route, reqBody, nil)
        reqInf.RemoteAddr = remoteAddr
+       reqInf.StatusCode = resp.StatusCode
        if err != nil {
                return alerts, reqInf, err
        }
diff --git a/traffic_ops/v3-client/type.go b/traffic_ops/v3-client/type.go
index fe32257..ce1fdc1 100644
--- a/traffic_ops/v3-client/type.go
+++ b/traffic_ops/v3-client/type.go
@@ -72,7 +72,7 @@ func (to *Session) UpdateTypeByID(id int, typ tc.Type) 
(tc.Alerts, ReqInf, error
 // If a 'useInTable' parameter is passed, the returned Types are restricted to 
those with
 // that exact 'useInTable' property. Only exactly 1 or exactly 0 'useInTable' 
parameters may
 // be passed; passing more will result in an error being returned.
-func (to *Session) GetTypesWithHdr(header http.Header, useInTable []string) 
([]tc.Type, ReqInf, error) {
+func (to *Session) GetTypesWithHdr(header http.Header, useInTable ...string) 
([]tc.Type, ReqInf, error) {
        if len(useInTable) > 1 {
                return nil, ReqInf{}, errors.New("Please pass in a single value 
for the 'useInTable' parameter")
        }
@@ -114,7 +114,7 @@ func (to *Session) GetTypesWithHdr(header http.Header, 
useInTable []string) ([]t
 // 'useInTable' parameters may be passed; passing more will result in an error 
being returned.
 // Deprecated: GetTypes will be removed in 6.0. Use GetTypesWithHdr.
 func (to *Session) GetTypes(useInTable ...string) ([]tc.Type, ReqInf, error) {
-       return to.GetTypesWithHdr(nil, useInTable)
+       return to.GetTypesWithHdr(nil, useInTable...)
 }
 
 // GetTypeByID GETs a Type by the Type ID, and filters by http header params 
in the request.
diff --git 
a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html
 
b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html
index bc3e2ec..345044e 100644
--- 
a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.DNS.tpl.html
@@ -59,7 +59,7 @@ under the License.
                     <li role="menuitem"><a ng-click="viewOrigins()">Manage 
Origins</a></li>
                     <li role="menuitem"><a ng-click="viewRegexes()">Manage 
Regexes</a></li>
                     <li role="menuitem"><a 
ng-click="viewCapabilities()">Manage Required Server Capabilities</a></li>
-                    <li ng-if="::(!deliveryService.topology)" 
role="menuitem"><a ng-click="viewServers()">Manage Servers</a></li>
+                    <li role="menuitem"><a ng-click="viewServers()">Manage 
Servers</a></li>
                     <li role="menuitem"><a 
ng-click="viewStaticDnsEntries()">Manage Static DNS Entries</a></li>
                 </ul>
             </div>
diff --git 
a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html
 
b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html
index 5ba80a3..fc0d626 100644
--- 
a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.HTTP.tpl.html
@@ -59,7 +59,7 @@ under the License.
                     <li role="menuitem"><a ng-click="viewOrigins()">Manage 
Origins</a></li>
                     <li role="menuitem"><a ng-click="viewRegexes()">Manage 
Regexes</a></li>
                     <li role="menuitem"><a 
ng-click="viewCapabilities()">Manage Required Server Capabilities</a></li>
-                    <li ng-if="::(!deliveryService.topology)" 
role="menuitem"><a ng-click="viewServers()">Manage Servers</a></li>
+                    <li role="menuitem"><a ng-click="viewServers()">Manage 
Servers</a></li>
                     <li role="menuitem"><a 
ng-click="viewStaticDnsEntries()">Manage Static DNS Entries</a></li>
                 </ul>
             </div>
diff --git 
a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html
 
b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html
index e02acd8..7def518 100644
--- 
a/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/form/deliveryService/form.deliveryService.anyMap.tpl.html
@@ -51,7 +51,7 @@ under the License.
                     <hr class="divider"/>
                     <li role="menuitem"><a ng-click="viewCharts()">View 
Charts</a></li>
                     <hr class="divider"/>
-                    <li ng-if="::(!deliveryService.topology)" 
role="menuitem"><a ng-click="viewServers()">Manage Servers</a></li>
+                    <li role="menuitem"><a ng-click="viewServers()">Manage 
Servers</a></li>
                 </ul>
             </div>
         </div>
diff --git 
a/traffic_portal/app/src/common/modules/form/topology/form.topology.tpl.html 
b/traffic_portal/app/src/common/modules/form/topology/form.topology.tpl.html
index a1481e9..b2be7bf 100644
--- a/traffic_portal/app/src/common/modules/form/topology/form.topology.tpl.html
+++ b/traffic_portal/app/src/common/modules/form/topology/form.topology.tpl.html
@@ -87,7 +87,7 @@ under the License.
             <a ng-if="node.cachegroup" title="View Servers Assigned to 
{{::node.cachegroup}}" class="btn btn-primary btn-xs" data-nodrag 
ng-click="viewCacheGroupServers(node)" style="margin-right: 8px;">
                 <i class="fa fa-server"></i>
             </a>
-            <a title="Add child cache groups to {{nodeLabel(node)}}" 
class="btn btn-primary btn-xs" data-nodrag ng-click="addCacheGroups(node, 
this)" style="margin-right: 8px;">
+            <a title="Add child cache groups to {{nodeLabel(node)}}" 
class="btn btn-primary btn-xs add-child-cg-btn" data-nodrag 
ng-click="addCacheGroups(node, this)" style="margin-right: 8px;">
                 <i class="fa fa-plus"></i>
             </a>
             <a ng-if="node.cachegroup" title="Remove {{::node.cachegroup}} 
Cache Group" class="btn btn-danger btn-xs" data-nodrag 
ng-click="deleteCacheGroup(node, this)">
diff --git 
a/traffic_portal/app/src/common/modules/table/deliveryServiceServers/table.deliveryServiceServers.tpl.html
 
b/traffic_portal/app/src/common/modules/table/deliveryServiceServers/table.deliveryServiceServers.tpl.html
index c0ae775..e1d17db 100644
--- 
a/traffic_portal/app/src/common/modules/table/deliveryServiceServers/table.deliveryServiceServers.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/table/deliveryServiceServers/table.deliveryServiceServers.tpl.html
@@ -22,7 +22,7 @@ under the License.
         <ol class="breadcrumb pull-left">
             <li><a href="#!/delivery-services">Delivery Services</a></li>
             <li><a name="dsLink" ng-href="{{'#!/delivery-services/' + 
deliveryService.id + '?type=' + 
deliveryService.type}}">{{::deliveryService.xmlId}}</a></li>
-            <li class="active">Servers</li>
+            <li class="active">Servers <small 
ng-if="deliveryService.topology">[via {{deliveryService.topology}} 
Topology]</small></li>
         </ol>
         <div class="pull-right">
             <div class="form-inline" role="search">
@@ -76,7 +76,7 @@ under the License.
         </li>
         <hr class="divider"/>
         <li role="menuitem">
-            <button type="button" ng-click="confirmRemoveServer(server, 
$event)" ng-disabled="deliveryService.topology">Remove Server from Delivery 
Service</button>
+            <button type="button" ng-click="confirmRemoveServer(server, 
$event)" ng-disabled="deliveryService.topology || (!isEdge(server) && 
!isOrigin(server))">Remove Server from Delivery Service</button>
         </li>
         <hr class="divider"/>
         <li role="menuitem">
diff --git 
a/traffic_portal/app/src/common/modules/table/topologies/table.topologies.tpl.html
 
b/traffic_portal/app/src/common/modules/table/topologies/table.topologies.tpl.html
index 1cf20b3..608e0a2 100644
--- 
a/traffic_portal/app/src/common/modules/table/topologies/table.topologies.tpl.html
+++ 
b/traffic_portal/app/src/common/modules/table/topologies/table.topologies.tpl.html
@@ -23,7 +23,7 @@ under the License.
             <li class="active">Topologies</li>
         </ol>
         <div class="pull-right">
-            <button type="button" class="btn btn-primary" title="Create 
Topology" ng-click="createTopology()"><i class="fa fa-plus"></i></button>
+            <button type="button" name="createTopologyBtn" class="btn 
btn-primary" title="Create Topology" ng-click="createTopology()"><i class="fa 
fa-plus"></i></button>
             <button type="button" class="btn btn-default" title="Refresh" 
ng-click="refresh()"><i class="fa fa-refresh"></i></button>
         </div>
         <div class="clearfix"></div>
diff --git 
a/traffic_portal/app/src/modules/private/deliveryServices/servers/index.js 
b/traffic_portal/app/src/modules/private/deliveryServices/servers/index.js
index f54b518..db8bd6f 100644
--- a/traffic_portal/app/src/modules/private/deliveryServices/servers/index.js
+++ b/traffic_portal/app/src/modules/private/deliveryServices/servers/index.js
@@ -31,11 +31,7 @@ module.exports = 
angular.module('trafficPortal.private.deliveryServices.servers'
                                                                return 
deliveryServiceService.getDeliveryService($stateParams.deliveryServiceId);
                                                        },
                                                        servers: 
function(deliveryService, $stateParams, serverService) {
-                                                               if 
(deliveryService.topology) {
-                                                                       return 
serverService.getServers({ topology: deliveryService.topology });
-                                                               } else {
-                                                                       return 
serverService.getServers({ dsId: $stateParams.deliveryServiceId });
-                                                               }
+                                                               return 
serverService.getServers({ dsId: $stateParams.deliveryServiceId, orderby: 
'hostName' });
                                                        },
                                                        filter: function() {
                                                                return null;
diff --git a/traffic_portal/app/src/styles/main.scss 
b/traffic_portal/app/src/styles/main.scss
index 37dc352..e232b5c 100644
--- a/traffic_portal/app/src/styles/main.scss
+++ b/traffic_portal/app/src/styles/main.scss
@@ -138,6 +138,9 @@ body.nav-sm .container.body .main-content {
     background-color: white;
     border-radius: 0;
     font-size: 24px;
+    small {
+      font-size: medium;
+    }
   }
 
   .input-group-addon > label {
diff --git a/traffic_portal/test/end_to_end/conf.json 
b/traffic_portal/test/end_to_end/conf.json
index aa1ecb6..6cf787a 100644
--- a/traffic_portal/test/end_to_end/conf.json
+++ b/traffic_portal/test/end_to_end/conf.json
@@ -19,6 +19,7 @@
       "login/login-spec.js",
       "CDNs/cdns-spec.js",
       "cacheGroups/cache-groups-spec.js",
+      "topologies/topologies-spec.js",
       "profiles/profiles-spec.js",
       "divisions/divisions-spec.js",
       "regions/regions-spec.js",
diff --git 
a/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js 
b/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js
index 6b9281e..100f112 100644
--- a/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js
+++ b/traffic_portal/test/end_to_end/deliveryServices/delivery-services-spec.js
@@ -264,7 +264,7 @@ describe('Traffic Portal Delivery Services Suite', 
function() {
        });
 
        it('should populate and submit the delivery service form', function() {
-               console.log('Creating a HTTP DS for ' + mockVals.dnsXmlId);
+               console.log('Creating a HTTP DS with a topology for ' + 
mockVals.httpXmlId);
                
expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/delivery-services/new?type=HTTP");
                expect(pageData.createButton.isEnabled()).toBe(false);
                // set required fields
@@ -288,6 +288,8 @@ describe('Traffic Portal Delivery Services Suite', 
function() {
                commonFunctions.selectDropdownbyNum(pageData.protocol, 1);
                // all required fields have been set, create button should be 
enabled
                expect(pageData.createButton.isEnabled()).toBe(true);
+               // set topology
+               commonFunctions.selectDropdownbyNum(pageData.topology, 1);
                pageData.createButton.click();
        });
 
@@ -355,6 +357,16 @@ describe('Traffic Portal Delivery Services Suite', 
function() {
                });
        });
 
+       it('should navigate back to the HTTP delivery service and view all 
servers utilized per the assigned topology', function() {
+               console.log('Viewing all servers utilized by ' + 
mockVals.httpXmlId);
+               pageData.dsLink.click();
+               pageData.moreBtn.click();
+               pageData.manageServersMenuItem.click();
+               
expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toMatch(commonFunctions.urlPath(browser.baseUrl)+"#!/delivery-services/[0-9]+/servers");
+               console.log('The ability to assign servers is disabled for ' + 
mockVals.httpXmlId);
+               expect(pageData.selectServersBtn.isEnabled()).toBe(false);
+       });
+
        it('should navigate back to the HTTP delivery service and delete it', 
function() {
                console.log('Deleting ' + mockVals.httpXmlId);
                pageData.dsLink.click();
diff --git a/traffic_portal/test/end_to_end/deliveryServices/pageData.js 
b/traffic_portal/test/end_to_end/deliveryServices/pageData.js
index d30ef04..f17a010 100644
--- a/traffic_portal/test/end_to_end/deliveryServices/pageData.js
+++ b/traffic_portal/test/end_to_end/deliveryServices/pageData.js
@@ -35,6 +35,7 @@ module.exports = function(){
        this.cdn=element(by.name('cdn'));
        this.orgServerFqdn=element(by.name('orgServerFqdn'));
        this.protocol=element(by.name('protocol'));
+       this.topology=element(by.name('topology'));
        this.longDesc=element(by.name('longDesc'));
        this.remapText=element(by.name('remapText'));
        this.createButton=element(by.buttonText('Create'));
diff --git a/traffic_portal/test/end_to_end/topologies/pageData.js 
b/traffic_portal/test/end_to_end/topologies/pageData.js
new file mode 100644
index 0000000..043ec8f
--- /dev/null
+++ b/traffic_portal/test/end_to_end/topologies/pageData.js
@@ -0,0 +1,27 @@
+/*
+ * 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.
+ */
+
+module.exports = function(){
+       this.name=element(by.name('name'));
+       this.description=element(by.id('description'));
+       this.addChildCacheGroupBtn=element(by.css('.add-child-cg-btn'));
+       this.selectFormSubmitButton=element(by.buttonText('Submit'));
+       this.selectAllCB=element(by.id('selectAllCB'));
+       this.createButton=element(by.buttonText('Create'));
+};
diff --git a/traffic_portal/test/end_to_end/topologies/topologies-spec.js 
b/traffic_portal/test/end_to_end/topologies/topologies-spec.js
new file mode 100644
index 0000000..ec0987a
--- /dev/null
+++ b/traffic_portal/test/end_to_end/topologies/topologies-spec.js
@@ -0,0 +1,71 @@
+/*
+ * 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.
+ */
+
+var pd = require('./pageData.js');
+var cfunc = require('../common/commonFunctions.js');
+
+describe('Traffic Portal Topologies Test Suite', function() {
+       const pageData = new pd();
+       const commonFunctions = new cfunc();
+       const ec = protractor.ExpectedConditions;
+       const myNewTopology = {
+               name: 'topology-' + 
commonFunctions.shuffle('abcdefghijklmonpqrstuvwxyz0123456789'),
+               desc: 'topology-' + 
commonFunctions.shuffle('abcdefghijklmonpqrstuvwxyz0123456789')
+       };
+
+       it('should go to the topologies page', function() {
+               console.log("Go to the topologies page");
+               browser.setLocation("topologies");
+               
expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/topologies");
+       });
+
+       it('should verify CSV link exists ', function() {
+               console.log("Verify CSV button exists");
+               
expect(element(by.css('.dt-button.buttons-csv')).isPresent()).toBe(true);
+       });
+
+       it('should open new topology form page', function() {
+               console.log("Open new topology form page");
+               
browser.driver.findElement(by.name('createTopologyBtn')).click();
+               
expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/topologies/new");
+       });
+
+       it('should build a new topology', function () {
+               console.log("Building a new topology");
+               pageData.addChildCacheGroupBtn.click();
+               expect(pageData.selectFormSubmitButton.isEnabled()).toBe(false);
+               
browser.driver.findElement(by.name('selectFormDropdown')).sendKeys('EDGE_LOC');
+               expect(pageData.selectFormSubmitButton.isEnabled()).toBe(true);
+               pageData.selectFormSubmitButton.click();
+               browser.wait(ec.presenceOf(pageData.selectAllCB), 5000);
+               pageData.selectAllCB.click();
+               pageData.selectFormSubmitButton.click();
+       });
+
+       it('should fill out the rest of the topology form, create button is 
enabled and submit', function () {
+               console.log("Filling out topology form, check create button is 
enabled and submit");
+               expect(pageData.createButton.isEnabled()).toBe(false);
+               pageData.name.sendKeys(myNewTopology.name);
+               pageData.description.sendKeys(myNewTopology.desc);
+               expect(pageData.createButton.isEnabled()).toBe(true);
+               pageData.createButton.click();
+               
expect(browser.getCurrentUrl().then(commonFunctions.urlPath)).toEqual(commonFunctions.urlPath(browser.baseUrl)+"#!/topologies");
+       });
+
+});

Reply via email to