From 83f1492463ffd840bd73f2f321cba6890a12b955 Mon Sep 17 00:00:00 2001 From: Zach Hoffman Date: Mon, 30 Mar 2020 21:59:16 -0600 Subject: [PATCH 001/112] Topology Goose migrations - Create topology table - Create topology_cachegroup table - Create topology_cachegroup_parents table - Goose migration to add topology column to deliveryservice table --- .../20200402133144_create_topology_tables.sql | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 traffic_ops/app/db/migrations/20200402133144_create_topology_tables.sql diff --git a/traffic_ops/app/db/migrations/20200402133144_create_topology_tables.sql b/traffic_ops/app/db/migrations/20200402133144_create_topology_tables.sql new file mode 100644 index 0000000000..3e9f3232e8 --- /dev/null +++ b/traffic_ops/app/db/migrations/20200402133144_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 SERIAL 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; From 8b60fa6a39ffda1ab408c08e7fa780cf389c9b14 Mon Sep 17 00:00:00 2001 From: Zach Hoffman Date: Wed, 1 Apr 2020 23:38:04 -0600 Subject: [PATCH 002/112] POST /topologies --- lib/go-tc/topologies.go | 34 +++++++ .../traffic_ops_golang/routing/routes.go | 3 + .../traffic_ops_golang/topology/topologies.go | 97 +++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 lib/go-tc/topologies.go create mode 100644 traffic_ops/traffic_ops_golang/topology/topologies.go diff --git a/lib/go-tc/topologies.go b/lib/go-tc/topologies.go new file mode 100644 index 0000000000..9e913edd25 --- /dev/null +++ b/lib/go-tc/topologies.go @@ -0,0 +1,34 @@ +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. + */ + +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"` +} + +type TopologyNode struct { + Id int `json:"-" db:"id"` + Cachegroup string `json:"cachegroup" db:"cachegroup"` + Parents []int `json:"parents"` + LastUpdated *TimeNoMod `json:"-" db:"last_updated"` +} diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go index a1d70abb12..cf1e5333f0 100644 --- a/traffic_ops/traffic_ops_golang/routing/routes.go +++ b/traffic_ops/traffic_ops_golang/routing/routes.go @@ -24,6 +24,7 @@ import ( "encoding/json" "errors" "fmt" + "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/topology" "net" "net/http" "net/http/httputil" @@ -279,6 +280,8 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) { {api.Version{2, 0}, http.MethodPost, `regions/?$`, api.CreateHandler(®ion.TORegion{}), auth.PrivLevelOperations, Authenticated, nil, 2288334488, noPerlBypass}, {api.Version{2, 0}, http.MethodDelete, `regions/?$`, api.DeleteHandler(®ion.TORegion{}), auth.PrivLevelOperations, Authenticated, nil, 2232626758, noPerlBypass}, + {api.Version{2, 0}, http.MethodPost, `topologies/?$`, api.CreateHandler(&topology.TOTopology{}), auth.PrivLevelOperations, Authenticated, nil, 3871452221, noPerlBypass}, + // get all edge servers associated with a delivery service (from deliveryservice_server table) {api.Version{2, 0}, http.MethodGet, `deliveryserviceserver/?$`, dsserver.ReadDSSHandlerV14, auth.PrivLevelReadOnly, Authenticated, nil, 2946145033, noPerlBypass}, 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 0000000000..9b97e3eabf --- /dev/null +++ b/traffic_ops/traffic_ops_golang/topology/topologies.go @@ -0,0 +1,97 @@ +package topology + +import ( + "github.com/apache/trafficcontrol/lib/go-tc" + "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api" +) + +type TOTopology struct { + api.APIInfoImpl `json:"-"` + tc.Topology +} + +func (topology *TOTopology) SetLastUpdated(time tc.TimeNoMod) { topology.LastUpdated = &time } + +func (topology TOTopology) GetKeyFieldsInfo() []api.KeyFieldInfo { + return []api.KeyFieldInfo{{"name", api.GetStringKey}} +} + +func (topology *TOTopology) GetType() string { + return "topology" +} + +func (topology *TOTopology) Validate() error { + return nil +} + +// Implementation of the Identifier, Validator interface functions +func (topology TOTopology) GetKeys() (map[string]interface{}, bool) { + return map[string]interface{}{"name": topology.Name}, true +} + +func (topology *TOTopology) SetKeys(keys map[string]interface{}) { + topology.Name, _ = keys["name"].(string) +} + +func (topology *TOTopology) GetAuditName() string { + return topology.Name +} + +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 { + userErr, sysErr, errCode := api.ParseDBError(err) + return userErr, sysErr, errCode + } + nodeCount := len(topology.Nodes) + for index := 0; index < nodeCount; index++ { + node := &topology.Nodes[index] + err := tx.QueryRow(nodeInsertQuery(), topology.Name, node.Cachegroup).Scan(&node.Id, &topology.Name, &node.Cachegroup, &node.LastUpdated) + if err != nil { + userErr, sysErr, errCode := api.ParseDBError(err) + return userErr, sysErr, errCode + } + } + + for index := 0; index < nodeCount; index++ { + node := &topology.Nodes[index] + for rank := 1; rank <= len(node.Parents); rank++ { + parent := topology.Nodes[node.Parents[rank-1]] + err := tx.QueryRow(nodeParentInsertQuery(), node.Id, parent.Id, &rank).Scan(&node.Id, &parent.Id, &rank) + if err != nil { + userErr, sysErr, errCode := api.ParseDBError(err) + return userErr, sysErr, errCode + } + } + } + + return nil, nil, 0 +} + +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, $2) +RETURNING id, topology, cachegroup, last_updated +` + return query +} + +func nodeParentInsertQuery() string { + query := ` +INSERT INTO topology_cachegroup_parents (child, parent, rank) +VALUES ($1, $2, $3) +RETURNING child, parent, rank +` + return query +} From 70b90dd2e996802a5e8ebaaf8a7c31284e81eba6 Mon Sep 17 00:00:00 2001 From: Zach Hoffman Date: Wed, 8 Apr 2020 00:14:36 -0600 Subject: [PATCH 003/112] Create all nodes in a single query and all parents in a single query --- .../traffic_ops_golang/topology/topologies.go | 53 ++++++++++++++++--- 1 file changed, 47 insertions(+), 6 deletions(-) diff --git a/traffic_ops/traffic_ops_golang/topology/topologies.go b/traffic_ops/traffic_ops_golang/topology/topologies.go index 9b97e3eabf..6658988070 100644 --- a/traffic_ops/traffic_ops_golang/topology/topologies.go +++ b/traffic_ops/traffic_ops_golang/topology/topologies.go @@ -3,6 +3,7 @@ package topology import ( "github.com/apache/trafficcontrol/lib/go-tc" "github.com/apache/trafficcontrol/traffic_ops/traffic_ops_golang/api" + "github.com/lib/pq" ) type TOTopology struct { @@ -44,27 +45,67 @@ func (topology *TOTopology) Create() (error, error, int) { userErr, sysErr, errCode := api.ParseDBError(err) return userErr, sysErr, errCode } + nodeCount := len(topology.Nodes) + cachegroups := make([]string, nodeCount) for index := 0; index < nodeCount; index++ { node := &topology.Nodes[index] - err := tx.QueryRow(nodeInsertQuery(), topology.Name, node.Cachegroup).Scan(&node.Id, &topology.Name, &node.Cachegroup, &node.LastUpdated) + cachegroups[index] = node.Cachegroup + } + rows, err := tx.Query(nodeInsertQuery(), topology.Name, pq.Array(cachegroups)) + if err != nil { + userErr, sysErr, errCode := api.ParseDBError(err) + return userErr, sysErr, errCode + } + for index := 0; index < nodeCount; index++ { + node := &topology.Nodes[index] + rows.Next() + err = rows.Scan(&node.Id, &topology.Name, &node.Cachegroup) if err != nil { userErr, sysErr, errCode := api.ParseDBError(err) return userErr, sysErr, errCode } } + err = rows.Close() + if err != nil { + userErr, sysErr, errCode := api.ParseDBError(err) + return userErr, sysErr, errCode + } + children := []int{} + parents := []int{} + ranks := []int{} + for index := 0; index < nodeCount; index++ { + node := &topology.Nodes[index] + 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 = tx.Query(nodeParentInsertQuery(), pq.Array(children), pq.Array(parents), pq.Array(ranks)) + if err != nil { + userErr, sysErr, errCode := api.ParseDBError(err) + return userErr, sysErr, errCode + } for index := 0; index < nodeCount; index++ { node := &topology.Nodes[index] for rank := 1; rank <= len(node.Parents); rank++ { - parent := topology.Nodes[node.Parents[rank-1]] - err := tx.QueryRow(nodeParentInsertQuery(), node.Id, parent.Id, &rank).Scan(&node.Id, &parent.Id, &rank) + rows.Next() + parent := &topology.Nodes[node.Parents[rank-1]] + err = rows.Scan(&node.Id, &parent.Id, &rank) if err != nil { userErr, sysErr, errCode := api.ParseDBError(err) return userErr, sysErr, errCode } } } + err = rows.Close() + if err != nil { + userErr, sysErr, errCode := api.ParseDBError(err) + return userErr, sysErr, errCode + } return nil, nil, 0 } @@ -81,8 +122,8 @@ RETURNING name, description, last_updated func nodeInsertQuery() string { query := ` INSERT INTO topology_cachegroup (topology, cachegroup) -VALUES ($1, $2) -RETURNING id, topology, cachegroup, last_updated +VALUES ($1, unnest($2::text[])) +RETURNING id, topology, cachegroup ` return query } @@ -90,7 +131,7 @@ RETURNING id, topology, cachegroup, last_updated func nodeParentInsertQuery() string { query := ` INSERT INTO topology_cachegroup_parents (child, parent, rank) -VALUES ($1, $2, $3) +VALUES (unnest($1::int[]), unnest($2::int[]), unnest($3::int[])) RETURNING child, parent, rank ` return query From 165e98e72c4f1646b433657650a86ec2fa0e1d0a Mon Sep 17 00:00:00 2001 From: Zach Hoffman Date: Thu, 2 Apr 2020 10:35:36 -0600 Subject: [PATCH 004/112] Validation so far --- lib/go-tc/cachegroups.go | 4 ++ .../cachegroup/cachegroups.go | 15 +++++++ .../traffic_ops_golang/topology/topologies.go | 29 +++++++++++++- .../traffic_ops_golang/topology/validation.go | 40 +++++++++++++++++++ 4 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 traffic_ops/traffic_ops_golang/topology/validation.go diff --git a/lib/go-tc/cachegroups.go b/lib/go-tc/cachegroups.go index 7b9d3061db..829b86853f 100644 --- a/lib/go-tc/cachegroups.go +++ b/lib/go-tc/cachegroups.go @@ -95,3 +95,7 @@ type CachegroupQueueUpdatesRequest struct { CDN *CDNName `json:"cdn"` CDNID *util.JSONIntStr `json:"cdnId"` } + +const ( + EdgeCacheGroupType = "EDGE_LOC" +) diff --git a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go index 21f2cba1f5..de1d17d601 100644 --- a/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go +++ b/traffic_ops/traffic_ops_golang/cachegroup/cachegroups.go @@ -379,6 +379,21 @@ func (cg *TOCacheGroup) deleteCoordinate(coordinateID int) error { return nil } +func GetCacheGroupByName(name string, apiInfo *api.APIInfoImpl) (*tc.CacheGroupNullable, error) { + apiInfo.ReqInfo.Params = map[string]string{"name": name} + cacheGroup := TOCacheGroup{APIInfoImpl: *apiInfo} + result, userErr, sysErr, _ := cacheGroup.Read() + if userErr != nil || sysErr != nil { + return nil, util.JoinErrs([]error{userErr, sysErr}) + } + if len(result) == 0 { + return nil, fmt.Errorf("No cache group exists by the name of %v", name) + } + + cacheGroup = result[0].(TOCacheGroup) + return &cacheGroup.CacheGroupNullable, nil +} + func (cg *TOCacheGroup) Read() ([]interface{}, error, error, int) { // Query Parameters to Database Query column mappings // see the fields mapped in the SQL query diff --git a/traffic_ops/traffic_ops_golang/topology/topologies.go b/traffic_ops/traffic_ops_golang/topology/topologies.go index 6658988070..0cfa0ad2ce 100644 --- a/traffic_ops/traffic_ops_golang/topology/topologies.go +++ b/traffic_ops/traffic_ops_golang/topology/topologies.go @@ -1,8 +1,13 @@ package topology import ( + "fmt" "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" + validation "github.com/go-ozzo/ozzo-validation" "github.com/lib/pq" ) @@ -22,7 +27,29 @@ func (topology *TOTopology) GetType() string { } func (topology *TOTopology) Validate() error { - return nil + 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) + rules["length"] = validation.Validate(nodeCount, validation.Min(1)) + cacheGroups := make([]*tc.CacheGroupNullable, nodeCount) + var err error + for index := 0; index < nodeCount; index++ { + node := &topology.Nodes[index] + 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) + if cacheGroups[index], err = cachegroup.GetCacheGroupByName(node.Cachegroup, &topology.APIInfoImpl); err != nil { + rules[fmt.Sprintf("node %v parents size", index)] = fmt.Errorf("error getting cachegroup %v: %v", node.Cachegroup, err.Error()) + } + } + rules["duplicate cachegroup name"] = checkUniqueCacheGroupNames(topology.Nodes) + + for index := 0; index < nodeCount; index++ { + rules["parents edge type"] = checkForEdgeParents(&topology.Nodes, &cacheGroups, index) + } + errs := tovalidate.ToErrors(rules) + return util.JoinErrs(errs) } // Implementation of the Identifier, Validator interface functions 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 0000000000..5fa49e7195 --- /dev/null +++ b/traffic_ops/traffic_ops_golang/topology/validation.go @@ -0,0 +1,40 @@ +package topology + +import ( + "fmt" + "github.com/apache/trafficcontrol/lib/go-tc" + "github.com/apache/trafficcontrol/lib/go-util" +) + +func checkUniqueCacheGroupNames(nodes []tc.TopologyNode) error { + nodeCount := len(nodes) + cacheGroupNames := map[string]bool{} + for index := 0; index < nodeCount; index++ { + if _, exists := cacheGroupNames[nodes[index].Cachegroup]; exists { + return fmt.Errorf("cachegroup %v cannot be used more than once in the topology.", nodes[index].Cachegroup) + } + cacheGroupNames[nodes[index].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 checkForEdgeParents(nodes *[]tc.TopologyNode, cachegroups *[]*tc.CacheGroupNullable, nodeIndex int) error { + node := &(*nodes)[nodeIndex] + parentsLength := len(node.Parents) + errs := make([]error, parentsLength) + for parentIndex := 0; parentIndex < parentsLength; parentIndex++ { + cacheGroupType := (*cachegroups)[node.Parents[parentIndex]].Type + if *cacheGroupType == tc.EdgeCacheGroupType { + errs[parentIndex] = fmt.Errorf("cachegroup %v's type is %v; it cannot be a parent of %v.", (*nodes)[parentIndex].Cachegroup, tc.EdgeCacheGroupType, node.Cachegroup) + } + } + return util.JoinErrs(errs) +} From e73d2923a535608dbfeb7486ef1017856c2e5a8c Mon Sep 17 00:00:00 2001 From: Zach Hoffman Date: Sun, 5 Apr 2020 23:17:29 -0600 Subject: [PATCH 005/112] Topologies endpoints for TO Python client --- .../clients/python/trafficops/tosession.py | 49 +++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/traffic_control/clients/python/trafficops/tosession.py b/traffic_control/clients/python/trafficops/tosession.py index f6355a96da..f210449bde 100644 --- a/traffic_control/clients/python/trafficops/tosession.py +++ b/traffic_control/clients/python/trafficops/tosession.py @@ -21,6 +21,8 @@ # Core Modules import logging import sys +from requests import Response +from typing import Any, Dict, List, Tuple, Union # Third-party Modules import munch @@ -1866,6 +1868,53 @@ def delete_to_extension(self, extension_id=None): :raises: Union[LoginError, OperationError] """ + # + # Topologies + # + @api_request('post', 'topologies', ('2.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', ('2.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', ('2.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', ('2.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 # From 53770359712e8580d28a495c9728383dcad704cd Mon Sep 17 00:00:00 2001 From: Zach Hoffman Date: Mon, 6 Apr 2020 00:15:21 -0600 Subject: [PATCH 006/112] Topologies endpoint functions for Go client library --- lib/go-tc/topologies.go | 5 ++ traffic_ops/client/topology.go | 123 +++++++++++++++++++++++++++++++++ 2 files changed, 128 insertions(+) create mode 100644 traffic_ops/client/topology.go diff --git a/lib/go-tc/topologies.go b/lib/go-tc/topologies.go index 9e913edd25..949b8f3225 100644 --- a/lib/go-tc/topologies.go +++ b/lib/go-tc/topologies.go @@ -32,3 +32,8 @@ type TopologyNode struct { Parents []int `json:"parents"` LastUpdated *TimeNoMod `json:"-" db:"last_updated"` } + +type TopologiesResponse struct { + Response []Topology `json:"response"` + Alerts +} diff --git a/traffic_ops/client/topology.go b/traffic_ops/client/topology.go new file mode 100644 index 0000000000..19108d9fb1 --- /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-tc" + "net" + "net/http" + "net/url" +) + +const ( + API_TOPOLOGIES = apiBase + "/topologies" +) + +// CreateTopology creates a topology and returns the response. +func (to *Session) CreateTopology(top tc.Topology) (*tc.TopologiesResponse, 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, API_TOPOLOGIES, reqBody) + if err != nil { + return nil, reqInf, err + } + defer resp.Body.Close() + var topResp tc.TopologiesResponse + 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, API_TOPOLOGIES, nil) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return nil, reqInf, err + } + defer resp.Body.Close() + + 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", API_TOPOLOGIES, 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 resp.Body.Close() + + 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) +} + +// Update a Topology by ID +func (to *Session) UpdateTopologyByID(id int, pl tc.Topology) (tc.Alerts, ReqInf, error) { + + var remoteAddr net.Addr + reqBody, err := json.Marshal(pl) + reqInf := ReqInf{CacheHitStatus: CacheHitStatusMiss, RemoteAddr: remoteAddr} + if err != nil { + return tc.Alerts{}, reqInf, err + } + route := fmt.Sprintf("%s/%d", API_TOPOLOGIES, id) + resp, remoteAddr, err := to.request(http.MethodPut, route, reqBody) + if err != nil { + return tc.Alerts{}, reqInf, err + } + defer resp.Body.Close() + var alerts tc.Alerts + err = json.NewDecoder(resp.Body).Decode(&alerts) + return alerts, reqInf, nil +} + +// DeleteTopology deletes the given topology by name. +func (to *Session) DeleteTopology(name string) (tc.Alerts, ReqInf, error) { + reqUrl := fmt.Sprintf("%s?name=%s", API_TOPOLOGIES, 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 resp.Body.Close() + var alerts tc.Alerts + if err = json.NewDecoder(resp.Body).Decode(&alerts); err != nil { + return tc.Alerts{}, reqInf, err + } + return alerts, reqInf, nil +} From 3c6502574ad443f6b27bc80389c5fe58c5eef4a2 Mon Sep 17 00:00:00 2001 From: Zach Hoffman Date: Wed, 8 Apr 2020 15:11:43 -0600 Subject: [PATCH 007/112] GET /topologies --- .../traffic_ops_golang/routing/routes.go | 1 + .../traffic_ops_golang/topology/topologies.go | 90 +++++++++++++++++++ 2 files changed, 91 insertions(+) diff --git a/traffic_ops/traffic_ops_golang/routing/routes.go b/traffic_ops/traffic_ops_golang/routing/routes.go index cf1e5333f0..bf410e3b06 100644 --- a/traffic_ops/traffic_ops_golang/routing/routes.go +++ b/traffic_ops/traffic_ops_golang/routing/routes.go @@ -281,6 +281,7 @@ func Routes(d ServerData) ([]Route, []RawRoute, http.Handler, error) { {api.Version{2, 0}, http.MethodDelete, `regions/?$`, api.DeleteHandler(®ion.TORegion{}), auth.PrivLevelOperations, Authenticated, nil, 2232626758, noPerlBypass}, {api.Version{2, 0}, http.MethodPost, `topologies/?$`, api.CreateHandler(&topology.TOTopology{}), auth.PrivLevelOperations, Authenticated, nil, 3871452221, noPerlBypass}, + {api.Version{2, 0}, http.MethodGet, `topologies/?$`, api.ReadHandler(&topology.TOTopology{}), auth.PrivLevelReadOnly, Authenticated, nil, 3871452222, noPerlBypass}, // get all edge servers associated with a delivery service (from deliveryservice_server table) diff --git a/traffic_ops/traffic_ops_golang/topology/topologies.go b/traffic_ops/traffic_ops_golang/topology/topologies.go index 0cfa0ad2ce..bee11a17d9 100644 --- a/traffic_ops/traffic_ops_golang/topology/topologies.go +++ b/traffic_ops/traffic_ops_golang/topology/topologies.go @@ -1,14 +1,17 @@ package topology import ( + "errors" "fmt" "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" ) type TOTopology struct { @@ -16,6 +19,14 @@ type TOTopology struct { tc.Topology } +func (topology *TOTopology) ParamColumns() map[string]dbhelpers.WhereColumnInfo { + return map[string]dbhelpers.WhereColumnInfo{ + "name": dbhelpers.WhereColumnInfo{"t.name", nil}, + "description": dbhelpers.WhereColumnInfo{"t.description", nil}, + "lastUpdated": dbhelpers.WhereColumnInfo{"t.last_updated", nil}, + } +} + func (topology *TOTopology) SetLastUpdated(time tc.TimeNoMod) { topology.LastUpdated = &time } func (topology TOTopology) GetKeyFieldsInfo() []api.KeyFieldInfo { @@ -136,6 +147,69 @@ func (topology *TOTopology) Create() (error, error, int) { return nil, nil, 0 } +func (t *TOTopology) Read() ([]interface{}, error, error, int) { + where, orderBy, pagination, queryValues, errs := dbhelpers.BuildWhereAndOrderByAndPagination(t.ReqInfo.Params, t.ParamColumns()) + if len(errs) > 0 { + return nil, util.JoinErrs(errs), nil, http.StatusBadRequest + } + query := selectQuery() + where + orderBy + pagination + rows, err := t.ReqInfo.Tx.NamedQuery(query, queryValues) + if err != nil { + return nil, nil, errors.New("topology read: querying: " + err.Error()), http.StatusInternalServerError + } + defer rows.Close() + + interfaces := []interface{}{} + topologies := map[string]*tc.Topology{} + topology := tc.Topology{} + indices := map[int]int{} + for index := 0; rows.Next(); index++ { + var name, description string + var 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{} + topologies[name] = &topology + topology.Name = name + topology.Description = description + topology.LastUpdated = &lastUpdated + } + topology.Nodes = append(topology.Nodes, topologyNode) + } + + for _, topology := range topologies { + nodes := &topology.Nodes + nodeCount := len(topology.Nodes) + nodeMap := map[int]int{} + for index := 0; index < nodeCount; index++ { + nodeMap[(*nodes)[index].Id] = index + } + for nodeIndex := 0; nodeIndex < nodeCount; nodeIndex++ { + node := &(*nodes)[nodeIndex] + for parentIndex := 0; parentIndex < len((*node).Parents); parentIndex++ { + (*node).Parents[parentIndex] = nodeMap[(*node).Parents[parentIndex]] + } + } + interfaces = append(interfaces, *topology) + } + return interfaces, nil, nil, 0 +} func insertQuery() string { query := ` @@ -163,3 +237,19 @@ 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))) AS parents + FROM topology_cachegroup tc2 + INNER JOIN topology_cachegroup_parents tcp ON tc2.id = tcp.child + WHERE tc2.cachegroup = tc.cachegroup + GROUP BY tcp.rank ORDER BY tcp.rank ASC + ) +FROM topology t +JOIN topology_cachegroup tc on t.name = tc.topology +` + return query +} From 3c13fcc11e39c552abffbc3296d4d2ac343395f2 Mon Sep 17 00:00:00 2001 From: Jeremy Mitchell Date: Wed, 25 Mar 2020 12:34:11 -0600 Subject: [PATCH 008/112] adds toplogy views --- traffic_portal/app/src/app.js | 7 + .../assets/js/angular-ui-tree-min_2.22.6.js | 6 + .../app/src/common/api/TopologyService.js | 218 ++++++++++++++++++ traffic_portal/app/src/common/api/index.js | 1 + .../form/topology/FormTopologyController.js | 193 ++++++++++++++++ .../edit/FormEditTopologyController.js | 72 ++++++ .../modules/form/topology/edit/index.js | 21 ++ .../form/topology/form.topology.tpl.html | 113 +++++++++ .../src/common/modules/form/topology/index.js | 21 ++ .../modules/navigation/navigation.tpl.html | 1 + .../topologies/TableTopologiesController.js | 47 ++++ .../common/modules/table/topologies/index.js | 21 ++ .../topologies/table.topologies.tpl.html | 49 ++++ traffic_portal/app/src/index.html | 2 + .../modules/private/topologies/edit/index.js | 39 ++++ .../src/modules/private/topologies/index.js | 34 +++ .../modules/private/topologies/list/index.js | 39 ++++ .../private/topologies/topologies.tpl.html | 22 ++ traffic_portal/app/src/styles/main.scss | 155 +++++++++++++ 19 files changed, 1061 insertions(+) create mode 100644 traffic_portal/app/src/assets/js/angular-ui-tree-min_2.22.6.js create mode 100644 traffic_portal/app/src/common/api/TopologyService.js create mode 100644 traffic_portal/app/src/common/modules/form/topology/FormTopologyController.js create mode 100644 traffic_portal/app/src/common/modules/form/topology/edit/FormEditTopologyController.js create mode 100644 traffic_portal/app/src/common/modules/form/topology/edit/index.js create mode 100644 traffic_portal/app/src/common/modules/form/topology/form.topology.tpl.html create mode 100644 traffic_portal/app/src/common/modules/form/topology/index.js create mode 100644 traffic_portal/app/src/common/modules/table/topologies/TableTopologiesController.js create mode 100644 traffic_portal/app/src/common/modules/table/topologies/index.js create mode 100644 traffic_portal/app/src/common/modules/table/topologies/table.topologies.tpl.html create mode 100644 traffic_portal/app/src/modules/private/topologies/edit/index.js create mode 100644 traffic_portal/app/src/modules/private/topologies/index.js create mode 100644 traffic_portal/app/src/modules/private/topologies/list/index.js create mode 100644 traffic_portal/app/src/modules/private/topologies/topologies.tpl.html diff --git a/traffic_portal/app/src/app.js b/traffic_portal/app/src/app.js index 3e1d0fd703..c1fd818993 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,9 @@ 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/types/edit').name, require('./modules/private/types/list').name, require('./modules/private/types/new').name, @@ -317,6 +321,8 @@ 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/type').name, require('./common/modules/form/type/edit').name, require('./common/modules/form/type/new').name, @@ -387,6 +393,7 @@ 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/types').name, require('./common/modules/table/typeCacheGroups').name, require('./common/modules/table/typeDeliveryServices').name, 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 0000000000..6d261e83a1 --- /dev/null +++ b/traffic_portal/app/src/assets/js/angular-ui-tree-min_2.22.6.js @@ -0,0 +1,6 @@ +/** + * @license Angular UI Tree v2.22.6 + * (c) 2010-2017. https://github.com/angular-ui-tree/angular-ui-tree + * License: 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";angular.module("ui.tree").controller("TreeHandleController",["$scope","$element",function(e,n){this.scope=e,e.$element=n,e.$nodeScope=null,e.$type="uiTreeHandle"}])}(),function(){"use strict";angular.module("ui.tree").controller("TreeNodeController",["$scope","$element",function(e,n){function t(e){if(!e)return 0;var n,o,l,r=0,a=e.childNodes();if(!a||0===a.length)return 0;for(l=a.length-1;l>=0;l--)n=a[l],o=1+t(n),r=Math.max(r,o);return r}this.scope=e,e.$element=n,e.$modelValue=null,e.$parentNodeScope=null,e.$childNodesScope=null,e.$parentNodesScope=null,e.$treeScope=null,e.$handleScope=null,e.$type="uiTreeNode",e.$$allowNodeDrop=!1,e.collapsed=!1,e.expandOnHover=!1,e.init=function(t){var o=t[0];e.$treeScope=t[1]?t[1].scope:null,e.$parentNodeScope=o.scope.$nodeScope,e.$modelValue=o.scope.$modelValue[e.$index],e.$parentNodesScope=o.scope,o.scope.initSubNode(e),n.on("$destroy",function(){o.scope.destroySubNode(e)})},e.index=function(){return e.$parentNodesScope.$modelValue.indexOf(e.$modelValue)},e.dragEnabled=function(){return!(e.$treeScope&&!e.$treeScope.dragEnabled)},e.isSibling=function(n){return e.$parentNodesScope==n.$parentNodesScope},e.isChild=function(n){var t=e.childNodes();return t&&t.indexOf(n)>-1},e.prev=function(){var n=e.index();return n>0?e.siblings()[n-1]:null},e.siblings=function(){return e.$parentNodesScope.childNodes()},e.childNodesCount=function(){return e.childNodes()?e.childNodes().length:0},e.hasChild=function(){return e.childNodesCount()>0},e.childNodes=function(){return e.$childNodesScope&&e.$childNodesScope.$modelValue?e.$childNodesScope.childNodes():null},e.accept=function(n,t){return e.$childNodesScope&&e.$childNodesScope.$modelValue&&e.$childNodesScope.accept(n,t)},e.remove=function(){return e.$parentNodesScope.removeNode(e)},e.toggle=function(){e.collapsed=!e.collapsed,e.$treeScope.$callbacks.toggle(e.collapsed,e)},e.collapse=function(){e.collapsed=!0},e.expand=function(){e.collapsed=!1},e.depth=function(){var n=e.$parentNodeScope;return n?n.depth()+1:1},e.maxSubDepth=function(){return e.$childNodesScope?t(e.$childNodesScope):0}}])}(),function(){"use strict";angular.module("ui.tree").controller("TreeNodesController",["$scope","$element","$timeout",function(e,n,t){this.scope=e,e.$element=n,e.$modelValue=null,e.$nodeScope=null,e.$treeScope=null,e.$type="uiTreeNodes",e.$nodesMap={},e.nodropEnabled=!1,e.maxDepth=0,e.cloneEnabled=!1,e.initSubNode=function(n){if(!n.$modelValue)return null;e.$nodesMap[n.$modelValue.$$hashKey]=n},e.destroySubNode=function(n){if(!n.$modelValue)return null;e.$nodesMap[n.$modelValue.$$hashKey]=null},e.accept=function(n,t){return e.$treeScope.$callbacks.accept(n,e,t)},e.beforeDrag=function(n){return e.$treeScope.$callbacks.beforeDrag(n)},e.isParent=function(n){return n.$parentNodesScope==e},e.hasChild=function(){return e.$modelValue.length>0},e.removeNode=function(n){var o=e.$modelValue.indexOf(n.$modelValue);return o>-1?(t(function(){e.$modelValue.splice(o,1)[0]}),e.$treeScope.$callbacks.removed(n)):null},e.insertNode=function(n,o){t(function(){e.$modelValue.splice(n,0,o)})},e.childNodes=function(){var n,t=[];if(e.$modelValue)for(n=0;n0&&e.depth()+n.maxSubDepth()+1>t}}])}(),function(){"use strict";angular.module("ui.tree").controller("TreeController",["$scope","$element",function(e,n){this.scope=e,e.$element=n,e.$nodesScope=null,e.$type="uiTree",e.$emptyElm=null,e.$dropzoneElm=null,e.$callbacks=null,e.dragEnabled=!0,e.emptyPlaceholderEnabled=!0,e.maxDepth=0,e.dragDelay=0,e.cloneEnabled=!1,e.nodropEnabled=!1,e.dropzoneEnabled=!1,e.isEmpty=function(){return e.$nodesScope&&e.$nodesScope.$modelValue&&0===e.$nodesScope.$modelValue.length},e.place=function(n){e.$nodesScope.$element.append(n),e.$emptyElm.remove()},this.resetEmptyElement=function(){e.$nodesScope.$modelValue&&0!==e.$nodesScope.$modelValue.length||!e.emptyPlaceholderEnabled?e.$emptyElm.remove():n.append(e.$emptyElm)},this.resetDropzoneElement=function(){e.$nodesScope.$modelValue&&0===e.$nodesScope.$modelValue.length||!e.dropzoneEnabled?e.$dropzoneElm.remove():n.append(e.$dropzoneElm)},e.resetEmptyElement=this.resetEmptyElement,e.resetDropzoneElement=this.resetDropzoneElement}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTree",["treeConfig","$window",function(e,n){return{restrict:"A",scope:!0,controller:"TreeController",link:function(t,o,l,r){var a,i,d,c={accept:null,beforeDrag:null},s={};angular.extend(s,e),s.treeClass&&o.addClass(s.treeClass),"table"===o.prop("tagName").toLowerCase()?(t.$emptyElm=angular.element(n.document.createElement("tr")),i=o.find("tr"),d=i.length>0?angular.element(i).children().length:1e6,a=angular.element(n.document.createElement("td")).attr("colspan",d),t.$emptyElm.append(a)):(t.$emptyElm=angular.element(n.document.createElement("div")),t.$dropzoneElm=angular.element(n.document.createElement("div"))),s.emptyTreeClass&&t.$emptyElm.addClass(s.emptyTreeClass),s.dropzoneClass&&t.$dropzoneElm.addClass(s.dropzoneClass),t.$watch("$nodesScope.$modelValue.length",function(e){angular.isNumber(e)&&(r.resetEmptyElement(),r.resetDropzoneElement())},!0),t.$watch(l.dragEnabled,function(e){"boolean"==typeof e&&(t.dragEnabled=e)}),t.$watch(l.emptyPlaceholderEnabled,function(e){"boolean"==typeof e&&(t.emptyPlaceholderEnabled=e,r.resetEmptyElement())}),t.$watch(l.nodropEnabled,function(e){"boolean"==typeof e&&(t.nodropEnabled=e)}),t.$watch(l.dropzoneEnabled,function(e){"boolean"==typeof e&&(t.dropzoneEnabled=e,r.resetDropzoneElement())}),t.$watch(l.cloneEnabled,function(e){"boolean"==typeof e&&(t.cloneEnabled=e)}),t.$watch(l.maxDepth,function(e){"number"==typeof e&&(t.maxDepth=e)}),t.$watch(l.dragDelay,function(e){"number"==typeof e&&(t.dragDelay=e)}),c.accept=function(e,n,t){return!(n.nodropEnabled||n.$treeScope.nodropEnabled||n.outOfDepth(e))},c.beforeDrag=function(e){return!0},c.expandTimeoutStart=function(){},c.expandTimeoutCancel=function(){},c.expandTimeoutEnd=function(){},c.removed=function(e){},c.dropped=function(e){},c.dragStart=function(e){},c.dragMove=function(e){},c.dragStop=function(e){},c.beforeDrop=function(e){},c.toggle=function(e,n){},t.$watch(l.uiTree,function(e,n){angular.forEach(e,function(e,n){c[n]&&"function"==typeof e&&(c[n]=e)}),t.$callbacks=c},!0)}}}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTreeHandle",["treeConfig",function(e){return{require:"^uiTreeNode",restrict:"A",scope:!0,controller:"TreeHandleController",link:function(n,t,o,l){var r={};angular.extend(r,e),r.handleClass&&t.addClass(r.handleClass),n!=l.scope&&(n.$nodeScope=l.scope,l.scope.$handleScope=n)}}}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTreeNode",["treeConfig","UiTreeHelper","$window","$document","$timeout","$q",function(e,n,t,o,l,r){return{require:["^uiTreeNodes","^uiTree"],restrict:"A",controller:"TreeNodeController",link:function(a,i,d,c){var s,u,p,m,f,h,$,g,b,v,N,S,E,y,x,C,T,w,D,H,O,Y,X,A,V,k,z,M={},I="ontouchstart"in window,P=null,L=document.body,W=document.documentElement;angular.extend(M,e),M.nodeClass&&i.addClass(M.nodeClass),a.init(c),a.collapsed=!!n.getNodeAttribute(a,"collapsed")||e.defaultCollapsed,a.expandOnHover=!!n.getNodeAttribute(a,"expandOnHover"),a.scrollContainer=n.getNodeAttribute(a,"scrollContainer")||d.scrollContainer||null,a.sourceOnly=a.nodropEnabled||a.$treeScope.nodropEnabled,a.$watch(d.collapsed,function(e){"boolean"==typeof e&&(a.collapsed=e)}),a.$watch("collapsed",function(e){n.setNodeAttribute(a,"collapsed",e),d.$set("collapsed",e)}),a.$watch(d.expandOnHover,function(e){"boolean"!=typeof e&&"number"!=typeof e||(a.expandOnHover=e)}),a.$watch("expandOnHover",function(e){n.setNodeAttribute(a,"expandOnHover",e),d.$set("expandOnHover",e)}),d.$observe("scrollContainer",function(e){"string"==typeof e&&(a.scrollContainer=e)}),a.$watch("scrollContainer",function(e){n.setNodeAttribute(a,"scrollContainer",e),d.$set("scrollContainer",e),$=document.querySelector(e)}),a.$on("angular-ui-tree:collapse-all",function(){a.collapsed=!0}),a.$on("angular-ui-tree:expand-all",function(){a.collapsed=!1}),S=function(e){if((I||2!==e.button&&3!==e.which)&&!(e.uiTreeDragging||e.originalEvent&&e.originalEvent.uiTreeDragging)){var l,r,d,c,$,g,S,E,y,x=angular.element(e.target);if(l=n.treeNodeHandlerContainerOfElement(x),l&&(x=angular.element(l)),r=i.clone(),E=n.elementIsTreeNode(x),y=n.elementIsTreeNodeHandle(x),(E||y)&&!(E&&n.elementContainsTreeNodeHandler(x)||"input"==(d=x.prop("tagName").toLowerCase())||"textarea"==d||"button"==d||"select"==d)){for(V=angular.element(e.target),k=V[0].attributes["ui-tree"];V&&V[0]&&V[0]!==i&&!k;){if(V[0].attributes&&(k=V[0].attributes["ui-tree"]),n.nodrag(V))return;V=V.parent()}a.beforeDrag(a)&&(e.uiTreeDragging=!0,e.originalEvent&&(e.originalEvent.uiTreeDragging=!0),e.preventDefault(),$=n.eventObj(e),s=!0,u=n.dragInfo(a),z=u.source.$treeScope.$id,c=i.prop("tagName"),"tr"===c.toLowerCase()?(m=angular.element(t.document.createElement(c)),g=angular.element(t.document.createElement("td")).addClass(M.placeholderClass).attr("colspan",i[0].children.length),m.append(g)):m=angular.element(t.document.createElement(c)).addClass(M.placeholderClass),f=angular.element(t.document.createElement(c)),M.hiddenClass&&f.addClass(M.hiddenClass),p=n.positionStarted($,i),m.css("height",i.prop("offsetHeight")+"px"),h=angular.element(t.document.createElement(a.$parentNodesScope.$element.prop("tagName"))).addClass(a.$parentNodesScope.$element.attr("class")).addClass(M.dragClass),h.css("width",n.width(i)+"px"),h.css("z-index",9999),S=(i[0].querySelector(".angular-ui-tree-handle")||i[0]).currentStyle,S&&(document.body.setAttribute("ui-tree-cursor",o.find("body").css("cursor")||""),o.find("body").css({cursor:S.cursor+"!important"})),a.sourceOnly&&m.css("display","none"),i.after(m),i.after(f),u.isClone()&&a.sourceOnly?h.append(r):h.append(i),o.find("body").append(h),h.css({left:$.pageX-p.offsetX+"px",top:$.pageY-p.offsetY+"px"}),b={placeholder:m,dragging:h},O(),a.$apply(function(){a.$treeScope.$callbacks.dragStart(u.eventArgs(b,p))}),v=Math.max(L.scrollHeight,L.offsetHeight,W.clientHeight,W.scrollHeight,W.offsetHeight),N=Math.max(L.scrollWidth,L.offsetWidth,W.clientWidth,W.scrollWidth,W.offsetWidth))}}},E=function(e){var o,r,i,d,c,f,S,E,y,x,C,T,w,D,H,O,Y,X,V,k,I,L,W=n.eventObj(e);if(h){if(e.preventDefault(),t.getSelection?t.getSelection().removeAllRanges():t.document.selection&&t.document.selection.empty(),r=W.pageX-p.offsetX,i=W.pageY-p.offsetY,r<0&&(r=0),i<0&&(i=0),i+10>v&&(i=v-10),r+10>N&&(r=N-10),h.css({left:r+"px",top:i+"px"}),$?(f=$.getBoundingClientRect(),d=$.scrollTop,c=d+$.clientHeight,f.bottomW.clientY&&d>0&&(H=Math.min(d,10),$.scrollTop-=H)):(d=window.pageYOffset||t.document.documentElement.scrollTop,c=d+(window.innerHeight||t.document.clientHeight||t.document.clientHeight),cW.pageY&&(H=Math.min(d,10),window.scrollBy(0,-H))),n.positionMoved(e,p,s),s)return void(s=!1);if(E=W.pageX-(t.pageXOffset||t.document.body.scrollLeft||t.document.documentElement.scrollLeft)-(t.document.documentElement.clientLeft||0),y=W.pageY-(t.pageYOffset||t.document.body.scrollTop||t.document.documentElement.scrollTop)-(t.document.documentElement.clientTop||0),angular.isFunction(h.hide)?h.hide():(x=h[0].style.display,h[0].style.display="none"),t.document.elementFromPoint(E,y),T=angular.element(t.document.elementFromPoint(E,y)),A=n.treeNodeHandlerContainerOfElement(T),A&&(T=angular.element(A)),angular.isFunction(h.show)?h.show():h[0].style.display=x,n.elementIsTree(T)?C=T.controller("uiTree").scope:n.elementIsTreeNodeHandle(T)?C=T.controller("uiTreeHandle").scope:n.elementIsTreeNode(T)?C=T.controller("uiTreeNode").scope:n.elementIsTreeNodes(T)?C=T.controller("uiTreeNodes").scope:n.elementIsPlaceholder(T)?C=T.controller("uiTreeNodes").scope:n.elementIsDropzone(T)?(C=T.controller("uiTree").scope,L=!0):T.controller("uiTreeNode")&&(C=T.controller("uiTreeNode").scope),C&&C.$treeScope&&C.$treeScope.$id&&C.$treeScope.$id===z&&p.dirAx)p.distX>0&&(o=u.prev())&&!o.collapsed&&o.accept(a,o.childNodesCount())&&(o.$childNodesScope.$element.append(m),u.moveTo(o.$childNodesScope,o.childNodes(),o.childNodesCount())),p.distX<0&&(u.next()||(S=u.parentNode())&&S.$parentNodesScope.accept(a,S.index()+1)&&(S.$element.after(m),u.moveTo(S.$parentNodesScope,S.siblings(),S.index()+1)));else{if(w=!1,!C)return;if(!C.$treeScope||C.$parent.nodropEnabled||C.$treeScope.nodropEnabled||m.css("display",""),"uiTree"===C.$type&&C.dragEnabled&&(w=C.isEmpty()),"uiTreeHandle"===C.$type&&(C=C.$nodeScope),"uiTreeNode"!==C.$type&&!w&&!L)return void(M.appendChildOnHover&&!u.next()&&g&&(S=u.parentNode(),S.$element.after(m),u.moveTo(S.$parentNodesScope,S.siblings(),S.index()+1),g=!1));P&&m.parent()[0]!=P.$element[0]&&(P.resetEmptyElement(),P.resetDropzoneElement(),P=null),w?(P=C,C.$nodesScope.accept(a,0)&&u.moveTo(C.$nodesScope,C.$nodesScope.childNodes(),0)):L?(P=C,C.$nodesScope.accept(a,C.$nodesScope.childNodes().length)&&u.moveTo(C.$nodesScope,C.$nodesScope.childNodes(),C.$nodesScope.childNodes().length)):C.dragEnabled()&&(angular.isDefined(a.expandTimeoutOn)&&a.expandTimeoutOn!==C.id&&(l.cancel(a.expandTimeout),delete a.expandTimeout,delete a.expandTimeoutOn,a.$callbacks.expandTimeoutCancel()),C.collapsed&&(!0===a.expandOnHover||angular.isNumber(a.expandOnHover)&&0===a.expandOnHover?(C.collapsed=!1,C.$treeScope.$callbacks.toggle(!1,C)):!1!==a.expandOnHover&&angular.isNumber(a.expandOnHover)&&a.expandOnHover>0&&angular.isUndefined(a.expandTimeoutOn)&&(a.expandTimeoutOn=C.$id,a.$callbacks.expandTimeoutStart(),a.expandTimeout=l(function(){a.$callbacks.expandTimeoutEnd(),C.collapsed=!1,C.$treeScope.$callbacks.toggle(!1,C)},a.expandOnHover))),T=C.$element,O=n.offset(T),V=n.height(T),k=C.$childNodesScope?C.$childNodesScope.$element:null,I=k?n.height(k):0,V-=I,X=M.appendChildOnHover?.25*V:n.height(T)/2,Y=W.pageY0?D.exec(function(){x(e)},a.dragDelay):x(e)}),i.bind("touchend touchcancel mouseup",function(){a.dragDelay>0&&D.cancel()})},H(),O=function(){angular.element(o).bind("touchend",T),angular.element(o).bind("touchcancel",T),angular.element(o).bind("touchmove",C),angular.element(o).bind("mouseup",T),angular.element(o).bind("mousemove",C),angular.element(o).bind("mouseleave",w),angular.element(o).bind("keydown",X)},Y=function(){angular.element(o).unbind("touchend",T),angular.element(o).unbind("touchcancel",T),angular.element(o).unbind("touchmove",C),angular.element(o).unbind("mouseup",T),angular.element(o).unbind("mousemove",C),angular.element(o).unbind("mouseleave",w),angular.element(o).unbind("keydown",X)}}}}])}(),function(){"use strict";angular.module("ui.tree").directive("uiTreeNodes",["treeConfig","$window",function(e){return{require:["ngModel","?^uiTreeNode","^uiTree"],restrict:"A",scope:!0,controller:"TreeNodesController",link:function(n,t,o,l){var r={},a=l[0],i=l[1],d=l[2];angular.extend(r,e),r.nodesClass&&t.addClass(r.nodesClass),i?(i.scope.$childNodesScope=n,n.$nodeScope=i.scope):d.scope.$nodesScope=n,n.$treeScope=d.scope,a&&(a.$render=function(){n.$modelValue=a.$modelValue}),n.$watch(function(){return o.maxDepth},function(e){"number"==typeof e&&(n.maxDepth=e)}),n.$watch(function(){return o.nodropEnabled},function(e){void 0!==e&&(n.nodropEnabled=!0)},!0)}}}])}(),function(){"use strict";function e(e,n){if(void 0===n)return null;for(var t=n.parentNode,o=1,l="function"==typeof t.setAttribute&&t.hasAttribute(e)?t:null;t&&"function"==typeof t.setAttribute&&!t.hasAttribute(e);){if(t=t.parentNode,l=t,t===document.documentElement){l=null;break}o++}return l}angular.module("ui.tree").factory("UiTreeHelper",["$document","$window","treeConfig",function(n,t,o){return{nodesData:{},setNodeAttribute:function(e,n,t){if(!e.$modelValue)return null;var o=this.nodesData[e.$modelValue.$$hashKey];o||(o={},this.nodesData[e.$modelValue.$$hashKey]=o),o[n]=t},getNodeAttribute:function(e,n){if(!e.$modelValue)return null;var t=this.nodesData[e.$modelValue.$$hashKey];return t?t[n]:null},nodrag:function(e){return void 0!==e.attr("data-nodrag")&&"false"!==e.attr("data-nodrag")},eventObj:function(e){var n=e;return void 0!==e.targetTouches?n=e.targetTouches.item(0):void 0!==e.originalEvent&&void 0!==e.originalEvent.targetTouches&&(n=e.originalEvent.targetTouches.item(0)),n},dragInfo:function(e){return{source:e,sourceInfo:{cloneModel:!0===e.$treeScope.cloneEnabled?angular.copy(e.$modelValue):void 0,nodeScope:e,index:e.index(),nodesScope:e.$parentNodesScope},index:e.index(),siblings:e.siblings().slice(0),parent:e.$parentNodesScope,resetParent:function(){this.parent=e.$parentNodesScope},moveTo:function(e,n,t){this.parent=e,this.siblings=n.slice(0);var o=this.siblings.indexOf(this.source);o>-1&&(this.siblings.splice(o,1),this.source.index()0?this.siblings[this.index-1]:null},next:function(){return this.index0&&(o=e.originalEvent.touches[0].pageX,l=e.originalEvent.touches[0].pageY),t.offsetX=o-this.offset(n).left,t.offsetY=l-this.offset(n).top,t.startX=t.lastX=o,t.startY=t.lastY=l,t.nowX=t.nowY=t.distX=t.distY=t.dirAx=0,t.dirX=t.dirY=t.lastDirX=t.lastDirY=t.distAxX=t.distAxY=0,t},positionMoved:function(e,n,t){var o,l=e.pageX,r=e.pageY;if(e.originalEvent&&e.originalEvent.touches&&e.originalEvent.touches.length>0&&(l=e.originalEvent.touches[0].pageX,r=e.originalEvent.touches[0].pageY),n.lastX=n.nowX,n.lastY=n.nowY,n.nowX=l,n.nowY=r,n.distX=n.nowX-n.lastX,n.distY=n.nowY-n.lastY,n.lastDirX=n.dirX,n.lastDirY=n.dirY,n.dirX=0===n.distX?0:n.distX>0?1:-1,n.dirY=0===n.distY?0:n.distY>0?1:-1,o=Math.abs(n.distX)>Math.abs(n.distY)?1:0,t)return n.dirAx=o,void(n.moving=!0);n.dirAx!==o?(n.distAxX=0,n.distAxY=0):(n.distAxX+=Math.abs(n.distX),0!==n.dirX&&n.dirX!==n.lastDirX&&(n.distAxX=0),n.distAxY+=Math.abs(n.distY),0!==n.dirY&&n.dirY!==n.lastDirY&&(n.distAxY=0)),n.dirAx=o},elementIsTreeNode:function(e){return void 0!==e.attr("ui-tree-node")},elementIsTreeNodeHandle:function(e){return void 0!==e.attr("ui-tree-handle")},elementIsTree:function(e){return void 0!==e.attr("ui-tree")},elementIsTreeNodes:function(e){return void 0!==e.attr("ui-tree-nodes")},elementIsPlaceholder:function(e){return e.hasClass(o.placeholderClass)},elementIsDropzone:function(e){return e.hasClass(o.dropzoneClass)},elementContainsTreeNodeHandler:function(e){return e[0].querySelectorAll("[ui-tree-handle]").length>=1},treeNodeHandlerContainerOfElement:function(n){return e("ui-tree-handle",n[0])}}}])}(); \ 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 0000000000..24a05554a7 --- /dev/null +++ b/traffic_portal/app/src/common/api/TopologyService.js @@ -0,0 +1,218 @@ +/* + * 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 [ + { + "name": "FooTopology", + "desc": "a topology for Foo DSes", + "nodes": [ + { + "id": 0, + "cachegroup": "aurora", + "parents": [ + 1, + 2 + ] + }, + { + "id": 1, + "cachegroup": "denver", + "parents": [ + 5, + 6 + ] + }, + { + "id": 2, + "cachegroup": "sac", + "parents": [ + 5, + 6 + ] + }, + { + "id": 3, + "cachegroup": "boston", + "parents": [ + 6, + 5 + ] + }, + { + "id": 4, + "cachegroup": "albany", + "parents": [ + 5, + 6 + ] + }, + { + "id": 5, + "cachegroup": "mid-west", + "parents": [] + }, + { + "id": 6, + "cachegroup": "mid-east", + "parents": [] + } + ] + } + ]; + // return $http.get(ENV.api['root'] + 'topologies', {params: queryParams}).then( + // function (result) { + // return result.data.response; + // }, + // function (err) { + // throw err; + // } + // ) + }; + + this.getTopology = function(id) { + return { + "name": "Topology 1", + "desc": "a topology for Foo DSes", + "children": [ + { + "id": 3, + "depth": 0, + "secParent": "", + "title": "mid-west", + "name": "mid-west", + "type": "MID_LOC", + "nodrop": true, + "children": [ + { + "id": 18, + "depth": 1, + "secParent": "mid-east", + "title": "denver", + "name": "denver", + "type": "MID_LOC", + "children": [ + { + "id": 41, + "depth": 2, + "secParent": "sacramento", + "size": 100, + "title": "aurora", + "name": "aurora", + "type": "EDGE_LOC", + "children": [] + } + ] + }, + { + "id": 1, + "depth": 1, + "secParent": "mid-east", + "title": "sacramento", + "name": "sacramento", + "children": [] + } + ] + }, + { + "id": 2, + "depth": 0, + "secParent": "", + "title": "mid-east", + "name": "mid-east", + "nodrop": true, + "children": [ + { + "id": 21, + "depth": 1, + "secParent": "mid-west", + "size": 100, + "title": "boston", + "name": "boston", + "children": [] + }, + { + "id": 22, + "depth": 1, + "secParent": "mid-west", + "size": 200, + "title": "albany", + "name": "albany", + "children": [] + } + ] + } + ] + }; + // return $http.get(ENV.api['root'] + 'topologies', {params: {id: id}}).then( + // function (result) { + // return result.data.response[0]; + // }, + // function (err) { + // throw err; + // } + // ) + }; + + this.createTopology = function(topology) { + return $http.post(ENV.api['root'] + 'topologies', topology).then( + function(result) { + messageModel.setMessages([ { level: 'success', text: 'Topology created' } ], true); + locationUtils.navigateToPath('/topologies'); + return result; + }, + function(err) { + messageModel.setMessages(err.data.alerts, false); + throw err; + } + ); + }; + + this.updateTopology = function(topology) { + return $http.put(ENV.api['root'] + 'topologies/' + topology.id, topology).then( + function(result) { + messageModel.setMessages([ { level: 'success', text: 'Topology updated' } ], false); + return result; + }, + function(err) { + messageModel.setMessages(err.data.alerts, false); + throw err; + } + ); + }; + + this.deleteTopology = function(id) { + return $http.delete(ENV.api['root'] + "topologies/" + id).then( + function(result) { + messageModel.setMessages([ { level: 'success', text: 'Topology deleted' } ], true); + 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 7ee157a6b4..079b6d9001 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/form/topology/FormTopologyController.js b/traffic_portal/app/src/common/modules/form/topology/FormTopologyController.js new file mode 100644 index 0000000000..3151c7db4b --- /dev/null +++ b/traffic_portal/app/src/common/modules/form/topology/FormTopologyController.js @@ -0,0 +1,193 @@ +/* + * 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, $scope, $location, formUtils, locationUtils, serverService) { + + $scope.topology = topology; + + $scope.massaged = []; + + $scope.treeOptions = { + beforeDrop: function(evt) { + console.log('drop'); + console.log(evt); + let node = evt.source.nodeScope.$modelValue.cachegroup, + parent = (evt.dest.nodesScope.$parent.$modelValue) ? evt.dest.nodesScope.$parent.$modelValue.cachegroup : 'root' + return confirm("Move " + node + " under " + parent + "?"); + } + }; + + var massage = function(topology) { + var roots = []; // things without parent + + // make them accessible by guid on this map + var all = {}; + + topology.nodes.forEach(function(node, index) { + all[index] = node; + }); + + // connect childrens to its parent, and split roots apart + Object.keys(all).forEach(function (guid) { + var item = all[guid]; + if (!('children' in item)) { + item.children = [] + } + if (item.parents.length === 0) { + roots.push(item) + } else if (item.parents[0] in all) { + var p = all[item.parents[0]] + if (!('children' in p)) { + p.children = [] + } + p.children.push(item); + // add secParent to each node + if (item.parents.length === 2 && item.parents[1] in all) { + item.secParent = all[item.parents[1]].cachegroup; + } + } + }); + + console.log(roots); + + $scope.massaged = roots; + }; + + $scope.target = [ + { + "id": 3, + "depth": 0, + "secParent": "", + "title": "mid-west", + "name": "mid-west", + "type": "MID_LOC", + "nodrop": true, + "children": [ + { + "id": 18, + "depth": 1, + "secParent": "mid-east", + "title": "denver", + "name": "denver", + "type": "MID_LOC", + "children": [ + { + "id": 41, + "depth": 2, + "secParent": "sacramento", + "size": 100, + "title": "aurora", + "name": "aurora", + "type": "EDGE_LOC", + "children": [] + } + ] + }, + { + "id": 1, + "depth": 1, + "secParent": "mid-east", + "title": "sacramento", + "name": "sacramento", + "children": [] + } + ] + }, + { + "id": 2, + "depth": 0, + "secParent": "", + "title": "mid-east", + "name": "mid-east", + "nodrop": true, + "children": [ + { + "id": 21, + "depth": 1, + "secParent": "mid-west", + "size": 100, + "title": "boston", + "name": "boston", + "children": [] + }, + { + "id": 22, + "depth": 1, + "secParent": "mid-west", + "size": 200, + "title": "albany", + "name": "albany", + "children": [] + } + ] + } + ]; + + $scope.navigateToPath = locationUtils.navigateToPath; + + $scope.hasError = formUtils.hasError; + + $scope.hasPropertyError = formUtils.hasPropertyError; + + $scope.second = function() { + alert('add 2nd parent'); + }; + + $scope.remove = function(scope) { + console.log(scope); + // if (confirm("Remove " + evt.$modelValue.cachegroup + " and all its children?")){ + // scope.remove(); + // } + }; + + $scope.toggle = function(scope) { + scope.toggle(); + }; + + $scope.newSubItem = function(scope) { + var nodeData = scope.$modelValue; + nodeData.children.push({ + id: nodeData.id * 10 + nodeData.children.length, + secParent: "", + cachegroup: nodeData.cachegroup + '.' + (nodeData.children.length + 1), + children: [] + }); + }; + + $scope.viewServers = function(scope) { + var nodeData = scope.$modelValue; + alert('open dialog with cachegroup servers'); + // serverService.getServers({ cachegroup: nodeData.id }) + // .then(function(result) { + // debugger; + // $scope.cacheGroupServers = result; + // }); + + }; + + let init = function() { + massage(angular.copy($scope.topology)); + }; + init(); + + +}; + +FormTopologyController.$inject = ['topology', '$scope', '$location', 'formUtils', 'locationUtils', 'serverService']; +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 0000000000..9feffd93ff --- /dev/null +++ b/traffic_portal/app/src/common/modules/form/topology/edit/FormEditTopologyController.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 FormEditTopologyController = function(topologies, $scope, $controller, $uibModal, $anchorScroll, locationUtils, topologyService) { + + // extends the FormTopologyController to inherit common methods + angular.extend(this, $controller('FormTopologyController', { topology: topologies[0], $scope: $scope })); + + var deleteTopology = function(topology) { + topologyService.deleteTopology(topology.id) + .then(function() { + locationUtils.navigateToPath('/topologies'); + }); + }; + + $scope.topologyName = angular.copy($scope.topology.name); + + $scope.settings = { + isNew: false, + saveLabel: 'Update' + }; + + $scope.save = function(topology) { + topologyService.updateTopology(topology). + then(function() { + $scope.topologyName = angular.copy(topology.name); + $anchorScroll(); // scrolls window to top + }); + }; + + $scope.confirmDelete = function(topology) { + var params = { + title: 'Delete Topology: ' + topology.name, + key: topology.name + }; + var 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); + }, function () { + // do nothing + }); + }; + +}; + +FormEditTopologyController.$inject = ['topologies', '$scope', '$controller', '$uibModal', '$anchorScroll', 'locationUtils', 'topologyService']; +module.exports = FormEditTopologyController; diff --git a/traffic_portal/app/src/common/modules/form/topology/edit/index.js b/traffic_portal/app/src/common/modules/form/topology/edit/index.js new file mode 100644 index 0000000000..f3c7ad8558 --- /dev/null +++ b/traffic_portal/app/src/common/modules/form/topology/edit/index.js @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = 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 0000000000..26f54866f8 --- /dev/null +++ b/traffic_portal/app/src/common/modules/form/topology/form.topology.tpl.html @@ -0,0 +1,113 @@ + + +
+
+ +
+
+
+
+
+
+ +
+ + Required + No spaces + +
+
+
+ +
+ + Required +
+
+
+ +
+
+
    +
  1. +
+
+
+
+ +
+ +
+
+
+ {{info}} +
+
{{ topology.nodes | json }}
+
+
+
+ {{info}} +
+
{{ massaged | json }}
+
+
+
+ {{info}} +
+
{{ target | json }}
+
+
+ + +
+
+ + + + + + diff --git a/traffic_portal/app/src/common/modules/form/topology/index.js b/traffic_portal/app/src/common/modules/form/topology/index.js new file mode 100644 index 0000000000..9ebfc4fa82 --- /dev/null +++ b/traffic_portal/app/src/common/modules/form/topology/index.js @@ -0,0 +1,21 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +module.exports = angular.module('trafficPortal.form.topology', []) + .controller('FormTopologyController', require('./FormTopologyController')); 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 0dd58da6a6..dc4d0a09d8 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 @@
  • Topology