diff --git a/.gitignore b/.gitignore index 0a116f850780386a9fe1010b22164f4c7dbf8228..8b8419968a620ed2329dacb166d5e8ae4ae4b0c1 100644 --- a/.gitignore +++ b/.gitignore @@ -156,6 +156,7 @@ cython_debug/ # Other /tmp +/netphony-network-protocols # Sqlite *.db diff --git a/manifests/bgpls_speakerservice.yaml b/manifests/bgpls_speakerservice.yaml new file mode 100644 index 0000000000000000000000000000000000000000..5488566bc02bf6edeaff3a1536e9641334c07fad --- /dev/null +++ b/manifests/bgpls_speakerservice.yaml @@ -0,0 +1,72 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +apiVersion: apps/v1 +kind: Deployment +metadata: + name: bgpls-speakerservice +spec: + selector: + matchLabels: + app: bgpls-speakerservice + replicas: 1 + template: + metadata: + labels: + app: bgpls-speakerservice + spec: + terminationGracePeriodSeconds: 5 + containers: + - name: server + image: localhost:32000/tfs/bgpls_speaker:dev + imagePullPolicy: Always + ports: + - containerPort: 10030 + - containerPort: 9192 + env: + - name: LOG_LEVEL + value: "DEBUG" + readinessProbe: + exec: + command: ["/bin/grpc_health_probe", "-addr=:10030"] + livenessProbe: + exec: + command: ["/bin/grpc_health_probe", "-addr=:10030"] + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 500m + memory: 512Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: bgpls-speakerservice + labels: + app: bgpls-speakerservice +spec: + type: ClusterIP + selector: + app: bgpls-speakerservice + ports: + - name: grpc + protocol: TCP + port: 10030 + targetPort: 10030 + - name: metrics + protocol: TCP + port: 9192 + targetPort: 9192 diff --git a/src/common/Constants.py b/src/common/Constants.py index a7bf198a7204677ed3669fc28a2c3528a5936425..a44758a7954e85a56fd8ae3c5321b2fa5e6551ca 100644 --- a/src/common/Constants.py +++ b/src/common/Constants.py @@ -16,7 +16,7 @@ import logging from enum import Enum # Default logging level -DEFAULT_LOG_LEVEL = logging.WARNING +DEFAULT_LOG_LEVEL = logging.DEBUG # Default gRPC server settings DEFAULT_GRPC_BIND_ADDRESS = '0.0.0.0' @@ -49,6 +49,7 @@ class ServiceNameEnum(Enum): INTERDOMAIN = 'interdomain' PATHCOMP = 'pathcomp' WEBUI = 'webui' + BGPLS = 'bgpls-speaker' # Used for test and debugging only DLT_GATEWAY = 'dltgateway' @@ -68,6 +69,7 @@ DEFAULT_SERVICE_GRPC_PORTS = { ServiceNameEnum.CYBERSECURITY.value : 10000, ServiceNameEnum.INTERDOMAIN .value : 10010, ServiceNameEnum.PATHCOMP .value : 10020, + ServiceNameEnum.BGPLS .value : 10030, # Used for test and debugging only ServiceNameEnum.DLT_GATEWAY .value : 50051, diff --git a/src/common/DeviceTypes.py b/src/common/DeviceTypes.py index a47b6be3c6acac5be0ebbe262ba0988f536bf083..99255defdb6b5ee155607536a2e13d23b97b2d3a 100644 --- a/src/common/DeviceTypes.py +++ b/src/common/DeviceTypes.py @@ -28,7 +28,6 @@ class DeviceTypeEnum(Enum): EMULATED_P4_SWITCH = 'emu-p4-switch' EMULATED_PACKET_ROUTER = 'emu-packet-router' EMULATED_PACKET_SWITCH = 'emu-packet-switch' - EMULATED_BGPLS_ASNUMBER = 'emu-bgpls-asnumber' # Real device types DATACENTER = 'datacenter' @@ -39,5 +38,4 @@ class DeviceTypeEnum(Enum): P4_SWITCH = 'p4-switch' PACKET_ROUTER = 'packet-router' PACKET_SWITCH = 'packet-switch' - XR_CONSTELLATION = 'xr-constellation' - BGPLS_ASNUMBER = 'bgpls-asnumber' \ No newline at end of file + XR_CONSTELLATION = 'xr-constellation' \ No newline at end of file diff --git a/src/common/tools/object_factory/Device.py b/src/common/tools/object_factory/Device.py index d35cb684cb55181b7bc6d665e00f45ebf351c758..0cc4555d455bf28ac2143a5d58b87e084a8360c7 100644 --- a/src/common/tools/object_factory/Device.py +++ b/src/common/tools/object_factory/Device.py @@ -43,9 +43,6 @@ DEVICE_MICROWAVE_DRIVERS = [DeviceDriverEnum.DEVICEDRIVER_IETF_NETWORK_TOPOLOGY] DEVICE_P4_TYPE = DeviceTypeEnum.P4_SWITCH.value DEVICE_P4_DRIVERS = [DeviceDriverEnum.DEVICEDRIVER_P4] -DEVICE_BGPLS_TYPE = DeviceTypeEnum.BGPLS_ASNUMBER.value -DEVICE_BGPLS_DRIVERS = [DeviceDriverEnum.DEVICEDRIVER_BGPLS] - def json_device_id(device_uuid : str): return {'device_uuid': {'uuid': device_uuid}} diff --git a/src/compute/service/__main__.py b/src/compute/service/__main__.py index 9705e3187ffff633a4d127855c1c57afcf397e39..8621df44477376b19a2c1400047cfb5dcd85701c 100644 --- a/src/compute/service/__main__.py +++ b/src/compute/service/__main__.py @@ -13,6 +13,7 @@ # limitations under the License. import logging, signal, sys, threading +from compute.service.rest_server.nbi_plugins.ietf_topolgy import register_ietf_topology from prometheus_client import start_http_server from common.Constants import ServiceNameEnum from common.Settings import ( @@ -60,6 +61,7 @@ def main(): rest_server = RestServer() register_debug_api(rest_server) register_ietf_l2vpn(rest_server) + register_ietf_topology(rest_server) rest_server.start() # Wait for Ctrl+C or termination signal diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Constants.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Constants.py new file mode 100644 index 0000000000000000000000000000000000000000..f95b532af4ba01968d17bc3958e1cffbf84a5e7f --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Constants.py @@ -0,0 +1,77 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +DEFAULT_MTU = 1512 +DEFAULT_ADDRESS_FAMILIES = ['IPV4'] +DEFAULT_BGP_AS = 65000 +DEFAULT_BGP_ROUTE_TARGET = '{:d}:{:d}'.format(DEFAULT_BGP_AS, 333) + +# TODO: improve definition of bearer mappings + +# Bearer mappings: +# device_uuid:endpoint_uuid => ( +# device_uuid, endpoint_uuid, router_id, route_dist, sub_if_index, +# address_ip, address_prefix, remote_router, circuit_id) + +BEARER_MAPPINGS = { + # OFC'22 + 'R1-EMU:13/1/2': ('R1-EMU', '13/1/2', '10.10.10.1', '65000:100', 400, '3.3.2.1', 24, None, None), + 'R2-EMU:13/1/2': ('R2-EMU', '13/1/2', '12.12.12.1', '65000:120', 450, '3.4.2.1', 24, None, None), + 'R3-EMU:13/1/2': ('R3-EMU', '13/1/2', '20.20.20.1', '65000:200', 500, '3.3.1.1', 24, None, None), + 'R4-EMU:13/1/2': ('R4-EMU', '13/1/2', '22.22.22.1', '65000:220', 550, '3.4.1.1', 24, None, None), + + # OECC/PSC'22 - domain 1 + 'R1@D1:3/1' : ('R1@D1', '3/1', '10.0.1.1', '65001:101', 100, '1.1.3.1', 24, None, None), + 'R1@D1:3/2' : ('R1@D1', '3/2', '10.0.1.1', '65001:101', 100, '1.1.3.2', 24, None, None), + 'R1@D1:3/3' : ('R1@D1', '3/3', '10.0.1.1', '65001:101', 100, '1.1.3.3', 24, None, None), + 'R2@D1:3/1' : ('R2@D1', '3/1', '10.0.1.2', '65001:102', 100, '1.2.3.1', 24, None, None), + 'R2@D1:3/2' : ('R2@D1', '3/2', '10.0.1.2', '65001:102', 100, '1.2.3.2', 24, None, None), + 'R2@D1:3/3' : ('R2@D1', '3/3', '10.0.1.2', '65001:102', 100, '1.2.3.3', 24, None, None), + 'R3@D1:3/1' : ('R3@D1', '3/1', '10.0.1.3', '65001:103', 100, '1.3.3.1', 24, None, None), + 'R3@D1:3/2' : ('R3@D1', '3/2', '10.0.1.3', '65001:103', 100, '1.3.3.2', 24, None, None), + 'R3@D1:3/3' : ('R3@D1', '3/3', '10.0.1.3', '65001:103', 100, '1.3.3.3', 24, None, None), + 'R4@D1:3/1' : ('R4@D1', '3/1', '10.0.1.4', '65001:104', 100, '1.4.3.1', 24, None, None), + 'R4@D1:3/2' : ('R4@D1', '3/2', '10.0.1.4', '65001:104', 100, '1.4.3.2', 24, None, None), + 'R4@D1:3/3' : ('R4@D1', '3/3', '10.0.1.4', '65001:104', 100, '1.4.3.3', 24, None, None), + + # OECC/PSC'22 - domain 2 + 'R1@D2:3/1' : ('R1@D2', '3/1', '10.0.2.1', '65002:101', 100, '2.1.3.1', 24, None, None), + 'R1@D2:3/2' : ('R1@D2', '3/2', '10.0.2.1', '65002:101', 100, '2.1.3.2', 24, None, None), + 'R1@D2:3/3' : ('R1@D2', '3/3', '10.0.2.1', '65002:101', 100, '2.1.3.3', 24, None, None), + 'R2@D2:3/1' : ('R2@D2', '3/1', '10.0.2.2', '65002:102', 100, '2.2.3.1', 24, None, None), + 'R2@D2:3/2' : ('R2@D2', '3/2', '10.0.2.2', '65002:102', 100, '2.2.3.2', 24, None, None), + 'R2@D2:3/3' : ('R2@D2', '3/3', '10.0.2.2', '65002:102', 100, '2.2.3.3', 24, None, None), + 'R3@D2:3/1' : ('R3@D2', '3/1', '10.0.2.3', '65002:103', 100, '2.3.3.1', 24, None, None), + 'R3@D2:3/2' : ('R3@D2', '3/2', '10.0.2.3', '65002:103', 100, '2.3.3.2', 24, None, None), + 'R3@D2:3/3' : ('R3@D2', '3/3', '10.0.2.3', '65002:103', 100, '2.3.3.3', 24, None, None), + 'R4@D2:3/1' : ('R4@D2', '3/1', '10.0.2.4', '65002:104', 100, '2.4.3.1', 24, None, None), + 'R4@D2:3/2' : ('R4@D2', '3/2', '10.0.2.4', '65002:104', 100, '2.4.3.2', 24, None, None), + 'R4@D2:3/3' : ('R4@D2', '3/3', '10.0.2.4', '65002:104', 100, '2.4.3.3', 24, None, None), + + # ECOC'22 + 'DC1-GW:CS1-GW1': ('CS1-GW1', '10/1', '5.5.1.1', None, 0, None, None, '5.5.2.1', 111), + 'DC1-GW:CS1-GW2': ('CS1-GW2', '10/1', '5.5.1.2', None, 0, None, None, '5.5.2.2', 222), + 'DC2-GW:CS2-GW1': ('CS2-GW1', '10/1', '5.5.2.1', None, 0, None, None, '5.5.1.1', 111), + 'DC2-GW:CS2-GW2': ('CS2-GW2', '10/1', '5.5.2.2', None, 0, None, None, '5.5.1.2', 222), + + # NetworkX'22 + 'R1:1/2': ('R1', '1/2', '5.1.1.2', None, 0, None, None, None, None), + 'R1:1/3': ('R1', '1/3', '5.1.1.3', None, 0, None, None, None, None), + 'R2:1/2': ('R2', '1/2', '5.2.1.2', None, 0, None, None, None, None), + 'R2:1/3': ('R2', '1/3', '5.2.1.3', None, 0, None, None, None, None), + 'R3:1/2': ('R3', '1/2', '5.3.1.2', None, 0, None, None, None, None), + 'R3:1/3': ('R3', '1/3', '5.3.1.3', None, 0, None, None, None, None), + 'R4:1/2': ('R4', '1/2', '5.4.1.2', None, 0, None, None, None, None), + 'R4:1/3': ('R4', '1/3', '5.4.1.3', None, 0, None, None, None, None), +} diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Topology_Service.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Topology_Service.py new file mode 100644 index 0000000000000000000000000000000000000000..f7ed15fdf39975449fff29ca08deb9667eabffe4 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Topology_Service.py @@ -0,0 +1,76 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +import logging +from flask import request +from flask.json import jsonify +from flask_restful import Resource +from common.proto.context_pb2 import SliceStatusEnum +from common.tools.context_queries.Slice import get_slice +from context.client.ContextClient import ContextClient +from slice.client.SliceClient import SliceClient +from .tools.Authentication import HTTP_AUTH +from .tools.HttpStatusCodes import HTTP_GATEWAYTIMEOUT, HTTP_NOCONTENT, HTTP_OK, HTTP_SERVERERROR + +LOGGER = logging.getLogger(__name__) + +class Topology_Service(Resource): + @HTTP_AUTH.login_required + def get(self, vpn_id : str): + LOGGER.debug('VPN_Id: {:s}'.format(str(vpn_id))) + LOGGER.debug('Request: {:s}'.format(str(request))) + + try: + context_client = ContextClient() + + target = get_slice(context_client, vpn_id, rw_copy=True) + if target is None: + raise Exception('VPN({:s}) not found in database'.format(str(vpn_id))) + + if target.slice_id.slice_uuid.uuid != vpn_id: # pylint: disable=no-member + raise Exception('Slice retrieval failed. Wrong Slice Id was returned') + + slice_ready_status = SliceStatusEnum.SLICESTATUS_ACTIVE + slice_status = target.slice_status.slice_status # pylint: disable=no-member + response = jsonify({}) + response.status_code = HTTP_OK if slice_status == slice_ready_status else HTTP_GATEWAYTIMEOUT + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Something went wrong Retrieving VPN({:s})'.format(str(vpn_id))) + response = jsonify({'error': str(e)}) + response.status_code = HTTP_SERVERERROR + return response + + @HTTP_AUTH.login_required + def delete(self, vpn_id : str): + LOGGER.debug('VPN_Id: {:s}'.format(str(vpn_id))) + LOGGER.debug('Request: {:s}'.format(str(request))) + + try: + context_client = ContextClient() + + target = get_slice(context_client, vpn_id) + if target is None: + LOGGER.warning('VPN({:s}) not found in database. Nothing done.'.format(str(vpn_id))) + else: + if target.slice_id.slice_uuid.uuid != vpn_id: # pylint: disable=no-member + raise Exception('Slice retrieval failed. Wrong Slice Id was returned') + slice_client = SliceClient() + slice_client.DeleteSlice(target.slice_id) + response = jsonify({}) + response.status_code = HTTP_NOCONTENT + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Something went wrong Deleting VPN({:s})'.format(str(vpn_id))) + response = jsonify({'error': str(e)}) + response.status_code = HTTP_SERVERERROR + return response diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Topology_Services.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Topology_Services.py new file mode 100644 index 0000000000000000000000000000000000000000..505e80f868d58810944dd65d9207cdde68b72a68 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Topology_Services.py @@ -0,0 +1,79 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +import logging +from typing import Dict, List +from common.tools.grpc.Tools import grpc_message_list_to_json_string, grpc_message_to_json_string +from compute.service.rest_server.nbi_plugins.debug_api.Tools import format_grpc_to_json, grpc_topology_id +from context.client.ContextClient import ContextClient +from flask import request +from flask.json import jsonify +from flask_restful import Resource +from werkzeug.exceptions import UnsupportedMediaType +from common.Constants import DEFAULT_CONTEXT_NAME +from common.proto.context_pb2 import Empty, SliceStatusEnum, Slice +from slice.client.SliceClient import SliceClient +from .schemas.vpn_service import SCHEMA_VPN_SERVICE +from .tools.Authentication import HTTP_AUTH +from .tools.HttpStatusCodes import HTTP_CREATED, HTTP_SERVERERROR +from .tools.Validator import validate_message + +LOGGER = logging.getLogger(__name__) + +class Topology_Services(Resource): + @HTTP_AUTH.login_required + def get(self): + + context_client=ContextClient() + context_client.connect() + ctx_ids=context_client.ListContextIds(Empty()) + topos=[] + for ctx in ctx_ids.context_ids: + topo_ids=context_client.ListTopologyIds(ctx) + context_uuid=ctx + for ids in topo_ids.topology_ids: + topos.append(context_client.GetTopology(ids)) + topology_uuid=ids + LOGGER.info("nbi topo %s",topos) + LOGGER.info("nbi topo JSON %s",grpc_message_list_to_json_string(topos)) + response=format_grpc_to_json(context_client.GetTopologyDetails(topology_uuid)) + context_client.close() + return response + + @HTTP_AUTH.login_required + def post(self): + if not request.is_json: raise UnsupportedMediaType('JSON payload is required') + request_data : Dict = request.json + LOGGER.debug('Request: {:s}'.format(str(request_data))) + validate_message(SCHEMA_VPN_SERVICE, request_data) + + vpn_services : List[Dict] = request_data['ietf-l2vpn-svc:vpn-service'] + for vpn_service in vpn_services: + try: + # pylint: disable=no-member + slice_request = Slice() + slice_request.slice_id.context_id.context_uuid.uuid = DEFAULT_CONTEXT_NAME + slice_request.slice_id.slice_uuid.uuid = vpn_service['vpn-id'] + slice_request.slice_status.slice_status = SliceStatusEnum.SLICESTATUS_PLANNED + + slice_client = SliceClient() + slice_client.CreateSlice(slice_request) + + response = jsonify({}) + response.status_code = HTTP_CREATED + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Something went wrong Creating Service {:s}'.format(str(request))) + response = jsonify({'error': str(e)}) + response.status_code = HTTP_SERVERERROR + return response diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Topology_SiteNetworkAccesses.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Topology_SiteNetworkAccesses.py new file mode 100644 index 0000000000000000000000000000000000000000..93bcb84701f7b06205a7e7dc1513250d0c62c0d1 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/Topology_SiteNetworkAccesses.py @@ -0,0 +1,157 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +import logging +from typing import Dict, Optional +from flask import request +from flask.json import jsonify +from flask.wrappers import Response +from flask_restful import Resource +from werkzeug.exceptions import UnsupportedMediaType +from common.proto.context_pb2 import Slice +from common.tools.context_queries.Slice import get_slice +from common.tools.grpc.ConfigRules import update_config_rule_custom +from common.tools.grpc.Constraints import ( + update_constraint_custom_dict, update_constraint_endpoint_location, update_constraint_endpoint_priority, + update_constraint_sla_availability) +from common.tools.grpc.EndPointIds import update_endpoint_ids +from common.tools.grpc.Tools import grpc_message_to_json_string +from context.client.ContextClient import ContextClient +from slice.client.SliceClient import SliceClient +from .schemas.site_network_access import SCHEMA_SITE_NETWORK_ACCESS +from .tools.Authentication import HTTP_AUTH +from .tools.HttpStatusCodes import HTTP_NOCONTENT, HTTP_SERVERERROR +from .tools.Validator import validate_message +from .Constants import ( + BEARER_MAPPINGS, DEFAULT_ADDRESS_FAMILIES, DEFAULT_BGP_AS, DEFAULT_BGP_ROUTE_TARGET, DEFAULT_MTU) + +LOGGER = logging.getLogger(__name__) + +def process_site_network_access(context_client : ContextClient, site_id : str, site_network_access : Dict) -> Slice: + vpn_id = site_network_access['vpn-attachment']['vpn-id'] + encapsulation_type = site_network_access['connection']['encapsulation-type'] + cvlan_id = site_network_access['connection']['tagged-interface'][encapsulation_type]['cvlan-id'] + + bearer_reference = site_network_access['bearer']['bearer-reference'] + + access_priority : Optional[int] = site_network_access.get('availability', {}).get('access-priority') + single_active : bool = len(site_network_access.get('availability', {}).get('single-active', [])) > 0 + all_active : bool = len(site_network_access.get('availability', {}).get('all-active', [])) > 0 + + diversity_constraints = site_network_access.get('access-diversity', {}).get('constraints', {}).get('constraint', []) + raise_if_differs = True + diversity_constraints = { + constraint['constraint-type']:([ + target[0] + for target in constraint['target'].items() + if len(target[1]) == 1 + ][0], raise_if_differs) + for constraint in diversity_constraints + } + + mapping = BEARER_MAPPINGS.get(bearer_reference) + if mapping is None: + msg = 'Specified Bearer({:s}) is not configured.' + raise Exception(msg.format(str(bearer_reference))) + ( + device_uuid, endpoint_uuid, router_id, route_dist, sub_if_index, + address_ip, address_prefix, remote_router, circuit_id + ) = mapping + + target = get_slice(context_client, vpn_id, rw_copy=True) + if target is None: raise Exception('VPN({:s}) not found in database'.format(str(vpn_id))) + + endpoint_ids = target.slice_endpoint_ids # pylint: disable=no-member + config_rules = target.slice_config.config_rules # pylint: disable=no-member + constraints = target.slice_constraints # pylint: disable=no-member + + endpoint_id = update_endpoint_ids(endpoint_ids, device_uuid, endpoint_uuid) + + service_settings_key = '/settings' + update_config_rule_custom(config_rules, service_settings_key, { + 'mtu' : (DEFAULT_MTU, True), + 'address_families': (DEFAULT_ADDRESS_FAMILIES, True), + 'bgp_as' : (DEFAULT_BGP_AS, True), + 'bgp_route_target': (DEFAULT_BGP_ROUTE_TARGET, True), + }) + + endpoint_settings_key = '/device[{:s}]/endpoint[{:s}]/settings'.format(device_uuid, endpoint_uuid) + field_updates = {} + if router_id is not None: field_updates['router_id' ] = (router_id, True) + if route_dist is not None: field_updates['route_distinguisher'] = (route_dist, True) + if sub_if_index is not None: field_updates['sub_interface_index'] = (sub_if_index, True) + if cvlan_id is not None: field_updates['vlan_id' ] = (cvlan_id, True) + if address_ip is not None: field_updates['address_ip' ] = (address_ip, True) + if address_prefix is not None: field_updates['address_prefix' ] = (address_prefix, True) + if remote_router is not None: field_updates['remote_router' ] = (remote_router, True) + if circuit_id is not None: field_updates['circuit_id' ] = (circuit_id, True) + update_config_rule_custom(config_rules, endpoint_settings_key, field_updates) + + if len(diversity_constraints) > 0: + update_constraint_custom_dict(constraints, 'diversity', diversity_constraints) + + update_constraint_endpoint_location(constraints, endpoint_id, region=site_id) + if access_priority is not None: update_constraint_endpoint_priority(constraints, endpoint_id, access_priority) + if single_active or all_active: + # assume 1 disjoint path per endpoint/location included in service/slice + location_endpoints = {} + for constraint in constraints: + if constraint.WhichOneof('constraint') != 'endpoint_location': continue + str_endpoint_id = grpc_message_to_json_string(constraint.endpoint_location.endpoint_id) + str_location_id = grpc_message_to_json_string(constraint.endpoint_location.location) + location_endpoints.setdefault(str_location_id, set()).add(str_endpoint_id) + num_endpoints_per_location = {len(endpoints) for endpoints in location_endpoints.values()} + num_disjoint_paths = min(num_endpoints_per_location) + update_constraint_sla_availability(constraints, num_disjoint_paths, all_active, 0.0) + + return target + +def process_list_site_network_access( + context_client : ContextClient, slice_client : SliceClient, site_id : str, request_data : Dict + ) -> Response: + + LOGGER.debug('Request: {:s}'.format(str(request_data))) + validate_message(SCHEMA_SITE_NETWORK_ACCESS, request_data) + + errors = [] + for site_network_access in request_data['ietf-l2vpn-svc:site-network-access']: + sna_request = process_site_network_access(context_client, site_id, site_network_access) + LOGGER.debug('sna_request = {:s}'.format(grpc_message_to_json_string(sna_request))) + try: + slice_client.UpdateSlice(sna_request) + except Exception as e: # pylint: disable=broad-except + msg = 'Something went wrong Updating VPN {:s}' + LOGGER.exception(msg.format(grpc_message_to_json_string(sna_request))) + errors.append({'error': str(e)}) + + response = jsonify(errors) + response.status_code = HTTP_NOCONTENT if len(errors) == 0 else HTTP_SERVERERROR + return response + +class Topology_SiteNetworkAccesses(Resource): + @HTTP_AUTH.login_required + def post(self, site_id : str): + if not request.is_json: raise UnsupportedMediaType('JSON payload is required') + LOGGER.debug('Site_Id: {:s}'.format(str(site_id))) + context_client = ContextClient() + slice_client = SliceClient() + return process_list_site_network_access(context_client, slice_client, site_id, request.json) + + @HTTP_AUTH.login_required + def put(self, site_id : str): + if not request.is_json: raise UnsupportedMediaType('JSON payload is required') + LOGGER.debug('Site_Id: {:s}'.format(str(site_id))) + context_client = ContextClient() + slice_client = SliceClient() + return process_list_site_network_access(context_client, slice_client, site_id, request.json) diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/__init__.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..36ade4b622199f5b2e68795b6943c4c18520014b --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/__init__.py @@ -0,0 +1,36 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +# RFC 8466 - L2VPN Service Model (L2SM) +# Ref: https://datatracker.ietf.org/doc/html/rfc8466 + +from flask_restful import Resource +from compute.service.rest_server.RestServer import RestServer +from .Topology_Services import Topology_Services +from .Topology_Service import Topology_Service +from .Topology_SiteNetworkAccesses import Topology_SiteNetworkAccesses + +URL_PREFIX = '/data/topology' + +def _add_resource(rest_server : RestServer, resource : Resource, *urls, **kwargs): + urls = [(URL_PREFIX + url) for url in urls] + rest_server.add_resource(resource, *urls, **kwargs) + +def register_ietf_topology(rest_server : RestServer): + _add_resource(rest_server, Topology_Services, + '/bgpls') + _add_resource(rest_server, Topology_Service, + '/vpn-services/vpn-service=<vpn_id>', '/vpn-services/vpn-service=<vpn_id>/') + _add_resource(rest_server, Topology_SiteNetworkAccesses, + '/sites/site=<site_id>/site-network-accesses', '/sites/site=<site_id>/site-network-accesses/') diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/schemas/Common.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/schemas/Common.py new file mode 100644 index 0000000000000000000000000000000000000000..d708953ec952c68150a22023c5ef304683d4571a --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/schemas/Common.py @@ -0,0 +1,16 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +# String pattern for UUIDs such as '3fd942ee-2dc3-41d1-aeec-65aa85d117b2' +REGEX_UUID = r'[a-fA-F0-9]{8}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{4}\-[a-fA-F0-9]{12}' diff --git a/src/device/service/drivers/bgpls/__init__.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/schemas/__init__.py similarity index 100% rename from src/device/service/drivers/bgpls/__init__.py rename to src/compute/service/rest_server/nbi_plugins/ietf_topolgy/schemas/__init__.py diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/schemas/site_network_access.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/schemas/site_network_access.py new file mode 100644 index 0000000000000000000000000000000000000000..ec39e30ca27b93e76c7959de0f1d7d7a9ceba8c8 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/schemas/site_network_access.py @@ -0,0 +1,80 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +# Example request: +# request = {'ietf-l2vpn-svc:site-network-access': [{ +# 'network-access-id': '3fd942ee-2dc3-41d1-aeec-65aa85d117b2', +# 'vpn-attachment': {'vpn-id': '954b1b53-4a8c-406d-9eff-750ec2c9a258', +# 'site-role': 'any-to-any-role'}, +# 'connection': {'encapsulation-type': 'dot1q-vlan-tagged', 'tagged-interface': { +# 'dot1q-vlan-tagged': {'cvlan-id': 1234}}}, +# 'bearer': {'bearer-reference': '1a'} +# }]} + +from .Common import REGEX_UUID + +SCHEMA_SITE_NETWORK_ACCESS = { + '$schema': 'https://json-schema.org/draft/2020-12/schema', + 'type': 'object', + 'required': ['ietf-l2vpn-svc:site-network-access'], + 'properties': { + 'ietf-l2vpn-svc:site-network-access': { + 'type': 'array', + 'minItems': 1, + 'maxItems': 1, # by now we do not support multiple site-network-access in the same message + 'items': { + 'type': 'object', + 'required': ['network-access-id', 'vpn-attachment', 'connection', 'bearer'], + 'properties': { + 'network-access-id': {'type': 'string', 'pattern': REGEX_UUID}, + 'vpn-attachment': { + 'type': 'object', + 'required': ['vpn-id', 'site-role'], + 'properties': { + 'vpn-id': {'type': 'string', 'pattern': REGEX_UUID}, + 'site-role': {'type': 'string', 'minLength': 1}, + }, + }, + 'connection': { + 'type': 'object', + 'required': ['encapsulation-type', 'tagged-interface'], + 'properties': { + 'encapsulation-type': {'enum': ['dot1q-vlan-tagged']}, + 'tagged-interface': { + 'type': 'object', + 'required': ['dot1q-vlan-tagged'], + 'properties': { + 'dot1q-vlan-tagged': { + 'type': 'object', + 'required': ['cvlan-id'], + 'properties': { + 'cvlan-id': {'type': 'integer', 'minimum': 1, 'maximum': 4094}, + }, + }, + }, + }, + }, + }, + 'bearer': { + 'type': 'object', + 'required': ['bearer-reference'], + 'properties': { + 'bearer-reference': {'type': 'string', 'minLength': 1}, + }, + }, + }, + }, + }, + }, +} diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/schemas/vpn_service.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/schemas/vpn_service.py new file mode 100644 index 0000000000000000000000000000000000000000..bccc2319bdd0692e3f487c8f879a0fd2dd521b4b --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/schemas/vpn_service.py @@ -0,0 +1,46 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +# Example request: +# request = {'ietf-l2vpn-svc:vpn-service': [{ +# 'vpn-id': 'c6270231-f1de-4687-b2ed-7b58f9105775', +# 'vpn-svc-type': 'vpws', +# 'svc-topo': 'any-to-any', +# 'customer-name': 'osm' +# }]} + +from .Common import REGEX_UUID + +SCHEMA_VPN_SERVICE = { + '$schema': 'https://json-schema.org/draft/2020-12/schema', + 'type': 'object', + 'required': ['ietf-l2vpn-svc:vpn-service'], + 'properties': { + 'ietf-l2vpn-svc:vpn-service': { + 'type': 'array', + 'minItems': 1, + 'maxItems': 1, # by now we do not support multiple vpn-service in the same message + 'items': { + 'type': 'object', + 'required': ['vpn-id', 'vpn-svc-type', 'svc-topo', 'customer-name'], + 'properties': { + 'vpn-id': {'type': 'string', 'pattern': REGEX_UUID}, + 'vpn-svc-type': {'enum': ['vpws', 'vpls']}, + 'svc-topo': {'enum': ['any-to-any']}, + 'customer-name': {'const': 'osm'}, + }, + } + } + }, +} diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/Authentication.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/Authentication.py new file mode 100644 index 0000000000000000000000000000000000000000..4577f4f0ffba31071c1045f22c9a6eb37e6eed39 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/Authentication.py @@ -0,0 +1,25 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +from flask_httpauth import HTTPBasicAuth +from werkzeug.security import check_password_hash +from compute.Config import RESTAPI_USERS + +HTTP_AUTH = HTTPBasicAuth() + +@HTTP_AUTH.verify_password +def verify_password(username, password): + if username not in RESTAPI_USERS: return None + if not check_password_hash(RESTAPI_USERS[username], password): return None + return username diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/HttpStatusCodes.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/HttpStatusCodes.py new file mode 100644 index 0000000000000000000000000000000000000000..fe22d9ee80970d2bfd3d305f40511f433242df0a --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/HttpStatusCodes.py @@ -0,0 +1,20 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +HTTP_OK = 200 +HTTP_CREATED = 201 +HTTP_NOCONTENT = 204 +HTTP_BADREQUEST = 400 +HTTP_SERVERERROR = 500 +HTTP_GATEWAYTIMEOUT = 504 \ No newline at end of file diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/Validator.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/Validator.py new file mode 100644 index 0000000000000000000000000000000000000000..942609ed227b24b2e482654c54d2bb3647700e1c --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/Validator.py @@ -0,0 +1,35 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + +from typing import List +from flask.json import jsonify +from jsonschema import _utils +from jsonschema.validators import validator_for +from jsonschema.protocols import Validator +from jsonschema.exceptions import ValidationError +from werkzeug.exceptions import BadRequest +from .HttpStatusCodes import HTTP_BADREQUEST + +def validate_message(schema, message): + validator_class = validator_for(schema) + validator : Validator = validator_class(schema) + errors : List[ValidationError] = sorted(validator.iter_errors(message), key=str) + if len(errors) == 0: return + response = jsonify([ + {'message': str(error.message), 'schema': str(error.schema), 'validator': str(error.validator), + 'where': str(_utils.format_as_index(container='message', indices=error.relative_path))} + for error in errors + ]) + response.status_code = HTTP_BADREQUEST + raise BadRequest(response=response) diff --git a/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/__init__.py b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..1549d9811aa5d1c193a44ad45d0d7773236c0612 --- /dev/null +++ b/src/compute/service/rest_server/nbi_plugins/ietf_topolgy/tools/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# 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. + diff --git a/src/device/service/DeviceServiceServicerImpl.py b/src/device/service/DeviceServiceServicerImpl.py index 76acd203e4a63e0e7e8ad925a8af1a9edd0d56df..9a84d0df2c88293bbbdbb532435d75a91fee0e93 100644 --- a/src/device/service/DeviceServiceServicerImpl.py +++ b/src/device/service/DeviceServiceServicerImpl.py @@ -125,12 +125,6 @@ class DeviceServiceServicerImpl(DeviceServiceServicer): device_id = context_client.SetDevice(device) device = context_client.GetDevice(device_id) - # Checkear aqui si son dos driver y si uno es bgpls Connect ****tid-bgp-speaker**** - if DeviceDriverEnum.DEVICEDRIVER_BGPLS in device.device_drivers: - LOGGER.info("Es BGPLS (ConfigureDevice)") - # config_BGPLSDriver.driverSettings(context_client) - # config_BGPLSDriver.driverConnect(self.driver_instance_cache,device,device_uuid) - if request.device_operational_status != DeviceOperationalStatusEnum.DEVICEOPERATIONALSTATUS_UNDEFINED: device.device_operational_status = request.device_operational_status diff --git a/src/device/service/drivers/__init__.py b/src/device/service/drivers/__init__.py index 17fb8c8f12e7988a89bec8aede92d38b3c31944a..69c82f9f22b2efb8a6d26ba7554bb482c7a70232 100644 --- a/src/device/service/drivers/__init__.py +++ b/src/device/service/drivers/__init__.py @@ -36,8 +36,7 @@ DRIVERS.append( DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER, DeviceTypeEnum.EMULATED_P4_SWITCH, DeviceTypeEnum.EMULATED_PACKET_ROUTER, - DeviceTypeEnum.EMULATED_PACKET_SWITCH, - DeviceTypeEnum.EMULATED_BGPLS_ASNUMBER, + DeviceTypeEnum.EMULATED_PACKET_SWITCH #DeviceTypeEnum.DATACENTER, #DeviceTypeEnum.MICROWAVE_RADIO_SYSTEM, @@ -127,14 +126,4 @@ if LOAD_ALL_DEVICE_DRIVERS: FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.XR_CONSTELLATION, FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_XR, } - ])) -if LOAD_ALL_DEVICE_DRIVERS: - from .bgpls.BGPLSDriver import BGPLSDriver # pylint: disable=wrong-import-position - DRIVERS.append( - (BGPLSDriver, [ - { - # Values :TODO ¿ - FilterFieldEnum.DEVICE_TYPE: DeviceTypeEnum.BGPLS_ASNUMBER, - FilterFieldEnum.DRIVER : DeviceDriverEnum.DEVICEDRIVER_BGPLS, - } ])) \ No newline at end of file diff --git a/src/device/service/drivers/bgpls/BGPLSDriver.py b/src/device/service/drivers/bgpls/BGPLSDriver.py deleted file mode 100644 index be7b4656e4f2e274c182b0ed9686e4127e81ae97..0000000000000000000000000000000000000000 --- a/src/device/service/drivers/bgpls/BGPLSDriver.py +++ /dev/null @@ -1,233 +0,0 @@ -# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) -# -# 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. - -import json, logging,threading, queue,time,signal -from datetime import datetime, timedelta -from typing import Any, Iterator, List, Optional, Tuple, Union -# from apscheduler.executors.pool import ThreadPoolExecutor -# from apscheduler.job import Job -# from apscheduler.jobstores.memory import MemoryJobStore -# from apscheduler.schedulers.background import BackgroundScheduler -# from common.method_wrappers.Decorator import MetricTypeEnum, MetricsPool, metered_subclass_method, INF -# from common.type_checkers.Checkers import chk_float, chk_length, chk_string, chk_type - - -import logging,anytree, json, pytz, queue, re, threading -import grpc -from .Tools import grpcComms,Link,Node,UpdateInfo -from .protos import grpcService_pb2_grpc -from .protos import grpcService_pb2 - - -from concurrent import futures -from lxml import etree -import os -import subprocess -from multiprocessing import Pool - -from device.service.driver_api._Driver import _Driver -from apscheduler.executors.pool import ThreadPoolExecutor -from apscheduler.job import Job -from apscheduler.jobstores.memory import MemoryJobStore -from apscheduler.schedulers.background import BackgroundScheduler -from common.method_wrappers.Decorator import MetricTypeEnum, MetricsPool, metered_subclass_method, INF -from common.type_checkers.Checkers import chk_float, chk_length, chk_string, chk_type -from device.service.driver_api._Driver import _Driver -from device.service.driver_api.AnyTreeTools import TreeNode, dump_subtree, get_subnode, set_subnode_value - -SERVER_ADDRESS = 'localhost:2021' -SERVER_ID = 1 -_ONE_DAY_IN_SECONDS = 60 * 60 * 24 - -XML_FILE="BGP4Parameters_3.xml" -XML_CONFIG_FILE="TMConfiguration_guillermo.xml" - -LOGGER = logging.getLogger(__name__) - - -# Esto tiene que heredar de _Driver¿ -class BGPLSDriver(_Driver): - """ - This class gets the current topology from a bgps speaker module in java - and updates the posible new devices to add in the context topology. - Needs the address, port and as_number from the device giving de information via bgpls - to the java module. - """ - def __init__(self, address : str, port : int, asNumber : int,**settings) -> None: # pylint: disable=super-init-not-called - self.__lock = threading.Lock() - self.__started = threading.Event() - self.__terminate = threading.Event() - self.__out_samples = queue.Queue() - # self.__scheduler = BackgroundScheduler(daemon=True) # scheduler used to emulate sampling events - # self.__scheduler.configure( - # jobstores = {'default': MemoryJobStore()}, - # executors = {'default': ThreadPoolExecutor(max_workers=1)}, - # job_defaults = {'coalesce': False, 'max_instances': 3}, - # timezone=pytz.utc) - # TODO: atributos necesarios - # self.__server=asyncio.run(grpc.aio.server()) - - self.__address=address - self.__port=port - self.__asNumber=asNumber - self.__configFile=XML_CONFIG_FILE - self.__process=0 - self.__comms=grpcComms - - - - async def Connect(self) -> bool: - # TODO: Metodos necesarios para conectarte al speaker - LOGGER.info("CONNECT BGPLSDriver") - # If started, assume it is already connected - if self.__started.is_set(): return True - # self.__scheduler.start() - self.__started.set() #notifyAll -->event.is_set() - # 10 workers ? - with self.__lock: - self.__server = grpc.server(futures.ThreadPoolExecutor(max_workers=10)) - grpcService_pb2_grpc.add_updateServiceServicer_to_server(self, self.__server) - self.__server.add_insecure_port(SERVER_ADDRESS) - # server.add_secure_port(SERVER_ADDRESS) - LOGGER.info("Starting server on %s", SERVER_ADDRESS) - await self.__server.start() - try: - while True: - time.sleep(_ONE_DAY_IN_SECONDS) - except KeyboardInterrupt: - LOGGER.info("DISCONNECT") - self.Disconnect() - return True - - - def Disconnect(self) -> bool: - # TODO: channel grpc close - self.__terminate.set() - # If not started, assume it is already disconnected - if not self.__started.is_set(): return True - - LOGGER.info("Keyboard interrupt, stop server") - self.__server.stop(0) - # Disconnect triggers deactivation of sampling events - # self.__scheduler.shutdown() - exit(0) - return True - - # @metered_subclass_method(METRICS_POOL) - def update(self,request, context) -> bool: - """ - Processes the messages recived by de grpc server - """ - with self.__lock: - #TODO: Get update - LOGGER.info("(server)SimpleMethod called by client the message: %s" % (request)) - response = grpcService_pb2.updateResponse(greeting="OK") - link=None - node=None # inicializar en otro lado¿ - for linkIn in request.link: - link=Link(linkIn.remoteID,linkIn.localID,linkIn.remoteIP,linkIn.localIP) - for nodeIn in request.node: - node=Node(nodeIn.nodeName,nodeIn.nodeID) - up=UpdateInfo(link,node) - return response - - def GetState(self, blocking=False, terminate : Optional[threading.Event] = None) -> Iterator[Tuple[str, Any]]: - while True: - if self.__terminate.is_set(): break - if terminate is not None and terminate.is_set(): break - try: - sample = self.__out_samples.get(block=blocking, timeout=0.1) - except queue.Empty: - if blocking: continue - return - if sample is None: continue - yield sample - - def setPeer(self) -> bool: - """ - Sets XML existing config file with peer address and port. TODO: as_number - """ - - XMLParser = etree.XMLParser(remove_blank_text=False) - tree = etree.parse(XML_FILE, parser=XMLParser) - root = tree.getroot() - peerAddress = root.find(".//peer") - peerAddress.text=self.__address - peerPort = root.find(".//peerPort") - peerPort.text=str(self.__port) - tree.write(XML_FILE) #with ... as .. - return True - - def execBGPSpeaker(self) -> bool: - """ - Executes java BGPLS speaker - """ - # CHECKEAR muchas cosas - LOGGER.debug("Before exec") - with subprocess.Popen(['java -jar bgp_ls.jar '+ XML_CONFIG_FILE], - shell=True,start_new_session=True) as self.__process: - # # logging.debug(self.__process.stdout.read()) - return True - - def endBGPSpeaker(self) -> bool: - """ - Starts timer to kill java BGPLS Speaker - """ - LOGGER.debug("end to sleep") - time.sleep(15) - LOGGER.debug("PID: %d",self.__process.pid) - LOGGER.debug("Group PID: %d",os.getpgid(self.__process.pid)) - os.killpg(os.getpgid(self.__process.pid), signal.SIGKILL) - self.__process.kill() - return True - - def runThreads(self): - # with futures.ThreadPoolExecutor(max_workers=4) as executor: - # executor.submit(bgpDriver.ConnectNotWait) - # executor.submit(bgpDriver.execBGPSpeaker) - # executor.submit(bgpDriver.endBGPSpeaker) - t1=threading.Thread(name="t1",target=bgpDriver.Connect) - t2=threading.Thread(name="t2",target=bgpDriver.execBGPSpeaker) - t3=threading.Thread(name="t3",target=bgpDriver.endBGPSpeaker) - t1.start() - t2.start() - t3.start() - return True - - def getCurrentTopo(): - # import common.tools.Device - # get_devices_in_topology( - # context_client : ContextClient, context_id : ContextId, topology_uuid : str) - - return True - -def quit(signal, _frame): - LOGGER.info("Interrupted by %d, shutting down" % signal) - bgpDriver.Disconnect() - -if __name__ == "__main__": - - logging.basicConfig(level=logging.DEBUG) - for sig in ('TERM', 'HUP', 'INT'): - signal.signal(getattr(signal, 'SIG'+sig), quit) - # TODO: add port connection speaker - bgpDriver=BGPLSDriver("10.95.90.76",179,65006) - bgpDriver.setPeer() - bgpDriver.runThreads() - - - - - - diff --git a/src/device/service/drivers/bgpls/Tools.py b/src/device/service/drivers/bgpls/Tools.py deleted file mode 100644 index 61f52ad51c8b6dd0639a63dd2b297e508f642b5e..0000000000000000000000000000000000000000 --- a/src/device/service/drivers/bgpls/Tools.py +++ /dev/null @@ -1,111 +0,0 @@ - -from .protos import grpcService_pb2_grpc -from .protos import grpcService_pb2 - -import logging -LOGGER = logging.getLogger(__name__) -import os - -class UpdateInfo: - def __init__(self,link,node): - self.link=link - self.node=node -class Link: - def __init__(self,rID,lID,rIP,lIP): - self.rID=rID - self.lID=lID - self.rIP=rIP - self.lIP=lIP -class Node: - def __init__(self,name,nid): - self.Name=name - self.ID=nid - -class grpcComms(grpcService_pb2_grpc.updateServiceServicer): - - def update(self,request, context) -> bool: - """ - Processes the messages recived by de grpc server - """ - with self.__lock: - #TODO: Get update - print("(server)SimpleMethod called by client the message: %s" % (request)) - response = grpcService_pb2.updateResponse(greeting="OK") - link=None - node=None # inicializar en otro lado¿ - for linkIn in request.link: - link=Link(linkIn.remoteID,linkIn.localID,linkIn.remoteIP,linkIn.localIP) - for nodeIn in request.node: - node=Node(nodeIn.nodeName,nodeIn.nodeID) - up=UpdateInfo(link,node) - return response - -from common.proto.context_pb2 import Device ,DeviceDriverEnum,ContextId,Empty -from device.service.driver_api.DriverInstanceCache import DriverInstanceCache -from device.service.driver_api.FilterFields import FilterFieldEnum, get_device_driver_filter_fields -from device.service.Tools import get_connect_rules -from . import BGPLSDriver as bgpls -from context.client.ContextClient import ContextClient -from common.Constants import DEFAULT_CONTEXT_NAME -from common.tools.object_factory.Context import json_context_id - -ADMIN_CONTEXT_ID = ContextId(**json_context_id(DEFAULT_CONTEXT_NAME)) - -class config_BGPLSDriver(): - - - def driverSettings( context_client : ContextClient): - - list_topo=[] - LOGGER.info("Context (driverSettings) **************%s",context_client) - # context_uuid = context_id['context_uuid']['uuid'] - # response = context.GetContext(ADMIN_CONTEXT_ID) - contexts : ContextList = context_client.ListContexts(Empty()) - for context_ in contexts.contexts: - context_uuid : str = context_.context_id.context_uuid.uuid - context_name : str = context_.name - topologies : TopologyList = context_client.ListTopologies(context_.context_id) - # topologies : TopologyList=context_client.ListTopologies(context_client) - for topology_ in topologies.topologies: - #topology_uuid : str = topology_.topology_id.topology_uuid.uuid - topology_name : str = topology_.name - context_topology_name = 'Context({:s}):Topology({:s})'.format(context_name, topology_name) - # Topos=context.GetTopology(list_topo.topology_id) - LOGGER.info("topo (driverSettings) %s",topology_) - details=context_client.GetTopologyDetails(topology_.topology_id) - LOGGER.info("details (driverSettings) %s",details) - devices=context_client.ListDevices(Empty()) - # LOGGER.info("devices (driverSettings) %s",devices) - for device_ in devices.devices: - LOGGER.info("device_ (driverSettings) %s",device_.name) - for config_rule_ in device_.device_config.config_rules: - if config_rule_.custom.resource_key == "_connect/address": - LOGGER.info("device_.resource_value-addr (driverSettings) %s", - config_rule_.custom.resource_value) - - # LOGGER.info("response getContext (driverSettings) %s",response) - # list_topo=context.ListTopologies(response.context_id) - # LOGGER.info("list_topo (driverSettings) %s",list_topo) - # Topos=context.GetTopology(list_topo.topology_id) - # LOGGER.info("topo (driverSettings) %s",Topos) - return - - def driverConnect(driver_instance_cache : DriverInstanceCache,device : Device, - device_uuid): - bgpls_instance=[driver for driver in device.device_drivers if DeviceDriverEnum.DEVICEDRIVER_BGPLS] - LOGGER.info(" (driverConnect) class bgpls: %s",bgpls_instance) - driver_filter_fields = get_device_driver_filter_fields(device) - connect_rules = get_connect_rules(device.device_config) - address = connect_rules.get('address', '127.0.0.1') - port = connect_rules.get('port', '0') - settings = connect_rules.get('settings', '{}') - driver : _Driver = driver_instance_cache.get( - device_uuid, filter_fields=driver_filter_fields, address=address, port=port, settings=settings) - LOGGER.info(" (driverConnect) driver: %s",driver) - bgpDriver=bgpls.BGPLSDriver("10.95.90.76",179,65006) - cwd = os.getcwd() - LOGGER.info("Current working directory:", cwd) - bgpDriver.setPeer() - bgpDriver.runThreads() - driver.Connect() - return diff --git a/src/device/service/drivers/bgpls/protos/grpcService.proto b/src/device/service/drivers/bgpls/protos/grpcService.proto deleted file mode 100644 index ef3642c30a301087189db9d1e1a2abba09ba209c..0000000000000000000000000000000000000000 --- a/src/device/service/drivers/bgpls/protos/grpcService.proto +++ /dev/null @@ -1,40 +0,0 @@ -syntax = "proto3"; -package src.main.proto; - -//el modulo java abre la comunicacion -//cliente(java) manda la info al servidor(python) -//el modulo en python responde con ok - -message updateRequest { - - repeated nodeInfo node = 1; - // repeated : se da la posibilidad de mandar 0 o varios - repeated linkInfo link = 2; - - // There are many more basics types, like Enum, Map - // See https://developers.google.com/protocol-buffers/docs/proto3 - // for more information. -} - -message nodeInfo{ - string nodeName=1; - string nodeID=2; -} - -message linkInfo{ - string remoteID=1; - string localID=2; - - string remoteIP=3; - string localIP=4; -} - -message updateResponse { - string greeting = 1; -} - -// Defining a Service, a Service can have multiple RPC operations -service updateService { - // MODIFY HERE: Update the return to streaming return. - rpc update(updateRequest) returns (updateResponse); -} \ No newline at end of file diff --git a/src/device/service/drivers/bgpls/protos/grpcService_pb2.py b/src/device/service/drivers/bgpls/protos/grpcService_pb2.py deleted file mode 100644 index 47f75875c38186d9e578a96b103a3697a1cfeadf..0000000000000000000000000000000000000000 --- a/src/device/service/drivers/bgpls/protos/grpcService_pb2.py +++ /dev/null @@ -1,33 +0,0 @@ -# -*- coding: utf-8 -*- -# Generated by the protocol buffer compiler. DO NOT EDIT! -# source: grpcService.proto -"""Generated protocol buffer code.""" -from google.protobuf.internal import builder as _builder -from google.protobuf import descriptor as _descriptor -from google.protobuf import descriptor_pool as _descriptor_pool -from google.protobuf import symbol_database as _symbol_database -# @@protoc_insertion_point(imports) - -_sym_db = _symbol_database.Default() - - - - -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11grpcService.proto\x12\x0esrc.main.proto\"_\n\rupdateRequest\x12&\n\x04node\x18\x01 \x03(\x0b\x32\x18.src.main.proto.nodeInfo\x12&\n\x04link\x18\x02 \x03(\x0b\x32\x18.src.main.proto.linkInfo\",\n\x08nodeInfo\x12\x10\n\x08nodeName\x18\x01 \x01(\t\x12\x0e\n\x06nodeID\x18\x02 \x01(\t\"P\n\x08linkInfo\x12\x10\n\x08remoteID\x18\x01 \x01(\t\x12\x0f\n\x07localID\x18\x02 \x01(\t\x12\x10\n\x08remoteIP\x18\x03 \x01(\t\x12\x0f\n\x07localIP\x18\x04 \x01(\t\"\"\n\x0eupdateResponse\x12\x10\n\x08greeting\x18\x01 \x01(\t2X\n\rupdateService\x12G\n\x06update\x12\x1d.src.main.proto.updateRequest\x1a\x1e.src.main.proto.updateResponseb\x06proto3') - -_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, globals()) -_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'grpcService_pb2', globals()) -if _descriptor._USE_C_DESCRIPTORS == False: - - DESCRIPTOR._options = None - _UPDATEREQUEST._serialized_start=37 - _UPDATEREQUEST._serialized_end=132 - _NODEINFO._serialized_start=134 - _NODEINFO._serialized_end=178 - _LINKINFO._serialized_start=180 - _LINKINFO._serialized_end=260 - _UPDATERESPONSE._serialized_start=262 - _UPDATERESPONSE._serialized_end=296 - _UPDATESERVICE._serialized_start=298 - _UPDATESERVICE._serialized_end=386 -# @@protoc_insertion_point(module_scope) diff --git a/src/device/service/drivers/bgpls/protos/grpcService_pb2_grpc.py b/src/device/service/drivers/bgpls/protos/grpcService_pb2_grpc.py deleted file mode 100644 index c8bbda558d60b1108bfcb1ff60fbe755bb2d75c3..0000000000000000000000000000000000000000 --- a/src/device/service/drivers/bgpls/protos/grpcService_pb2_grpc.py +++ /dev/null @@ -1,70 +0,0 @@ -# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! -"""Client and server classes corresponding to protobuf-defined services.""" -import grpc - -from . import grpcService_pb2 as grpcService__pb2 - - -class updateServiceStub(object): - """Defining a Service, a Service can have multiple RPC operations - """ - - def __init__(self, channel): - """Constructor. - - Args: - channel: A grpc.Channel. - """ - self.update = channel.unary_unary( - '/src.main.proto.updateService/update', - request_serializer=grpcService__pb2.updateRequest.SerializeToString, - response_deserializer=grpcService__pb2.updateResponse.FromString, - ) - - -class updateServiceServicer(object): - """Defining a Service, a Service can have multiple RPC operations - """ - - def update(self, request, context): - """MODIFY HERE: Update the return to streaming return. - """ - context.set_code(grpc.StatusCode.UNIMPLEMENTED) - context.set_details('Method not implemented!') - raise NotImplementedError('Method not implemented!') - - -def add_updateServiceServicer_to_server(servicer, server): - rpc_method_handlers = { - 'update': grpc.unary_unary_rpc_method_handler( - servicer.update, - request_deserializer=grpcService__pb2.updateRequest.FromString, - response_serializer=grpcService__pb2.updateResponse.SerializeToString, - ), - } - generic_handler = grpc.method_handlers_generic_handler( - 'src.main.proto.updateService', rpc_method_handlers) - server.add_generic_rpc_handlers((generic_handler,)) - - - # This class is part of an EXPERIMENTAL API. -class updateService(object): - """Defining a Service, a Service can have multiple RPC operations - """ - - @staticmethod - def update(request, - target, - options=(), - channel_credentials=None, - call_credentials=None, - insecure=False, - compression=None, - wait_for_ready=None, - timeout=None, - metadata=None): - return grpc.experimental.unary_unary(request, target, '/src.main.proto.updateService/update', - grpcService__pb2.updateRequest.SerializeToString, - grpcService__pb2.updateResponse.FromString, - options, channel_credentials, - insecure, call_credentials, compression, wait_for_ready, timeout, metadata) diff --git a/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py b/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py index b38907668fd2faec199b5e2821efb1b8919e55da..8f40fec452a5dd0b4d3eb3e7805746d200e447cc 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ConstantsMappings.py @@ -102,8 +102,6 @@ DEVICE_TYPE_TO_LAYER = { DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER.value : DeviceLayerEnum.OPTICAL_DEVICE, DeviceTypeEnum.OPTICAL_TRANSPONDER.value : DeviceLayerEnum.OPTICAL_DEVICE, - DeviceTypeEnum.BGPLS_ASNUMBER.value : DeviceLayerEnum.APPLICATION_CONTROLLER, #TODO: value - DeviceTypeEnum.EMULATED_BGPLS_ASNUMBER.value : DeviceLayerEnum.APPLICATION_CONTROLLER, #TODO: } DEVICE_LAYER_TO_SERVICE_TYPE = { diff --git a/src/webui/Dockerfile b/src/webui/Dockerfile index 7c718890fcf3f07b32f66eca2ecab41f2eb30fbb..795d5d7f7b5a7ba403599c467761f920959f264d 100644 --- a/src/webui/Dockerfile +++ b/src/webui/Dockerfile @@ -84,6 +84,8 @@ COPY --chown=webui:webui src/service/client/. service/client/ COPY --chown=webui:webui src/slice/__init__.py slice/__init__.py COPY --chown=webui:webui src/slice/client/. slice/client/ COPY --chown=webui:webui src/webui/. webui/ +COPY --chown=webui:webui src/bgpls_speaker/__init__.py bgpls_speaker/__init__.py +COPY --chown=webui:webui src/bgpls_speaker/client/. bgpls_speaker/client/ # Start the service ENTRYPOINT ["python", "-m", "webui.service"] diff --git a/src/webui/service/device/routes.py b/src/webui/service/device/routes.py index 32fc6babc8990bbb5dfd2868f60b11e35c369119..4c6a2a1189804be157eb96ba48a52bfd2d0c59cb 100644 --- a/src/webui/service/device/routes.py +++ b/src/webui/service/device/routes.py @@ -65,8 +65,10 @@ def add(): (DeviceOperationalStatusEnum.Value(key), key.replace('DEVICEOPERATIONALSTATUS_', ''))) # items for Device Type field + form.device_type.choices= [] for device_type in DeviceTypeEnum: - form.device_type.choices.append((device_type.value,device_type.value)) + if device_type: + form.device_type.choices.append((device_type.value,device_type.value)) if form.validate_on_submit(): device_obj = Device() @@ -120,8 +122,6 @@ def add(): device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_ONF_TR_352) if form.device_drivers_xr.data: device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_XR) - if form.device_drivers_bgpls.data: - device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_BGPLS) device_obj.device_drivers.extend(device_drivers) # pylint: disable=no-member try: diff --git a/src/webui/service/templates/topology/add.html b/src/webui/service/templates/topology/add.html new file mode 100644 index 0000000000000000000000000000000000000000..317926383e6c2a7b858e1c87938a2c671c1afa49 --- /dev/null +++ b/src/webui/service/templates/topology/add.html @@ -0,0 +1,162 @@ +<!-- + Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) + + 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. +--> + +{% extends 'base.html' %} + +{% block content %} +<h1>Add New Device</h1> +<h3>Node name: {{form.device_name}}</h3> +<br /> +<form id="add_device" method="POST"> + {{ form.hidden_tag() }} + <fieldset> + <div class="row mb-3"> + {{ form.device_id.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.device_id.errors %} + {{ form.device_id(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.device_id.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.device_id(class="form-control") }} + {% endif %} + </div> + </div> + <br /> + <div class="row mb-3"> + {{ form.device_type.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.device_type.errors %} + {{ form.device_type(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.device_type.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.device_type(class="form-select")}} + {% endif %} + </div> + </div> + <br /> + <div class="row mb-3"> + {{ form.operational_status.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.operational_status.errors %} + {{ form.operational_status(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.operational_status.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.operational_status(class="form-select") }} + {% endif %} + </div> + </div> + <br /> + <div class="row mb-3"> + <div class="col-sm-2 col-form-label">Drivers</div> + <div class="col-sm-10"> + {% if form.device_drivers_undefined.errors %} + {{ form.device_drivers_undefined(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.device_drivers_undefined.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.device_drivers_undefined }} {{ form.device_drivers_undefined.label(class="col-sm-3 + col-form-label") }} + {{ form.device_drivers_openconfig }} {{ form.device_drivers_openconfig.label(class="col-sm-3 + col-form-label") }} + {{ form.device_drivers_transport_api }} {{ form.device_drivers_transport_api.label(class="col-sm-3 + col-form-label") }} + <br />{{ form.device_drivers_p4 }} {{ form.device_drivers_p4.label(class="col-sm-3 col-form-label") }} + {{ form.device_drivers_ietf_network_topology }} {{ + form.device_drivers_ietf_network_topology.label(class="col-sm-3 + col-form-label") }} + {{ form.device_drivers_onf_tr_352 }} {{ form.device_drivers_onf_tr_352.label(class="col-sm-3 + col-form-label") }}<br /> + {{ form.device_drivers_xr }} {{ form.device_drivers_xr.label(class="col-sm-3 + col-form-label") }} + {% endif %} + </div> + </div> + <br /> + Configuration Rules <br /> + <div class="row mb-3"> + {{ form.device_config_address.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.device_config_address.errors %} + {{ form.device_config_address(class="form-control is-invalid", rows=5) }} + <div class="invalid-feedback"> + {% for error in form.device_config_address.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.device_config_address(class="form-control", rows=5) }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form.device_config_port.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.device_config_port.errors %} + {{ form.device_config_port(class="form-control is-invalid", rows=5) }} + <div class="invalid-feedback"> + {% for error in form.device_config_port.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.device_config_port(class="form-control", rows=5) }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form.device_config_settings.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.device_config_settings.errors %} + {{ form.device_config_settings(class="form-control is-invalid", rows=5) }} + <div class="invalid-feedback"> + {% for error in form.device_config_settings.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.device_config_settings(class="form-control", rows=5) }} + {% endif %} + </div> + </div> + <br /> + <div class="d-grid gap-2 d-md-flex justify-content-md-start"> + <button type="submit" class="btn btn-primary"> + <i class="bi bi-plus-circle-fill"></i> + {{ submit_text }} + </button> + <button type="button" class="btn btn-block btn-secondary" onclick="javascript: history.back()"> + <i class="bi bi-box-arrow-in-left"></i> + Cancel + </button> + </div> + </fieldset> +</form> +{% endblock %} \ No newline at end of file diff --git a/src/webui/service/templates/topology/addSpeaker.html b/src/webui/service/templates/topology/addSpeaker.html new file mode 100644 index 0000000000000000000000000000000000000000..26ec8c94e59b72d5cc153ac70617ece7ab48119e --- /dev/null +++ b/src/webui/service/templates/topology/addSpeaker.html @@ -0,0 +1,88 @@ +<!-- + Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) + + 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. + --> + +{% extends 'base.html' %} + +{% block content %} + <h1>Add Device </h1> + <!-- TEST --> + <h5>New bglps speaker</h5> + + <br /> + <div> + <form action='{{ url_for("topology.addSpeaker")}}' method="post"> + {{ form.hidden_tag() }} + <fieldset> + <div class="row mb-3"> + {{ form.speaker_address.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.speaker_address.errors %} + {{ form.speaker_address(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.speaker_address.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.speaker_address(class="form-control") }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form.speaker_port.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.speaker_port.errors %} + {{ form.speaker_port(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.speaker_port.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.speaker_port(class="form-control") }} + {% endif %} + </div> + </div> + <div class="row mb-3"> + {{ form.speaker_as.label(class="col-sm-2 col-form-label") }} + <div class="col-sm-10"> + {% if form.speaker_as.errors %} + {{ form.speaker_as(class="form-control is-invalid") }} + <div class="invalid-feedback"> + {% for error in form.speaker_as.errors %} + <span>{{ error }}</span> + {% endfor %} + </div> + {% else %} + {{ form.speaker_as(class="form-control") }} + {% endif %} + </div> + </div> + </fieldset> + <button type="submit" class="btn btn-primary"> + <i class="bi bi-plus-circle-fill"></i> + Add Speaker + </button> + </form> + </div> + + + <script src="https://d3js.org/d3.v4.min.js"></script> + <div id="topology"></div> + <script src="{{ url_for('js.topology_js') }}"></script> + +{% endblock %} + \ No newline at end of file diff --git a/src/webui/service/templates/topology/detail.html b/src/webui/service/templates/topology/detail.html deleted file mode 100644 index aad221c85e73b98869b90f59117228b77c02fa43..0000000000000000000000000000000000000000 --- a/src/webui/service/templates/topology/detail.html +++ /dev/null @@ -1,37 +0,0 @@ -<!-- - Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) - - 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. - --> - -{% extends 'base.html' %} - -{% block content %} - <h1>Device {{ device.name }} ({{ device.device_id.device_uuid.uuid }})</h1> - <!-- TEST --> - <h5>Type: {{ device.device_type }}</h5> - <div> - <label for="nombre">Device IP:</label> - <input type="text" id="ip" name="ip"> - <label for="nombre">Device User:</label> - <input type="text" id="user" name="user"> - <label for="nombre">Device Password:</label> - <input type="text" id="pass" name="pass"> - </div> - - <script src="https://d3js.org/d3.v4.min.js"></script> - <div id="topology"></div> - <script src="{{ url_for('js.topology_js') }}"></script> - -{% endblock %} - \ No newline at end of file diff --git a/src/webui/service/templates/topology/editSpeakers.html b/src/webui/service/templates/topology/editSpeakers.html new file mode 100644 index 0000000000000000000000000000000000000000..f98c453199d40865c7d8501344808cecf5a4e1a6 --- /dev/null +++ b/src/webui/service/templates/topology/editSpeakers.html @@ -0,0 +1,70 @@ +<!-- + Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) + + 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. +--> + + +{% extends 'base.html' %} +{% block content %} + <h1>Topology</h1> + + <div class="row"> + + <div class="col"> + {{ speakers | length }} speakers found in context <i>{{ session['context_uuid'] }}</i> + </div> + </div> + + <div class="col"> + <a href="{{ url_for('topology.addSpeaker') }}" class="btn btn-primary" style="margin-bottom: 10px;"> + <i class="bi bi-plus"></i> + Add BGPLS Speaker + </a> + </div> + + <table class="table table-striped table-hover"> + <thead> + <tr> + <th scope="col">Speaker IP Address</th> + <th scope="col">Speaker As Number</th> + <th scope="col"></th> + </tr> + </thead> + <tbody> + {% if speakers %} + {% for speaker in speakers %} + <tr> + <td>{{ speaker.address }}</td> + <td>{{ speaker.asNumber }}</td> + <td> + <a type="button" class="btn btn-danger" href="{{ url_for('topology.disconnectSpeaker',speaker_address=speaker.address)}}"> + <i class="bi bi-x-square"></i> + </a> + + </td> + </tr> + {% endfor %} + {% else %} + <tr> + <td colspan="3">No devices found</td> + </tr> + {% endif %} + </tbody> + </table> + +<script src="https://d3js.org/d3.v4.min.js"></script> + <div id="topology"></div> + <script src="{{ url_for('js.topology_js') }}"></script> + +{% endblock %} \ No newline at end of file diff --git a/src/webui/service/templates/topology/home.html b/src/webui/service/templates/topology/home.html index 27b864d3bb6aefb9d1586838a2466238cbe83299..1603699436fb0bdb0e46310c166c8f17c8712110 100644 --- a/src/webui/service/templates/topology/home.html +++ b/src/webui/service/templates/topology/home.html @@ -22,34 +22,78 @@ <div class="row"> <div class="col"> - {{ devices | length }} devices found in context <i>{{ session['context_uuid'] }}</i> + {{ speakers | length }} speakers found in context <i>{{ session['context_uuid'] }}</i> </div> </div> - - + <div class="row"> + <div class="col"> + <a href="{{ url_for('topology.addSpeaker') }}" class="btn btn-primary" style="margin-bottom: 10px;"> + <i class="bi bi-plus"></i> + Add BGPLS Speaker + </a> + </div> + + <div class="col"> + <a href="{{ url_for('topology.editSpeakers') }}" class="btn btn-primary" style="margin-bottom: 10px;"> + <i class="bi bi-pencil-square"></i> + Edit BGPLS Speakers + </a> + </div> + </div> + <table class="table table-striped table-hover"> <thead> <tr> - <th scope="col">Name</th> - <th scope="col">Config Rules</th> + <th scope="col">Node name</th> + <th scope="col">Local IGP Id</th> + <th scope="col">Remote IGP Id</th> + <th scope="col">Learnt from</th> <th scope="col"></th> </tr> </thead> <tbody> - {% if devices %} - {% for device in devices %} + {% if disdev %} + {% for device in disdev %} <tr> - <td>{{ device.name }}</td> - <td>{{ device.device_config.config_rules | length }}</td> + <td>{{ device.nodeName }}</td> + <td>{{ device.igpID }}</td> + <td></td> + <td>{{ device.learntFrom }}</td> <td> <div class="col"> - <a href="{{ url_for('topology.detail', device_uuid=device.device_id.device_uuid.uuid) }}"> + <a href='{{ url_for("topology.add",device_name=device.nodeName)}}'> <i class="bi bi-plus"></i> Add Device </a> </div> + </td> + </tr> + {% endfor %} + {% else %} + <tr> + <td colspan="3">No devices found</td> + </tr> + {% endif %} + </tbody> + <tbody> + {% if dislink %} + {% for link in dislink %} + <tr> + <td></td> + <td>{{ link.local.nodeName }}</td> + <td>{{ link.remote.nodeName }}</td> + <td>{{ link.learntFrom }}</td> + <td> + <div class="col"> + <a href=''> + <i class="bi bi-plus"></i> + Add Link + </a> + </div> + + </td> </tr> {% endfor %} diff --git a/src/webui/service/topology/forms.py b/src/webui/service/topology/forms.py index ca039a9e2edbbd0176f513c150db6b120e3432ae..55c5d34d1de1e6fc715a99b3d7ceadcdd5d5f6c9 100644 --- a/src/webui/service/topology/forms.py +++ b/src/webui/service/topology/forms.py @@ -37,7 +37,6 @@ class AddDeviceForm(FlaskForm): device_drivers_ietf_network_topology = BooleanField('IETF_NETWORK_TOPOLOGY') device_drivers_onf_tr_352 = BooleanField('ONF_TR_352') device_drivers_xr = BooleanField('XR') - device_drivers_bgpls = BooleanField('BGPLS') device_config_address = StringField('connect/address',default='127.0.0.1',validators=[DataRequired(), Length(min=5)]) device_config_port = StringField('connect/port',default='0',validators=[DataRequired(), Length(min=1)]) device_config_settings = TextAreaField('connect/settings',default='{}',validators=[DataRequired(), Length(min=2)]) @@ -77,4 +76,11 @@ class DescriptorForm(FlaskForm): validators=[ FileAllowed(['json'], 'JSON Descriptors only!') ]) + submit = SubmitField('Submit') + +class SpeakerForm(FlaskForm): + + speaker_address = StringField('ip',default='127.0.0.1',validators=[DataRequired(), Length(min=5)]) + speaker_port = StringField('port',default='179',validators=[DataRequired(), Length(min=1)]) + speaker_as = StringField('as',default='65000',validators=[DataRequired(), Length(min=1)]) submit = SubmitField('Submit') \ No newline at end of file diff --git a/src/webui/service/topology/routes.py b/src/webui/service/topology/routes.py index 710461b47b5dd500995beacf5f6244be41b04886..498252cfb4665aff1a2cab77d64da5f41ea7bd13 100644 --- a/src/webui/service/topology/routes.py +++ b/src/webui/service/topology/routes.py @@ -12,22 +12,28 @@ # See the License for the specific language governing permissions and # limitations under the License. -import json -from flask import current_app, render_template, Blueprint, flash, session, redirect, url_for +import json,logging +from flask import current_app, render_template, Blueprint, flash, session, redirect, url_for, request from common.proto.context_pb2 import ( - ConfigActionEnum, Device, DeviceDriverEnum, DeviceId, DeviceList, DeviceOperationalStatusEnum, Empty, TopologyId) + ConfigActionEnum, ConfigRule, ConfigRule_Custom, Device, DeviceConfig, DeviceDriverEnum, DeviceId, DeviceList, DeviceOperationalStatusEnum, + Empty, EndPoint, EndPointId, TopologyId, Uuid) from common.tools.object_factory.Context import json_context_id from common.tools.object_factory.Topology import json_topology_id from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from webui.service.device.forms import AddDeviceForm from common.DeviceTypes import DeviceTypeEnum -from webui.service.topology.forms import ConfigForm +from webui.service.topology.forms import ConfigForm, SpeakerForm from webui.service.topology.forms import UpdateDeviceForm -topology = Blueprint('topology', __name__, url_prefix='/topoloy') +from bgpls_speaker.client.BgplsClient import BgplsClient +from common.proto.bgpls_pb2 import (BgplsSpeaker, DiscoveredDeviceList,DiscoveredDevice,DiscoveredLinkList,DiscoveredLink, NodeDescriptors) + +topology = Blueprint('topology', __name__, url_prefix='/topology') context_client = ContextClient() device_client = DeviceClient() +bgpls_client = BgplsClient() +logger = logging.getLogger(__name__) @topology.get('/') def home(): @@ -42,28 +48,57 @@ def home(): json_topo_id = json_topology_id(topology_uuid, context_id=json_context_id(context_uuid)) grpc_topology = context_client.GetTopology(TopologyId(**json_topo_id)) topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids} - grpc_devices: DeviceList = context_client.ListDevices(Empty()) - context_client.close() + + if grpc_topology is None: + flash('Context({:s})/Topology({:s}) not found'.format(str(context_uuid), str(topology_uuid)), 'danger') + devices = [] + else: + topo_device_uuids = {device_id.device_uuid.uuid for device_id in grpc_topology.device_ids} + grpc_devices: DeviceList = context_client.ListDevices(Empty()) + devices = [ + device for device in grpc_devices.devices + if device.device_id.device_uuid.uuid in topo_device_uuids + ] + + # ListNewDevices discovered from bgpls + logger.info('topology/home') + bgpls_client.connect() + logger.info('bgpls_client.connect %s',bgpls_client) + discovered_device_list = bgpls_client.ListDiscoveredDevices(Empty()) + logger.info('discoveredDeviceList %s',discovered_device_list) + # List Links discovered from bgpls + discovered_link_list = bgpls_client.ListDiscoveredLinks(Empty()) + logger.info('discoveredLinkList %s',discovered_link_list) + # List current open speaker connections + speaker_list=bgpls_client.ListBgplsSpeakers(Empty()) - devices = [ - device for device in grpc_devices.devices - if device.device_id.device_uuid.uuid in topo_device_uuids - ] + + context_client.close() + bgpls_client.close() return render_template( 'topology/home.html', devices=devices, dde=DeviceDriverEnum, - dose=DeviceOperationalStatusEnum) - -@topology.route('add', methods=['GET', 'POST']) -def add(): + dose=DeviceOperationalStatusEnum,disdev=discovered_device_list.discovereddevices, + dislink=discovered_link_list.discoveredlinks,speakers=speaker_list.speakers) + +@topology.route('add/<path:device_name>', methods=['GET', 'POST']) +def add(device_name): + """" + Add a discovered device from bgpls protocol. Populate form from + existent info in bgpls. + """ + # TODO: Conect to device and get necessary info form = AddDeviceForm() + logger.info('topology/add') + # listing enum values form.operational_status.choices = [] for key, _ in DeviceOperationalStatusEnum.DESCRIPTOR.values_by_name.items(): form.operational_status.choices.append( (DeviceOperationalStatusEnum.Value(key), key.replace('DEVICEOPERATIONALSTATUS_', ''))) - + + form.device_type.choices = [] # items for Device Type field for device_type in DeviceTypeEnum: form.device_type.choices.append((device_type.value,device_type.value)) @@ -71,23 +106,23 @@ def add(): if form.validate_on_submit(): device_obj = Device() # Device UUID: - device_obj.device_id.device_uuid.uuid = form.device_id.data + device_obj.device_id.device_uuid.uuid = form.device_id.data # pylint: disable=no-member # Device type: device_obj.device_type = str(form.device_type.data) # Device configurations: - config_rule = device_obj.device_config.config_rules.add() + config_rule = device_obj.device_config.config_rules.add() # pylint: disable=no-member config_rule.action = ConfigActionEnum.CONFIGACTION_SET config_rule.custom.resource_key = '_connect/address' config_rule.custom.resource_value = form.device_config_address.data - config_rule = device_obj.device_config.config_rules.add() + config_rule = device_obj.device_config.config_rules.add() # pylint: disable=no-member config_rule.action = ConfigActionEnum.CONFIGACTION_SET config_rule.custom.resource_key = '_connect/port' config_rule.custom.resource_value = form.device_config_port.data - config_rule = device_obj.device_config.config_rules.add() + config_rule = device_obj.device_config.config_rules.add() # pylint: disable=no-member config_rule.action = ConfigActionEnum.CONFIGACTION_SET config_rule.custom.resource_key = '_connect/settings' @@ -95,9 +130,10 @@ def add(): device_config_settings = json.loads(form.device_config_settings.data) except: # pylint: disable=bare-except device_config_settings = form.device_config_settings.data - + logger.info('(topology/add) config settings %s', form.device_config_settings.data) if isinstance(device_config_settings, dict): config_rule.custom.resource_value = json.dumps(device_config_settings) + logger.info('(topology/add) config settings is instance to json') else: config_rule.custom.resource_value = str(device_config_settings) @@ -119,19 +155,30 @@ def add(): device_obj.device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_ONF_TR_352) if form.device_drivers_xr.data: device_obj.device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_XR) - if form.device_drivers_bgpls.data: - device_obj.device_drivers.append(DeviceDriverEnum.DEVICEDRIVER_BGPLS) try: device_client.connect() + logger.info('add device from speaker:%s',device_obj) response: DeviceId = device_client.AddDevice(device_obj) device_client.close() flash(f'New device was created with ID "{response.device_uuid.uuid}".', 'success') + bgpls_client.connect() + bgpls_client.NotifyAddNodeToContext(NodeDescriptors(nodeName=device_obj.device_id.device_uuid.uuid)) + bgpls_client.close() return redirect(url_for('device.home')) except Exception as e: flash(f'Problem adding the device. {e.details()}', 'danger') - return render_template('device/add.html', form=form, + # Prefill data with discovered info from speaker + # Device Name from bgpls + form.device_name=device_name + device=device_name + form.device_id.data=device_name + # Default values (TODO: NOT WORKING) + form.device_type.data=DeviceTypeEnum.EMULATED_PACKET_ROUTER + form.device_config_settings.data=str('{"username": "admin", "password": "admin"}') + + return render_template('topology/add.html', form=form, device=device, submit_text='Add New Device') @topology.route('detail/<path:device_uuid>', methods=['GET', 'POST']) @@ -145,88 +192,54 @@ def detail(device_uuid: str): dde=DeviceDriverEnum, dose=DeviceOperationalStatusEnum) -@topology.get('<path:device_uuid>/delete') -def delete(device_uuid): - try: - - # first, check if device exists! - # request: DeviceId = DeviceId() - # request.device_uuid.uuid = device_uuid - # response: Device = client.GetDevice(request) - # TODO: finalize implementation - - request = DeviceId() - request.device_uuid.uuid = device_uuid - device_client.connect() - response = device_client.DeleteDevice(request) - device_client.close() - - flash(f'Device "{device_uuid}" deleted successfully!', 'success') - except Exception as e: - flash(f'Problem deleting device "{device_uuid}": {e.details()}', 'danger') - current_app.logger.exception(e) - return redirect(url_for('device.home')) - -@topology.route('<path:device_uuid>/addconfig', methods=['GET', 'POST']) -def addconfig(device_uuid): - form = ConfigForm() - request = DeviceId() - request.device_uuid.uuid = device_uuid - context_client.connect() - response = context_client.GetDevice(request) - context_client.close() - +@topology.route('addSpeaker', methods=['GET', 'POST']) +def addSpeaker(): + # Conectar con bgpls¿ + bgpls_client.connect() + form = SpeakerForm() if form.validate_on_submit(): - device = Device() - device.CopyFrom(response) - config_rule = device.device_config.config_rules.add() - config_rule.action = ConfigActionEnum.CONFIGACTION_SET - config_rule.custom.resource_key = form.device_key_config.data - config_rule.custom.resource_value = form.device_value_config.data - try: - device_client.connect() - response: DeviceId = device_client.ConfigureDevice(device) - device_client.close() - flash(f'New configuration was created with ID "{response.device_uuid.uuid}".', 'success') - return redirect(url_for('device.home')) - except Exception as e: - flash(f'Problem adding the device. {e.details()}', 'danger') + logger.info('addSpeaker ip:%s',form.speaker_address.data) + bgpls_client.AddBgplsSpeaker(BgplsSpeaker(address=form.speaker_address.data,port=form.speaker_port.data,asNumber=form.speaker_as.data)) + flash(f'Speaker "{form.speaker_address.data}:{form.speaker_port.data}" added successfully!', 'success') + bgpls_client.close() + return render_template('topology/addSpeaker.html',form=form) + +@topology.route('formSpeaker', methods=['GET','POST']) +def formSpeaker(): + # Conectar con bgpls¿ + form = SpeakerForm() + if request.method=="POST": + address = form.speaker_address.data + port = form.speaker_port.data + as_ = form.speaker_as.data + logger.info("FORM formSpeaker: %s %s %s", address,port,as_) + + flash(f'Speaker "{address}:{port}" added successfully!', 'success') - return render_template('topology/addconfig.html', form=form, submit_text='Add New Configuration') + return redirect(url_for('topology.home')) + # return 'Form submitted' -@topology.route('updateconfig', methods=['GET', 'POST']) -def updateconfig(): +@topology.route('editSpeakers', methods=['GET','POST']) +def editSpeakers(): + + speakers=[] + bgpls_client.connect() + speaker_list=bgpls_client.ListBgplsSpeakers(Empty()) + speakers_ids=[speaker for speaker in speaker_list.speakers if speaker.id] + speakers=[bgpls_client.GetSpeakerInfoFromId(ids) for ids in speakers_ids] - return render_template('topology/updateconfig.html') + bgpls_client.close() + return render_template('topology/editSpeakers.html',speakers=speakers) -@topology.route('<path:device_uuid>/update', methods=['GET', 'POST']) -def update(device_uuid): - form = UpdateDeviceForm() - request = DeviceId() - request.device_uuid.uuid = device_uuid - context_client.connect() - response = context_client.GetDevice(request) - context_client.close() +@topology.route('disconnectSpeaker/<path:speaker_address>', methods=['GET','POST']) +def disconnectSpeaker(speaker_address): - # listing enum values - form.update_operational_status.choices = [] - for key, value in DeviceOperationalStatusEnum.DESCRIPTOR.values_by_name.items(): - form.update_operational_status.choices.append((DeviceOperationalStatusEnum.Value(key), key.replace('DEVICEOPERATIONALSTATUS_', ''))) - - form.update_operational_status.default = response.device_operational_status + bgpls_client.connect() + current_speaker=BgplsSpeaker(address=speaker_address) + logger.info('Disconnecting speaker: %s...',speaker_address) + bgpls_client.DisconnectFromSpeaker(current_speaker) + bgpls_client.close() - if form.validate_on_submit(): - device = Device() - device.CopyFrom(response) - device.device_operational_status = form.update_operational_status.data - try: - device_client.connect() - response: DeviceId = device_client.ConfigureDevice(device) - device_client.close() - flash(f'Status of device with ID "{response.device_uuid.uuid}" was updated.', 'success') - return redirect(url_for('device.home')) - except Exception as e: - flash(f'Problem updating the device. {e.details()}', 'danger') - return render_template('topology/update.html', device=response, form=form, submit_text='Update Device') + return redirect(url_for('topology.editSpeakers')) \ No newline at end of file