You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@trafficcontrol.apache.org by ra...@apache.org on 2020/05/13 22:50:14 UTC
[trafficcontrol] branch master updated: Flexible Topologies in
Traffic Ops and Traffic Portal (#4633)
This is an automated email from the ASF dual-hosted git repository.
rawlin 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 88f4f2a Flexible Topologies in Traffic Ops and Traffic Portal (#4633)
88f4f2a is described below
commit 88f4f2aad0820ed81f4a57c0ed1bf2ff0b7436c1
Author: Zach Hoffman <za...@zrhoffman.net>
AuthorDate: Wed May 13 16:50:03 2020 -0600
Flexible Topologies in Traffic Ops and Traffic Portal (#4633)
* Topology Goose migrations
- Create topology table
- Create topology_cachegroup table
- Create topology_cachegroup_parents table
- Goose migration to add topology column to deliveryservice table
* POST /topologies
* Validation so far
* Topologies endpoints for TO Python client
* Topologies endpoint functions for Go client library
* Create all nodes in a single query and all parents in a single query
* adds toplogy views
* adds ability to add cache groups to a top
* adds fake origin layer
* only allow org_loc to be at top of topology and moved some files around
* adds the ability to see all servers assigned to a topology cg
* GET /topologies
* adds the ability to view servers for a cachgroup from the cg assignment view
* adds the ability to select a 2nd parent cg
* adds the ability to assign a second parent to nodes
* some code cleanup
* allows the selection of an item as well as determining whether a selection is required to submit the form
* allows the selection of an item as well as determining whether a selection is required to submit the form
* adds error message to node and cleanup
* hooks up topology save in a fake way until api is ready
* moves some more functionality into topologyutils
* turning off create temporarily
* makes a copy of the topology nodes to break references
* added note to json section
* removes uneccesary id and type from normalized topology
* fixed but with root type
* disables save button if topology has errors
* fixes the select all button
* fixes column widths in cg selection pane
* adds the ability to create a topology
* messing with some styles
* changes top.desc to top.description and hooks up GET and POST topologies
* adds MIT license info for angular-ui-tree component
* moves tree css into own file
* moves css back to correct spot
* adds license info for angular-ui-tree css
* moving fake stubs and halting non-implemented functionality
* Fix merge conflicts
* Bugfix: Order parents inside array_agg to support 2 parents
* fixes bug where parent was not being set properly when only one valid parent (zrhoffman/trafficcontrol#2)
* Abstract out adding nodes and adding parents into their own methods
* performed some cleanup and added some comments (#3)
* Bugfix: Return a propert HTTP status code from Read()
* PUT /topologies?name={{name}}
* Select query: Specify that we wants parents of a cachegroup in a
specific topology, not just parents of a cachegroup
* more cleanup (#4)
* Bugfix: Rebind topology variable for a new topology
* PUT /topologies?name={{name}} route
* Bugfix: Do not mutate params when getting a cachegroup by name
* hooks in topologies PUT (#5)
* DELETE /topologies?name={{name}}
* hooks topology delete up in TP (#6)
* Make tc.Topology.Nodes a pointer
* Check for cycles in the topology
* Check for leaf mids
* Validation improvements
- Fix test for no nodes
- Do not perform validation related to cachegroup types if all
cachegroups were not found
* Topologies API tests
* Fix topologies API tests
- Make the cyclical nodes topology have a cycle
- Negate condition that asserts that checks if POSTing the invalid
topology succeeded
* Do not let a topology node be a parent of itself
* Topologies docs
* Add license headers
* TO Changelog entry
* adds expand on hover (zrhoffman/trafficcontrol#7)
* PUT and DELETE should be auth.PrivLevelOperations
* removes some uses of underscore.js (zrhoffman/trafficcontrol#8)
* hides topologies menu item until feature is complete (zrhoffman/trafficcontrol#9)
* Newer timestamps for Topologies goose migration
* changes per PR review (zrhoffman/trafficcontrol#10)
* minifies angular-ui-tree css
* code simplification and cleanup per PR review
* adds button types
* safe way of invoking query params
* removes debugging pane (zrhoffman/trafficcontrol#11)
* Underline/overline just right
* Use spaces for alignment
* gofmt
* fixes license issues (zrhoffman/trafficcontrol#12)
* Return err, not nil
* Return the result of api.ParseDBError() instead of binding it to
userErr, sysErr, and errCode and then returning
* Add function name and period
* Simplify child/parent/rank uniqueness constraint
* id column: serial -> bigserial
* Add an extra newline before each sub-list
* Add a period
* Add a content type to the POST request
* Add a content-type header to the PUT request
* Add godocs
* Capitalize Topology
And link to the glossary
* Remove tabs from migration
* Revert "Simplify child/parent/rank uniqueness constraint"
This reverts commit fbd99fae0ff38fed859f010418282681096c46b4.
* Move Topologies from API v2.0 to v3.0
* Updated docs API version
* adds button type (zrhoffman/trafficcontrol#13)
* upgrades TP from api v2 to v3 (zrhoffman/trafficcontrol#14)
* reverts TP to v2 and introduces v3 for topologies (zrhoffman/trafficcontrol#15)
* fixes bug where if you select all and then filter, the all selection would not filter. (zrhoffman/trafficcontrol#16)
* Do not use pointers for topology nodes
* Fix warnings
* Group import with other TO imports
* UpdateTopologyByID -> UpdateTopology
* No need to load Parameters objects for Topologies tests
* Swap comments
* Make a struct for invalid topologies instead of twi separate slices
* Make failed assertions fatal
* - Test UpdateTopologies
- Fix UpdateTopologies function definition
* Fix mixed up variable names from resolving "Receiver names are
different" warning in 36e085494f
* Retrieve all cache groups for topology validation in a single query
* Rename cachegroup type constants
- EdgeCacheGroupType -> CacheGroupEdgeTypeName
- MidCacheGroupType -> CacheGroupMidTypeName
* Move parentCachegroup2 fixture to v3 API tests
* Correct term: Topology -> Cache Group
* gofmt
* Defer closing DB connections
* No need to delete nodes and parents when deleting an entire topology
* Remove unnecessary blank argument
* Use log.Close() to close responses
* adds changelog entry and shows topologies menu item in TP (zrhoffman/trafficcontrol#17)
Co-authored-by: Jeremy Mitchell <mi...@gmail.com>
Co-authored-by: Jeremy Mitchell <mi...@users.noreply.github.com>
---
.dependency_license | 2 +
CHANGELOG.md | 3 +
LICENSE | 5 +
docs/source/api/v3/topologies.rst | 593 +++++++++++++++++++++
docs/source/glossary.rst | 4 +
lib/go-tc/cachegroups.go | 1 +
lib/go-tc/enum.go | 6 +-
lib/go-tc/topologies.go | 49 ++
licenses/MIT-angular_ui_tree | 20 +
.../clients/python/trafficops/tosession.py | 49 ++
.../20200422101648_create_topology_tables.sql | 68 +++
traffic_ops/client/topology.go | 123 +++++
traffic_ops/testing/api/v3/tc-fixtures.json | 49 ++
traffic_ops/testing/api/v3/topologies_test.go | 141 +++++
traffic_ops/testing/api/v3/traffic_control_test.go | 1 +
traffic_ops/testing/api/v3/withobjs_test.go | 2 +
.../traffic_ops_golang/api/shared_handlers_test.go | 2 +-
.../traffic_ops_golang/cachegroup/cachegroups.go | 47 ++
traffic_ops/traffic_ops_golang/routing/routes.go | 6 +
.../traffic_ops_golang/topology/cycle_detection.go | 124 +++++
.../traffic_ops_golang/topology/topologies.go | 473 ++++++++++++++++
.../traffic_ops_golang/topology/validation.go | 115 ++++
.../trafficstats/deliveryservice.go | 8 +-
traffic_portal/app/src/app.js | 11 +
.../src/assets/css/angular-ui-tree.min_2.22.6.css | 2 +
.../src/assets/js/angular-ui-tree.min_2.22.6.js | 2 +
.../app/src/common/api/TopologyService.js | 72 +++
traffic_portal/app/src/common/api/index.js | 1 +
.../dialog/select/DialogSelectController.js | 4 +
.../modules/dialog/select/dialog.select.tpl.html | 2 +-
.../form/topology/FormTopologyController.js | 257 +++++++++
.../topology/edit/FormEditTopologyController.js | 71 +++
.../modules/form/topology/edit/index.js} | 11 +-
.../modules/form/topology/form.topology.tpl.html | 93 ++++
.../modules/form/topology/index.js} | 11 +-
.../form/topology/new/FormNewTopologyController.js | 44 ++
.../modules/form/topology/new/index.js} | 11 +-
.../common/modules/navigation/navigation.tpl.html | 1 +
.../table/topologies/TableTopologiesController.js} | 29 +-
.../modules/table/topologies/index.js} | 11 +-
.../table/topologies/table.topologies.tpl.html | 51 ++
.../TableTopologyCacheGroupServersController.js} | 44 +-
.../table/topologyCacheGroupServers/index.js} | 11 +-
.../table.topologyCacheGroupServers.tpl.html} | 31 +-
.../TableSelectTopologyCacheGroupsController.js | 198 +++++++
.../modules/table/topologyCacheGroups/index.js} | 11 +-
.../table.selectTopologyCacheGroups.tpl.html | 55 ++
.../app/src/common/service/utils/TopologyUtils.js | 111 ++++
.../app/src/common/service/utils/index.js | 3 +-
traffic_portal/app/src/index.html | 2 +
.../private/topologies/edit}/index.js | 35 +-
.../private/topologies/index.js} | 24 +-
.../private/topologies/list/index.js} | 29 +-
.../private/topologies/new/index.js} | 34 +-
.../modules/private/topologies/topologies.tpl.html | 22 +
traffic_portal/app/src/scripts/config.js | 2 +-
traffic_portal/app/src/styles/main.scss | 33 +-
57 files changed, 3080 insertions(+), 140 deletions(-)
diff --git a/.dependency_license b/.dependency_license
index 7ca957d..941f286 100644
--- a/.dependency_license
+++ b/.dependency_license
@@ -115,6 +115,8 @@ traffic_portal/app/src/assets/js/chartjs/angular-chart\..*, BSD
traffic_portal/app/src/assets/css/jsonformatter\..*, Apache
traffic_portal/app/src/assets/js/jsonformatter\..*, Apache
traffic_portal/app/src/assets/js/fast-json-patch\..*, MIT
+traffic_portal/app/src/assets/css/angular-ui-tree\..*, MIT
+traffic_portal/app/src/assets/js/angular-ui-tree\..*, MIT
traffic_portal/app/src/assets/css/colReorder.dataTables\..*, MIT
traffic_portal/app/src/assets/js/colReorder.dataTables\..*, MIT
traffic_portal/app/src/assets/js/dataTables.buttons\..*, MIT
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 20588b7..5101f7a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/).
### Added
- Traffic Ops API v3
- Added an optional readiness check service to cdn-in-a-box that exits successfully when it is able to get a `200 OK` from all delivery services
+- [Flexible Topologies (in progress)](https://github.com/apache/trafficcontrol/blob/master/blueprints/flexible-topologies.md)
+ - Traffic Ops: Added an API 3.0 endpoint, /api/3.0/topologies, to create, read, update and delete flexible topologies.
+ - Traffic Portal: Added the ability to create, read, update and delete flexible topologies.
### Fixed
- Fixed the `GET /api/x/jobs` and `GET /api/x/jobs/:id` Traffic Ops API routes to allow falling back to Perl via the routing blacklist
diff --git a/LICENSE b/LICENSE
index 544a666..0d0f1f9 100644
--- a/LICENSE
+++ b/LICENSE
@@ -273,6 +273,11 @@ For the angular-moment-picker component:
@traffic_portal/app/src/assets/js/moment-picker/angular-moment-picker.min_0.10.2.js
./licenses/MIT-angular_moment_picker
+For the angular-ui-tree component:
+@traffic_portal/app/src/assets/css/angular-ui-tree.min_2.22.6.css
+@traffic_portal/app/src/assets/js/angular-ui-tree.min_2.22.6.js
+./licenses/MIT-angular_ui_tree
+
For the Chart.js component:
@traffic_portal/app/src/assets/js/chartjs/Chart.min_2.7.2.js
./licenses/MIT-chartjs
diff --git a/docs/source/api/v3/topologies.rst b/docs/source/api/v3/topologies.rst
new file mode 100644
index 0000000..3648a4a
--- /dev/null
+++ b/docs/source/api/v3/topologies.rst
@@ -0,0 +1,593 @@
+..
+..
+.. Licensed under the Apache License, Version 2.0 (the "License");
+.. you may not use this file except in compliance with the License.
+.. You may obtain a copy of the License at
+..
+.. http://www.apache.org/licenses/LICENSE-2.0
+..
+.. Unless required by applicable law or agreed to in writing, software
+.. distributed under the License is distributed on an "AS IS" BASIS,
+.. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+.. See the License for the specific language governing permissions and
+.. limitations under the License.
+..
+
+.. _to-api-topologies:
+
+**************
+``topologies``
+**************
+
+``GET``
+=======
+Retrieves :term:`Topologies`.
+
+:Auth. Required: Yes
+:Roles Required: "read-only"
+:Response Type: Array
+
+Request Structure
+-----------------
+.. table:: Request Query Parameters
+
+ +------+----------+-----------------------------------------------------+
+ | Name | Required | Description |
+ +======+==========+=====================================================+
+ | name | no | Return the :term:`Topology` with this name |
+ +------+----------+-----------------------------------------------------+
+
+.. code-block:: http
+ :caption: Request Example
+
+ GET /api/3.0/topologies HTTP/1.1
+ User-Agent: python-requests/2.23.0
+ Accept-Encoding: gzip, deflate
+ Accept: */*
+ Connection: keep-alive
+ Cookie: mojolicious=...
+
+Response Structure
+------------------
+:description: A short sentence that describes the :term:`Topology`.
+:lastUpdated: The date and time at which this :term:`Topology` was last updated, in ISO-like format
+:name: The name of the :term:`Topology`. This can only be letters, numbers, and dashes.
+:nodes: An array of nodes in the :term:`Topology`
+
+ :cachegroup: The name of a :term:`Cache Group`
+ :parents: The indices of the parents of this node in the nodes array, 0-indexed. 2 parents max
+
+.. code-block:: http
+ :caption: Response Example
+
+ HTTP/1.1 200 OK
+ Access-Control-Allow-Credentials: true
+ Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
+ Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
+ Access-Control-Allow-Origin: *
+ Content-Encoding: gzip
+ Content-Type: application/json
+ Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 13 Apr 2020 18:22:32 GMT; Max-Age=3600; HttpOnly
+ Whole-Content-Sha512: lF4MCJCinuQWz0flLAAZBrzbuPVsHrNn2BtTozRZojEjGpm76IsXBQK5QOwSwBoHac+D0C1Z3p7M8kdjcfgIIg==
+ X-Server-Name: traffic_ops_golang/
+ Date: Mon, 13 Apr 2020 17:22:32 GMT
+ Content-Length: 205
+
+ {
+ "response": [
+ {
+ "description": "This is my topology",
+ "name": "my-topology",
+ "nodes": [
+ {
+ "cachegroup": "edge1",
+ "parents": [
+ 7
+ ]
+ },
+ {
+ "cachegroup": "edge2",
+ "parents": [
+ 7,
+ 8
+ ]
+ },
+ {
+ "cachegroup": "edge3",
+ "parents": [
+ 8,
+ 9
+ ]
+ },
+ {
+ "cachegroup": "edge4",
+ "parents": [
+ 9
+ ]
+ },
+ {
+ "cachegroup": "mid1",
+ "parents": []
+ },
+ {
+ "cachegroup": "mid2",
+ "parents": [
+ 4
+ ]
+ },
+ {
+ "cachegroup": "mid3",
+ "parents": [
+ 4
+ ]
+ },
+ {
+ "cachegroup": "mid4",
+ "parents": [
+ 5
+ ]
+ },
+ {
+ "cachegroup": "mid5",
+ "parents": [
+ 5,
+ 6
+ ]
+ },
+ {
+ "cachegroup": "mid6",
+ "parents": [
+ 6
+ ]
+ }
+ ],
+ "lastUpdated": "2020-04-13 17:12:34+00"
+ }
+ ]
+ }
+
+``POST``
+========
+Create a new :term:`Topology`.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Response Type: Object
+
+Request Structure
+-----------------
+:description: A short sentence that describes the topology.
+:name: The name of the topology. This can only be letters, numbers, and dashes.
+:nodes: An array of nodes in the :term:`Topology`
+
+ :cachegroup: The name of a :term:`Cache Group`
+ :parents: The indices of the parents of this node in the nodes array, 0-indexed. 2 parents max
+
+.. code-block:: http
+ :caption: Request Example
+
+ POST /api/3.0/topologies HTTP/1.1
+ User-Agent: python-requests/2.23.0
+ Accept-Encoding: gzip, deflate
+ Accept: */*
+ Connection: keep-alive
+ Cookie: mojolicious=...
+ Content-Length: 924
+ Content-Type: application/json
+
+ {
+ "name": "my-topology",
+ "description": "This is my topology",
+ "nodes": [
+ {
+ "cachegroup": "edge1",
+ "parents": [
+ 7
+ ]
+ },
+ {
+ "cachegroup": "edge2",
+ "parents": [
+ 7,
+ 8
+ ]
+ },
+ {
+ "cachegroup": "edge3",
+ "parents": [
+ 8,
+ 9
+ ]
+ },
+ {
+ "cachegroup": "edge4",
+ "parents": [
+ 9
+ ]
+ },
+ {
+ "cachegroup": "mid1",
+ "parents": []
+ },
+ {
+ "cachegroup": "mid2",
+ "parents": [
+ 4
+ ]
+ },
+ {
+ "cachegroup": "mid3",
+ "parents": [
+ 4
+ ]
+ },
+ {
+ "cachegroup": "mid4",
+ "parents": [
+ 5
+ ]
+ },
+ {
+ "cachegroup": "mid5",
+ "parents": [
+ 5,
+ 6
+ ]
+ },
+ {
+ "cachegroup": "mid6",
+ "parents": [
+ 6
+ ]
+ }
+ ]
+ }
+
+Response Structure
+------------------
+:description: A short sentence that describes the topology.
+:lastUpdated: The date and time at which this :term:`Topology` was last updated, in ISO-like format
+:name: The name of the topology. This can only be letters, numbers, and dashes.
+:nodes: An array of nodes in the :term:`Topology`
+
+ :cachegroup: The name of a :term:`Cache Group`
+ :parents: The indices of the parents of this node in the nodes array, 0-indexed. 2 parents max
+
+.. code-block:: http
+ :caption: Response Example
+
+ HTTP/1.1 200 OK
+ Access-Control-Allow-Credentials: true
+ Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
+ Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
+ Access-Control-Allow-Origin: *
+ Content-Encoding: gzip
+ Content-Type: application/json
+ Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 13 Apr 2020 18:12:34 GMT; Max-Age=3600; HttpOnly
+ Whole-Content-Sha512: ftNcDRjYCDMkQM+o/szayKZriQZHGpcT0vNY0HpKgy88i0pXeEEeLGbUPh6LXtK7TvL76EgGECTzvCkcm+2LVA==
+ X-Server-Name: traffic_ops_golang/
+ Date: Mon, 13 Apr 2020 17:12:34 GMT
+ Content-Length: 239
+
+ {
+ "alerts": [
+ {
+ "text": "topology was created.",
+ "level": "success"
+ }
+ ],
+ "response": {
+ "description": "This is my topology",
+ "name": "my-topology",
+ "nodes": [
+ {
+ "cachegroup": "edge1",
+ "parents": [
+ 7
+ ]
+ },
+ {
+ "cachegroup": "edge2",
+ "parents": [
+ 7,
+ 8
+ ]
+ },
+ {
+ "cachegroup": "edge3",
+ "parents": [
+ 8,
+ 9
+ ]
+ },
+ {
+ "cachegroup": "edge4",
+ "parents": [
+ 9
+ ]
+ },
+ {
+ "cachegroup": "mid1",
+ "parents": []
+ },
+ {
+ "cachegroup": "mid2",
+ "parents": [
+ 4
+ ]
+ },
+ {
+ "cachegroup": "mid3",
+ "parents": [
+ 4
+ ]
+ },
+ {
+ "cachegroup": "mid4",
+ "parents": [
+ 5
+ ]
+ },
+ {
+ "cachegroup": "mid5",
+ "parents": [
+ 5,
+ 6
+ ]
+ },
+ {
+ "cachegroup": "mid6",
+ "parents": [
+ 6
+ ]
+ }
+ ],
+ "lastUpdated": "2020-04-13 17:12:34+00"
+ }
+ }
+
+``PUT``
+=======
+Updates a specific :term:`Topology`
+
+Request Structure
+-----------------
+.. table:: Request Query Parameters
+
+ +------+----------+---------------------------------------------------------+
+ | Name | Required | Description |
+ +======+==========+=========================================================+
+ | name | yes | The name of the :term:`Topology` to be updated |
+ +------+----------+---------------------------------------------------------+
+
+:description: A short sentence that describes the :term:`Topology`.
+:name: The name of the :term:`Topology`. This can only be letters, numbers, and dashes.
+:nodes: An array of nodes in the :term:`Topology`
+
+ :cachegroup: The name of a :term:`Cache Group`
+ :parents: The indices of the parents of this node in the nodes array, 0-indexed. 2 parents max
+
+.. code-block:: http
+ :caption: Request Example
+
+ PUT /api/3.0/topologies?name=my-topology HTTP/1.1
+ User-Agent: python-requests/2.23.0
+ Accept-Encoding: gzip, deflate
+ Accept: */*
+ Connection: keep-alive
+ Cookie: mojolicious=...
+ Content-Length: 853
+ Content-Type: application/json
+
+ {
+ "name": "my-topology",
+ "description": "The description is updated, too",
+ "nodes": [
+ {
+ "cachegroup": "edge1",
+ "parents": [
+ 6
+ ]
+ },
+ {
+ "cachegroup": "edge2",
+ "parents": [
+ 6,
+ 7
+ ]
+ },
+ {
+ "cachegroup": "edge3",
+ "parents": [
+ 7,
+ 8
+ ]
+ },
+ {
+ "cachegroup": "edge4",
+ "parents": [
+ 8
+ ]
+ },
+ {
+ "cachegroup": "mid2",
+ "parents": []
+ },
+ {
+ "cachegroup": "mid3",
+ "parents": []
+ },
+ {
+ "cachegroup": "mid4",
+ "parents": [
+ 4
+ ]
+ },
+ {
+ "cachegroup": "mid5",
+ "parents": [
+ 4,
+ 5
+ ]
+ },
+ {
+ "cachegroup": "mid6",
+ "parents": [
+ 5
+ ]
+ }
+ ]
+ }
+
+Response Structure
+------------------
+:description: A short sentence that describes the :term:`Topology`.
+:lastUpdated: The date and time at which this :term:`Topology` was last updated, in ISO-like format
+:name: The name of the :term:`Topology`. This can only be letters, numbers, and dashes.
+:nodes: An array of nodes in the :term:`Topology`
+
+ :cachegroup: The name of a :term:`Cache Group`
+ :parents: The indices of the parents of this node in the nodes array, 0-indexed. 2 parents max
+
+.. code-block:: http
+ :caption: Response Example
+
+ HTTP/1.1 200 OK
+ Access-Control-Allow-Credentials: true
+ Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
+ Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
+ Access-Control-Allow-Origin: *
+ Content-Encoding: gzip
+ Content-Type: application/json
+ Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 13 Apr 2020 18:33:13 GMT; Max-Age=3600; HttpOnly
+ Whole-Content-Sha512: WVOtsoOVrEWcVjWM2TmT5DXy/a5Q0ygTZEQRhbkHHUmz9dgVLK1F5Joc9jtKA8gZu8/eM5+Tqqguh3mzrhAy/Q==
+ X-Server-Name: traffic_ops_golang/
+ Date: Mon, 13 Apr 2020 17:33:13 GMT
+ Content-Length: 237
+
+ {
+ "alerts": [
+ {
+ "text": "topology was updated.",
+ "level": "success"
+ }
+ ],
+ "response": {
+ "description": "The description is updated, too",
+ "name": "my-topology",
+ "nodes": [
+ {
+ "cachegroup": "edge1",
+ "parents": [
+ 6
+ ]
+ },
+ {
+ "cachegroup": "edge2",
+ "parents": [
+ 6,
+ 7
+ ]
+ },
+ {
+ "cachegroup": "edge3",
+ "parents": [
+ 7,
+ 8
+ ]
+ },
+ {
+ "cachegroup": "edge4",
+ "parents": [
+ 8
+ ]
+ },
+ {
+ "cachegroup": "mid2",
+ "parents": []
+ },
+ {
+ "cachegroup": "mid3",
+ "parents": []
+ },
+ {
+ "cachegroup": "mid4",
+ "parents": [
+ 4
+ ]
+ },
+ {
+ "cachegroup": "mid5",
+ "parents": [
+ 4,
+ 5
+ ]
+ },
+ {
+ "cachegroup": "mid6",
+ "parents": [
+ 5
+ ]
+ }
+ ],
+ "lastUpdated": "2020-04-13 17:33:13+00"
+ }
+ }
+
+``DELETE``
+==========
+Deletes a specific :term:`Topology`.
+
+:Auth. Required: Yes
+:Roles Required: "admin" or "operations"
+:Response Type: ``undefined``
+
+
+Request Structure
+-----------------
+.. table:: Request Query Parameters
+
+ +------+----------+---------------------------------------------------------+
+ | Name | Required | Description |
+ +======+==========+=========================================================+
+ | name | yes | The name of the :term:`Topology` to be deleted |
+ +------+----------+---------------------------------------------------------+
+
+.. code-block:: http
+ :caption: Request Example
+
+ DELETE /api/3.0/topologies?name=my-topology HTTP/1.1
+ User-Agent: python-requests/2.23.0
+ Accept-Encoding: gzip, deflate
+ Accept: */*
+ Connection: keep-alive
+ Cookie: mojolicious=...
+ Content-Length: 0
+
+Response Structure
+------------------
+
+.. code-block:: http
+ :caption: Response Example
+
+ HTTP/1.1 200 OK
+ Access-Control-Allow-Credentials: true
+ Access-Control-Allow-Headers: Origin, X-Requested-With, Content-Type, Accept, Set-Cookie, Cookie
+ Access-Control-Allow-Methods: POST,GET,OPTIONS,PUT,DELETE
+ Access-Control-Allow-Origin: *
+ Content-Encoding: gzip
+ Content-Type: application/json
+ Set-Cookie: mojolicious=...; Path=/; Expires=Mon, 13 Apr 2020 18:35:32 GMT; Max-Age=3600; HttpOnly
+ Whole-Content-Sha512: yErJobzG9IA0khvqZQK+Yi7X4pFVvOqxn6PjrdzN5DnKVm/K8Kka3REul1XmKJnMXVRY8RayoEVGDm16mBFe4Q==
+ X-Server-Name: traffic_ops_golang/
+ Date: Mon, 13 Apr 2020 17:35:32 GMT
+ Content-Length: 87
+
+ {
+ "alerts": [
+ {
+ "text": "topology was deleted.",
+ "level": "success"
+ }
+ ]
+ }
diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst
index 455dfd5..3f90813 100644
--- a/docs/source/glossary.rst
+++ b/docs/source/glossary.rst
@@ -390,6 +390,10 @@ 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
+ Topologies
+ A structure composed of :term:`Cache Groups` and parent relationships, which is assignable to one or more :term:`Delivery Services`.
+
Type
Types
A :dfn:`Type` defines a type of some kind of object configured in Traffic Ops. Unfortunately, that is exactly as specific as this definition can be.
diff --git a/lib/go-tc/cachegroups.go b/lib/go-tc/cachegroups.go
index 7b9d306..f073d70 100644
--- a/lib/go-tc/cachegroups.go
+++ b/lib/go-tc/cachegroups.go
@@ -95,3 +95,4 @@ type CachegroupQueueUpdatesRequest struct {
CDN *CDNName `json:"cdn"`
CDNID *util.JSONIntStr `json:"cdnId"`
}
+
diff --git a/lib/go-tc/enum.go b/lib/go-tc/enum.go
index bcc5739..0cb8e44 100644
--- a/lib/go-tc/enum.go
+++ b/lib/go-tc/enum.go
@@ -76,7 +76,11 @@ const MidTypePrefix = "MID"
const OriginTypeName = "ORG"
-const CacheGroupOriginTypeName = "ORG_LOC"
+const (
+ CacheGroupEdgeTypeName = "EDGE_LOC"
+ CacheGroupMidTypeName = "MID_LOC"
+ CacheGroupOriginTypeName = "ORG_LOC"
+)
const GlobalProfileName = "GLOBAL"
diff --git a/lib/go-tc/topologies.go b/lib/go-tc/topologies.go
new file mode 100644
index 0000000..e7c6644
--- /dev/null
+++ b/lib/go-tc/topologies.go
@@ -0,0 +1,49 @@
+package tc
+
+/*
+ * 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.
+ */
+
+// Topology holds the name and set of TopologyNodes that comprise a flexible topology.
+type Topology struct {
+ Description string `json:"description" db:"description"`
+ Name string `json:"name" db:"name"`
+ Nodes []TopologyNode `json:"nodes"`
+ LastUpdated *TimeNoMod `json:"lastUpdated" db:"last_updated"`
+}
+
+// TopologyNode holds a reference to a cachegroup and the indices of up to 2 parent
+// nodes in the same topology's array of nodes.
+type TopologyNode struct {
+ Id int `json:"-" db:"id"`
+ Cachegroup string `json:"cachegroup" db:"cachegroup"`
+ Parents []int `json:"parents"`
+ LastUpdated *TimeNoMod `json:"-" db:"last_updated"`
+}
+
+// TopologiesResponse models the JSON object returned for a single topology in a response.
+type TopologyResponse struct {
+ Response Topology `json:"response"`
+ Alerts
+}
+
+// TopologiesResponse models the JSON object returned for a list of topologies in a response.
+type TopologiesResponse struct {
+ Response []Topology `json:"response"`
+ Alerts
+}
diff --git a/licenses/MIT-angular_ui_tree b/licenses/MIT-angular_ui_tree
new file mode 100644
index 0000000..f80a016
--- /dev/null
+++ b/licenses/MIT-angular_ui_tree
@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2014
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/traffic_control/clients/python/trafficops/tosession.py b/traffic_control/clients/python/trafficops/tosession.py
index f6355a9..3a4e12f 100644
--- a/traffic_control/clients/python/trafficops/tosession.py
+++ b/traffic_control/clients/python/trafficops/tosession.py
@@ -21,6 +21,8 @@ Requires Python Version >= 3.6
# Core Modules
import logging
import sys
+from requests import Response
+from typing import Any, Dict, List, Tuple, Union
# Third-party Modules
import munch
@@ -1867,6 +1869,53 @@ class TOSession(RestApiSession):
"""
#
+ # Topologies
+ #
+ @api_request('post', 'topologies', ('3.0',))
+ def create_topology(self, data: Dict[str, Any]=None) -> Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], Response]:
+ """
+ :ref:`to-api-topologies`
+ :param data: The Topology data to use for Topology creation.
+ :type data: Dict[str, Any]
+ :rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], Response]
+ :raises: Union[LoginError, OperationError]
+ """
+
+ @api_request('get', 'topologies', ('3.0',))
+ def create_topology(self, data: Dict[str, Any]=None) -> Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], Response]:
+ """
+ :ref:`to-api-topologies`
+ Get Topologies.
+ :ref:`to-api-topologies`
+ :rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], Response]
+ :raises: Union[LoginError, OperationError]
+ """
+
+ @api_request('put', 'topologies', ('3.0',))
+ def update_topologies(self, data: Dict[str, Any]=None, query_params: Dict[str, Any]=None) -> Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], Response]:
+ """
+ Update a Topology
+ :ref:`to-api-topologies`
+ :param data: The new values for the Topology
+ :type data: Dict[str, Any]
+ :type query: Dict[str, Any]
+ :param query_params: 'id' is a required parameter, defining the Topology to be replaced.
+ :rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], Response]
+ :raises: Union[LoginError, OperationError]
+ """
+
+ @api_request('delete', 'topologies', ('3.0',))
+ def delete_topology(self, query_params: Dict[str, Any]=None) -> Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], Response]:
+ """
+ Delete a Topology
+ :ref:`to-api-topologies`
+ :param topology_id: The ID of the Topology to delete
+ :type topology_id: int
+ :rtype: Tuple[Union[Dict[str, Any], List[Dict[str, Any]]], Response]
+ :raises: Union[LoginError, OperationError]
+ """
+
+ #
# Types
#
@api_request('get', 'types', ('2.0',))
diff --git a/traffic_ops/app/db/migrations/20200422101648_create_topology_tables.sql b/traffic_ops/app/db/migrations/20200422101648_create_topology_tables.sql
new file mode 100644
index 0000000..f3c1f60
--- /dev/null
+++ b/traffic_ops/app/db/migrations/20200422101648_create_topology_tables.sql
@@ -0,0 +1,68 @@
+/*
+ * 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.
+ */
+
+-- +goose Up
+-- SQL in section 'Up' is executed when this migration is applied
+CREATE TABLE topology (
+ name text PRIMARY KEY,
+ description text NOT NULL,
+ last_updated timestamp with time zone DEFAULT now() NOT NULL
+);
+DROP TRIGGER IF EXISTS on_update_current_timestamp ON topology;
+CREATE TRIGGER on_update_current_timestamp BEFORE UPDATE ON topology FOR EACH ROW EXECUTE PROCEDURE on_update_current_timestamp_last_updated();
+
+CREATE TABLE topology_cachegroup (
+ id BIGSERIAL PRIMARY KEY,
+ topology text NOT NULL,
+ cachegroup text NOT NULL,
+ last_updated timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT topology_cachegroup_cachegroup_fkey FOREIGN KEY (cachegroup) REFERENCES cachegroup(name) ON UPDATE CASCADE ON DELETE RESTRICT,
+ CONSTRAINT topology_cachegroup_topology_fkey FOREIGN KEY (topology) REFERENCES topology(name) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT unique_topology_cachegroup UNIQUE (topology, cachegroup)
+);
+CREATE INDEX topology_cachegroup_cachegroup_fkey ON topology_cachegroup USING btree (cachegroup);
+CREATE INDEX topology_cachegroup_topology_fkey ON topology_cachegroup USING btree (topology);
+DROP TRIGGER IF EXISTS on_update_current_timestamp ON topology_cachegroup;
+CREATE TRIGGER on_update_current_timestamp BEFORE UPDATE ON topology_cachegroup FOR EACH ROW EXECUTE PROCEDURE on_update_current_timestamp_last_updated();
+
+CREATE TABLE topology_cachegroup_parents (
+ child bigint NOT NULL,
+ parent bigint NOT NULL,
+ rank integer NOT NULL,
+ last_updated timestamp with time zone DEFAULT now() NOT NULL,
+ CONSTRAINT topology_cachegroup_parents_rank_check CHECK (rank = 1 OR rank = 2),
+ CONSTRAINT topology_cachegroup_parents_child_fkey FOREIGN KEY (child) REFERENCES topology_cachegroup(id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT topology_cachegroup_parents_parent_fkey FOREIGN KEY (parent) REFERENCES topology_cachegroup(id) ON UPDATE CASCADE ON DELETE CASCADE,
+ CONSTRAINT unique_child_rank UNIQUE (child, rank),
+ CONSTRAINT unique_child_parent UNIQUE (child, parent)
+);
+CREATE INDEX topology_cachegroup_parents_child_fkey ON topology_cachegroup_parents USING btree (child);
+CREATE INDEX topology_cachegroup_parents_parents_fkey ON topology_cachegroup_parents USING btree (parent);
+DROP TRIGGER IF EXISTS on_update_current_timestamp ON topology_cachegroup_parents;
+CREATE TRIGGER on_update_current_timestamp BEFORE UPDATE ON topology_cachegroup_parents FOR EACH ROW EXECUTE PROCEDURE on_update_current_timestamp_last_updated();
+
+ALTER TABLE deliveryservice
+ ADD COLUMN topology text,
+ ADD CONSTRAINT deliveryservice_topology_fkey FOREIGN KEY (topology) REFERENCES topology (name) ON UPDATE CASCADE ON DELETE RESTRICT;
+CREATE INDEX deliveryservice_topology_fkey ON deliveryservice USING btree (topology);
+
+-- +goose Down
+-- SQL section 'Down' is executed when this migration is rolled back
+ALTER TABLE deliveryservice DROP COLUMN topology;
+DROP TABLE topology_cachegroup_parents;
+DROP TABLE topology_cachegroup;
+DROP TABLE topology;
diff --git a/traffic_ops/client/topology.go b/traffic_ops/client/topology.go
new file mode 100644
index 0000000..bc95dff
--- /dev/null
+++ b/traffic_ops/client/topology.go
@@ -0,0 +1,123 @@
+/*
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+
+package client
+
+import (
+ "encoding/json"
+ "fmt"
+ "github.com/apache/trafficcontrol/lib/go-log"
+ "github.com/apache/trafficcontrol/lib/go-tc"
+ "net"
+ "net/http"
+ "net/url"
+)
+
+const (
+ ApiTopologies = apiBase + "/topologies"
+)
+
+// CreateTopology creates a topology and returns the response.
+func (to *Session) CreateTopology(top tc.Topology) (*tc.TopologyResponse, ReqInf, error) {
+ var remoteAddr net.Addr
+ reqBody, err := json.Marshal(top)
+ reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+ if err != nil {
+ return nil, reqInf, err
+ }
+ resp, remoteAddr, err := to.request(http.MethodPost, ApiTopologies, reqBody)
+ if err != nil {
+ return nil, reqInf, err
+ }
+ defer log.Close(resp.Body, "unable to close response")
+ var topResp tc.TopologyResponse
+ if err = json.NewDecoder(resp.Body).Decode(&topResp); err != nil {
+ return nil, reqInf, err
+ }
+ return &topResp, reqInf, nil
+}
+
+// GetTopologies returns all topologies.
+func (to *Session) GetTopologies() ([]tc.Topology, ReqInf, error) {
+ resp, remoteAddr, err := to.request(http.MethodGet, ApiTopologies, nil)
+ reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+ if err != nil {
+ return nil, reqInf, err
+ }
+ defer log.Close(resp.Body, "unable to close response")
+
+ var data tc.TopologiesResponse
+ if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+ return nil, reqInf, err
+ }
+
+ return data.Response, reqInf, nil
+}
+
+// GetTopology returns the given topology by name.
+func (to *Session) GetTopology(name string) (*tc.Topology, ReqInf, error) {
+ reqUrl := fmt.Sprintf("%s?name=%s", ApiTopologies, url.QueryEscape(name))
+ resp, remoteAddr, err := to.request(http.MethodGet, reqUrl, nil)
+ reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+ if err != nil {
+ return nil, reqInf, err
+ }
+ defer log.Close(resp.Body, "unable to close response")
+
+ var data tc.TopologiesResponse
+ if err := json.NewDecoder(resp.Body).Decode(&data); err != nil {
+ return nil, reqInf, err
+ }
+
+ if len(data.Response) == 1 {
+ return &data.Response[0], reqInf, nil
+ }
+ return nil, reqInf, fmt.Errorf("expected one topology in response, instead got: %+v", data.Response)
+}
+
+// UpdateTopology updates a Topology by name.
+func (to *Session) UpdateTopology(name string, t tc.Topology) (*tc.TopologyResponse, ReqInf, error) {
+ var remoteAddr net.Addr
+ reqBody, err := json.Marshal(t)
+ reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+ if err != nil {
+ return nil, reqInf, err
+ }
+ route := fmt.Sprintf("%s?name=%s", ApiTopologies, name)
+ resp, remoteAddr, err := to.request(http.MethodPut, route, reqBody)
+ if err != nil {
+ return nil, reqInf, err
+ }
+ defer log.Close(resp.Body, "unable to close response")
+ var response = new(tc.TopologyResponse)
+ err = json.NewDecoder(resp.Body).Decode(response)
+ return response, reqInf, err
+}
+
+// DeleteTopology deletes the given topology by name.
+func (to *Session) DeleteTopology(name string) (tc.Alerts, ReqInf, error) {
+ reqUrl := fmt.Sprintf("%s?name=%s", ApiTopologies, url.QueryEscape(name))
+ resp, remoteAddr, err := to.request(http.MethodDelete, reqUrl, nil)
+ reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr}
+ if err != nil {
+ return tc.Alerts{}, reqInf, err
+ }
+ defer log.Close(resp.Body, "unable to close response")
+ var alerts tc.Alerts
+ if err = json.NewDecoder(resp.Body).Decode(&alerts); err != nil {
+ return tc.Alerts{}, reqInf, err
+ }
+ return alerts, reqInf, nil
+}
diff --git a/traffic_ops/testing/api/v3/tc-fixtures.json b/traffic_ops/testing/api/v3/tc-fixtures.json
index 3ed7661..9fc1cc2 100644
--- a/traffic_ops/testing/api/v3/tc-fixtures.json
+++ b/traffic_ops/testing/api/v3/tc-fixtures.json
@@ -34,6 +34,13 @@
{
"latitude": 0,
"longitude": 0,
+ "name": "parentCachegroup2",
+ "shortName": "pg2",
+ "typeName": "MID_LOC"
+ },
+ {
+ "latitude": 0,
+ "longitude": 0,
"name": "secondaryCachegroup",
"shortName": "sg1",
"typeName": "MID_LOC"
@@ -2222,6 +2229,48 @@
"parentName": "root"
}
],
+ "topologies": [
+ {
+ "name": "my-topology",
+ "description": "a topology",
+ "nodes": [
+ {
+ "cachegroup": "parentCachegroup",
+ "parents": []
+ },
+ {
+ "cachegroup": "secondaryCachegroup",
+ "parents": []
+ },
+ {
+ "cachegroup": "cachegroup1",
+ "parents": [0, 1]
+ }
+ ]
+ },
+ {
+ "name": "another-topology",
+ "description": "another topology",
+ "nodes": [
+ {
+ "cachegroup": "parentCachegroup",
+ "parents": []
+ },
+ {
+ "cachegroup": "cachegroup1",
+ "parents": [0]
+ },
+ {
+ "cachegroup": "secondaryCachegroup",
+ "parents": []
+ },
+ {
+ "cachegroup": "cachegroup2",
+ "parents": [2]
+ }
+ ]
+ }
+ ],
"types": [
{
"description": "Host header regular expression",
diff --git a/traffic_ops/testing/api/v3/topologies_test.go b/traffic_ops/testing/api/v3/topologies_test.go
new file mode 100644
index 0000000..1577abb
--- /dev/null
+++ b/traffic_ops/testing/api/v3/topologies_test.go
@@ -0,0 +1,141 @@
+package v3
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+ "fmt"
+ "github.com/apache/trafficcontrol/lib/go-tc"
+ "reflect"
+ "testing"
+)
+
+type topologyTestCase struct {
+ reasonToFail string
+ tc.Topology
+}
+
+func TestTopologies(t *testing.T) {
+ WithObjs(t, []TCObj{Types, CacheGroups, Topologies}, func() {
+ UpdateTestTopologies(t)
+ ValidationTestTopologies(t)
+ })
+}
+
+func CreateTestTopologies(t *testing.T) {
+ var (
+ postResponse *tc.TopologyResponse
+ err error
+ )
+ for _, topology := range testData.Topologies {
+ if postResponse, _, err = TOSession.CreateTopology(topology); err != nil {
+ t.Fatalf("could not CREATE topology: %v", err)
+ }
+ postResponse.Response.LastUpdated = nil
+ if !reflect.DeepEqual(topology, postResponse.Response) {
+ t.Fatalf("Topology in response should be the same as the one POSTed. expected: %v\nactual: %v", topology, postResponse.Response)
+ }
+ t.Log("Response: ", postResponse)
+ }
+}
+
+func ValidationTestTopologies(t *testing.T) {
+ invalidTopologyTestCases := []topologyTestCase{
+ {reasonToFail: "no nodes", Topology: tc.Topology{Name: "empty-top", Description: "Invalid because there are no nodes", Nodes: []tc.TopologyNode{}}},
+ {reasonToFail: "a node listing itself as a parent", Topology: tc.Topology{Name: "self-parent", Description: "Invalid because a node lists itself as a parent", Nodes: []tc.TopologyNode{
+ {Cachegroup: "cachegroup1", Parents: []int{1}},
+ {Cachegroup: "parentCachegroup", Parents: []int{1}},
+ }}},
+ {reasonToFail: "duplicate parents", Topology: tc.Topology{}},
+ {reasonToFail: "too many parents", Topology: tc.Topology{Name: "duplicate-parents", Description: "Invalid because a node lists the same parent twice", Nodes: []tc.TopologyNode{
+ {Cachegroup: "cachegroup1", Parents: []int{1, 1}},
+ {Cachegroup: "parentCachegroup", Parents: []int{}},
+ }}},
+ {reasonToFail: "too many parents", Topology: tc.Topology{Name: "too-many-parents", Description: "Invalid because a node has more than 2 parents", Nodes: []tc.TopologyNode{
+ {Cachegroup: "parentCachegroup", Parents: []int{}},
+ {Cachegroup: "secondaryCachegroup", Parents: []int{}},
+ {Cachegroup: "parentCachegroup2", Parents: []int{}},
+ {Cachegroup: "cachegroup1", Parents: []int{0, 1, 2}},
+ }}},
+ {reasonToFail: "a parent edge", Topology: tc.Topology{Name: "parent-edge", Description: "Invalid because an edge is a parent", Nodes: []tc.TopologyNode{
+ {Cachegroup: "cachegroup1", Parents: []int{1}},
+ {Cachegroup: "cachegroup2", Parents: []int{}},
+ }}},
+ {reasonToFail: "a leaf mid", Topology: tc.Topology{Name: "leaf-mid", Description: "Invalid because a mid is a leaf node", Nodes: []tc.TopologyNode{
+ {Cachegroup: "parentCachegroup", Parents: []int{1}},
+ {Cachegroup: "secondaryCachegroup", Parents: []int{}},
+ }}},
+ {reasonToFail: "cyclical nodes", Topology: tc.Topology{Name: "cyclical-nodes", Description: "Invalid because it contains cycles", Nodes: []tc.TopologyNode{
+ {Cachegroup: "cachegroup1", Parents: []int{1, 2}},
+ {Cachegroup: "parentCachegroup", Parents: []int{2}},
+ {Cachegroup: "secondaryCachegroup", Parents: []int{1}},
+ }}},
+ }
+ for _, testCase := range invalidTopologyTestCases {
+ if _, _, err := TOSession.CreateTopology(testCase.Topology); err == nil {
+ t.Fatalf("expected POST with %v to return an error, actual: nil", testCase.reasonToFail)
+ }
+ }
+}
+
+func updateSingleTopology(topology tc.Topology) error {
+ updateResponse, _, err := TOSession.UpdateTopology(topology.Name, topology)
+ if err != nil {
+ return fmt.Errorf("cannot PUT topology: %v - %v", err, updateResponse)
+ }
+ updateResponse.Response.LastUpdated = nil
+ if !reflect.DeepEqual(topology, updateResponse.Response) {
+ return fmt.Errorf("Topologies should be equal after updating. expected: %v\nactual: %v", topology, updateResponse.Response)
+ }
+ return nil
+}
+
+func UpdateTestTopologies(t *testing.T) {
+ topologiesCount := len(testData.Topologies)
+ for index := range testData.Topologies {
+ topology := testData.Topologies[(index+1)%topologiesCount]
+ topology.Name = testData.Topologies[index].Name // We cannot update a topology's name
+ if err := updateSingleTopology(topology); err != nil {
+ t.Fatalf(err.Error())
+ }
+ }
+ // Revert test topologies
+ for _, topology := range testData.Topologies {
+ if err := updateSingleTopology(topology); err != nil {
+ t.Fatalf(err.Error())
+ }
+ }
+}
+
+func DeleteTestTopologies(t *testing.T) {
+ for _, top := range testData.Topologies {
+ delResp, _, err := TOSession.DeleteTopology(top.Name)
+ if err != nil {
+ t.Fatalf("cannot DELETE topology: %v - %v", err, delResp)
+ }
+
+ topology, _, err := TOSession.GetTopology(top.Name)
+ if err == nil {
+ t.Fatalf("expected error trying to GET deleted topology: %s, actual: nil", top.Name)
+ }
+ if topology != nil {
+ t.Fatalf("expected nil trying to GET deleted topology: %s, actual: non-nil", top.Name)
+ }
+ }
+}
diff --git a/traffic_ops/testing/api/v3/traffic_control_test.go b/traffic_ops/testing/api/v3/traffic_control_test.go
index 8fe0a1c..b6423fc 100644
--- a/traffic_ops/testing/api/v3/traffic_control_test.go
+++ b/traffic_ops/testing/api/v3/traffic_control_test.go
@@ -50,6 +50,7 @@ type TrafficControl struct {
StatsSummaries []tc.StatsSummary `json:"statsSummaries"`
Tenants []tc.Tenant `json:"tenants"`
ServerCheckExtensions []tc.ServerCheckExtensionNullable `json:"servercheck_extensions"`
+ Topologies []tc.Topology `json:"topologies"`
Types []tc.Type `json:"types"`
SteeringTargets []tc.SteeringTargetNullable `json:"steeringTargets"`
Serverchecks []tc.ServercheckRequestNullable `json:"serverchecks"`
diff --git a/traffic_ops/testing/api/v3/withobjs_test.go b/traffic_ops/testing/api/v3/withobjs_test.go
index c0d6a0e..0f7996e 100644
--- a/traffic_ops/testing/api/v3/withobjs_test.go
+++ b/traffic_ops/testing/api/v3/withobjs_test.go
@@ -66,6 +66,7 @@ const (
SteeringTargets
Tenants
ServerCheckExtensions
+ Topologies
Types
Users
)
@@ -106,6 +107,7 @@ var withFuncs = map[TCObj]TCObjFuncs{
SteeringTargets: {SetupSteeringTargets, DeleteTestSteeringTargets},
Tenants: {CreateTestTenants, DeleteTestTenants},
ServerCheckExtensions: {CreateTestServerCheckExtensions, DeleteTestServerCheckExtensions},
+ Topologies: {CreateTestTopologies, DeleteTestTopologies},
Types: {CreateTestTypes, DeleteTestTypes},
Users: {CreateTestUsers, ForceDeleteTestUsers},
}
diff --git a/traffic_ops/traffic_ops_golang/api/shared_handlers_test.go b/traffic_ops/traffic_ops_golang/api/shared_handlers_test.go
index 6cef2fc..010b0a5 100644
--- a/traffic_ops/traffic_ops_golang/api/shared_handlers_test.go
+++ b/traffic_ops/traffic_ops_golang/api/shared_handlers_test.go
@@ -40,7 +40,7 @@ type tester struct {
APIInfoImpl `json:"-"`
userErr error //only for testing
sysErr error //only for testing
- errCode int //only for testing
+ errCode int //only for testing
}
var cfg = config.Config{ConfigTrafficOpsGolang: config.ConfigTrafficOpsGolang{DBQueryTimeoutSeconds: 20}}
diff --git a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
index 21f2cba..102560e 100644
--- a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
+++ b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go
@@ -379,6 +379,46 @@ func (cg *TOCacheGroup) deleteCoordinate(coordinateID int) error {
return nil
}
+func GetCacheGroupsByName(names []string, Tx *sqlx.Tx) (map[string]tc.CacheGroupNullable, error, error, int) {
+ query := SelectQuery() + multipleCacheGroupsWhere()
+ namesPqArray := pq.Array(names)
+ rows, err := Tx.Query(query, namesPqArray)
+ if err != nil {
+ userErr, sysErr, errCode := api.ParseDBError(err)
+ return nil, userErr, sysErr, errCode
+ }
+ defer log.Close(rows, "unable to close DB connection")
+ cacheGroupMap := map[string]tc.CacheGroupNullable{}
+ for rows.Next() {
+ var s tc.CacheGroupNullable
+ lms := make([]tc.LocalizationMethod, 0)
+ cgfs := make([]string, 0)
+ if err = rows.Scan(
+ &s.ID,
+ &s.Name,
+ &s.ShortName,
+ &s.Latitude,
+ &s.Longitude,
+ pq.Array(&lms),
+ &s.ParentCachegroupID,
+ &s.ParentName,
+ &s.SecondaryParentCachegroupID,
+ &s.SecondaryParentName,
+ &s.Type,
+ &s.TypeID,
+ &s.LastUpdated,
+ pq.Array(&cgfs),
+ &s.FallbackToClosest,
+ ); err != nil {
+ return nil, nil, errors.New("cachegroup read: scanning: " + err.Error()), http.StatusInternalServerError
+ }
+ s.LocalizationMethods = &lms
+ s.Fallbacks = &cgfs
+ cacheGroupMap[*s.Name] = s
+ }
+ return cacheGroupMap, nil, nil, http.StatusOK
+}
+
func (cg *TOCacheGroup) Read() ([]interface{}, error, error, int) {
// Query Parameters to Database Query column mappings
// see the fields mapped in the SQL query
@@ -621,6 +661,13 @@ LEFT JOIN cachegroup AS cgp ON cachegroup.parent_cachegroup_id = cgp.id
LEFT JOIN cachegroup AS cgs ON cachegroup.secondary_parent_cachegroup_id = cgs.id`
}
+func multipleCacheGroupsWhere() string {
+ return `
+WHERE
+cachegroup.name = ANY ($1)
+`
+}
+
func UpdateQuery() string {
// to disambiguate struct scans, the named
// parameter 'type_id' is an alias to cachegroup.type
diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go
index aa27cc6..5bfd8ae 100644
--- a/traffic_ops/traffic_ops_golang/routing/routes.go
+++ b/traffic_ops/traffic_ops_golang/routing/routes.go
@@ -88,6 +88,7 @@ import (
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/steering"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/steeringtargets"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/systeminfo"
+ "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/topology"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/trafficstats"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/types"
"github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/urisigning"
@@ -282,6 +283,11 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) {
{api.Version{3, 0}, http.MethodPost, `regions/?$`, api.CreateHandler(®ion.TORegion{}), auth.PrivLevelOperations, Authenticated, nil, 22883344883, noPerlBypass},
{api.Version{3, 0}, http.MethodDelete, `regions/?$`, api.DeleteHandler(®ion.TORegion{}), auth.PrivLevelOperations, Authenticated, nil, 22326267583, noPerlBypass},
+ {api.Version{3, 0}, http.MethodPost, `topologies/?$`, api.CreateHandler(&topology.TOTopology{}), auth.PrivLevelOperations, Authenticated, nil, 3871452221, noPerlBypass},
+ {api.Version{3, 0}, http.MethodGet, `topologies/?$`, api.ReadHandler(&topology.TOTopology{}), auth.PrivLevelReadOnly, Authenticated, nil, 3871452222, noPerlBypass},
+ {api.Version{3, 0}, http.MethodPut, `topologies/?$`, api.UpdateHandler(&topology.TOTopology{}), auth.PrivLevelOperations, Authenticated, nil, 3871452223, noPerlBypass},
+ {api.Version{3, 0}, http.MethodDelete, `topologies/?$`, api.DeleteHandler(&topology.TOTopology{}), auth.PrivLevelOperations, Authenticated, nil, 3871452224, noPerlBypass},
+
// get all edge servers associated with a delivery service (from deliveryservice_server table)
{api.Version{3, 0}, http.MethodGet, `deliveryserviceserver/?$`, dsserver.ReadDSSHandlerV14, auth.PrivLevelReadOnly, Authenticated, nil, 29461450333, noPerlBypass},
diff --git a/traffic_ops/traffic_ops_golang/topology/cycle_detection.go b/traffic_ops/traffic_ops_golang/topology/cycle_detection.go
new file mode 100644
index 0000000..2344bf0
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/topology/cycle_detection.go
@@ -0,0 +1,124 @@
+package topology
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+ "github.com/apache/trafficcontrol/lib/go-tc"
+ "math"
+)
+
+type TarjanNode struct {
+ tc.TopologyNode
+ Index *int
+ LowLink *int
+ OnStack *bool
+}
+
+type NodeStack []*TarjanNode
+type Graph []*TarjanNode
+type Component []tc.TopologyNode
+
+type Tarjan struct {
+ Graph *Graph
+ Stack *NodeStack
+ Components []Component
+ Index int
+}
+
+func (stack *NodeStack) push(node *TarjanNode) {
+ *stack = append(append([]*TarjanNode{}, *stack...), node)
+}
+
+func (stack *NodeStack) pop() *TarjanNode {
+ length := len(*stack)
+ node := (*stack)[length-1]
+ *stack = (*stack)[:length-1]
+ return node
+}
+
+func tarjan(nodes []tc.TopologyNode) [][]tc.TopologyNode {
+ structs := Tarjan{
+ Graph: &Graph{},
+ Stack: &NodeStack{},
+ Components: []Component{},
+ Index: 0,
+ }
+ for _, node := range nodes {
+ tarjanNode := TarjanNode{TopologyNode: node, LowLink: new(int)}
+ *tarjanNode.LowLink = 500
+ *structs.Graph = append(*structs.Graph, &tarjanNode)
+ }
+ structs.Stack = &NodeStack{}
+ structs.Index = 0
+ for _, vertex := range *structs.Graph {
+ if vertex.Index == nil {
+ structs.strongConnect(vertex)
+ }
+ }
+ var components [][]tc.TopologyNode
+ for _, component := range structs.Components {
+ var componentArray []tc.TopologyNode
+ for _, node := range component {
+ componentArray = append(componentArray, node)
+ }
+ components = append(components, componentArray)
+ }
+ return components
+}
+
+func (structs *Tarjan) nextComponent() (Component, int) {
+ var component = Component{}
+ index := len(structs.Components)
+ structs.Components = append(structs.Components, component)
+ return component, index
+}
+
+func (structs *Tarjan) strongConnect(node *TarjanNode) {
+ stack := structs.Stack
+ node.Index = new(int)
+ *node.Index = structs.Index
+ node.LowLink = new(int)
+ *node.LowLink = structs.Index
+ structs.Index++
+ stack.push(node)
+ node.OnStack = new(bool)
+ *node.OnStack = true
+
+ for _, parentIndex := range node.Parents {
+ parent := (*structs.Graph)[parentIndex]
+ if parent.Index == nil {
+ structs.strongConnect(parent)
+ *(*parent).LowLink = int(math.Min(float64(*node.LowLink), float64(*parent.LowLink)))
+ } else if *parent.OnStack {
+ *node.LowLink = int(math.Min(float64(*node.LowLink), float64(*parent.Index)))
+ }
+ }
+
+ if *node.LowLink == *node.Index {
+ component, index := structs.nextComponent()
+ var otherNode *TarjanNode
+ for node != otherNode {
+ otherNode = stack.pop()
+ *otherNode.OnStack = false
+ component = append(component, otherNode.TopologyNode)
+ }
+ structs.Components[index] = component
+ }
+}
diff --git a/traffic_ops/traffic_ops_golang/topology/topologies.go b/traffic_ops/traffic_ops_golang/topology/topologies.go
new file mode 100644
index 0000000..595d5da
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/topology/topologies.go
@@ -0,0 +1,473 @@
+package topology
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+ "errors"
+ "fmt"
+ "github.com/apache/trafficcontrol/lib/go-log"
+ "github.com/apache/trafficcontrol/lib/go-tc"
+ "github.com/apache/trafficcontrol/lib/go-tc/tovalidate"
+ "github.com/apache/trafficcontrol/lib/go-util"
+ "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api"
+ "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/cachegroup"
+ "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/dbhelpers"
+ validation "github.com/go-ozzo/ozzo-validation"
+ "github.com/lib/pq"
+ "net/http"
+)
+
+// TOTopology is a type alias on which we can define functions.
+type TOTopology struct {
+ api.APIInfoImpl `json:"-"`
+ tc.Topology
+}
+
+// DeleteQueryBase holds a delete query with no WHERE clause and is a
+// requirement of the api.GenericOptionsDeleter interface.
+func (topology *TOTopology) DeleteQueryBase() string {
+ return deleteQueryBase()
+}
+
+// ParamColumns maps query parameters to their respective database columns.
+func (topology *TOTopology) ParamColumns() map[string]dbhelpers.WhereColumnInfo {
+ return map[string]dbhelpers.WhereColumnInfo{
+ "name": {Column: "t.name"},
+ "description": {Column: "t.description"},
+ "lastUpdated": {Column: "t.last_updated"},
+ }
+}
+
+// GenericOptionsDeleter is required by the api.GenericOptionsDeleter interface
+// and is called by api.GenericOptionsDelete().
+func (topology *TOTopology) DeleteKeyOptions() map[string]dbhelpers.WhereColumnInfo {
+ return topology.ParamColumns()
+}
+
+func (topology *TOTopology) SetLastUpdated(time tc.TimeNoMod) { topology.LastUpdated = &time }
+
+// GetKeyFieldsInfo is a requirement of the api.Updater interface.
+func (topology TOTopology) GetKeyFieldsInfo() []api.KeyFieldInfo {
+ return []api.KeyFieldInfo{{"name", api.GetStringKey}}
+}
+
+// GetType returns the human-readable type of TOTopology as a string.
+func (topology *TOTopology) GetType() string {
+ return "topology"
+}
+
+// Validate is a requirement of the api.Validator interface.
+func (topology *TOTopology) Validate() error {
+ nameRule := validation.NewStringRule(tovalidate.IsAlphanumericUnderscoreDash, "must consist of only alphanumeric, dash, or underscore characters.")
+ rules := validation.Errors{}
+ rules["name"] = validation.Validate(topology.Name, validation.Required, nameRule)
+
+ nodeCount := len(topology.Nodes)
+ if nodeCount < 1 {
+ rules["length"] = fmt.Errorf("must provide 1 or more node, %v found", nodeCount)
+ }
+ var (
+ cacheGroups = make([]tc.CacheGroupNullable, nodeCount)
+ cacheGroupsExist = true
+ err error
+ userErr error
+ sysErr error
+ cacheGroupMap map[string]tc.CacheGroupNullable
+ exists bool
+ )
+ _ = err
+ cacheGroupNames := make([]string, len(topology.Nodes))
+ for index, node := range topology.Nodes {
+ rules[fmt.Sprintf("node %v parents size", index)] = validation.Validate(node.Parents, validation.Length(0, 2))
+ rules[fmt.Sprintf("node %v duplicate parents", index)] = checkForDuplicateParents(topology.Nodes, index)
+ rules[fmt.Sprintf("node %v self parent", index)] = checkForSelfParents(topology.Nodes, index)
+ cacheGroupNames[index] = node.Cachegroup
+ }
+ if cacheGroupMap, userErr, sysErr, _ = cachegroup.GetCacheGroupsByName(cacheGroupNames, topology.APIInfoImpl.ReqInfo.Tx); userErr != nil || sysErr != nil {
+ var err error
+ message := "could not get cachegroups"
+ if userErr != nil {
+ err = fmt.Errorf("%s: %s", message, userErr.Error())
+ }
+ return err
+ }
+ cacheGroups = make([]tc.CacheGroupNullable, len(topology.Nodes))
+ for index, node := range topology.Nodes {
+ if cacheGroups[index], exists = cacheGroupMap[node.Cachegroup]; !exists {
+ rules[fmt.Sprintf("cachegroup %s not found", node.Cachegroup)] = fmt.Errorf("node %d references nonexistent cachegroup %s", index, node.Cachegroup)
+ cacheGroupsExist = false
+ }
+ }
+ rules["duplicate cachegroup name"] = checkUniqueCacheGroupNames(topology.Nodes)
+
+ if cacheGroupsExist {
+ for index, node := range topology.Nodes {
+ rules[fmt.Sprintf("parent '%v' edge type", node.Cachegroup)] = checkForEdgeParents(topology.Nodes, cacheGroups, index)
+ }
+
+ for _, leafMid := range checkForLeafMids(topology.Nodes, cacheGroups) {
+ rules[fmt.Sprintf("node %v leaf mid", leafMid.Cachegroup)] = fmt.Errorf("cachegroup %v's type is %v; it cannot be a leaf (it must have at least 1 child)", leafMid.Cachegroup, tc.CacheGroupMidTypeName)
+ }
+ }
+ rules["topology cycles"] = checkForCycles(topology.Nodes)
+
+ errs := tovalidate.ToErrors(rules)
+ return util.JoinErrs(errs)
+}
+
+// Implementation of the Identifier, Validator interface functions
+func (topology TOTopology) GetKeys() (map[string]interface{}, bool) {
+ return map[string]interface{}{"name": topology.Name}, true
+}
+
+// SetKeys is a requirement of the api.Updater interface and is called by
+// api.UpdateHandler().
+func (topology *TOTopology) SetKeys(keys map[string]interface{}) {
+ topology.Name, _ = keys["name"].(string)
+}
+
+// GetAuditName is a requirement of the api.Identifier interface.
+func (topology *TOTopology) GetAuditName() string {
+ return topology.Name
+}
+
+// Create is a requirement of the api.Creator interface.
+func (topology *TOTopology) Create() (error, error, int) {
+ tx := topology.APIInfo().Tx.Tx
+ err := tx.QueryRow(insertQuery(), topology.Name, topology.Description).Scan(&topology.Name, &topology.Description, &topology.LastUpdated)
+ if err != nil {
+ return api.ParseDBError(err)
+ }
+
+ if userErr, sysErr, errCode := topology.addNodes(); userErr != nil || sysErr != nil {
+ return userErr, sysErr, errCode
+ }
+
+ if userErr, sysErr, errCode := topology.addParents(); userErr != nil || sysErr != nil {
+ return userErr, sysErr, errCode
+ }
+
+ return nil, nil, 0
+}
+
+// Read is a requirement of the api.Reader interface and is called by api.ReadHandler().
+func (topology *TOTopology) Read() ([]interface{}, error, error, int) {
+ where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(topology.ReqInfo.Params, topology.ParamColumns())
+ if len(errs) > 0 {
+ return nil, util.JoinErrs(errs), nil, http.StatusBadRequest
+ }
+ query := selectQuery() + where + orderBy + pagination
+ rows, err := topology.ReqInfo.Tx.NamedQuery(query, queryValues)
+ if err != nil {
+ return nil, nil, errors.New("topology read: querying: " + err.Error()), http.StatusInternalServerError
+ }
+ defer log.Close(rows, "unable to close DB connection")
+
+ var interfaces []interface{}
+ topologies := map[string]*tc.Topology{}
+ indices := map[int]int{}
+ for index := 0; rows.Next(); index++ {
+ var (
+ name, description string
+ lastUpdated tc.TimeNoMod
+ )
+ topologyNode := tc.TopologyNode{}
+ topologyNode.Parents = []int{}
+ var parents pq.Int64Array
+ if err = rows.Scan(
+ &name,
+ &description,
+ &lastUpdated,
+ &topologyNode.Id,
+ &topologyNode.Cachegroup,
+ &parents,
+ ); err != nil {
+ return nil, nil, errors.New("topology read: scanning: " + err.Error()), http.StatusInternalServerError
+ }
+ for _, id := range parents {
+ topologyNode.Parents = append(topologyNode.Parents, int(id))
+ }
+ indices[topologyNode.Id] = index
+ if _, exists := topologies[name]; !exists {
+ topology := tc.Topology{Nodes: []tc.TopologyNode{}}
+ topologies[name] = &topology
+ topology.Name = name
+ topology.Description = description
+ topology.LastUpdated = &lastUpdated
+ }
+ topologies[name].Nodes = append(topologies[name].Nodes, topologyNode)
+ }
+
+ for _, topology := range topologies {
+ nodeMap := map[int]int{}
+ for index, node := range topology.Nodes {
+ nodeMap[node.Id] = index
+ }
+ for _, node := range topology.Nodes {
+ for parentIndex := 0; parentIndex < len(node.Parents); parentIndex++ {
+ node.Parents[parentIndex] = nodeMap[node.Parents[parentIndex]]
+ }
+ }
+ interfaces = append(interfaces, *topology)
+ }
+ return interfaces, nil, nil, http.StatusOK
+}
+
+func (topology *TOTopology) removeParents() error {
+ _, err := topology.ReqInfo.Tx.Exec(deleteParentsQuery(), topology.Name)
+ if err != nil {
+ return errors.New("topology update: error deleting old parents: " + err.Error())
+ }
+ return nil
+}
+
+func (topology *TOTopology) removeNodes(cachegroups *[]string) error {
+ _, err := topology.ReqInfo.Tx.Exec(deleteNodesQuery(), topology.Name, pq.Array(*cachegroups))
+ if err != nil {
+ return errors.New("topology update: error removing old unused nodes: " + err.Error())
+ }
+ return nil
+}
+
+func (topology *TOTopology) addNodes() (error, error, int) {
+ var cachegroupsToInsert []string
+ var indices = make([]int, 0)
+ for index, node := range topology.Nodes {
+ if node.Id == 0 {
+ cachegroupsToInsert = append(cachegroupsToInsert, node.Cachegroup)
+ indices = append(indices, index)
+ }
+ }
+ if len(cachegroupsToInsert) == 0 {
+ return nil, nil, http.StatusOK
+ }
+ rows, err := topology.ReqInfo.Tx.Query(nodeInsertQuery(), topology.Name, pq.Array(cachegroupsToInsert))
+ if err != nil {
+ return nil, errors.New("error adding nodes: " + err.Error()), http.StatusInternalServerError
+ }
+ defer log.Close(rows, "unable to close DB connection")
+ for _, index := range indices {
+ rows.Next()
+ err = rows.Scan(&topology.Nodes[index].Id, &topology.Name, &topology.Nodes[index].Cachegroup)
+ if err != nil {
+ return api.ParseDBError(err)
+ }
+ }
+ return nil, nil, http.StatusOK
+}
+
+func (topology *TOTopology) addParents() (error, error, int) {
+ var (
+ children []int
+ parents []int
+ ranks []int
+ )
+ for _, node := range topology.Nodes {
+ for rank := 1; rank <= len(node.Parents); rank++ {
+ parent := topology.Nodes[node.Parents[rank-1]]
+ children = append(children, node.Id)
+ parents = append(parents, parent.Id)
+ ranks = append(ranks, rank)
+ }
+ }
+ rows, err := topology.ReqInfo.Tx.Query(nodeParentInsertQuery(), pq.Array(children), pq.Array(parents), pq.Array(ranks))
+ if err != nil {
+ return api.ParseDBError(err)
+ }
+ defer log.Close(rows, "unable to close DB connection")
+ for _, node := range topology.Nodes {
+ for rank := 1; rank <= len(node.Parents); rank++ {
+ rows.Next()
+ parent := topology.Nodes[node.Parents[rank-1]]
+ err = rows.Scan(&node.Id, &parent.Id, &rank)
+ if err != nil {
+ return api.ParseDBError(err)
+ }
+ }
+ }
+ return nil, nil, http.StatusOK
+}
+
+func (topology *TOTopology) setDescription() (error, error, int) {
+ rows, err := topology.ReqInfo.Tx.Query(updateQuery(), topology.Description, topology.Name)
+ if err != nil {
+ return nil, fmt.Errorf("topology update: error setting the description for topology %v: %v", topology.Name, err.Error()), http.StatusInternalServerError
+ }
+ defer log.Close(rows, "unable to close DB connection")
+ for rows.Next() {
+ err = rows.Scan(&topology.Name, &topology.Description, &topology.LastUpdated)
+ if err != nil {
+ return api.ParseDBError(err)
+ }
+ }
+ return nil, nil, http.StatusOK
+}
+
+// Update is a requirement of the api.Updater interface.
+func (topology *TOTopology) Update() (error, error, int) {
+ topologies, userErr, sysErr, errCode := topology.Read()
+ if userErr != nil || sysErr != nil {
+ return userErr, sysErr, errCode
+ }
+ if len(topologies) != 1 {
+ return fmt.Errorf("cannot find exactly 1 topology with the query string provided"), nil, http.StatusBadRequest
+ }
+ oldTopology := TOTopology{APIInfoImpl: topology.APIInfoImpl, Topology: topologies[0].(tc.Topology)}
+ if userErr, sysErr, errCode := topology.setDescription(); userErr != nil || sysErr != nil {
+ return userErr, sysErr, errCode
+ }
+
+ if err := oldTopology.removeParents(); err != nil {
+ return nil, err, http.StatusInternalServerError
+ }
+ var oldNodes, newNodes = map[string]int{}, map[string]int{}
+ for index, node := range oldTopology.Nodes {
+ oldNodes[node.Cachegroup] = index
+ }
+ for index, node := range topology.Nodes {
+ newNodes[node.Cachegroup] = index
+ }
+ var toRemove []string
+ for cachegroupName := range oldNodes {
+ if _, exists := newNodes[cachegroupName]; !exists {
+ toRemove = append(toRemove, cachegroupName)
+ } else {
+ topology.Nodes[newNodes[cachegroupName]].Id = oldTopology.Nodes[oldNodes[cachegroupName]].Id
+ }
+ }
+ if len(toRemove) > 0 {
+ if err := oldTopology.removeNodes(&toRemove); err != nil {
+ return nil, err, http.StatusInternalServerError
+ }
+ }
+ if userErr, sysErr, errCode := topology.addNodes(); userErr != nil || sysErr != nil {
+ return userErr, sysErr, errCode
+ }
+ if userErr, sysErr, errCode := topology.addParents(); userErr != nil || sysErr != nil {
+ return userErr, sysErr, errCode
+ }
+
+ return nil, nil, http.StatusOK
+}
+
+// Delete is unused and simply satisfies the Deleter interface
+// (although TOTOpology is used as an OptionsDeleter)
+func (topology *TOTopology) Delete() (error, error, int) {
+ return nil, nil, 0
+}
+
+// OptionsDelete is a requirement of the OptionsDeleter interface.
+func (topology *TOTopology) OptionsDelete() (error, error, int) {
+ topologies, userErr, sysErr, errCode := topology.Read()
+ if userErr != nil || sysErr != nil {
+ return userErr, sysErr, errCode
+ }
+ if len(topologies) != 1 {
+ return fmt.Errorf("cannot find exactly 1 topology with the query string provided"), nil, http.StatusBadRequest
+ }
+ topology.Topology = topologies[0].(tc.Topology)
+
+ var cachegroups []string
+ for _, node := range topology.Nodes {
+ cachegroups = append(cachegroups, node.Cachegroup)
+ }
+ return api.GenericOptionsDelete(topology)
+}
+
+func insertQuery() string {
+ query := `
+INSERT INTO topology (name, description)
+VALUES ($1, $2)
+RETURNING name, description, last_updated
+`
+ return query
+}
+
+func nodeInsertQuery() string {
+ query := `
+INSERT INTO topology_cachegroup (topology, cachegroup)
+VALUES ($1, unnest($2::text[]))
+RETURNING id, topology, cachegroup
+`
+ return query
+}
+
+func nodeParentInsertQuery() string {
+ query := `
+INSERT INTO topology_cachegroup_parents (child, parent, rank)
+VALUES (unnest($1::int[]), unnest($2::int[]), unnest($3::int[]))
+RETURNING child, parent, rank
+`
+ return query
+}
+
+func selectQuery() string {
+ query := `
+SELECT t.name, t.description, t.last_updated,
+tc.id, tc.cachegroup,
+ (SELECT COALESCE (ARRAY_AGG (CAST (tcp.parent as INT) ORDER BY tcp.rank ASC)) AS parents
+ FROM topology_cachegroup tc2
+ INNER JOIN topology_cachegroup_parents tcp ON tc2.id = tcp.child
+ WHERE tc2.topology = tc.topology
+ AND tc2.cachegroup = tc.cachegroup
+ )
+FROM topology t
+JOIN topology_cachegroup tc on t.name = tc.topology
+`
+ return query
+}
+
+func deleteQueryBase() string {
+ query := `
+DELETE FROM topology t
+`
+ return query
+}
+
+func deleteNodesQuery() string {
+ query := `
+DELETE FROM topology_cachegroup tc
+WHERE tc.topology = $1
+AND tc.cachegroup = ANY ($2::text[])
+`
+ return query
+}
+
+func deleteParentsQuery() string {
+ query := `
+DELETE FROM topology_cachegroup_parents tcp
+WHERE tcp.child IN
+ (SELECT tc.id
+ FROM topology t
+ JOIN topology_cachegroup tc on t.name = tc.topology
+ WHERE t.name = $1)
+`
+ return query
+}
+
+func updateQuery() string {
+ query := `
+UPDATE topology t SET
+description = $1
+WHERE t.name = $2
+RETURNING t.name, t.description, t.last_updated
+`
+ return query
+}
diff --git a/traffic_ops/traffic_ops_golang/topology/validation.go b/traffic_ops/traffic_ops_golang/topology/validation.go
new file mode 100644
index 0000000..c4270ac
--- /dev/null
+++ b/traffic_ops/traffic_ops_golang/topology/validation.go
@@ -0,0 +1,115 @@
+package topology
+
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import (
+ "fmt"
+ "github.com/apache/trafficcontrol/lib/go-tc"
+ "github.com/apache/trafficcontrol/lib/go-util"
+)
+
+func checkUniqueCacheGroupNames(nodes []tc.TopologyNode) error {
+ cacheGroupNames := map[string]bool{}
+ for _, node := range nodes {
+ if _, exists := cacheGroupNames[node.Cachegroup]; exists {
+ return fmt.Errorf("cachegroup %v cannot be used more than once in the topology", node.Cachegroup)
+ }
+ cacheGroupNames[node.Cachegroup] = true
+ }
+ return nil
+}
+
+func checkForDuplicateParents(nodes []tc.TopologyNode, index int) error {
+ parents := nodes[index].Parents
+ if len(parents) != 2 || parents[0] != parents[1] {
+ return nil
+ }
+ return fmt.Errorf("cachegroup %v cannot be both a primary and secondary parent of cachegroup %v", nodes[parents[0]].Cachegroup, nodes[index].Cachegroup)
+}
+
+func checkForSelfParents(nodes []tc.TopologyNode, index int) error {
+ for _, parentIndex := range nodes[index].Parents {
+ if index == parentIndex {
+ return fmt.Errorf("cachegroup %v cannot be a parent of itself", index)
+ }
+ }
+ return nil
+}
+
+func checkForEdgeParents(nodes []tc.TopologyNode, cachegroups []tc.CacheGroupNullable, nodeIndex int) error {
+ node := nodes[nodeIndex]
+ errs := make([]error, len(node.Parents))
+ for parentIndex := range node.Parents {
+ cacheGroupType := cachegroups[node.Parents[parentIndex]].Type
+ if *cacheGroupType == tc.CacheGroupEdgeTypeName {
+ errs[parentIndex] = fmt.Errorf("cachegroup %v's type is %v; it cannot be a parent of %v", nodes[parentIndex].Cachegroup, tc.CacheGroupEdgeTypeName, node.Cachegroup)
+ }
+ }
+ return util.JoinErrs(errs)
+}
+
+func checkForLeafMids(nodes []tc.TopologyNode, cacheGroups []tc.CacheGroupNullable) []tc.TopologyNode {
+ isLeafMid := make([]bool, len(nodes))
+ for index := range isLeafMid {
+ isLeafMid[index] = true
+ }
+ for index, node := range nodes {
+ if *cacheGroups[index].Type == tc.CacheGroupEdgeTypeName {
+ isLeafMid[index] = false
+ }
+ for _, parentIndex := range node.Parents {
+ if !isLeafMid[parentIndex] {
+ continue
+ }
+ isLeafMid[parentIndex] = false
+ }
+ }
+
+ var leafMids []tc.TopologyNode
+ for index, node := range nodes {
+ if isLeafMid[index] {
+ leafMids = append(leafMids, node)
+ }
+ }
+ return leafMids
+}
+
+func checkForCycles(nodes []tc.TopologyNode) error {
+ components := tarjan(nodes)
+ var errs []error
+ for _, component := range components {
+ if len(component) > 1 {
+ errString := "cycle detected between cachegroups "
+ var node tc.TopologyNode
+ for _, node = range component {
+ errString += node.Cachegroup + ", "
+ }
+ length := len(errString)
+ cachegroupNameLength := len(node.Cachegroup)
+ errString = errString[0:length-2-cachegroupNameLength-2] + " and " + errString[length-2-cachegroupNameLength:length-2]
+ errs = append(errs, fmt.Errorf(errString))
+ }
+ }
+ if len(errs) == 0 {
+ return nil
+ }
+ errs = append([]error{fmt.Errorf("topology cannot have cycles")}, errs...)
+ return util.JoinErrs(errs)
+}
diff --git a/traffic_ops/traffic_ops_golang/trafficstats/deliveryservice.go b/traffic_ops/traffic_ops_golang/trafficstats/deliveryservice.go
index 066b505..3a05e5d 100644
--- a/traffic_ops/traffic_ops_golang/trafficstats/deliveryservice.go
+++ b/traffic_ops/traffic_ops_golang/trafficstats/deliveryservice.go
@@ -210,8 +210,8 @@ func handleRequest(w http.ResponseWriter, r *http.Request, client *influx.Client
if summary != nil {
resp.Summary = &tc.TrafficDSStatsSummary{
TrafficStatsSummary: *summary,
- TotalKiloBytes: kBs,
- TotalTransactions: txns,
+ TotalKiloBytes: kBs,
+ TotalTransactions: txns,
}
} else {
resp.Summary = &tc.TrafficDSStatsSummary{}
@@ -278,8 +278,8 @@ func handleLegacyRequest(w http.ResponseWriter, r *http.Request, client *influx.
if summary != nil {
resp.Summary = &tc.LegacyTrafficDSStatsSummary{
TrafficStatsSummary: *summary,
- TotalBytes: kBs,
- TotalTransactions: txns,
+ TotalBytes: kBs,
+ TotalTransactions: txns,
}
} else {
resp.Summary = &tc.LegacyTrafficDSStatsSummary{}
diff --git a/traffic_portal/app/src/app.js b/traffic_portal/app/src/app.js
index 3e1d0fd..1b2e477 100644
--- a/traffic_portal/app/src/app.js
+++ b/traffic_portal/app/src/app.js
@@ -33,6 +33,7 @@ var trafficPortal = angular.module('trafficPortal', [
'ngSanitize',
'ngRoute',
'ui.router',
+ 'ui.tree',
'ui.bootstrap',
'ui.bootstrap.contextMenu',
'app.templates',
@@ -201,6 +202,10 @@ var trafficPortal = angular.module('trafficPortal', [
require('./modules/private/tenants/new').name,
require('./modules/private/tenants/users').name,
require('./modules/private/types').name,
+ require('./modules/private/topologies').name,
+ require('./modules/private/topologies/edit').name,
+ require('./modules/private/topologies/list').name,
+ require('./modules/private/topologies/new').name,
require('./modules/private/types/edit').name,
require('./modules/private/types/list').name,
require('./modules/private/types/new').name,
@@ -317,6 +322,9 @@ var trafficPortal = angular.module('trafficPortal', [
require('./common/modules/form/tenant').name,
require('./common/modules/form/tenant/edit').name,
require('./common/modules/form/tenant/new').name,
+ require('./common/modules/form/topology').name,
+ require('./common/modules/form/topology/edit').name,
+ require('./common/modules/form/topology/new').name,
require('./common/modules/form/type').name,
require('./common/modules/form/type/edit').name,
require('./common/modules/form/type/new').name,
@@ -387,6 +395,9 @@ var trafficPortal = angular.module('trafficPortal', [
require('./common/modules/table/tenants').name,
require('./common/modules/table/tenantDeliveryServices').name,
require('./common/modules/table/tenantUsers').name,
+ require('./common/modules/table/topologies').name,
+ require('./common/modules/table/topologyCacheGroups').name,
+ require('./common/modules/table/topologyCacheGroupServers').name,
require('./common/modules/table/types').name,
require('./common/modules/table/typeCacheGroups').name,
require('./common/modules/table/typeDeliveryServices').name,
diff --git a/traffic_portal/app/src/assets/css/angular-ui-tree.min_2.22.6.css b/traffic_portal/app/src/assets/css/angular-ui-tree.min_2.22.6.css
new file mode 100644
index 0000000..dbf6650
--- /dev/null
+++ b/traffic_portal/app/src/assets/css/angular-ui-tree.min_2.22.6.css
@@ -0,0 +1,2 @@
+/* Angular UI Tree v2.22.6 - (c) 2010-2017. https://github.com/angular-ui-tree/angular-ui-tree - MIT */
+.angular-ui-tree-empty{border:1px dashed #bbb;min-height:100px;background-color:#e5e5e5;background-image:-webkit-linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff),-webkit-linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff);background-image:linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff),linear-gradient(45deg,#fff 25%,transparent 0,transparent 75%,#fff 0,#fff);background-size:60px 60px;background-position:0 0,30px 30p [...]
\ No newline at end of file
diff --git a/traffic_portal/app/src/assets/js/angular-ui-tree.min_2.22.6.js b/traffic_portal/app/src/assets/js/angular-ui-tree.min_2.22.6.js
new file mode 100644
index 0000000..86a7741
--- /dev/null
+++ b/traffic_portal/app/src/assets/js/angular-ui-tree.min_2.22.6.js
@@ -0,0 +1,2 @@
+/* Angular UI Tree v2.22.6 - (c) 2010-2017. https://github.com/angular-ui-tree/angular-ui-tree - MIT */
+!function(){"use strict";angular.module("ui.tree",[]).constant("treeConfig",{treeClass:"angular-ui-tree",emptyTreeClass:"angular-ui-tree-empty",dropzoneClass:"angular-ui-tree-dropzone",hiddenClass:"angular-ui-tree-hidden",nodesClass:"angular-ui-tree-nodes",nodeClass:"angular-ui-tree-node",handleClass:"angular-ui-tree-handle",placeholderClass:"angular-ui-tree-placeholder",dragClass:"angular-ui-tree-drag",dragThreshold:3,defaultCollapsed:!1,appendChildOnHover:!0})}(),function(){"use strict [...]
\ No newline at end of file
diff --git a/traffic_portal/app/src/common/api/TopologyService.js b/traffic_portal/app/src/common/api/TopologyService.js
new file mode 100644
index 0000000..87a7f6d
--- /dev/null
+++ b/traffic_portal/app/src/common/api/TopologyService.js
@@ -0,0 +1,72 @@
+/*
+ * 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 TopologyService = function($http, ENV, locationUtils, messageModel) {
+
+ this.getTopologies = function(queryParams) {
+ return $http.get(ENV.api['latest'] + 'topologies', { params: queryParams }).then(
+ function(result) {
+ return result.data.response;
+ },
+ function(err) {
+ throw err;
+ }
+ );
+ };
+
+ this.createTopology = function(topology) {
+ return $http.post(ENV.api['latest'] + 'topologies', topology).then(
+ function(result) {
+ return result;
+ },
+ function(err) {
+ messageModel.setMessages(err.data.alerts, false);
+ throw err;
+ }
+ );
+ };
+
+ this.updateTopology = function(topology) {
+ return $http.put(ENV.api['latest'] + 'topologies', topology, { params: { name: topology.name } }).then(
+ function(result) {
+ return result;
+ },
+ function(err) {
+ messageModel.setMessages(err.data.alerts, false);
+ throw err;
+ }
+ );
+ };
+
+ this.deleteTopology = function(topology) {
+ return $http.delete(ENV.api['latest'] + "topologies", { params: { name: topology.name } }).then(
+ function(result) {
+ return result;
+ },
+ function(err) {
+ messageModel.setMessages(err.data.alerts, true);
+ throw err;
+ }
+ );
+ };
+
+};
+
+TopologyService.$inject = ['$http', 'ENV', 'locationUtils', 'messageModel'];
+module.exports = TopologyService;
diff --git a/traffic_portal/app/src/common/api/index.js b/traffic_portal/app/src/common/api/index.js
index 7ee157a..079b6d9 100644
--- a/traffic_portal/app/src/common/api/index.js
+++ b/traffic_portal/app/src/common/api/index.js
@@ -53,6 +53,7 @@ module.exports = angular.module('trafficPortal.api', [])
.service('statusService', require('./StatusService'))
.service('tenantService', require('./TenantService'))
.service('toolsService', require('./ToolsService'))
+ .service('topologyService', require('./TopologyService'))
.service('typeService', require('./TypeService'))
.service('trafficPortalService', require('./TrafficPortalService'))
.service('userService', require('./UserService'))
diff --git a/traffic_portal/app/src/common/modules/dialog/select/DialogSelectController.js b/traffic_portal/app/src/common/modules/dialog/select/DialogSelectController.js
index d2fccc9..2d3fac7 100644
--- a/traffic_portal/app/src/common/modules/dialog/select/DialogSelectController.js
+++ b/traffic_portal/app/src/common/modules/dialog/select/DialogSelectController.js
@@ -46,6 +46,10 @@ var DialogSelectController = function(params, collection, $scope, $uibModalInsta
}
$scope.key = $scope.params.key || 'id';
+
+ $scope.required = ($scope.params.required !== undefined) ? $scope.params.required : true;
+
+ $scope.selectedItemKeyValue = $scope.params.selectedItemKeyValue || null;
};
init();
diff --git a/traffic_portal/app/src/common/modules/dialog/select/dialog.select.tpl.html b/traffic_portal/app/src/common/modules/dialog/select/dialog.select.tpl.html
index 881a32d..0d3522e 100644
--- a/traffic_portal/app/src/common/modules/dialog/select/dialog.select.tpl.html
+++ b/traffic_portal/app/src/common/modules/dialog/select/dialog.select.tpl.html
@@ -24,7 +24,7 @@ under the License.
<div class="modal-body">
<p ng-bind-html="params.message"></p>
<form name="selectForm" novalidate>
- <select class="form-control" name="selectFormDropdown" ng-model="selectedItemKeyValue" ng-options="item[key] as label(item) for item in collection" required>
+ <select class="form-control" name="selectFormDropdown" ng-model="selectedItemKeyValue" ng-options="item[key] as label(item) for item in collection" ng-required="{{::required}}">
<option value="">Select...</option>
</select>
</form>
diff --git a/traffic_portal/app/src/common/modules/form/topology/FormTopologyController.js b/traffic_portal/app/src/common/modules/form/topology/FormTopologyController.js
new file mode 100644
index 0000000..c912f40
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/topology/FormTopologyController.js
@@ -0,0 +1,257 @@
+/*
+ * 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 FormTopologyController = function(topology, cacheGroups, $anchorScroll, $scope, $location, $uibModal, formUtils, locationUtils, topologyUtils, messageModel) {
+
+ let cacheGroupNamesInTopology = [];
+
+ let hydrateTopology = function() {
+ // add some needed fields to each cache group (aka node) of a topology
+ topology.nodes.forEach(function(node) {
+ let cacheGroup = cacheGroups.find( function(cg) { return cg.name === node.cachegroup} );
+ Object.assign(node, { id: cacheGroup.id, type: cacheGroup.typeName });
+ });
+ };
+
+ let removeSecParentReferences = function(topologyTree, secParentName) {
+ // when a cache group is removed, any references to the cache group as a secParent need to be removed
+ topologyTree.forEach(function(node) {
+ if (node.secParent && node.secParent === secParentName) {
+ node.secParent = '';
+ }
+ if (node.children && node.children.length > 0) {
+ removeSecParentReferences(node.children, secParentName);
+ }
+ });
+ };
+
+ // build a list of cache group names currently in the topology
+ let buildCacheGroupNamesInTopology = function(topologyTree, fromScratch) {
+ if (fromScratch) cacheGroupNamesInTopology = [];
+ topologyTree.forEach(function(node) {
+ if (node.cachegroup) {
+ cacheGroupNamesInTopology.push(node.cachegroup);
+ }
+ if (node.children && node.children.length > 0) {
+ buildCacheGroupNamesInTopology(node.children, false);
+ }
+ });
+ };
+
+ $scope.topology = topology;
+
+ $scope.topologyTree = [];
+
+ $scope.topologyTreeOptions = {
+ beforeDrop: function(evt) {
+ let node = evt.source.nodeScope.$modelValue,
+ parent = evt.dest.nodesScope.$parent.$modelValue;
+
+ if (!parent || !node) {
+ return false; // no dropping outside the toplogy tree and you need a node to drop
+ }
+
+ // ORG_LOC cannot have a parent
+ if (node.type === 'ORG_LOC' && parent.cachegroup) {
+ $anchorScroll(); // scrolls window to top
+ messageModel.setMessages([ { level: 'error', text: 'Cache groups of ORG_LOC type must not have a parent.' } ], false);
+ return false;
+ }
+
+ // EDGE_LOC cannot have children
+ if (parent.type === 'EDGE_LOC') {
+ $anchorScroll(); // scrolls window to top
+ messageModel.setMessages([ { level: 'error', text: 'Cache groups of EDGE_LOC type must not have children.' } ], false);
+ return false;
+ }
+
+ // update the parent and secParent fields of the node on successful drop
+ if (parent.cachegroup) {
+ node.parent = parent.cachegroup; // change the node parent based on where the node is dropped
+ if (node.parent === node.secParent) {
+ // node parent and secParent cannot be the same
+ node.secParent = "";
+ }
+ } else {
+ // the node was dropped at the root of the topology. no parents.
+ node.parent = "";
+ node.secParent = "";
+ }
+ return true;
+ }
+ };
+
+ $scope.navigateToPath = locationUtils.navigateToPath;
+
+ $scope.hasError = formUtils.hasError;
+
+ $scope.hasPropertyError = formUtils.hasPropertyError;
+
+ $scope.nodeLabel = function(node) {
+ return node.cachegroup || 'TOPOLOGY';
+ };
+
+ $scope.editSecParent = function(node) {
+
+ if (!node.parent) return; // if a node has no parent, it can't have a second parent
+
+ buildCacheGroupNamesInTopology($scope.topologyTree, true);
+
+ /* Cache groups that can act as a second parent include:
+ 1. cache groups of type ORG_LOC or MID_LOC (not EDGE_LOC)
+ 2. cache groups that are not currently acting as the primary parent
+ 3. cache groups that exist currently in the topology
+ */
+ let eligibleSecParentCandidates = cacheGroups.filter(function(cg) {
+ return cg.typeName !== 'EDGE_LOC' &&
+ (node.parent && node.parent !== cg.name) &&
+ cacheGroupNamesInTopology.includes(cg.name);
+ });
+
+ let params = {
+ title: 'Select a secondary parent',
+ message: 'Please select a secondary parent that is part of the ' + topology.name + ' topology',
+ key: 'name',
+ required: false,
+ selectedItemKeyValue: node.secParent
+ };
+ let modalInstance = $uibModal.open({
+ templateUrl: 'common/modules/dialog/select/dialog.select.tpl.html',
+ controller: 'DialogSelectController',
+ size: 'md',
+ resolve: {
+ params: function () {
+ return params;
+ },
+ collection: function() {
+ return eligibleSecParentCandidates;
+ }
+ }
+ });
+ modalInstance.result.then(function(selectedSecParent) {
+ if (selectedSecParent) {
+ node.secParent = selectedSecParent.name;
+ } else {
+ node.secParent = '';
+ }
+ });
+ };
+
+ $scope.deleteCacheGroup = function(node, scope) {
+ if (node.cachegroup) {
+ removeSecParentReferences($scope.topologyTree, node.cachegroup);
+ scope.remove();
+ }
+ };
+
+ $scope.toggle = function(scope) {
+ scope.toggle();
+ };
+
+ $scope.hasNodeError = function(node) {
+ if (node.type !== 'EDGE_LOC' && node.children.length === 0) {
+ return true;
+ }
+ return false;
+ };
+
+ $scope.isOrigin = function(node) {
+ return node.type === 'ROOT' || node.type === 'ORG_LOC';
+ };
+
+ $scope.isMid = function(node) {
+ return node.type === 'MID_LOC';
+ };
+
+ $scope.hasChildren = function(node) {
+ return node.children.length > 0;
+ };
+
+ $scope.addCacheGroups = function(parent, scope) {
+
+ if (parent.type === 'EDGE_LOC') {
+ // can't add children to EDGE_LOC. button should be hidden anyhow.
+ return;
+ }
+
+ // cache groups already in the topology cannot be selected again for addition
+ buildCacheGroupNamesInTopology($scope.topologyTree, true);
+
+ let modalInstance = $uibModal.open({
+ templateUrl: 'common/modules/table/topologyCacheGroups/table.selectTopologyCacheGroups.tpl.html',
+ controller: 'TableSelectTopologyCacheGroupsController',
+ size: 'lg',
+ resolve: {
+ parent: function() {
+ return parent;
+ },
+ topology: function() {
+ return topology;
+ },
+ cacheGroups: function(cacheGroupService) {
+ return cacheGroupService.getCacheGroups();
+ },
+ usedCacheGroupNames: function() {
+ return cacheGroupNamesInTopology;
+ }
+ }
+ });
+ modalInstance.result.then(function(result) {
+ let nodeData = scope.$modelValue,
+ cacheGroupNodes = result.selectedCacheGroups.map(function(cg) {
+ return {
+ id: cg.id,
+ cachegroup: cg.name,
+ type: cg.typeName,
+ parent: (result.parent) ? result.parent : '',
+ secParent: result.secParent,
+ children: []
+ }
+ });
+ cacheGroupNodes.forEach(function(node) {
+ nodeData.children.unshift(node);
+ });
+ });
+ };
+
+ $scope.viewCacheGroupServers = function(node) {
+ $uibModal.open({
+ templateUrl: 'common/modules/table/topologyCacheGroupServers/table.topologyCacheGroupServers.tpl.html',
+ controller: 'TableTopologyCacheGroupServersController',
+ size: 'lg',
+ resolve: {
+ cacheGroupName: function() {
+ return node.cachegroup;
+ },
+ cacheGroupServers: function(serverService) {
+ return serverService.getServers({ cachegroup: node.id });
+ }
+ }
+ });
+ };
+
+ let init = function() {
+ hydrateTopology();
+ $scope.topologyTree = topologyUtils.getTopologyTree($scope.topology);
+ };
+ init();
+};
+
+FormTopologyController.$inject = ['topology', 'cacheGroups', '$anchorScroll', '$scope', '$location', '$uibModal', 'formUtils', 'locationUtils', 'topologyUtils', 'messageModel'];
+module.exports = FormTopologyController;
diff --git a/traffic_portal/app/src/common/modules/form/topology/edit/FormEditTopologyController.js b/traffic_portal/app/src/common/modules/form/topology/edit/FormEditTopologyController.js
new file mode 100644
index 0000000..326ff69
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/topology/edit/FormEditTopologyController.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 FormEditTopologyController = function(topologies, cacheGroups, $scope, $controller, $uibModal, $anchorScroll, locationUtils, topologyService, messageModel, topologyUtils) {
+
+ // extends the FormTopologyController to inherit common methods
+ angular.extend(this, $controller('FormTopologyController', { topology: topologies[0], cacheGroups: cacheGroups, $scope: $scope }));
+
+ let deleteTopology = function(topology) {
+ topologyService.deleteTopology(topology)
+ .then(function() {
+ messageModel.setMessages([ { level: 'success', text: 'Topology deleted' } ], true);
+ locationUtils.navigateToPath('/topologies');
+ });
+ };
+
+ $scope.topologyName = angular.copy($scope.topology.name);
+
+ $scope.settings = {
+ isNew: false,
+ saveLabel: 'Update'
+ };
+
+ $scope.save = function(name, description, topologyTree) {
+ let normalizedTopology = topologyUtils.getNormalizedTopology(name, description, topologyTree);
+ topologyService.updateTopology(normalizedTopology).
+ then(function() {
+ messageModel.setMessages([ { level: 'success', text: 'Topology updated' } ], false);
+ });
+ };
+
+ $scope.confirmDelete = function(topology) {
+ let params = {
+ title: 'Delete Topology: ' + topology.name,
+ key: topology.name
+ };
+ let modalInstance = $uibModal.open({
+ templateUrl: 'common/modules/dialog/delete/dialog.delete.tpl.html',
+ controller: 'DialogDeleteController',
+ size: 'md',
+ resolve: {
+ params: function () {
+ return params;
+ }
+ }
+ });
+ modalInstance.result.then(function() {
+ deleteTopology(topology);
+ });
+ };
+
+};
+
+FormEditTopologyController.$inject = ['topologies', 'cacheGroups', '$scope', '$controller', '$uibModal', '$anchorScroll', 'locationUtils', 'topologyService', 'messageModel', 'topologyUtils'];
+module.exports = FormEditTopologyController;
diff --git a/traffic_portal/app/src/scripts/config.js b/traffic_portal/app/src/common/modules/form/topology/edit/index.js
similarity index 83%
copy from traffic_portal/app/src/scripts/config.js
copy to traffic_portal/app/src/common/modules/form/topology/edit/index.js
index 2c2ba1f..f3c7ad8 100644
--- a/traffic_portal/app/src/scripts/config.js
+++ b/traffic_portal/app/src/common/modules/form/topology/edit/index.js
@@ -17,12 +17,5 @@
* under the License.
*/
-// this is the config the TO UI uses
-
-"use strict";
-
-angular.module('config', [])
-
-.constant('ENV', { api: { root:'/api/2.0/', legacy: '/api/1.5/' } })
-
-;
+module.exports = angular.module('trafficPortal.form.topology.edit', [])
+ .controller('FormEditTopologyController', require('./FormEditTopologyController'));
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
new file mode 100644
index 0000000..966867e
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/topology/form.topology.tpl.html
@@ -0,0 +1,93 @@
+<!--
+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.
+-->
+
+<div class="x_panel">
+ <div class="x_title">
+ <ol class="breadcrumb pull-left">
+ <li><a ng-click="navigateToPath('/topologies')">Topologies</a></li>
+ <li class="active">{{topologyName}}</li>
+ </ol>
+ <div class="clearfix"></div>
+ </div>
+ <div class="x_content">
+ <br>
+ <form name="topologyForm" class="form-horizontal form-label-left" novalidate>
+ <div class="form-group" ng-class="{'has-error': hasError(topologyForm.name), 'has-feedback': hasError(topologyForm.name)}">
+ <label class="control-label col-md-2 col-sm-2 col-xs-12">Name *</label>
+ <div class="col-md-10 col-sm-10 col-xs-12">
+ <input name="name" type="text" class="form-control" ng-model="topology.name" ng-disabled="!settings.isNew" pattern="[A-Za-z0-9]([A-Za-z\-0-9]*[A-Za-z0-9])?" required autofocus>
+ <small class="input-error" ng-show="hasPropertyError(topologyForm.name, 'required')">Required</small>
+ <small class="input-error" ng-show="hasPropertyError(topologyForm.name, 'pattern')">No special characters, periods, underscores, or spaces and cannot begin or end with a hyphen</small>
+ <span ng-show="hasError(topologyForm.name)" class="form-control-feedback"><i class="fa fa-times"></i></span>
+ </div>
+ </div>
+ <div class="form-group" ng-class="{'has-error': hasError(topologyForm.description), 'has-feedback': hasError(topologyForm.description)}">
+ <label class="control-label col-md-2 col-sm-2 col-xs-12">Description *</label>
+ <div class="col-md-10 col-sm-10 col-xs-12">
+ <textarea id="description" name="description" rows="3" cols="17" class="form-control" ng-model="topology.description" maxlength="256" required></textarea>
+ <small class="input-error" ng-show="hasPropertyError(topologyForm.description, 'required')">Required</small>
+ </div>
+ </div>
+ <div class="form-group">
+ <label class="control-label col-md-2 col-sm-2 col-xs-12">Cache Groups *</label>
+ <div class="col-md-10 col-sm-10 col-xs-12">
+ <div id="tree-root" ui-tree="topologyTreeOptions" >
+ <ol ui-tree-nodes ng-model="topologyTree">
+ <li ng-repeat="node in topologyTree" ui-tree-node ng-include="'nodes_renderer.html'"></li>
+ </ol>
+ </div>
+ </div>
+ </div>
+ <div class="modal-footer">
+ <button type="button" class="btn btn-danger" ng-show="!settings.isNew" ng-click="confirmDelete(topology)">Delete</button>
+ <button type="button" class="btn btn-success" ng-disabled="topologyForm.$invalid" ng-click="save(topology.name, topology.description, topologyTree)">{{settings.saveLabel}}</button>
+ </div>
+ </form>
+ </div>
+</div>
+
+<script type="text/ng-template" id="nodes_renderer.html">
+ <input name="error" ng-if="hasNodeError(node)" ng-model="error" type="hidden" required>
+ <div id="{{node.cachegroup}}" ui-tree-handle class="tree-node tree-node-content"
+ ng-class="{ 'error': hasNodeError(node), 'origin': isOrigin(node), 'mid': isMid(node) }">
+ <div class="tree-node-label pull-left">
+ <a class="tree-toggle btn btn-primary btn-xs" ng-if="node.type !== 'ROOT' && hasChildren(node)" data-nodrag ng-click="toggle(this)">
+ <i class="fa" ng-class="collapsed ? 'fa-caret-right' : 'fa-caret-down'"></i>
+ </a> {{nodeLabel(node)}} <small>{{node.type}}</small>
+ </div>
+ <div ng-if="hasNodeError(node)" class="error-msg">1+ child required for {{node.type}}</div>
+ <div class="pull-right">
+ <a ng-show="node.cachegroup && node.parent" title="Set Secondary Parent Cache Group for {{node.cachegroup}}" class="btn btn-primary btn-xs" data-nodrag ng-click="editSecParent(node)" style="margin-right: 8px;">
+ 2nd: {{(node.secParent) ? node.secParent : 'None'}}
+ </a>
+ <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 ng-disabled="node.type === 'EDGE_LOC'" title="{{(node.type !== 'EDGE_LOC') ? 'Add cache groups to ' + node.cachegroup : 'Cache groups cannot be added to EDGE_LOC cache groups'}}" class="btn btn-primary btn-xs" 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)">
+ <i class="fa fa-times"></i>
+ </a>
+ </div>
+ </div>
+ <ol ui-tree-nodes="" ng-model="node.children" ng-class="{hidden: collapsed}">
+ <li ng-repeat="node in node.children" ui-tree-node data-expand-on-hover="true" ng-include="'nodes_renderer.html'"></li>
+ </ol>
+</script>
diff --git a/traffic_portal/app/src/scripts/config.js b/traffic_portal/app/src/common/modules/form/topology/index.js
similarity index 83%
copy from traffic_portal/app/src/scripts/config.js
copy to traffic_portal/app/src/common/modules/form/topology/index.js
index 2c2ba1f..9ebfc4f 100644
--- a/traffic_portal/app/src/scripts/config.js
+++ b/traffic_portal/app/src/common/modules/form/topology/index.js
@@ -17,12 +17,5 @@
* under the License.
*/
-// this is the config the TO UI uses
-
-"use strict";
-
-angular.module('config', [])
-
-.constant('ENV', { api: { root:'/api/2.0/', legacy: '/api/1.5/' } })
-
-;
+module.exports = angular.module('trafficPortal.form.topology', [])
+ .controller('FormTopologyController', require('./FormTopologyController'));
diff --git a/traffic_portal/app/src/common/modules/form/topology/new/FormNewTopologyController.js b/traffic_portal/app/src/common/modules/form/topology/new/FormNewTopologyController.js
new file mode 100644
index 0000000..1528b16
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/form/topology/new/FormNewTopologyController.js
@@ -0,0 +1,44 @@
+/*
+ * 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 FormNewTopologyController = function(topology, cacheGroups, $scope, $controller, locationUtils, topologyService, messageModel, topologyUtils) {
+
+ // extends the FormTopologyController to inherit common methods
+ angular.extend(this, $controller('FormTopologyController', { topology: topology, cacheGroups: cacheGroups, $scope: $scope }));
+
+ $scope.topologyName = 'New';
+
+ $scope.settings = {
+ isNew: true,
+ saveLabel: 'Create'
+ };
+
+ $scope.save = function(name, description, topologyTree) {
+ let normalizedTopology = topologyUtils.getNormalizedTopology(name, description, topologyTree);
+ topologyService.createTopology(normalizedTopology).
+ then(function() {
+ messageModel.setMessages([ { level: 'success', text: 'Topology created' } ], true);
+ locationUtils.navigateToPath('/topologies');
+ });
+ };
+
+};
+
+FormNewTopologyController.$inject = ['topology', 'cacheGroups', '$scope', '$controller', 'locationUtils', 'topologyService', 'messageModel', 'topologyUtils'];
+module.exports = FormNewTopologyController;
diff --git a/traffic_portal/app/src/scripts/config.js b/traffic_portal/app/src/common/modules/form/topology/new/index.js
similarity index 83%
copy from traffic_portal/app/src/scripts/config.js
copy to traffic_portal/app/src/common/modules/form/topology/new/index.js
index 2c2ba1f..4608439 100644
--- a/traffic_portal/app/src/scripts/config.js
+++ b/traffic_portal/app/src/common/modules/form/topology/new/index.js
@@ -17,12 +17,5 @@
* under the License.
*/
-// this is the config the TO UI uses
-
-"use strict";
-
-angular.module('config', [])
-
-.constant('ENV', { api: { root:'/api/2.0/', legacy: '/api/1.5/' } })
-
-;
+module.exports = angular.module('trafficPortal.form.topology.new', [])
+ .controller('FormNewTopologyController', require('./FormNewTopologyController'));
diff --git a/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html b/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html
index 0dd58da..dc4d0a0 100644
--- a/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html
+++ b/traffic_portal/app/src/common/modules/navigation/navigation.tpl.html
@@ -52,6 +52,7 @@ under the License.
</li>
<li class="side-menu-category"><a href="javascript:void(0);"><i class="fa fa-sm fa-chevron-right"></i> Topology</span></a>
<ul class="nav child_menu" style="display: none">
+ <li class="side-menu-category-item" ng-if="hasCapability('topologies-read')" ng-class="{'current-page': isState('trafficPortal.private.topologies')}"><a href="/#!/topologies">Topologies</a></li>
<li class="side-menu-category-item" ng-if="hasCapability('cache-groups-read')" ng-class="{'current-page': isState('trafficPortal.private.cacheGroups')}"><a href="/#!/cache-groups">Cache Groups</a></li>
<li class="side-menu-category-item" ng-if="hasCapability('coordinates-read')" ng-class="{'current-page': isState('trafficPortal.private.coordinates')}"><a href="/#!/coordinates">Coordinates</a></li>
<li class="side-menu-category-item" ng-if="hasCapability('phys-locations-read')" ng-class="{'current-page': isState('trafficPortal.private.physLocations')}"><a href="/#!/phys-locations">Phys Locations</a></li>
diff --git a/traffic_portal/app/src/scripts/config.js b/traffic_portal/app/src/common/modules/table/topologies/TableTopologiesController.js
similarity index 51%
copy from traffic_portal/app/src/scripts/config.js
copy to traffic_portal/app/src/common/modules/table/topologies/TableTopologiesController.js
index 2c2ba1f..ce0b714 100644
--- a/traffic_portal/app/src/scripts/config.js
+++ b/traffic_portal/app/src/common/modules/table/topologies/TableTopologiesController.js
@@ -17,12 +17,31 @@
* under the License.
*/
-// this is the config the TO UI uses
+var TableTopologiesController = function(topologies, $scope, $state, locationUtils) {
-"use strict";
+ $scope.topologies = topologies;
-angular.module('config', [])
+ $scope.editTopology = function(name) {
+ locationUtils.navigateToPath('/topologies/edit?name=' + name);
+ };
-.constant('ENV', { api: { root:'/api/2.0/', legacy: '/api/1.5/' } })
+ $scope.createTopology = function() {
+ locationUtils.navigateToPath('/topologies/new');
+ };
-;
+ $scope.refresh = function() {
+ $state.reload(); // reloads all the resolves for the view
+ };
+
+ angular.element(document).ready(function () {
+ $('#topologiesTable').dataTable({
+ "aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]],
+ "iDisplayLength": 25,
+ "aaSorting": []
+ });
+ });
+
+};
+
+TableTopologiesController.$inject = ['topologies', '$scope', '$state', 'locationUtils'];
+module.exports = TableTopologiesController;
diff --git a/traffic_portal/app/src/scripts/config.js b/traffic_portal/app/src/common/modules/table/topologies/index.js
similarity index 83%
copy from traffic_portal/app/src/scripts/config.js
copy to traffic_portal/app/src/common/modules/table/topologies/index.js
index 2c2ba1f..9084dd9 100644
--- a/traffic_portal/app/src/scripts/config.js
+++ b/traffic_portal/app/src/common/modules/table/topologies/index.js
@@ -17,12 +17,5 @@
* under the License.
*/
-// this is the config the TO UI uses
-
-"use strict";
-
-angular.module('config', [])
-
-.constant('ENV', { api: { root:'/api/2.0/', legacy: '/api/1.5/' } })
-
-;
+module.exports = angular.module('trafficPortal.table.topologies', [])
+ .controller('TableTopologiesController', require('./TableTopologiesController'));
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
new file mode 100644
index 0000000..1cf20b3
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/topologies/table.topologies.tpl.html
@@ -0,0 +1,51 @@
+<!--
+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.
+-->
+
+<div class="x_panel">
+ <div class="x_title">
+ <ol class="breadcrumb pull-left">
+ <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" class="btn btn-default" title="Refresh" ng-click="refresh()"><i class="fa fa-refresh"></i></button>
+ </div>
+ <div class="clearfix"></div>
+ </div>
+ <div class="x_content">
+ <br>
+ <table id="topologiesTable" class="table responsive-utilities jambo_table">
+ <thead>
+ <tr class="headings">
+ <th>Name</th>
+ <th>Description</th>
+ <th>Cache Groups</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-click="editTopology(t.name)" ng-repeat="t in ::topologies">
+ <td data-search="^{{::t.name}}$">{{::t.name}}</td>
+ <td data-search="^{{::t.description}}$">{{::t.description}}</td>
+ <td >{{::t.nodes.length}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</div>
+
diff --git a/traffic_portal/app/src/common/modules/dialog/select/DialogSelectController.js b/traffic_portal/app/src/common/modules/table/topologyCacheGroupServers/TableTopologyCacheGroupServersController.js
similarity index 51%
copy from traffic_portal/app/src/common/modules/dialog/select/DialogSelectController.js
copy to traffic_portal/app/src/common/modules/table/topologyCacheGroupServers/TableTopologyCacheGroupServersController.js
index d2fccc9..93e1556 100644
--- a/traffic_portal/app/src/common/modules/dialog/select/DialogSelectController.js
+++ b/traffic_portal/app/src/common/modules/table/topologyCacheGroupServers/TableTopologyCacheGroupServersController.js
@@ -17,39 +17,39 @@
* under the License.
*/
-var DialogSelectController = function(params, collection, $scope, $uibModalInstance) {
+var TableTopologyCacheGroupServersController = function(cacheGroupName, cacheGroupServers, $scope, $uibModalInstance) {
- $scope.params = params;
+ let adjustTableColumns = function() {
+ window.setTimeout(function() {
+ $($.fn.dataTable.tables(true)).DataTable()
+ .columns.adjust();
+ },100);
+ };
- $scope.collection = collection;
+ $scope.cacheGroupName = cacheGroupName;
- $scope.selectedItemKeyValue = null;
-
- $scope.select = function() {
- const selectedItem = collection.find(function(item) {
- return item[$scope.key] === $scope.selectedItemKeyValue;
- });
- $uibModalInstance.close(selectedItem);
- };
+ $scope.cacheGroupServers = cacheGroupServers;
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
- var init = function() {
- if ($scope.params.label) {
- $scope.label = function(item) { return item[$scope.params.label]; }
- } else if ($scope.params.labelFunction) {
- $scope.label = $scope.params.labelFunction;
- } else {
- $scope.label = function(item) { return item['name']; }
- }
+ angular.element(document).ready(function () {
+ $('#topologyCacheGroupServersTable').dataTable({
+ "aLengthMenu": [[25, 50, 100, -1], [25, 50, 100, "All"]],
+ "iDisplayLength": 25,
+ "aaSorting": [],
+ "buttons": []
+ });
+ });
- $scope.key = $scope.params.key || 'id';
+ let init = function() {
+ // ensures the column headers are positioned correctly
+ adjustTableColumns();
};
init();
};
-DialogSelectController.$inject = ['params', 'collection', '$scope', '$uibModalInstance'];
-module.exports = DialogSelectController;
+TableTopologyCacheGroupServersController.$inject = ['cacheGroupName', 'cacheGroupServers', '$scope', '$uibModalInstance'];
+module.exports = TableTopologyCacheGroupServersController;
diff --git a/traffic_portal/app/src/scripts/config.js b/traffic_portal/app/src/common/modules/table/topologyCacheGroupServers/index.js
similarity index 80%
copy from traffic_portal/app/src/scripts/config.js
copy to traffic_portal/app/src/common/modules/table/topologyCacheGroupServers/index.js
index 2c2ba1f..2c4956e 100644
--- a/traffic_portal/app/src/scripts/config.js
+++ b/traffic_portal/app/src/common/modules/table/topologyCacheGroupServers/index.js
@@ -17,12 +17,5 @@
* under the License.
*/
-// this is the config the TO UI uses
-
-"use strict";
-
-angular.module('config', [])
-
-.constant('ENV', { api: { root:'/api/2.0/', legacy: '/api/1.5/' } })
-
-;
+module.exports = angular.module('trafficPortal.table.topologyCacheGroupServers', [])
+ .controller('TableTopologyCacheGroupServersController', require('./TableTopologyCacheGroupServersController'));
diff --git a/traffic_portal/app/src/common/modules/dialog/select/dialog.select.tpl.html b/traffic_portal/app/src/common/modules/table/topologyCacheGroupServers/table.topologyCacheGroupServers.tpl.html
similarity index 53%
copy from traffic_portal/app/src/common/modules/dialog/select/dialog.select.tpl.html
copy to traffic_portal/app/src/common/modules/table/topologyCacheGroupServers/table.topologyCacheGroupServers.tpl.html
index 881a32d..4100591 100644
--- a/traffic_portal/app/src/common/modules/dialog/select/dialog.select.tpl.html
+++ b/traffic_portal/app/src/common/modules/table/topologyCacheGroupServers/table.topologyCacheGroupServers.tpl.html
@@ -17,19 +17,32 @@ specific language governing permissions and limitations
under the License.
-->
+
+
<div class="modal-header">
<button type="button" class="close" ng-click="cancel()"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
- <h4 class="modal-title">{{::params.title}}</h4>
+ <h3 class="modal-title">Servers assigned to {{::cacheGroupName}}</h3>
</div>
<div class="modal-body">
- <p ng-bind-html="params.message"></p>
- <form name="selectForm" novalidate>
- <select class="form-control" name="selectFormDropdown" ng-model="selectedItemKeyValue" ng-options="item[key] as label(item) for item in collection" required>
- <option value="">Select...</option>
- </select>
- </form>
+ <table id="topologyCacheGroupServersTable" class="table responsive-utilities jambo_table" style="table-layout:fixed; width:100%;">
+ <thead>
+ <tr class="headings">
+ <th>Host</th>
+ <th>Domain</th>
+ <th>CDN</th>
+ <th>Status</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr ng-repeat="s in ::cacheGroupServers">
+ <td data-search="^{{::s.hostName}}$">{{::s.hostName}}</td>
+ <td data-search="^{{::s.domainName}}$">{{::s.domainName}}</td>
+ <td data-search="^{{::s.cdnName}}$">{{::s.cdnName}}</td>
+ <td data-search="^{{::s.status}}$">{{::s.status}}</td>
+ </tr>
+ </tbody>
+ </table>
</div>
<div class="modal-footer">
- <button class="btn btn-link" ng-click="cancel()">Cancel</button>
- <button class="btn btn-primary" ng-disabled="selectForm.$invalid" ng-click="select()">Submit</button>
+ <button type="button" class="btn btn-primary" ng-click="cancel()">Close</button>
</div>
diff --git a/traffic_portal/app/src/common/modules/table/topologyCacheGroups/TableSelectTopologyCacheGroupsController.js b/traffic_portal/app/src/common/modules/table/topologyCacheGroups/TableSelectTopologyCacheGroupsController.js
new file mode 100644
index 0000000..db7391f
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/topologyCacheGroups/TableSelectTopologyCacheGroupsController.js
@@ -0,0 +1,198 @@
+/*
+ * 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 TableSelectTopologyCacheGroupsController = function(parent, topology, cacheGroups, usedCacheGroupNames, $scope, $uibModal, $uibModalInstance, serverService) {
+
+ let selectedCacheGroups = [],
+ usedCacheGroupCount = 0;
+
+ let markVisibleCacheGroups = function(selected) {
+ let visibleCacheGroupIds = $('#availableCacheGroupsTable tr.cg-row').map(
+ function() {
+ return parseInt($(this).attr('id'));
+ }).get();
+ $scope.cacheGroups = cacheGroups.map(function(cg) {
+ if (cg['used'] === true) {
+ return cg;
+ }
+ if (selected && visibleCacheGroupIds.includes(cg.id)) {
+ cg['selected'] = true;
+ } else {
+ cg['selected'] = false;
+ }
+ return cg;
+ });
+ updateSelectedCount();
+ };
+
+ let decorateCacheGroups = function() {
+ $scope.cacheGroups = cacheGroups.map(function(cg) {
+ const isUsed = usedCacheGroupNames.find(function(usedCacheGroupName) { return usedCacheGroupName === cg.name });
+ if (isUsed) {
+ cg['selected'] = true;
+ cg['used'] = true;
+ usedCacheGroupCount++;
+ }
+ return cg;
+ });
+ };
+
+ let updateSelectedCount = function() {
+ let visibleCacheGroupIds = $('#availableCacheGroupsTable tr.cg-row').map(
+ function() {
+ return parseInt($(this).attr('id'));
+ }).get();
+
+ selectedCacheGroups = $scope.cacheGroups.filter(function(cg) { return visibleCacheGroupIds.includes(cg.id) && cg['selected'] === true && !cg['used'] } );
+ $('div.selected-count').html('<strong><span class="text-success">' + selectedCacheGroups.length + ' selected</span><span> | ' + usedCacheGroupCount + ' already used in topology</span></strong>');
+ };
+
+ $scope.parent = parent;
+
+ $scope.cacheGroups = cacheGroups.filter(function(cg) {
+ // all cg types (ORG_LOC, MID_LOC, EDGE_LOC) can be added to the root of a topology
+ // but only EDGE_LOC and MID_LOC can be added farther down the topology tree
+ if (parent.type === 'ROOT') return (cg.typeName === 'EDGE_LOC' || cg.typeName === 'MID_LOC' || cg.typeName === 'ORG_LOC');
+ return (cg.typeName === 'EDGE_LOC' || cg.typeName === 'MID_LOC');
+ });
+
+ $scope.selectAll = function($event) {
+ const checkbox = $event.target;
+ if (checkbox.checked) {
+ markVisibleCacheGroups(true);
+ } else {
+ markVisibleCacheGroups(false);
+ }
+ };
+
+ $scope.onChange = function(cg) {
+ if (cg.used) return;
+
+ cg.selected = !cg.selected;
+ updateSelectedCount();
+ };
+
+ $scope.viewCacheGroupServers = function(cg, $event) {
+ if ($event) {
+ $event.stopPropagation(); // this kills the click event so it doesn't trigger anything else
+ }
+ $uibModal.open({
+ templateUrl: 'common/modules/table/topologyCacheGroupServers/table.topologyCacheGroupServers.tpl.html',
+ controller: 'TableTopologyCacheGroupServersController',
+ size: 'lg',
+ resolve: {
+ cacheGroupName: function() {
+ return cg.name;
+ },
+ cacheGroupServers: function(serverService) {
+ return serverService.getServers({ cachegroup: cg.id });
+ }
+ }
+ });
+ };
+
+ $scope.submit = function() {
+ // cache groups that are eligible to be a secondary parent include cache groups that are:
+ let eligibleSecParentCandidates = cacheGroups.filter(function(cg) {
+ return cg.typeName !== 'EDGE_LOC' && // not an edge_loc cache group
+ (parent.cachegroup && parent.cachegroup !== cg.name) && // not the primary parent cache group
+ usedCacheGroupNames.includes(cg.name); // a cache group that exists in the topology
+ });
+ if (eligibleSecParentCandidates.length === 0) {
+ $uibModalInstance.close({ selectedCacheGroups: selectedCacheGroups, parent: parent.cachegroup, secParent: '' });
+ return;
+ }
+ let params = {
+ title: 'Assign secondary parent?',
+ message: 'Would you like to assign a secondary parent to the following cache groups?<br><br>primary parent = ' + parent.cachegroup + '<br><br>'
+ };
+ params.message += selectedCacheGroups.map(function(cg) { return cg.name }).join('<br>') + '<br><br>';
+ let modalInstance = $uibModal.open({
+ templateUrl: 'common/modules/dialog/confirm/dialog.confirm.tpl.html',
+ controller: 'DialogConfirmController',
+ size: 'md',
+ resolve: {
+ params: function () {
+ return params;
+ }
+ }
+ });
+ modalInstance.result.then(function() {
+ // user wants to select a secondary parent
+ let params = {
+ title: 'Select a secondary parent',
+ message: 'Please select a secondary parent that is part of the ' + topology.name + ' topology',
+ key: 'name'
+ };
+ let modalInstance = $uibModal.open({
+ templateUrl: 'common/modules/dialog/select/dialog.select.tpl.html',
+ controller: 'DialogSelectController',
+ size: 'md',
+ resolve: {
+ params: function () {
+ return params;
+ },
+ collection: function() {
+ // cache groups that are eligible to be a secondary parent include cache groups that are:
+ return eligibleSecParentCandidates;
+ }
+ }
+ });
+ modalInstance.result.then(function(cg) {
+ // user selected a secondary parent
+ $uibModalInstance.close({ selectedCacheGroups: selectedCacheGroups, parent: parent.cachegroup, secParent: cg.name });
+ }, function () {
+ // user apparently changed their mind and doesn't want to select a secondary parent
+ $uibModalInstance.close({ selectedCacheGroups: selectedCacheGroups, parent: parent.cachegroup, secParent: '' });
+ });
+ }, function () {
+ // user doesn't want to select a secondary parent
+ $uibModalInstance.close({ selectedCacheGroups: selectedCacheGroups, parent: parent.cachegroup, secParent: '' });
+ });
+ };
+
+ $scope.cancel = function () {
+ $uibModalInstance.dismiss('cancel');
+ };
+
+ angular.element(document).ready(function () {
+ decorateCacheGroups();
+
+ $('#availableCacheGroupsTable').DataTable({
+ "scrollY": "60vh",
+ "paging": false,
+ "order": [[ 1, 'asc' ]],
+ "dom": '<"selected-count">frtip',
+ "drawCallback": function() {
+ updateSelectedCount();
+ },
+ "columnDefs": [
+ { 'orderable': false, 'targets': [0,5] },
+ { "width": "5%", "targets": [ 0 ] },
+ { "width": "35%", "targets": [ 1 ] },
+ { "width": "15%", "targets": [ 2,3,4,5 ] }
+ ],
+ "stateSave": false
+ });
+ });
+
+};
+
+TableSelectTopologyCacheGroupsController.$inject = ['parent', 'topology', 'cacheGroups', 'usedCacheGroupNames', '$scope', '$uibModal', '$uibModalInstance', 'serverService'];
+module.exports = TableSelectTopologyCacheGroupsController;
diff --git a/traffic_portal/app/src/scripts/config.js b/traffic_portal/app/src/common/modules/table/topologyCacheGroups/index.js
similarity index 80%
copy from traffic_portal/app/src/scripts/config.js
copy to traffic_portal/app/src/common/modules/table/topologyCacheGroups/index.js
index 2c2ba1f..023d755 100644
--- a/traffic_portal/app/src/scripts/config.js
+++ b/traffic_portal/app/src/common/modules/table/topologyCacheGroups/index.js
@@ -17,12 +17,5 @@
* under the License.
*/
-// this is the config the TO UI uses
-
-"use strict";
-
-angular.module('config', [])
-
-.constant('ENV', { api: { root:'/api/2.0/', legacy: '/api/1.5/' } })
-
-;
+module.exports = angular.module('trafficPortal.table.topologyCacheGroups', [])
+ .controller('TableSelectTopologyCacheGroupsController', require('./TableSelectTopologyCacheGroupsController'));
diff --git a/traffic_portal/app/src/common/modules/table/topologyCacheGroups/table.selectTopologyCacheGroups.tpl.html b/traffic_portal/app/src/common/modules/table/topologyCacheGroups/table.selectTopologyCacheGroups.tpl.html
new file mode 100644
index 0000000..737a982
--- /dev/null
+++ b/traffic_portal/app/src/common/modules/table/topologyCacheGroups/table.selectTopologyCacheGroups.tpl.html
@@ -0,0 +1,55 @@
+<!--
+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.
+-->
+
+
+
+<div class="modal-header">
+ <button type="button" class="close" ng-click="cancel()"><span aria-hidden="true">×</span><span class="sr-only">Close</span></button>
+ <h3 class="modal-title">Add Cache Groups to {{(parent.cachegroup) ? parent.cachegroup : 'ROOT'}}</h3>
+</div>
+<div class="modal-body">
+ <table id="availableCacheGroupsTable" class="table responsive-utilities jambo_table" style="table-layout:fixed; width:100%;">
+ <thead>
+ <tr class="headings">
+ <th style="padding-left: 10px;"><input id="selectAllCB" type="checkbox" ng-click="selectAll($event)"></th>
+ <th>Name</th>
+ <th>Type</th>
+ <th>Latitude</th>
+ <th>Longitude</th>
+ <th style="text-align: right;">Actions</th>
+ </tr>
+ </thead>
+ <tbody>
+ <tr id="{{::cg.id}}" title="{{cg.used ? 'Cache group already used in topology' : ''}}" class="cg-row" ng-class="{ 'success': cg.selected && !cg.used, 'active': cg.used }" ng-repeat="cg in ::cacheGroups" ng-click="onChange(cg)">
+ <td><input type="checkbox" ng-model="cg.selected" ng-disabled="cg.used"></td>
+ <td data-search="^{{::cg.name}}$">{{::cg.name}}</td>
+ <td data-search="^{{::cg.typeName}}$">{{::cg.typeName}}</td>
+ <td data-search="^{{::cg.latitude}}$">{{::cg.latitude}}</td>
+ <td data-search="^{{::cg.longitude}}$">{{::cg.longitude}}</td>
+ <td style="text-align: right;">
+ <a class="link action-link" title="View Cache Group Servers" ng-click="viewCacheGroupServers(cg, $event)">View Servers</a>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+</div>
+<div class="modal-footer">
+ <button type="button" class="btn btn-link" ng-click="cancel()">cancel</button>
+ <button type="button" class="btn btn-primary" ng-click="submit()">Submit</button>
+</div>
diff --git a/traffic_portal/app/src/common/service/utils/TopologyUtils.js b/traffic_portal/app/src/common/service/utils/TopologyUtils.js
new file mode 100644
index 0000000..ec45c8a
--- /dev/null
+++ b/traffic_portal/app/src/common/service/utils/TopologyUtils.js
@@ -0,0 +1,111 @@
+/*
+ * 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 TopologyUtils = function() {
+
+ let normalizedTopology;
+
+ let flattenTopology = function(topologyTree, fromScratch) {
+ if (fromScratch) normalizedTopology.nodes = [];
+ topologyTree.forEach(function(node) {
+ if (node.cachegroup) {
+ normalizedTopology.nodes.push({
+ cachegroup: node.cachegroup,
+ parent: node.parent,
+ secParent: node.secParent,
+ parents: []
+ });
+ }
+ if (node.children && node.children.length > 0) {
+ flattenTopology(node.children, false);
+ }
+ });
+ };
+
+ let addNodeIndexes = function() {
+ normalizedTopology.nodes.forEach(function(currentNode) {
+ let parentNodeIndex = _.findIndex(normalizedTopology.nodes, function(node) { return currentNode.parent === node.cachegroup });
+ let secParentNodeIndex = _.findIndex(normalizedTopology.nodes, function(node) { return currentNode.secParent === node.cachegroup });
+ if (parentNodeIndex > -1) {
+ currentNode.parents.push(parentNodeIndex);
+ if (secParentNodeIndex > -1) {
+ currentNode.parents.push(secParentNodeIndex);
+ }
+ }
+ });
+ };
+
+ this.getNormalizedTopology = function(name, description, topologyTree) {
+ // build a normalized (flat) topology with parent indexes required for topology create/update
+ normalizedTopology = {
+ name: name,
+ description: description,
+ nodes: []
+ };
+ flattenTopology(topologyTree, true);
+ addNodeIndexes();
+ return normalizedTopology;
+ };
+
+ this.getTopologyTree = function(topology) {
+ let nodes = angular.copy(topology.nodes);
+ let roots = [], // topology items without parents (primary or secondary)
+ all = {};
+
+ nodes.forEach(function(node, index) {
+ all[index] = node;
+ });
+
+ // create children based on parent definitions
+ Object.keys(all).forEach(function (guid) {
+ let item = all[guid];
+ if (!('children' in item)) {
+ item.children = []
+ }
+ if (item.parents.length === 0) {
+ item.parent = "";
+ item.secParent = "";
+ roots.push(item)
+ } else if (item.parents[0] in all) {
+ let p = all[item.parents[0]]
+ if (!('children' in p)) {
+ p.children = []
+ }
+ p.children.push(item);
+ // add parent to each node
+ item.parent = all[item.parents[0]].cachegroup;
+ // add secParent to each node
+ if (item.parents.length === 2 && item.parents[1] in all) {
+ item.secParent = all[item.parents[1]].cachegroup;
+ }
+ }
+ });
+
+ return [
+ {
+ type: 'ROOT',
+ children: roots
+ }
+ ];
+ };
+
+};
+
+TopologyUtils.$inject = [];
+module.exports = TopologyUtils;
diff --git a/traffic_portal/app/src/common/service/utils/index.js b/traffic_portal/app/src/common/service/utils/index.js
index b499861..4221ef8 100644
--- a/traffic_portal/app/src/common/service/utils/index.js
+++ b/traffic_portal/app/src/common/service/utils/index.js
@@ -28,4 +28,5 @@ module.exports = angular.module('trafficPortal.utils', [])
.service('permissionUtils', require('./PermissionUtils'))
.service('serverUtils', require('./ServerUtils'))
.service('stringUtils', require('./StringUtils'))
- .service('tenantUtils', require('./TenantUtils'));
+ .service('tenantUtils', require('./TenantUtils'))
+ .service('topologyUtils', require('./TopologyUtils'));
diff --git a/traffic_portal/app/src/index.html b/traffic_portal/app/src/index.html
index 7ba73e1..0593391 100644
--- a/traffic_portal/app/src/index.html
+++ b/traffic_portal/app/src/index.html
@@ -40,6 +40,7 @@ under the License.
<link rel="stylesheet" media="all" href="resources/assets/css/jquery.dataTables.min_1.10.9.css">
<link rel="stylesheet" media="all" href="resources/assets/css/colReorder.dataTables.min_1.5.1.css">
<link rel="stylesheet" media="all" href="resources/assets/css/angular-moment-picker_0.10.2.css">
+ <link rel="stylesheet" media="all" href="resources/assets/css/angular-ui-tree.min_2.22.6.css">
</head>
@@ -51,6 +52,7 @@ under the License.
<script src="resources/assets/js/app.js"></script>
<script src="resources/assets/js/config.js"></script>
+ <script src="resources/assets/js/angular-ui-tree.min_2.22.6.js"></script>
<script src="resources/assets/js/jsonformatter.min_0.6.0.js"></script>
<script src="resources/assets/js/fast-json-patch_v2.1.0.js"></script>
<script src="resources/assets/js/downloadjs-min_v4.21.js"></script>
diff --git a/traffic_portal/app/src/common/service/utils/index.js b/traffic_portal/app/src/modules/private/topologies/edit/index.js
similarity index 52%
copy from traffic_portal/app/src/common/service/utils/index.js
copy to traffic_portal/app/src/modules/private/topologies/edit/index.js
index b499861..eb736bc 100644
--- a/traffic_portal/app/src/common/service/utils/index.js
+++ b/traffic_portal/app/src/modules/private/topologies/edit/index.js
@@ -17,15 +17,26 @@
* under the License.
*/
-module.exports = angular.module('trafficPortal.utils', [])
- .service('collectionUtils', require('./CollectionUtils'))
- .service('dateUtils', require('./DateUtils'))
- .service('deliveryServiceUtils', require('./DeliveryServiceUtils'))
- .service('fileUtils', require('./FileUtils'))
- .service('formUtils', require('./FormUtils'))
- .service('locationUtils', require('./LocationUtils'))
- .service('numberUtils', require('./NumberUtils'))
- .service('permissionUtils', require('./PermissionUtils'))
- .service('serverUtils', require('./ServerUtils'))
- .service('stringUtils', require('./StringUtils'))
- .service('tenantUtils', require('./TenantUtils'));
+module.exports = angular.module('trafficPortal.private.topologies.edit', [])
+ .config(function($stateProvider, $urlRouterProvider) {
+ $stateProvider
+ .state('trafficPortal.private.topologies.edit', {
+ url: '/edit?name',
+ views: {
+ topologiesContent: {
+ templateUrl: 'common/modules/form/topology/form.topology.tpl.html',
+ controller: 'FormEditTopologyController',
+ resolve: {
+ topologies: function($stateParams, topologyService) {
+ return topologyService.getTopologies({ name: $stateParams.name });
+ },
+ cacheGroups: function(cacheGroupService) {
+ return cacheGroupService.getCacheGroups();
+ }
+ }
+ }
+ }
+ })
+ ;
+ $urlRouterProvider.otherwise('/');
+ });
diff --git a/traffic_portal/app/src/scripts/config.js b/traffic_portal/app/src/modules/private/topologies/index.js
similarity index 66%
copy from traffic_portal/app/src/scripts/config.js
copy to traffic_portal/app/src/modules/private/topologies/index.js
index 2c2ba1f..caa3cde 100644
--- a/traffic_portal/app/src/scripts/config.js
+++ b/traffic_portal/app/src/modules/private/topologies/index.js
@@ -17,12 +17,18 @@
* under the License.
*/
-// this is the config the TO UI uses
-
-"use strict";
-
-angular.module('config', [])
-
-.constant('ENV', { api: { root:'/api/2.0/', legacy: '/api/1.5/' } })
-
-;
+module.exports = angular.module('trafficPortal.private.topologies', [])
+ .config(function($stateProvider, $urlRouterProvider) {
+ $stateProvider
+ .state('trafficPortal.private.topologies', {
+ url: 'topologies',
+ abstract: true,
+ views: {
+ privateContent: {
+ templateUrl: 'modules/private/topologies/topologies.tpl.html'
+ }
+ }
+ })
+ ;
+ $urlRouterProvider.otherwise('/');
+ });
diff --git a/traffic_portal/app/src/scripts/config.js b/traffic_portal/app/src/modules/private/topologies/list/index.js
similarity index 58%
copy from traffic_portal/app/src/scripts/config.js
copy to traffic_portal/app/src/modules/private/topologies/list/index.js
index 2c2ba1f..9f6bd90 100644
--- a/traffic_portal/app/src/scripts/config.js
+++ b/traffic_portal/app/src/modules/private/topologies/list/index.js
@@ -17,12 +17,23 @@
* under the License.
*/
-// this is the config the TO UI uses
-
-"use strict";
-
-angular.module('config', [])
-
-.constant('ENV', { api: { root:'/api/2.0/', legacy: '/api/1.5/' } })
-
-;
+module.exports = angular.module('trafficPortal.private.topologies.list', [])
+ .config(function($stateProvider, $urlRouterProvider) {
+ $stateProvider
+ .state('trafficPortal.private.topologies.list', {
+ url: '',
+ views: {
+ topologiesContent: {
+ templateUrl: 'common/modules/table/topologies/table.topologies.tpl.html',
+ controller: 'TableTopologiesController',
+ resolve: {
+ topologies: function(topologyService) {
+ return topologyService.getTopologies();
+ }
+ }
+ }
+ }
+ })
+ ;
+ $urlRouterProvider.otherwise('/');
+ });
diff --git a/traffic_portal/app/src/scripts/config.js b/traffic_portal/app/src/modules/private/topologies/new/index.js
similarity index 54%
copy from traffic_portal/app/src/scripts/config.js
copy to traffic_portal/app/src/modules/private/topologies/new/index.js
index 2c2ba1f..1dfcdbf 100644
--- a/traffic_portal/app/src/scripts/config.js
+++ b/traffic_portal/app/src/modules/private/topologies/new/index.js
@@ -17,12 +17,28 @@
* under the License.
*/
-// this is the config the TO UI uses
-
-"use strict";
-
-angular.module('config', [])
-
-.constant('ENV', { api: { root:'/api/2.0/', legacy: '/api/1.5/' } })
-
-;
+module.exports = angular.module('trafficPortal.private.topologies.new', [])
+ .config(function($stateProvider, $urlRouterProvider) {
+ $stateProvider
+ .state('trafficPortal.private.topologies.new', {
+ url: '/new',
+ views: {
+ topologiesContent: {
+ templateUrl: 'common/modules/form/topology/form.topology.tpl.html',
+ controller: 'FormNewTopologyController',
+ resolve: {
+ topology: function() {
+ return {
+ nodes: []
+ };
+ },
+ cacheGroups: function(cacheGroupService) {
+ return cacheGroupService.getCacheGroups();
+ }
+ }
+ }
+ }
+ })
+ ;
+ $urlRouterProvider.otherwise('/');
+ });
diff --git a/traffic_portal/app/src/modules/private/topologies/topologies.tpl.html b/traffic_portal/app/src/modules/private/topologies/topologies.tpl.html
new file mode 100644
index 0000000..d617199
--- /dev/null
+++ b/traffic_portal/app/src/modules/private/topologies/topologies.tpl.html
@@ -0,0 +1,22 @@
+<!--
+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.
+-->
+
+<div id="topologiesContainer">
+ <div ui-view="topologiesContent"></div>
+</div>
diff --git a/traffic_portal/app/src/scripts/config.js b/traffic_portal/app/src/scripts/config.js
index 2c2ba1f..c5423a2 100644
--- a/traffic_portal/app/src/scripts/config.js
+++ b/traffic_portal/app/src/scripts/config.js
@@ -23,6 +23,6 @@
angular.module('config', [])
-.constant('ENV', { api: { root:'/api/2.0/', legacy: '/api/1.5/' } })
+.constant('ENV', { api: { latest:'/api/3.0/', root:'/api/2.0/', legacy: '/api/1.5/' } })
;
diff --git a/traffic_portal/app/src/styles/main.scss b/traffic_portal/app/src/styles/main.scss
index 04a630d..a984fea 100644
--- a/traffic_portal/app/src/styles/main.scss
+++ b/traffic_portal/app/src/styles/main.scss
@@ -189,7 +189,36 @@ input[type="checkbox"].dirty {
box-shadow: 0 0 10px 5px blue;
}
+#tree-root {
+
+ .tree-node-content {
+ height: 45px;
+ text-align: center;
+ font-size: 14px;
+ margin-bottom: 10px;
+ small {
+ font-size: 65%;
+ }
+ .error-msg {
+ display: inline;
+ font-style: italic;
+ }
+ }
+ .tree-node.origin {
+ color: #004085;
+ background-color: #cce5ff;
+ border-color: #b8daff;
+ }
+ .tree-node.mid {
+ color: #383d41;
+ background-color: #e2e3e5;
+ border-color: #d6d8db;
+ }
+ .tree-node.error {
+ color: #721c24;
+ background-color: #f2dede;
+ border-color: #f5c6cb;
+ }
-
-
+}