From 5ddc1c468c9b12d1cb447493b206605cc9dbc7c2 Mon Sep 17 00:00:00 2001 From: armingol Date: Mon, 1 Sep 2025 06:40:25 +0000 Subject: [PATCH 01/21] Add IPOWDM and TAPI LSP service handlers and related configuration rules - Introduced IpowdmServiceHandler and Tapi_LSPServiceHandler to manage IPOWDM and TAPI LSP services respectively. - Implemented setup and teardown configuration rules for both service types. - Enhanced the existing algorithm to handle new service types and their configurations. - Updated ServiceTypes to include IPOWDM and TAPI LSP. - Added necessary imports and utility functions for handling new service configurations. - Modified existing service handler APIs to accommodate new service types. - Ensured logging and error handling are in place for better traceability. --- proto/context.proto | 24 ++- proto/ipowdm.proto | 24 +++ proto/tapi_lsp.proto | 28 +++ src/common/DeviceTypes.py | 1 + src/common/tools/object_factory/Service.py | 22 ++- src/common/type_checkers/Assertions.py | 4 + src/context/service/database/ConfigRule.py | 6 + src/context/service/database/Service.py | 4 + .../database/models/ConfigRuleModel.py | 2 + .../database/models/enums/ServiceType.py | 2 + src/device/service/Tools.py | 55 ++++-- .../drivers/ietf_l3vpn/IetfL3VpnDriver.py | 90 +++++---- .../drivers/ietf_l3vpn/templates/ipowdm.json | 43 ++++ .../drivers/ietf_l3vpn/templates/tools.py | 69 +++++++ .../drivers/optical_tfs/OpticalTfsDriver.py | 30 ++- .../drivers/optical_tfs/templates/lsp.json | 37 ++++ .../drivers/optical_tfs/templates/tools.py | 72 +++++++ .../frontend/service/algorithms/_Algorithm.py | 12 +- .../algorithms/tools/ComposeConfigRules.py | 21 +- .../service/algorithms/tools/ServiceTypes.py | 5 + .../java/org/etsi/tfs/policy/Serializer.java | 8 + .../service/ServiceServiceServicerImpl.py | 10 +- .../service_handler_api/FilterFields.py | 2 + .../ServiceHandlerFactory.py | 27 ++- .../service_handler_api/SettingsHandler.py | 80 +++++++- .../service/service_handler_api/Tools.py | 6 +- .../service/service_handlers/__init__.py | 14 ++ .../service_handlers/ipowdm/ConfigRules.py | 58 ++++++ .../ipowdm/IpowdmServiceHandler.py | 184 +++++++++++++++++ .../service_handlers/ipowdm/__init__.py | 13 ++ .../service_handlers/tapi_lsp/ConfigRules.py | 58 ++++++ .../tapi_lsp/Tapi_LSPServiceHandler.py | 186 ++++++++++++++++++ .../service_handlers/tapi_lsp/__init__.py | 13 ++ 33 files changed, 1126 insertions(+), 84 deletions(-) create mode 100644 proto/ipowdm.proto create mode 100644 proto/tapi_lsp.proto create mode 100644 src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json create mode 100644 src/device/service/drivers/ietf_l3vpn/templates/tools.py create mode 100644 src/device/service/drivers/optical_tfs/templates/lsp.json create mode 100644 src/device/service/drivers/optical_tfs/templates/tools.py create mode 100644 src/service/service/service_handlers/ipowdm/ConfigRules.py create mode 100644 src/service/service/service_handlers/ipowdm/IpowdmServiceHandler.py create mode 100644 src/service/service/service_handlers/ipowdm/__init__.py create mode 100644 src/service/service/service_handlers/tapi_lsp/ConfigRules.py create mode 100644 src/service/service/service_handlers/tapi_lsp/Tapi_LSPServiceHandler.py create mode 100644 src/service/service/service_handlers/tapi_lsp/__init__.py diff --git a/proto/context.proto b/proto/context.proto index b33750e80..3ffdd2815 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -19,7 +19,9 @@ package context; import "google/protobuf/any.proto"; import "acl.proto"; +import "ipowdm.proto"; import "kpi_sample_types.proto"; +import "tapi_lsp.proto"; service ContextService { rpc ListContextIds (Empty ) returns ( ContextIdList ) {} @@ -78,7 +80,7 @@ service ContextService { rpc RemoveConnection (ConnectionId ) returns ( Empty ) {} rpc GetConnectionEvents(Empty ) returns (stream ConnectionEvent ) {} - + // ------------------------------ Experimental ----------------------------- rpc GetOpticalConfig (Empty ) returns (OpticalConfigList) {} rpc SetOpticalConfig (OpticalConfig ) returns (OpticalConfigId ) {} @@ -203,7 +205,7 @@ message Component { // Defined previously in this sectio Uuid component_uuid = 1; string name = 2; string type = 3; - + map attributes = 4; // dict[attr.name => json.dumps(attr.value)] string parent = 5; } @@ -331,6 +333,8 @@ enum ServiceTypeEnum { SERVICETYPE_L1NM = 8; SERVICETYPE_INT = 9; SERVICETYPE_ACL = 10; + SERVICETYPE_TAPI_LSP = 11; + SERVICETYPE_IPOWDM = 12; } enum ServiceStatusEnum { @@ -544,11 +548,23 @@ message ConfigRule_ACL { acl.AclRuleSet rule_set = 2; } +message ConfigRule_IPOWDM { + EndPointId endpoint_id = 1; + ipowdm.IpowdmRuleSet rule_set = 2; +} + +message ConfigRule_TAPI_LSP { + EndPointId endpoint_id = 1; + tapi_lsp.TapiLspRuleSet rule_set = 2; +} + message ConfigRule { ConfigActionEnum action = 1; oneof config_rule { ConfigRule_Custom custom = 2; ConfigRule_ACL acl = 3; + ConfigRule_TAPI_LSP tapi_lsp = 4; + ConfigRule_IPOWDM ipowdm = 5; } } @@ -579,7 +595,7 @@ message Location { oneof location { string region = 1; GPS_Position gps_position = 2; - + string interface=3; string circuit_pack=4; } @@ -711,7 +727,7 @@ message OpticalLinkDetails { string dst_port = 3; string local_peer_port = 4; string remote_peer_port = 5 ; - bool used = 6 ; + bool used = 6 ; map c_slots = 7; map l_slots = 8; map s_slots = 9; diff --git a/proto/ipowdm.proto b/proto/ipowdm.proto new file mode 100644 index 000000000..a4b4085d4 --- /dev/null +++ b/proto/ipowdm.proto @@ -0,0 +1,24 @@ +// Copyright 2022-2025 ETSI OSG/SDG TeraFlowSDN (TFS) (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. + +syntax = "proto3"; +package ipowdm; + +message IpowdmRuleSet { + string src = 1; + string dst = 2; + string uuid = 3; + string bw = 4; + string unit = 5; +} \ No newline at end of file diff --git a/proto/tapi_lsp.proto b/proto/tapi_lsp.proto new file mode 100644 index 000000000..f46060048 --- /dev/null +++ b/proto/tapi_lsp.proto @@ -0,0 +1,28 @@ +// Copyright 2022-2025 ETSI OSG/SDG TeraFlowSDN (TFS) (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. + +syntax = "proto3"; +package tapi_lsp; + +message TapiLspRuleSet { + string src = 1; + string dst = 2; + string uuid = 3; + string bw = 4; + string direction = 5; + string layer_protocol_name = 6; + string layer_protocol_qualifier = 7; + string lower_frequency_mhz = 8; + string upper_frequency_mhz = 9; +} \ No newline at end of file diff --git a/src/common/DeviceTypes.py b/src/common/DeviceTypes.py index 7698097f8..008dccc74 100644 --- a/src/common/DeviceTypes.py +++ b/src/common/DeviceTypes.py @@ -53,6 +53,7 @@ class DeviceTypeEnum(Enum): OPEN_ROADM = 'openroadm' MORPHEUS = 'morpheus' OPENFLOW_RYU_CONTROLLER = 'openflow-ryu-controller' + IPOWDM_ROUTER = 'ipowdm-router' # ETSI TeraFlowSDN controller TERAFLOWSDN_CONTROLLER = 'teraflowsdn' diff --git a/src/common/tools/object_factory/Service.py b/src/common/tools/object_factory/Service.py index 2bfe50ccc..50f0a5420 100644 --- a/src/common/tools/object_factory/Service.py +++ b/src/common/tools/object_factory/Service.py @@ -92,4 +92,24 @@ def json_service_p4_planned( return json_service( service_uuid, ServiceTypeEnum.SERVICETYPE_L1NM, context_id=json_context_id(context_uuid), status=ServiceStatusEnum.SERVICESTATUS_PLANNED, endpoint_ids=endpoint_ids, constraints=constraints, - config_rules=config_rules) \ No newline at end of file + config_rules=config_rules) + +def json_service_ipowdm_planned( + service_uuid : str, endpoint_ids : List[Dict] = [], constraints : List[Dict] = [], + config_rules : List[Dict] = [], context_uuid : str = DEFAULT_CONTEXT_NAME + ): + + return json_service( + service_uuid, ServiceTypeEnum.SERVICETYPE_IPOWDM, context_id=json_context_id(context_uuid), + status=ServiceStatusEnum.SERVICESTATUS_PLANNED, endpoint_ids=endpoint_ids, constraints=constraints, + config_rules=config_rules) + +def json_service_tapi_lsp_planned( + service_uuid : str, endpoint_ids : List[Dict] = [], constraints : List[Dict] = [], + config_rules : List[Dict] = [], context_uuid : str = DEFAULT_CONTEXT_NAME + ): + + return json_service( + service_uuid, ServiceTypeEnum.SERVICETYPE_TAPI_LSP, context_id=json_context_id(context_uuid), + status=ServiceStatusEnum.SERVICESTATUS_PLANNED, endpoint_ids=endpoint_ids, constraints=constraints, + config_rules=config_rules) diff --git a/src/common/type_checkers/Assertions.py b/src/common/type_checkers/Assertions.py index e41b0d0d3..540db7d6c 100644 --- a/src/common/type_checkers/Assertions.py +++ b/src/common/type_checkers/Assertions.py @@ -109,6 +109,8 @@ def validate_service_type_enum(message): 'SERVICETYPE_E2E', 'SERVICETYPE_OPTICAL_CONNECTIVITY', 'SERVICETYPE_QKD', + 'SERVICETYPE_IPOWDM', + 'SERVICETYPE_TAPI_LSP', ] def validate_service_state_enum(message): @@ -146,6 +148,8 @@ def validate_uuid(message, allow_empty=False): CONFIG_RULE_TYPES = { 'custom', 'acl', + 'ipowdm', + 'tapi_lsp' } def validate_config_rule(message): assert isinstance(message, dict) diff --git a/src/context/service/database/ConfigRule.py b/src/context/service/database/ConfigRule.py index c9db5488c..0684fc10f 100644 --- a/src/context/service/database/ConfigRule.py +++ b/src/context/service/database/ConfigRule.py @@ -71,6 +71,12 @@ def compose_config_rules_data( _, _, endpoint_uuid = endpoint_get_uuid(config_rule.acl.endpoint_id, allow_random=False) rule_set_name = config_rule.acl.rule_set.name configrule_name = '{:s}:{:s}:{:s}:{:s}'.format(parent_kind, kind.value, endpoint_uuid, rule_set_name) + elif kind == ConfigRuleKindEnum.IPOWDM: + _, _, endpoint_uuid = endpoint_get_uuid(config_rule.ipowdm.endpoint_id, allow_random=False) + configrule_name = '{:s}:{:s}:{:s}'.format(parent_kind, kind.value, endpoint_uuid) + elif kind == ConfigRuleKindEnum.TAPI_LSP: + _, _, endpoint_uuid = endpoint_get_uuid(config_rule.tapi_lsp.endpoint_id, allow_random=False) + configrule_name = '{:s}:{:s}:{:s}'.format(parent_kind, kind.value, endpoint_uuid) else: MSG = 'Name for ConfigRule({:s}) cannot be inferred '+\ '(device_uuid={:s}, service_uuid={:s}, slice_uuid={:s})' diff --git a/src/context/service/database/Service.py b/src/context/service/database/Service.py index 9076fc025..b2c617915 100644 --- a/src/context/service/database/Service.py +++ b/src/context/service/database/Service.py @@ -88,6 +88,10 @@ def service_set(db_engine : Engine, messagebroker : MessageBroker, request : Ser service_type = grpc_to_enum__service_type(request.service_type) if service_type is None and request.service_type == ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY: service_type = "OPTICAL_CONNECTIVITY" + if service_type is None and request.service_type == ServiceTypeEnum.SERVICETYPE_IPOWDM: + service_type = "IPOWDM" + if service_type is None and request.service_type == ServiceTypeEnum.SERVICETYPE_TAPI_LSP: + service_type = "TAPI_LSP" service_status = grpc_to_enum__service_status(request.service_status.service_status) diff --git a/src/context/service/database/models/ConfigRuleModel.py b/src/context/service/database/models/ConfigRuleModel.py index 5462df672..dac7eaafd 100644 --- a/src/context/service/database/models/ConfigRuleModel.py +++ b/src/context/service/database/models/ConfigRuleModel.py @@ -23,6 +23,8 @@ from ._Base import _Base class ConfigRuleKindEnum(enum.Enum): CUSTOM = 'custom' ACL = 'acl' + IPOWDM = 'ipowdm' + TAPI_LSP = 'tapi_lsp' class DeviceConfigRuleModel(_Base): __tablename__ = 'device_configrule' diff --git a/src/context/service/database/models/enums/ServiceType.py b/src/context/service/database/models/enums/ServiceType.py index 93271c3b9..6408cdb5d 100644 --- a/src/context/service/database/models/enums/ServiceType.py +++ b/src/context/service/database/models/enums/ServiceType.py @@ -33,6 +33,8 @@ class ORM_ServiceTypeEnum(enum.Enum): QKD = ServiceTypeEnum.SERVICETYPE_QKD INT = ServiceTypeEnum.SERVICETYPE_INT ACL = ServiceTypeEnum.SERVICETYPE_ACL + IPOWDM = ServiceTypeEnum.SERVICETYPE_IPOWDM + TAPI_LSP = ServiceTypeEnum.SERVICETYPE_TAPI_LSP grpc_to_enum__service_type = functools.partial( grpc_to_enum, ServiceTypeEnum, ORM_ServiceTypeEnum) diff --git a/src/device/service/Tools.py b/src/device/service/Tools.py index a62a0d702..ecccf7298 100644 --- a/src/device/service/Tools.py +++ b/src/device/service/Tools.py @@ -155,7 +155,7 @@ def populate_endpoints( _sub_device.name = resource_value['name'] _sub_device.device_type = resource_value['type'] _sub_device.device_operational_status = resource_value['status'] - + # Sub-devices might not have a driver assigned. if 'drivers' in resource_value: drivers = resource_value['drivers'] @@ -299,9 +299,9 @@ def populate_initial_config_rules(device_uuid : str, device_config : DeviceConfi def compute_rules_to_add_delete( device : Device, request : Device ) -> Tuple[List[Tuple[str, Any]], List[Tuple[str, Any]]]: - # convert config rules from context into a dictionary + # convert config rules from context into a dictionary context_config_rules = {} - for config_rule in device.device_config.config_rules: + for config_rule in device.device_config.config_rules: config_rule_kind = config_rule.WhichOneof('config_rule') if config_rule_kind == 'custom': # process "custom" rules context_config_rules[config_rule.custom.resource_key] = config_rule.custom.resource_value # get the resource value of the rule resource @@ -310,25 +310,58 @@ def compute_rules_to_add_delete( endpoint_uuid = config_rule.acl.endpoint_id.endpoint_uuid.uuid # get the endpoint name acl_ruleset_name = config_rule.acl.rule_set.name # get the acl name ACL_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/acl_ruleset[{:s}]' - key_or_path = ACL_KEY_TEMPLATE.format(device_uuid, endpoint_uuid, acl_ruleset_name) + key_or_path = ACL_KEY_TEMPLATE.format(device_uuid, endpoint_uuid, acl_ruleset_name) context_config_rules[key_or_path] = grpc_message_to_json(config_rule.acl) # get the resource value of the acl - + elif config_rule_kind == 'ipowdm': + device_uuid = config_rule.ipowdm.endpoint_id.device_id.device_uuid.uuid + endpoint_uuid = config_rule.ipowdm.endpoint_id.endpoint_uuid.uuid + ipowdm_ruleset_name = config_rule.ipowdm.rule_set.name + IPOWDM_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/ipowdm_ruleset[{:s}]' + key_or_path = IPOWDM_KEY_TEMPLATE.format(device_uuid, endpoint_uuid, ipowdm_ruleset_name) + context_config_rules[key_or_path] = grpc_message_to_json(config_rule.ipowdm) + LOGGER.debug('context_config_rules [%s] = %s', key_or_path, context_config_rules[key_or_path]) + elif config_rule_kind == 'tapi_lsp': + device_uuid = config_rule.tapi_lsp.endpoint_id.device_id.device_uuid.uuid # get the device name + endpoint_uuid = config_rule.tapi_lsp.endpoint_id.endpoint_uuid.uuid # get the endpoint name + tapi_lsp_ruleset_name = config_rule.tapi_lsp.rule_set.name # get the ip_link name + TAPI_LSP_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/tapi_lsp_ruleset[{:s}]' + key_or_path = TAPI_LSP_KEY_TEMPLATE.format(device_uuid, endpoint_uuid, tapi_lsp_ruleset_name) + context_config_rules[key_or_path] = grpc_message_to_json(config_rule.tapi_lsp) + LOGGER.debug('context_config_rules [%s] = {context_config_rules[%s]}',key_or_path, key_or_path) + request_config_rules = [] for config_rule in request.device_config.config_rules: config_rule_kind = config_rule.WhichOneof('config_rule') - if config_rule_kind == 'custom': # resource management of "custom" rule + if config_rule_kind == 'custom': # resource management of "custom" rule request_config_rules.append(( config_rule.action, config_rule.custom.resource_key, config_rule.custom.resource_value )) - elif config_rule_kind == 'acl': # resource management of "acl" rule + elif config_rule_kind == 'acl': # resource management of "acl" rule device_uuid = config_rule.acl.endpoint_id.device_id.device_uuid.uuid endpoint_uuid = config_rule.acl.endpoint_id.endpoint_uuid.uuid acl_ruleset_name = config_rule.acl.rule_set.name ACL_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/acl_ruleset[{:s}]' - key_or_path = ACL_KEY_TEMPLATE.format(device_uuid, endpoint_uuid, acl_ruleset_name) + key_or_path = ACL_KEY_TEMPLATE.format(device_uuid, endpoint_uuid, acl_ruleset_name) request_config_rules.append(( config_rule.action, key_or_path, grpc_message_to_json(config_rule.acl) )) + elif config_rule_kind == 'ipowdm': # resource management of "ipowdm" rule + device_uuid = config_rule.ipowdm.endpoint_id.device_id.device_uuid.uuid + endpoint_uuid = config_rule.ipowdm.endpoint_id.endpoint_uuid.uuid + IPOWDM_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/ipowdm_ruleset' + key_or_path = IPOWDM_KEY_TEMPLATE.format(device_uuid, endpoint_uuid) + request_config_rules.append(( + config_rule.action, key_or_path, grpc_message_to_json(config_rule.ipowdm) + )) + LOGGER.debug('context_config_rules= %s', request_config_rules) + elif config_rule_kind == 'tapi_lsp': # resource management of "tapi_lsp" rule + device_uuid = config_rule.tapi_lsp.endpoint_id.device_id.device_uuid.uuid + endpoint_uuid = config_rule.tapi_lsp.endpoint_id.endpoint_uuid.uuid + TAPI_LSP_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/tapi_lsp_ruleset' + key_or_path = TAPI_LSP_KEY_TEMPLATE.format(device_uuid, endpoint_uuid) + request_config_rules.append(( + config_rule.action, key_or_path, grpc_message_to_json(config_rule.tapi_lsp) + )) resources_to_set : List[Tuple[str, Any]] = [] # key, value resources_to_delete : List[Tuple[str, Any]] = [] # key, value @@ -407,7 +440,7 @@ def subscribe_kpi(request : MonitoringSettings, driver : _Driver, monitoring_loo return errors def unsubscribe_kpi(request : MonitoringSettings, driver : _Driver, monitoring_loops : MonitoringLoops) -> List[str]: - kpi_uuid = request.kpi_id.kpi_id.uuid + kpi_uuid = request.kpi_id.kpi_id.uuid kpi_details = monitoring_loops.get_kpi_by_uuid(kpi_uuid) if kpi_details is None: @@ -502,7 +535,7 @@ def extract_resources(config : dict, device : Device) -> list[list[dict], dict]: else : resources.append(is_key_existed('channel_namespace', config)) resources.append(is_key_existed('add_transceiver', config)) - + conditions['is_opticalband'] = is_opticalband if 'flow' in config: #for tuple_value in config['flow'][device.name]: @@ -510,7 +543,7 @@ def extract_resources(config : dict, device : Device) -> list[list[dict], dict]: dest_vals = [] handled_flow = [] for tuple_value in config['flow']: - source_port = None + source_port = None destination_port = None source_port_uuid, destination_port_uuid = tuple_value if source_port_uuid != '0': diff --git a/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py b/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py index 79219e895..419fe319f 100644 --- a/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py +++ b/src/device/service/drivers/ietf_l3vpn/IetfL3VpnDriver.py @@ -23,6 +23,7 @@ from device.service.driver_api.ImportTopologyEnum import ImportTopologyEnum, get from .Constants import SPECIAL_RESOURCE_MAPPINGS from .TfsApiClient import TfsApiClient from .Tools import compose_resource_endpoint +from .templates.tools import create_request LOGGER = logging.getLogger(__name__) @@ -176,48 +177,59 @@ class IetfL3VpnDriver(_Driver): results = [] if len(resources) == 0: return results with self.__lock: - for resource in resources: - resource_key, resource_value = resource - if RE_IETF_L3VPN_OPERATION.match(resource_key): - operation_type = json.loads(resource_value)["type"] - results.append((resource_key, True)) - break + if 'ipowdm' in str(resources): + for resource in resources: + if 'ipowdm' in str(resource): + try: + create_request(resource) + LOGGER.info('Request created successfully') + results.append((resource, True)) + except Exception as e: + MSG = 'Invalid resource_value type: expected dict, got {:s}' + results.append((resource, e)) else: - raise Exception("operation type not found in resources") - for resource in resources: - LOGGER.info('resource = {:s}'.format(str(resource))) - resource_key, resource_value = resource - if not RE_IETF_L3VPN_DATA.match(resource_key): - continue - try: - resource_value = json.loads(resource_value) + for resource in resources: + resource_key, resource_value = resource + if RE_IETF_L3VPN_OPERATION.match(resource_key): + operation_type = json.loads(resource_value)["type"] + results.append((resource_key, True)) + break + else: + raise Exception("operation type not found in resources") + for resource in resources: + LOGGER.info('resource = {:s}'.format(str(resource))) + resource_key, resource_value = resource + if not RE_IETF_L3VPN_DATA.match(resource_key): + continue + try: + resource_value = json.loads(resource_value) - # if service_exists(self.__tfs_nbi_root, self.__auth, service_uuid): - # exc = NotImplementedError( - # "IETF L3VPN Service Update is still not supported" - # ) - # results.append((resource[0], exc)) - # continue - if operation_type == "create": - service_id = resource_value["ietf-l3vpn-svc:l3vpn-svc"][ - "vpn-services" - ]["vpn-service"][0]["vpn-id"] - self.tac.create_connectivity_service(resource_value) - elif operation_type == "update": - service_id = resource_value["ietf-l3vpn-svc:l3vpn-svc"][ - "vpn-services" - ]["vpn-service"][0]["vpn-id"] - self.tac.update_connectivity_service(resource_value) - else: - raise Exception("operation type not supported") - results.append((resource_key, True)) - except Exception as e: # pylint: disable=broad-except - LOGGER.exception( - "Unhandled error processing resource_key({:s})".format( - str(resource_key) + # if service_exists(self.__tfs_nbi_root, self.__auth, service_uuid): + # exc = NotImplementedError( + # "IETF L3VPN Service Update is still not supported" + # ) + # results.append((resource[0], exc)) + # continue + if operation_type == "create": + service_id = resource_value["ietf-l3vpn-svc:l3vpn-svc"][ + "vpn-services" + ]["vpn-service"][0]["vpn-id"] + self.tac.create_connectivity_service(resource_value) + elif operation_type == "update": + service_id = resource_value["ietf-l3vpn-svc:l3vpn-svc"][ + "vpn-services" + ]["vpn-service"][0]["vpn-id"] + self.tac.update_connectivity_service(resource_value) + else: + raise Exception("operation type not supported") + results.append((resource_key, True)) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception( + "Unhandled error processing resource_key({:s})".format( + str(resource_key) + ) ) - ) - results.append((resource_key, e)) + results.append((resource_key, e)) return results @metered_subclass_method(METRICS_POOL) diff --git a/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json b/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json new file mode 100644 index 000000000..9b30665a4 --- /dev/null +++ b/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json @@ -0,0 +1,43 @@ +{ + "services": [ + { + "service_id": { + "context_id": {"context_uuid": {"uuid": "admin"}}, + "service_uuid": {"uuid": "IP-Link2"} + }, + "service_type": 8, + "service_status": {"service_status": 1}, + "service_endpoint_ids": [ + {"device_id": {"device_uuid": {"uuid": ""}},"endpoint_uuid": {"uuid": ""}}, + {"device_id": {"device_uuid": {"uuid": ""}},"endpoint_uuid": {"uuid": ""}} + ], + "service_constraints": [], + + "service_config": {"config_rules": [ + {"action": 1, "ip_link": { + "endpoint_id": { + "device_id": {"device_uuid": {"uuid": ""}}, + "endpoint_uuid": {"uuid": ""} + }, + "rule_set": { + "ip" : "", + "mask": "", + "vlan": "" + } + }}, + {"action": 1, "ip_link": { + "endpoint_id": { + "device_id": {"device_uuid": {"uuid": ""}}, + "endpoint_uuid": {"uuid": ""} + }, + "rule_set": { + "ip" : "", + "mask": "", + "vlan": "" + } + }} + + ]} + } + ] +} \ No newline at end of file diff --git a/src/device/service/drivers/ietf_l3vpn/templates/tools.py b/src/device/service/drivers/ietf_l3vpn/templates/tools.py new file mode 100644 index 000000000..810a39035 --- /dev/null +++ b/src/device/service/drivers/ietf_l3vpn/templates/tools.py @@ -0,0 +1,69 @@ +# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (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 types import SimpleNamespace +import os +import json +import logging + +LOGGER = logging.getLogger(__name__) + +def create_request(resource_value): + LOGGER.info("Creating request for resource_value: %s", resource_value) + try: + BaseDir = os.path.dirname(os.path.abspath(__file__)) + json_path = os.path.join(BaseDir, 'ipowdm.json') + with open(json_path, 'r', encoding='utf-8') as f: + template = json.load(f) + + rule_set = resource_value[1]['rule_set'] + + # Completar los campos en el template cargado desde ipowdm.json + service = template["services"][0] + + # service_id + service["service_id"]["service_uuid"]["uuid"] = rule_set["uuid"] + + # endpoints + service_endpoints = service["service_endpoint_ids"] + service_endpoints[0]["device_id"]["device_uuid"]["uuid"] = rule_set["src"] + service_endpoints[0]["endpoint_uuid"]["uuid"] = rule_set + + service_endpoints[1]["device_id"]["device_uuid"]["uuid"] = rule_set["dst"] + service_endpoints[1]["endpoint_uuid"]["uuid"] = rule_set["dst"] + # config rules + config_rules = service["service_config"]["config_rules"] + + # regla 1 - endpoint origen + config_rules[0]["action"] = 1 + config_rules[0]["ip_link"]["endpoint_id"]["device_id"]["device_uuid"]["uuid"] = rule_set["src"] + config_rules[0]["ip_link"]["endpoint_id"]["endpoint_uuid"]["uuid"] = rule_set["src"] + config_rules[0]["ip_link"]["rule_set"]["ip"] = rule_set["bw"] + config_rules[0]["ip_link"]["rule_set"]["mask"] = rule_set["bw"] + config_rules[0]["ip_link"]["rule_set"]["vlan"] = rule_set["bw"] + + # regla 2 - endpoint destino + config_rules[1]["action"] = 1 + config_rules[1]["ip_link"]["endpoint_id"]["device_id"]["device_uuid"]["uuid"] = rule_set["dst"] + config_rules[1]["ip_link"]["endpoint_id"]["endpoint_uuid"]["uuid"] = rule_set["dst"] + config_rules[1]["ip_link"]["rule_set"]["ip"] = rule_set["bw"] + config_rules[1]["ip_link"]["rule_set"]["mask"] = rule_set["bw"] + config_rules[1]["ip_link"]["rule_set"]["vlan"] = rule_set["bw"] + + LOGGER.info("Sending POST request with payload: %s", json.dumps(template)) + response = requests.post(url, headers=headers, json=template, timeout=10) + return response + + except (OSError, json.JSONDecodeError, Exception) as e: + LOGGER.error("Error creating request: %s", str(e)) + return SimpleNamespace(status_code=500, text=str(e)) diff --git a/src/device/service/drivers/optical_tfs/OpticalTfsDriver.py b/src/device/service/drivers/optical_tfs/OpticalTfsDriver.py index 9cb2371b6..f3da49302 100644 --- a/src/device/service/drivers/optical_tfs/OpticalTfsDriver.py +++ b/src/device/service/drivers/optical_tfs/OpticalTfsDriver.py @@ -20,6 +20,7 @@ from common.type_checkers.Checkers import chk_string, chk_type from device.service.driver_api._Driver import _Driver, RESOURCE_ENDPOINTS, RESOURCE_SERVICES from device.service.driver_api.ImportTopologyEnum import ImportTopologyEnum, get_import_topology from .TfsApiClient import TfsApiClient +from .templates.tools import create_request #from .TfsOpticalClient import TfsOpticalClient LOGGER = logging.getLogger(__name__) @@ -119,15 +120,26 @@ class OpticalTfsDriver(_Driver): self.tac.check_credentials() for resource in resources: LOGGER.info('resource = {:s}'.format(str(resource))) - resource_key, resource_value = resource - try: - resource_value = json.loads(resource_value) - self.tac.setup_service(resource_value) - results.append((resource_key, True)) - except Exception as e: - MSG = 'Unhandled error processing resource_key({:s})' - LOGGER.exception(MSG.format(str(resource_key))) - results.append((resource_key, e)) + if 'tapi_lsp' in str(resource): + LOGGER.info('Processing tapi_lsp resource') + try: + create_request(resource) + LOGGER.info('Request created successfully') + results.append((resource, True)) + except Exception as e: + MSG = 'Invalid resource_value type: expected dict, got {:s}' + results.append((resource, e)) + else: + LOGGER.info('Processing non-tapi_lsp resource') + resource_key, resource_value = resource + try: + resource_value = json.loads(resource_value) + self.tac.setup_service(resource_value) + results.append((resource_key, True)) + except Exception as e: + MSG = 'Unhandled error processing resource_key({:s})' + LOGGER.exception(MSG.format(str(resource_key))) + results.append((resource_key, e)) return results @metered_subclass_method(METRICS_POOL) diff --git a/src/device/service/drivers/optical_tfs/templates/lsp.json b/src/device/service/drivers/optical_tfs/templates/lsp.json new file mode 100644 index 000000000..96f3517a1 --- /dev/null +++ b/src/device/service/drivers/optical_tfs/templates/lsp.json @@ -0,0 +1,37 @@ +{ + "tapi-connectivity:connectivity-service" : [ + { + "connectivity-direction" : "", + "end-point" : [ + { + "direction:" : "", + "layer-protocol-name" : "", + "layer-protocol-qualifier" : "", + "local-id" : "", + "service-interface-point" : { + "service-interface-point-uuid" : "" + } + }, + { + "direction:" : "", + "layer-protocol-name" : "", + "layer-protocol-qualifier" : "", + "local-id" : "", + "service-interface-point" : { + "service-interface-point-uuid" : "" + } + } + ], + "layer-protocol-name" : "", + "layer-protocol-qualifier" : "", + "requested-capacity" : { + "total-size" : { + "unit" : "", + "value" : "" + } + }, + "route-objective-function" : "10000", + "uuid" : "" + } + ] +} \ No newline at end of file diff --git a/src/device/service/drivers/optical_tfs/templates/tools.py b/src/device/service/drivers/optical_tfs/templates/tools.py new file mode 100644 index 000000000..be0b37bf8 --- /dev/null +++ b/src/device/service/drivers/optical_tfs/templates/tools.py @@ -0,0 +1,72 @@ +# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (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 +import logging +import os +from types import SimpleNamespace +import requests + +LOGGER = logging.getLogger(__name__) + +""" Create and send HTTP request based on a JSON template and provided resource value. + The JSON template is expected to be in the same directory as this script, named 'lsp.json'.""" +def create_request(resource_value): + LOGGER.info("Creating request lsp for resource_value: %s", resource_value) + try: + LOGGER.info("Loading JSON template from 'lsp.json'") + BaseDir = os.path.dirname(os.path.abspath(__file__)) + json_path = os.path.join(BaseDir, 'lsp.json') + with open(json_path, 'r', encoding='utf-8') as f: + template = json.load(f) + LOGGER.info("Template loaded successfully: %s", json.dumps(template, indent=2)) + rule_set = resource_value[1]['rule_set'] + + svc = template["tapi-connectivity:connectivity-service"][0] + LOGGER.info("Original template service: %s", json.dumps(svc, indent=2)) + svc["connectivity-direction"] = rule_set["direction"] + svc["layer-protocol-name"] = rule_set["layer_protocol_name"] + svc["layer-protocol-qualifier"] = rule_set["layer_protocol_qualifier"] + svc["requested-capacity"]["total-size"]["unit"] = "GHz" + svc["requested-capacity"]["total-size"]["value"] = rule_set["bw"] + svc["uuid"] = rule_set["uuid"] + LOGGER.info("Updated template: %s", json.dumps(template, indent=2)) + # src + svc["end-point"][0]["service-interface-point"]["service-interfacepoint-uuid"] = rule_set["src"] + svc["end-point"][0]["direction:"] = rule_set["direction"] + svc["end-point"][0]["layer-protocol-name"] = rule_set["layer-protocol-name"] + svc["end-point"][0]["layer-protocol-qualifier"] = rule_set["layer-protocol-qualifier"] + svc["end-point"][0]["local-id"] = rule_set["src"] + LOGGER.info("Source endpoint configured with UUID: %s", rule_set["src"]) + + # dst + svc["end-point"][1]["service-interface-point"]["service-interface-point-uuid"] = rule_set["dst"] + svc["end-point"][1]["direction:"] = rule_set["direction"] + svc["end-point"][1]["layer-protocol-name"] = rule_set["layer_protocol_name"] + svc["end-point"][1]["layer-protocol-qualifier"] = rule_set["layer_protocol_qualifier"] + svc["end-point"][1]["local-id"] = rule_set["dst"] + LOGGER.info("Destination endpoint configured with UUID: %s", rule_set["dst"]) + + url = "http://11.1.1.101:4901/3fb0df67-1c1d-546f-966a-8202f66677c0/restconf/data/tapi-common:context/tapi-connectivity:connectivity-context" + headers = { + "Content-Type": "application/json", + "Accept": "application/json" + } + LOGGER.info("Sending POST request to %s with payload: %s", url, json.dumps(template)) + response = requests.post(url, headers=headers, json=template, timeout=10) + + return response + + except (OSError, json.JSONDecodeError, requests.RequestException) as e: + LOGGER.error("Error creating request: %s", str(e)) diff --git a/src/pathcomp/frontend/service/algorithms/_Algorithm.py b/src/pathcomp/frontend/service/algorithms/_Algorithm.py index a5bfe1352..4cea8a0de 100644 --- a/src/pathcomp/frontend/service/algorithms/_Algorithm.py +++ b/src/pathcomp/frontend/service/algorithms/_Algorithm.py @@ -24,7 +24,7 @@ from pathcomp.frontend.Config import BACKEND_URL from .tools.EroPathToHops import eropath_to_hops from .tools.ComposeConfigRules import ( compose_device_config_rules, compose_l2nm_config_rules, compose_l3nm_config_rules, compose_tapi_config_rules, - generate_neighbor_endpoint_config_rules + generate_neighbor_endpoint_config_rules, compose_ipowdm_config_rules, compose_tapi_lsp_config_rules ) from .tools.ComposeRequest import compose_device, compose_link, compose_service from .tools.ComputeSubServices import ( @@ -136,7 +136,7 @@ class _Algorithm: if reply.status_code not in {requests.codes.ok}: # pylint: disable=no-member raise Exception('Backend error({:s}) for request({:s})'.format( str(self.raw_reply), json.dumps(request, sort_keys=True))) - + self.json_reply = reply.json() def add_connection_to_reply( @@ -189,6 +189,12 @@ class _Algorithm: elif service_type == ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE and rules_nb == 0: compose_tapi_config_rules(config_rules, service.service_config.config_rules) self.logger.info("Installing default rules for TAPI service") + elif service_type == ServiceTypeEnum.SERVICETYPE_IPOWDM and rules_nb == 0: + compose_ipowdm_config_rules(config_rules, service.service_config.config_rules) + self.logger.info("Installing default rules for IPOWDM service") + elif service_type == ServiceTypeEnum.SERVICETYPE_TAPI_LSP: + compose_tapi_lsp_config_rules(config_rules, service.service_config.config_rules) + self.logger.info("Installing default rules for TAPI LSP service") else: MSG = 'Unhandled generic Config Rules for service {:s} {:s}' self.logger.warning(MSG.format(str(service_uuid), str(ServiceTypeEnum.Name(service_type)))) @@ -307,7 +313,7 @@ class _Algorithm: service_key = (context_uuid, service_uuid) grpc_service = grpc_services.get(service_key) if grpc_service is None: raise Exception('Service({:s}) not found'.format(str(service_key))) - + #if connection_uuid in grpc_connections: continue grpc_connection = self.add_connection_to_reply(reply, str(uuid.uuid4()), grpc_service, path_hops) #grpc_connections[connection_uuid] = grpc_connection diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py index 073c3474f..ec45ee89d 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py @@ -54,6 +54,15 @@ TAPI_SETTINGS_FIELD_DEFAULTS = { 'direction' : 'UNIDIRECTIONAL', } +TAPI_LSP_SETINGS_FIELD_DEFAULTS = { + 'lsp_type' : 'TAPI_LSP_TYPE_UNI', + 'lsp_direction' : 'TAPI_LSP_DIRECTION_UNI', + 'lsp_bandwidth' : 50.0, + 'lsp_bandwidth_unit': 'GHz', + 'lsp_layer_protocol_name': 'PHOTONIC_MEDIA', + 'lsp_layer_protocol_qualifier': 'tapi-photonic-media:PHOTONIC_LAYER_QUALIFIER_NMC', +} + def find_custom_config_rule(config_rules : List, resource_name : str) -> Optional[Dict]: resource_value : Optional[Dict] = None for config_rule in config_rules: @@ -153,7 +162,7 @@ def compose_device_config_rules( device_endpoint_keys = set(itertools.product(device_keys, endpoint_keys)) if len(device_endpoint_keys.intersection(endpoints_traversed)) == 0: continue - + LOGGER.debug('[compose_device_config_rules] adding acl config rule') subservice_config_rules.append(config_rule) @@ -202,6 +211,16 @@ def compose_device_config_rules( LOGGER.debug('[compose_device_config_rules] end') +def compose_ipowdm_config_rules(main_service_config_rules : List, subservice_config_rules : List) -> None: + CONFIG_RULES: List[Tuple[str, dict]] = [(SETTINGS_RULE_NAME, L3NM_SETTINGS_FIELD_DEFAULTS)] + for rule_name, defaults in CONFIG_RULES: + compose_config_rules(main_service_config_rules, subservice_config_rules, rule_name, defaults) + +def compose_tapi_lsp_config_rules(main_service_config_rules : List, subservice_config_rules : List) -> None: + CONFIG_RULES: List[Tuple[str, dict]] = [(SETTINGS_RULE_NAME, TAPI_LSP_SETINGS_FIELD_DEFAULTS)] + for rule_name, defaults in CONFIG_RULES: + compose_config_rules(main_service_config_rules, subservice_config_rules, rule_name, defaults) + def pairwise(iterable : Iterable) -> Tuple[Iterable, Iterable]: # TODO: To be replaced by itertools.pairwise() when we move to Python 3.10 # Python 3.10 introduced method itertools.pairwise() diff --git a/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py index 2f55db0c6..f09edb737 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py @@ -26,6 +26,7 @@ PACKET_DEVICE_TYPES = { DeviceTypeEnum.IP_SDN_CONTROLLER, DeviceTypeEnum.EMULATED_IP_SDN_CONTROLLER, DeviceTypeEnum.PACKET_ROUTER, DeviceTypeEnum.EMULATED_PACKET_ROUTER, DeviceTypeEnum.PACKET_SWITCH, DeviceTypeEnum.EMULATED_PACKET_SWITCH, + DeviceTypeEnum.IPOWDM_ROUTER } L2_DEVICE_TYPES = { @@ -46,9 +47,13 @@ SERVICE_TYPE_L2NM = {ServiceTypeEnum.SERVICETYPE_L2NM} SERVICE_TYPE_L3NM = {ServiceTypeEnum.SERVICETYPE_L3NM} SERVICE_TYPE_LXNM = {ServiceTypeEnum.SERVICETYPE_L3NM, ServiceTypeEnum.SERVICETYPE_L2NM} SERVICE_TYPE_TAPI = {ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE} +SERVICE_TYPE_IPOWDM = {ServiceTypeEnum.SERVICETYPE_IPOWDM} +SERVICE_TYPE_TAPI_LSP = {ServiceTypeEnum.SERVICETYPE_TAPI_LSP} def get_service_type(device_type : DeviceTypeEnum, prv_service_type : ServiceTypeEnum) -> ServiceTypeEnum: if device_type in PACKET_DEVICE_TYPES and prv_service_type in SERVICE_TYPE_LXNM: return prv_service_type + if device_type in PACKET_DEVICE_TYPES and prv_service_type in SERVICE_TYPE_IPOWDM: return ServiceTypeEnum.SERVICETYPE_IPOWDM + if device_type in PACKET_DEVICE_TYPES and prv_service_type in SERVICE_TYPE_TAPI_LSP: return prv_service_type if device_type in L2_DEVICE_TYPES: return ServiceTypeEnum.SERVICETYPE_L2NM if device_type in OPTICAL_DEVICE_TYPES: return ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE if device_type in NETWORK_DEVICE_TYPES: return prv_service_type diff --git a/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java b/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java index bf7055e14..c502fa4b4 100644 --- a/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java +++ b/src/policy/src/main/java/org/etsi/tfs/policy/Serializer.java @@ -1166,6 +1166,10 @@ public class Serializer { return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_L3NM; case TAPI_CONNECTIVITY_SERVICE: return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE; + case IPOWDM: + return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_IPOWDM; + case TAPI_LSP: + return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_TAPI_LSP; case UNKNOWN: return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_UNKNOWN; default: @@ -1181,6 +1185,10 @@ public class Serializer { return ServiceTypeEnum.L3NM; case SERVICETYPE_TAPI_CONNECTIVITY_SERVICE: return ServiceTypeEnum.TAPI_CONNECTIVITY_SERVICE; + case SERVICETYPE_IPOWDM: + return ServiceTypeEnum.IPOWDM; + case SERVICETYPE_TAPI_LSP: + return ServiceTypeEnum.TAPI_LSP; case SERVICETYPE_UNKNOWN: case UNRECOGNIZED: default: diff --git a/src/service/service/ServiceServiceServicerImpl.py b/src/service/service/ServiceServiceServicerImpl.py index bf923eed9..e795ba94f 100644 --- a/src/service/service/ServiceServiceServicerImpl.py +++ b/src/service/service/ServiceServiceServicerImpl.py @@ -114,7 +114,7 @@ class ServiceServiceServicerImpl(ServiceServiceServicer): context_client, request.service_id, rw_copy=False, include_config_rules=True, include_constraints=True, include_endpoint_ids=True) - # Identify service constraints + # Identify service constraints num_disjoint_paths = None is_diverse = False gps_location_aware = False @@ -259,7 +259,7 @@ class ServiceServiceServicerImpl(ServiceServiceServicer): DEFAULT_TOPOLOGY_NAME, context_id_x) topology_details = context_client.GetTopologyDetails( TopologyId(**topology_id_x)) - + refresh_opticalcontroller(topology_id_x) # devices = get_devices_in_topology(context_client, TopologyId(**topology_id_x), ContextId(**context_id_x)) devices = topology_details.devices @@ -402,14 +402,14 @@ class ServiceServiceServicerImpl(ServiceServiceServicer): service.service_config.config_rules[0].custom.resource_value) ob_id = None flow_id = None - + if "ob_id" in c_rules_dict: ob_id = c_rules_dict["ob_id"] if ("flow_id" in c_rules_dict): flow_id = c_rules_dict["flow_id"] #if ("ob_id" in c_rules_dict): # ob_id = c_rules_dict["ob_id"] - + params['bitrate'] = bitrate params['dst' ] = dst params['src' ] = src @@ -560,7 +560,7 @@ class ServiceServiceServicerImpl(ServiceServiceServicer): str_old_connection = grpc_message_to_json_string(old_connection) extra_details = MSG.format(str_pathcomp_request, str_pathcomp_reply, str_old_connection) raise OperationFailedException('no-new-path-found', extra_details=extra_details) - + str_candidate_new_connections = [ grpc_message_to_json_string(candidate_new_connection) for candidate_new_connection in candidate_new_connections diff --git a/src/service/service/service_handler_api/FilterFields.py b/src/service/service/service_handler_api/FilterFields.py index a56bcf0f9..1bcabc82f 100644 --- a/src/service/service/service_handler_api/FilterFields.py +++ b/src/service/service/service_handler_api/FilterFields.py @@ -31,6 +31,8 @@ SERVICE_TYPE_VALUES = { ServiceTypeEnum.SERVICETYPE_QKD, ServiceTypeEnum.SERVICETYPE_INT, ServiceTypeEnum.SERVICETYPE_ACL, + ServiceTypeEnum.SERVICETYPE_IPOWDM, + ServiceTypeEnum.SERVICETYPE_TAPI_LSP, } DEVICE_DRIVER_VALUES = { diff --git a/src/service/service/service_handler_api/ServiceHandlerFactory.py b/src/service/service/service_handler_api/ServiceHandlerFactory.py index a5b3bed2a..45271dd72 100644 --- a/src/service/service/service_handler_api/ServiceHandlerFactory.py +++ b/src/service/service/service_handler_api/ServiceHandlerFactory.py @@ -119,11 +119,28 @@ def get_service_handler_class( str_service_key = grpc_message_to_json_string(service.service_id) - # Assume all devices involved in the service's connection must support at least one driver in common - common_device_drivers = get_common_device_drivers([ - get_device_supported_drivers(device) - for device in connection_devices.values() - ]) + # Checks if the service is of type ipowdm + if 'ipowdm' in str(service.service_config.config_rules): + ipowdm_device_uuid = service.service_config.config_rules[0].ipowdm.endpoint_id.device_id.device_uuid.uuid + for device in connection_devices.values(): + if device.name == ipowdm_device_uuid: + LOGGER.debug('Device(%s) supported drivers: %s', device.name, device.device_drivers) + common_device_drivers = device.device_drivers + # Checks if the service is of type tapi_lsp + elif 'tapi_lsp' in str(service.service_config.config_rules): + tapi_lsp_device_uuid = service.service_config.config_rules[0].tapi_lsp.endpoint_id.device_id.device_uuid.uuid + for device in connection_devices.values(): + if device.name == tapi_lsp_device_uuid: + LOGGER.debug('Device(%s) supported drivers: %s', device.name, device.device_drivers) + common_device_drivers = device.device_drivers + else: + for device in connection_devices.values(): + LOGGER.debug('Device(%s) supported drivers: %s', device.name, device.device_drivers) + + common_device_drivers = get_common_device_drivers([ + get_device_supported_drivers(device) + for device in connection_devices.values() + ]) filter_fields = { FilterFieldEnum.SERVICE_TYPE.value : service.service_type, # must be supported diff --git a/src/service/service/service_handler_api/SettingsHandler.py b/src/service/service/service_handler_api/SettingsHandler.py index b9b8b2950..a25f0d9c8 100644 --- a/src/service/service/service_handler_api/SettingsHandler.py +++ b/src/service/service/service_handler_api/SettingsHandler.py @@ -47,6 +47,20 @@ class SettingsHandler: ACL_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/index[{:d}]/acl_ruleset[{:s}]' key_or_path = ACL_KEY_TEMPLATE.format(device_uuid, endpoint_name,endpoint_index, acl_ruleset_name) value = grpc_message_to_json(config_rule.acl) + elif kind == 'ipowdm': + device_uuid = config_rule.ipowdm.endpoint_id.device_id.device_uuid.uuid + endpoint_uuid = config_rule.ipowdm.endpoint_id.endpoint_uuid.uuid + endpoint_name, endpoint_index = extract_endpoint_index(endpoint_uuid) + ipowdm_key_template = '/device[{:s}]/endpoint[{:s}]/index[{:d}]/ipowdm' + key_or_path = ipowdm_key_template.format(device_uuid, endpoint_name, endpoint_index) + value = grpc_message_to_json(config_rule.ipowdm) + elif kind == 'tapi_lsp': + device_uuid = config_rule.tapi_lsp.endpoint_id.device_id.device_uuid.uuid + endpoint_uuid = config_rule.tapi_lsp.endpoint_id.endpoint_uuid.uuid + endpoint_name, endpoint_index = extract_endpoint_index(endpoint_uuid) + TAPI_LSP_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/subindex[{:d}]/tapi_lsp' + key_or_path = TAPI_LSP_KEY_TEMPLATE.format(device_uuid, endpoint_name, endpoint_index) + value = config_rule.tapi_lsp else: MSG = 'Unsupported Kind({:s}) in ConfigRule({:s})' LOGGER.warning(MSG.format(str(kind), grpc_message_to_json_string(config_rule))) @@ -83,7 +97,7 @@ class SettingsHandler: if endpoint_settings is not None: return endpoint_settings return None - + def get_endpoint_acls(self, device : Device, endpoint : EndPoint) -> List [Tuple]: endpoint_name = endpoint.name device_keys = device.device_id.device_uuid.uuid, device.name @@ -93,12 +107,12 @@ class SettingsHandler: for endpoint_key in endpoint_keys: endpoint_settings_uri = '/device[{:s}]/endpoint[{:s}]'.format(device_key, endpoint_key) endpoint_settings = self.get(endpoint_settings_uri) - if endpoint_settings is None: continue + if endpoint_settings is None: continue endpoint_name, endpoint_index = extract_endpoint_index(endpoint_name) ACL_RULE_PREFIX = '/device[{:s}]/endpoint[{:s}]/'.format(device_key, endpoint_name) results = dump_subtree(endpoint_settings) - for res_key, res_value in results: + for res_key, res_value in results: if not res_key.startswith(ACL_RULE_PREFIX): continue if not "acl_ruleset" in res_key: continue acl_index = extract_index(res_value) @@ -106,6 +120,66 @@ class SettingsHandler: acl_rules.append((res_key, res_value)) return acl_rules + def get_endpoint_ipowdm(self, device : Device, endpoint : EndPoint) -> List [Tuple]: + endpoint_name = endpoint.name + device_keys = device.device_id.device_uuid.uuid, device.name + endpoint_keys = endpoint.endpoint_id.endpoint_uuid.uuid, endpoint.name + ipowdms = [] + LOGGER.debug('Getting IPOWDM for device(%s) ', device_keys) + + for device_key in device_keys: + LOGGER.debug('device_key = %s', device_key) + for endpoint_key in endpoint_keys: + endpoint_settings_uri = '/device[{:s}]/endpoint[{:s}]'.format(device_key, endpoint_key) + endpoint_settings = self.get(endpoint_settings_uri) + if endpoint_settings is None: continue + IPOWDM_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/'.format(device_key, endpoint_name) + + results = dump_subtree(endpoint_settings) + for res_key, res_value in results: + if not res_key.startswith(IPOWDM_KEY_TEMPLATE): continue + if not "ipowdm" in res_key: continue + ipowdms.append((res_key, res_value)) + setinterface_index = extract_index(res_value) + if not 'subindex[{:d}]'.format(setinterface_index) in res_key: continue + ipowdms.append((res_key, res_value)) + return ipowdms + + def get_endpoint_tapi_lsp(self, device : Device, endpoint : EndPoint) -> List [Tuple]: + endpoint_name = endpoint.name + device_keys = device.device_id.device_uuid.uuid, device.name + endpoint_keys = endpoint.endpoint_id.endpoint_uuid.uuid, endpoint.name + tapi_lsps = [] + LOGGER.debug('Getting TAPI LSPs for device(%s) ', device_keys) + for device_key in device_keys: + for endpoint_key in endpoint_keys: + endpoint_settings_uri = '/device[{:s}]/endpoint[{:s}]'.format(device_key, endpoint_key) + endpoint_settings = self.get(endpoint_settings_uri) + if endpoint_settings is None: continue + TAPI_LSP_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/'.format(device_key, endpoint_name) + + results = dump_subtree(endpoint_settings) + for res_key, res_value in results: + LOGGER.debug('Checking res_key %s', res_key) + if not res_key.startswith(TAPI_LSP_KEY_TEMPLATE): + LOGGER.debug('Skipping res_key %s not starting with %s', res_key, TAPI_LSP_KEY_TEMPLATE) + continue + if not "tapi_lsp" in res_key: + LOGGER.debug('Skipping res_key %s not containing tapi_lsp', res_key) + continue + if isinstance(res_value, str): + res_value = grpc_message_to_json(res_value, use_integers_for_enums=True) + tapi_lsps.append((res_key, res_value)) + setinterface_index = extract_index(res_value) + LOGGER.debug('setinterface_index = %d', setinterface_index) + if not 'subindex[{:d}]'.format(setinterface_index) in res_key: + LOGGER.debug('Skipping res_key %s not containing subindex[%d]', res_key, setinterface_index) + continue + tapi_lsps.append((res_key, res_value)) + LOGGER.debug('TAPI LSPs for device(%s) = %s', device_keys, tapi_lsps) + return tapi_lsps + + def set(self, key_or_path : Union[str, List[str]], value : Any) -> None: set_subnode_value(self.__resolver, self.__config, key_or_path, value) diff --git a/src/service/service/service_handler_api/Tools.py b/src/service/service/service_handler_api/Tools.py index fa132909a..78563355f 100644 --- a/src/service/service/service_handler_api/Tools.py +++ b/src/service/service/service_handler_api/Tools.py @@ -61,7 +61,7 @@ def get_device_endpoint_uuids(endpoint : Tuple[str, str, Optional[str]]) -> Tupl return device_uuid, endpoint_uuid def extract_endpoint_index(endpoint_name : str, default_index=0) -> Tuple[str, int]: - RE_PATTERN = '^(eth\-[0-9]+(?:\/[0-9]+)*)(?:\.([0-9]+))?$' + RE_PATTERN = r'^(eth\-[0-9]+(?:\/[0-9]+)*)(?:\.([0-9]+))?$' m = re.match(RE_PATTERN, endpoint_name) if m is None: return endpoint_name, default_index endpoint_name, index = m.groups() @@ -69,8 +69,8 @@ def extract_endpoint_index(endpoint_name : str, default_index=0) -> Tuple[str, i return endpoint_name, index def extract_index(res_value : str) -> int: - acl_value = grpc_message_to_json(res_value,use_integers_for_enums=True) - endpoint = acl_value.split("'endpoint_uuid': {'uuid': '") + res_value = str(grpc_message_to_json(res_value,use_integers_for_enums=True)) + endpoint = res_value.split("'endpoint_uuid': {'uuid': '") endpoint = endpoint[1].split("'}") _ , index = extract_endpoint_index(endpoint[0]) return index diff --git a/src/service/service/service_handlers/__init__.py b/src/service/service/service_handlers/__init__.py index 1aba88e30..31bca68e9 100644 --- a/src/service/service/service_handlers/__init__.py +++ b/src/service/service/service_handlers/__init__.py @@ -14,6 +14,7 @@ from common.proto.context_pb2 import DeviceDriverEnum, ServiceTypeEnum from ..service_handler_api.FilterFields import FilterFieldEnum +from .ipowdm.IpowdmServiceHandler import IpowdmServiceHandler from .l2nm_emulated.L2NMEmulatedServiceHandler import L2NMEmulatedServiceHandler from .l2nm_ietfl2vpn.L2NM_IETFL2VPN_ServiceHandler import L2NM_IETFL2VPN_ServiceHandler from .l3nm_ietfl3vpn.L3NM_IETFL3VPN_ServiceHandler import L3NM_IETFL3VPN_ServiceHandler @@ -30,6 +31,7 @@ from .p4_fabric_tna_int.p4_fabric_tna_int_service_handler import P4FabricINTServ from .p4_fabric_tna_l2_simple.p4_fabric_tna_l2_simple_service_handler import P4FabricL2SimpleServiceHandler from .p4_fabric_tna_l3.p4_fabric_tna_l3_service_handler import P4FabricL3ServiceHandler from .p4_fabric_tna_acl.p4_fabric_tna_acl_service_handler import P4FabricACLServiceHandler +from .tapi_lsp.Tapi_LSPServiceHandler import Tapi_LSPServiceHandler from .tapi_tapi.TapiServiceHandler import TapiServiceHandler from .tapi_xr.TapiXrServiceHandler import TapiXrServiceHandler from .optical_tfs.OpticalTfsServiceHandler import OpticalTfsServiceHandler @@ -178,5 +180,17 @@ SERVICE_HANDLERS = [ FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_L3NM, FilterFieldEnum.DEVICE_DRIVER : [DeviceDriverEnum.DEVICEDRIVER_RYU], } + ]), + (IpowdmServiceHandler, [ + { + FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_IPOWDM, + FilterFieldEnum.DEVICE_DRIVER : [DeviceDriverEnum.DEVICEDRIVER_IETF_L3VPN], + } + ]), + (Tapi_LSPServiceHandler, [ + { + FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_TAPI_LSP, + FilterFieldEnum.DEVICE_DRIVER : [DeviceDriverEnum.DEVICEDRIVER_OPTICAL_TFS], + } ]) ] diff --git a/src/service/service/service_handlers/ipowdm/ConfigRules.py b/src/service/service/service_handlers/ipowdm/ConfigRules.py new file mode 100644 index 000000000..e36dd7bab --- /dev/null +++ b/src/service/service/service_handlers/ipowdm/ConfigRules.py @@ -0,0 +1,58 @@ +# Copyright 2022-2025 ETSI OSG/SDG TeraFlowSDN (TFS) (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 Any, Dict, List, Optional, Tuple +from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set +from service.service.service_handler_api.AnyTreeTools import TreeNode +LOGGER = logging.getLogger(__name__) + +def get_value(field_name : str, *containers, default=None) -> Optional[Any]: + if len(containers) == 0: raise Exception('No containers specified') + for container in containers: + if field_name not in container: continue + return container[field_name] + return default + +def setup_config_rules( + endpoint_name : str, endpoint_ipowdm : List [Tuple] +) -> List[Dict]: + + json_config_rules = [ + ] + + for res_key, res_value in endpoint_ipowdm: + json_config_rules.append( + {'action': 1, 'ipowdm': res_value} + ) + + return json_config_rules + +def teardown_config_rules( + service_uuid : str, connection_uuid : str, device_uuid : str, endpoint_uuid : str, endpoint_name : str, + service_settings : TreeNode, device_settings : TreeNode, endpoint_settings : TreeNode +) -> List[Dict]: + + if service_settings is None: return [] + if device_settings is None: return [] + if endpoint_settings is None: return [] + + json_settings : Dict = service_settings.value + json_device_settings : Dict = device_settings.value + json_endpoint_settings : Dict = endpoint_settings.value + + settings = (json_settings, json_endpoint_settings, json_device_settings) + + json_config_rules = [] + return json_config_rules diff --git a/src/service/service/service_handlers/ipowdm/IpowdmServiceHandler.py b/src/service/service/service_handlers/ipowdm/IpowdmServiceHandler.py new file mode 100644 index 000000000..e22c5d28f --- /dev/null +++ b/src/service/service/service_handlers/ipowdm/IpowdmServiceHandler.py @@ -0,0 +1,184 @@ +# Copyright 2022-2025 ETSI OSG/SDG TeraFlowSDN (TFS) (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 +from typing import Any, List, Optional, Tuple, Union +from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method +from common.proto.context_pb2 import ConfigRule, DeviceId, Service +from common.tools.object_factory.Device import json_device_id +from common.type_checkers.Checkers import chk_type +from service.service.service_handler_api.Tools import get_device_endpoint_uuids, get_endpoint_matching +from service.service.service_handler_api._ServiceHandler import _ServiceHandler +from service.service.service_handler_api.SettingsHandler import SettingsHandler +from service.service.task_scheduler.TaskExecutor import TaskExecutor +from .ConfigRules import setup_config_rules, teardown_config_rules + +LOGGER = logging.getLogger(__name__) + +METRICS_POOL = MetricsPool('Service', 'Handler', labels={'handler': 'l3nm_openconfig'}) + +class IpowdmServiceHandler(_ServiceHandler): + def __init__( # pylint: disable=super-init-not-called + self, service : Service, task_executor : TaskExecutor, **settings + ) -> None: + self.__service = service + self.__task_executor = task_executor + self.__settings_handler = SettingsHandler(service.service_config, **settings) + + @metered_subclass_method(METRICS_POOL) + def SetEndpoint( + self, endpoints: List[Tuple[str, str, Optional[str]]], connection_uuid: Optional[str] = None + ) -> List[Union[bool, Exception]]: + chk_type('endpoints', endpoints, list) + endpoints = set(endpoints) # Remove duplicates + LOGGER.debug("[SetEndpoint] Called with endpoints: %s", endpoints) + LOGGER.debug("[SetEndpoint] Connection UUID: %s", connection_uuid) + + if len(endpoints) == 0: + LOGGER.warning("[SetEndpoint] No endpoints to process.") + return [] + + results = [] + for endpoint in endpoints: + + LOGGER.debug("[SetEndpoint] Processing endpoint tuple: %s", endpoint) + try: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + LOGGER.debug("[SetEndpoint] Device UUID: %s | Endpoint UUID: %s", device_uuid, endpoint_uuid) + + device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + + endpoint_ipowdm = self.__settings_handler.get_endpoint_ipowdm(device_obj, endpoint_obj) + for _, endpoint in endpoint_ipowdm: + LOGGER.debug("[SetEndpoint] Found endpoint: %s", endpoint) + if endpoint["endpoint_id"]["device_id"]["device_uuid"]["uuid"] != "TFS-PACKET": continue + LOGGER.debug("[SetEndpoint] endpoint_ipowdm: %s", endpoint_ipowdm) + + endpoint_name = endpoint_obj.name + LOGGER.debug("[SetEndpoint] Endpoint name: %s", endpoint_name) + + json_config_rules = setup_config_rules(endpoint_name, endpoint_ipowdm) + LOGGER.debug("[SetEndpoint] Generated json_config_rules: %s", json_config_rules) + + if len(json_config_rules) > 0: + LOGGER.info("[SetEndpoint] Applying %d config rules to device %s", len(json_config_rules), device_uuid) + del device_obj.device_config.config_rules[:] + json_config_rule = json_config_rules[0] + LOGGER.debug("[SetEndpoint] Adding config rule: %s", json_config_rule) + device_obj.device_config.config_rules.append(ConfigRule(**json_config_rule)) + + self.__task_executor.configure_device(device_obj) + LOGGER.info("[SetEndpoint] Configuration sent for device %s", device_uuid) + else: + LOGGER.warning("[SetEndpoint] No config rules generated for endpoint %s", endpoint_uuid) + + results.append(True) + + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('[SetEndpoint] Unable to SetEndpoint(%s)', str(endpoint)) + results.append(e) + + LOGGER.debug("[SetEndpoint] Final results: %s", results) + return results + + + @metered_subclass_method(METRICS_POOL) + def DeleteEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + + service_uuid = self.__service.service_id.service_uuid.uuid + settings = self.__settings_handler.get('/settings') + + results = [] + for endpoint in endpoints: + try: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + + device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + device_settings = self.__settings_handler.get_device_settings(device_obj) + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + endpoint_settings = self.__settings_handler.get_endpoint_settings(device_obj, endpoint_obj) + endpoint_name = endpoint_obj.name + + json_config_rules = teardown_config_rules( + service_uuid, connection_uuid, device_uuid, endpoint_uuid, endpoint_name, + settings, device_settings, endpoint_settings) + + if len(json_config_rules) > 0: + del device_obj.device_config.config_rules[:] + for json_config_rule in json_config_rules: + device_obj.device_config.config_rules.append(ConfigRule(**json_config_rule)) + self.__task_executor.configure_device(device_obj) + + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to DeleteEndpoint({:s})'.format(str(endpoint))) + results.append(e) + + return results + + @metered_subclass_method(METRICS_POOL) + def SetConstraint(self, constraints : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[SetConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + @metered_subclass_method(METRICS_POOL) + def DeleteConstraint(self, constraints : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[DeleteConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + @metered_subclass_method(METRICS_POOL) + def SetConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + results = [] + for resource in resources: + try: + resource_value = json.loads(resource[1]) + self.__settings_handler.set(resource[0], resource_value) + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to SetConfig({:s})'.format(str(resource))) + results.append(e) + + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + results = [] + for resource in resources: + try: + self.__settings_handler.delete(resource[0]) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to DeleteConfig({:s})'.format(str(resource))) + results.append(e) + + return results diff --git a/src/service/service/service_handlers/ipowdm/__init__.py b/src/service/service/service_handlers/ipowdm/__init__.py new file mode 100644 index 000000000..6242c89c7 --- /dev/null +++ b/src/service/service/service_handlers/ipowdm/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022-2025 ETSI OSG/SDG TeraFlowSDN (TFS) (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/service/service/service_handlers/tapi_lsp/ConfigRules.py b/src/service/service/service_handlers/tapi_lsp/ConfigRules.py new file mode 100644 index 000000000..0deb1625e --- /dev/null +++ b/src/service/service/service_handlers/tapi_lsp/ConfigRules.py @@ -0,0 +1,58 @@ +# Copyright 2022-2025 ETSI OSG/SDG TeraFlowSDN (TFS) (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 Any, Dict, List, Optional, Tuple +from common.tools.object_factory.ConfigRule import json_config_rule_delete, json_config_rule_set +from service.service.service_handler_api.AnyTreeTools import TreeNode +LOGGER = logging.getLogger(__name__) + +def get_value(field_name : str, *containers, default=None) -> Optional[Any]: + if len(containers) == 0: raise Exception('No containers specified') + for container in containers: + if field_name not in container: continue + return container[field_name] + return default + +def setup_config_rules( + endpoint_name : str, endpoint_tapi_lsp : List [Tuple] +) -> List[Dict]: + + json_config_rules = [ + ] + + for res_key, res_value in endpoint_tapi_lsp: + json_config_rules.append( + {'action': 1, 'tapi_lsp': res_value} + ) + + return json_config_rules + +def teardown_config_rules( + service_uuid : str, connection_uuid : str, device_uuid : str, endpoint_uuid : str, endpoint_name : str, + service_settings : TreeNode, device_settings : TreeNode, endpoint_settings : TreeNode +) -> List[Dict]: + + if service_settings is None: return [] + if device_settings is None: return [] + if endpoint_settings is None: return [] + + json_settings : Dict = service_settings.value + json_device_settings : Dict = device_settings.value + json_endpoint_settings : Dict = endpoint_settings.value + + settings = (json_settings, json_endpoint_settings, json_device_settings) + + json_config_rules = [] + return json_config_rules \ No newline at end of file diff --git a/src/service/service/service_handlers/tapi_lsp/Tapi_LSPServiceHandler.py b/src/service/service/service_handlers/tapi_lsp/Tapi_LSPServiceHandler.py new file mode 100644 index 000000000..36276e9d1 --- /dev/null +++ b/src/service/service/service_handlers/tapi_lsp/Tapi_LSPServiceHandler.py @@ -0,0 +1,186 @@ +# Copyright 2022-2025 ETSI OSG/SDG TeraFlowSDN (TFS) (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 +from typing import Any, List, Optional, Tuple, Union +from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method +from common.proto.context_pb2 import ConfigRule, DeviceId, Service +from common.tools.object_factory.Device import json_device_id +from common.type_checkers.Checkers import chk_type +from service.service.service_handler_api.Tools import get_device_endpoint_uuids, get_endpoint_matching +from service.service.service_handler_api._ServiceHandler import _ServiceHandler +from service.service.service_handler_api.SettingsHandler import SettingsHandler +from service.service.task_scheduler.TaskExecutor import TaskExecutor +from .ConfigRules import setup_config_rules, teardown_config_rules + +LOGGER = logging.getLogger(__name__) + +METRICS_POOL = MetricsPool('Service', 'Handler', labels={'handler': 'l3nm_openconfig'}) + +class Tapi_LSPServiceHandler(_ServiceHandler): + def __init__( # pylint: disable=super-init-not-called + self, service : Service, task_executor : TaskExecutor, **settings + ) -> None: + self.__service = service + self.__task_executor = task_executor + self.__settings_handler = SettingsHandler(service.service_config, **settings) + + @metered_subclass_method(METRICS_POOL) + def SetEndpoint( + self, endpoints: List[Tuple[str, str, Optional[str]]], connection_uuid: Optional[str] = None + ) -> List[Union[bool, Exception]]: + chk_type('endpoints', endpoints, list) + endpoints = set(endpoints) # Remove duplicates + LOGGER.debug("[SetEndpoint] Called with endpoints: %s", endpoints) + LOGGER.debug("[SetEndpoint] Connection UUID: %s", connection_uuid) + + if len(endpoints) == 0: + LOGGER.warning("[SetEndpoint] No endpoints to process.") + return [] + + results = [] + for endpoint in endpoints: + + LOGGER.debug("[SetEndpoint] Processing endpoint tuple: %s", endpoint) + try: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + LOGGER.debug("[SetEndpoint] Device UUID: %s | Endpoint UUID: %s", device_uuid, endpoint_uuid) + + device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + + endpoint_tapi_lsp = self.__settings_handler.get_endpoint_tapi_lsp(device_obj, endpoint_obj) + for _, endpoint in endpoint_tapi_lsp: + LOGGER.debug("[SetEndpoint] Found endpoint: %s", endpoint) + if endpoint.endpoint_id.device_id.device_uuid.uuid != "TFS-OPTICAL":continue + + LOGGER.debug("[SetEndpoint] endpoint_ipowdm: %s", endpoint_tapi_lsp) + + + endpoint_name = endpoint_obj.name + LOGGER.debug("[SetEndpoint] Endpoint name: %s", endpoint_name) + + json_config_rules = setup_config_rules(endpoint_name, endpoint_tapi_lsp) + LOGGER.debug("[SetEndpoint] Generated json_config_rules: %s", json_config_rules) + + if len(json_config_rules) > 0: + LOGGER.info("[SetEndpoint] Applying %d config rules to device %s", len(json_config_rules), device_uuid) + del device_obj.device_config.config_rules[:] + json_config_rule = json_config_rules[0] + LOGGER.debug("[SetEndpoint] Adding config rule: %s", json_config_rule) + device_obj.device_config.config_rules.append(ConfigRule(**json_config_rule)) + + self.__task_executor.configure_device(device_obj) + LOGGER.info("[SetEndpoint] Configuration sent for device %s", device_uuid) + else: + LOGGER.warning("[SetEndpoint] No config rules generated for endpoint %s", endpoint_uuid) + + results.append(True) + + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('[SetEndpoint] Unable to SetEndpoint(%s)', str(endpoint)) + results.append(e) + + LOGGER.debug("[SetEndpoint] Final results: %s", results) + return results + + + @metered_subclass_method(METRICS_POOL) + def DeleteEndpoint( + self, endpoints : List[Tuple[str, str, Optional[str]]], connection_uuid : Optional[str] = None + ) -> List[Union[bool, Exception]]: + chk_type('endpoints', endpoints, list) + if len(endpoints) == 0: return [] + + service_uuid = self.__service.service_id.service_uuid.uuid + settings = self.__settings_handler.get('/settings') + + results = [] + for endpoint in endpoints: + try: + device_uuid, endpoint_uuid = get_device_endpoint_uuids(endpoint) + + device_obj = self.__task_executor.get_device(DeviceId(**json_device_id(device_uuid))) + device_settings = self.__settings_handler.get_device_settings(device_obj) + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + endpoint_settings = self.__settings_handler.get_endpoint_settings(device_obj, endpoint_obj) + endpoint_name = endpoint_obj.name + + json_config_rules = teardown_config_rules( + service_uuid, connection_uuid, device_uuid, endpoint_uuid, endpoint_name, + settings, device_settings, endpoint_settings) + + if len(json_config_rules) > 0: + del device_obj.device_config.config_rules[:] + for json_config_rule in json_config_rules: + device_obj.device_config.config_rules.append(ConfigRule(**json_config_rule)) + self.__task_executor.configure_device(device_obj) + + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to DeleteEndpoint({:s})'.format(str(endpoint))) + results.append(e) + + return results + + @metered_subclass_method(METRICS_POOL) + def SetConstraint(self, constraints : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[SetConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + @metered_subclass_method(METRICS_POOL) + def DeleteConstraint(self, constraints : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('constraints', constraints, list) + if len(constraints) == 0: return [] + + msg = '[DeleteConstraint] Method not implemented. Constraints({:s}) are being ignored.' + LOGGER.warning(msg.format(str(constraints))) + return [True for _ in range(len(constraints))] + + @metered_subclass_method(METRICS_POOL) + def SetConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + results = [] + for resource in resources: + try: + resource_value = json.loads(resource[1]) + self.__settings_handler.set(resource[0], resource_value) + results.append(True) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to SetConfig({:s})'.format(str(resource))) + results.append(e) + + return results + + @metered_subclass_method(METRICS_POOL) + def DeleteConfig(self, resources : List[Tuple[str, Any]]) -> List[Union[bool, Exception]]: + chk_type('resources', resources, list) + if len(resources) == 0: return [] + + results = [] + for resource in resources: + try: + self.__settings_handler.delete(resource[0]) + except Exception as e: # pylint: disable=broad-except + LOGGER.exception('Unable to DeleteConfig({:s})'.format(str(resource))) + results.append(e) + + return results diff --git a/src/service/service/service_handlers/tapi_lsp/__init__.py b/src/service/service/service_handlers/tapi_lsp/__init__.py new file mode 100644 index 000000000..ec5fa1145 --- /dev/null +++ b/src/service/service/service_handlers/tapi_lsp/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022-2025 ETSI OSG/SDG TeraFlowSDN (TFS) (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. \ No newline at end of file -- GitLab From 168b56ca595ecb306f4aa53e2229a2b6a82cbf2a Mon Sep 17 00:00:00 2001 From: armingol Date: Thu, 11 Sep 2025 12:21:46 +0000 Subject: [PATCH 02/21] Enhance service definitions and API integration for IPoWDM and TAPI LSP - Added RuleEndpoint message to ipowdm.proto for better endpoint management. - Updated IpowdmRuleSet to use repeated RuleEndpoint for source and destination. - Enhanced tapi_lsp.proto with additional fields for tenant_uuid and link_uuid_path. - Modified ipowdm.json to include detailed service configurations and endpoints. - Improved tools.py for IPoWDM to handle requests with new structure and added error handling. - Updated lsp.json to include new media channel specifications and endpoint configurations. - Created Resources.py and Tools.py for E2E service management, including DELETE operations. - Registered new E2E API in app.py for handling service deletions. - Updated ServiceHandlerFactory.py and Tapi_LSPServiceHandler.py for better logging and context management. - Modified home.html to reflect the End-to-End service context in the UI. --- proto/ipowdm.proto | 18 +- proto/tapi_lsp.proto | 6 +- .../drivers/ietf_l3vpn/templates/ipowdm.json | 103 ++++++--- .../drivers/ietf_l3vpn/templates/tools.py | 152 +++++++++---- .../drivers/optical_tfs/templates/lsp.json | 19 +- .../drivers/optical_tfs/templates/tools.py | 62 ++++-- src/nbi/service/app.py | 3 +- src/nbi/service/e2e_services/Resources.py | 68 ++++++ src/nbi/service/e2e_services/Tools.py | 200 ++++++++++++++++++ src/nbi/service/e2e_services/__init__.py | 25 +++ .../ServiceHandlerFactory.py | 4 +- .../tapi_lsp/Tapi_LSPServiceHandler.py | 7 + src/webui/service/templates/main/home.html | 2 +- 13 files changed, 569 insertions(+), 100 deletions(-) create mode 100644 src/nbi/service/e2e_services/Resources.py create mode 100644 src/nbi/service/e2e_services/Tools.py create mode 100644 src/nbi/service/e2e_services/__init__.py diff --git a/proto/ipowdm.proto b/proto/ipowdm.proto index a4b4085d4..667467b16 100644 --- a/proto/ipowdm.proto +++ b/proto/ipowdm.proto @@ -15,10 +15,18 @@ syntax = "proto3"; package ipowdm; +message RuleEndpoint { + string uuid = 1; + string ip_address = 2; + string ip_mask = 3; + int32 vlan_id = 4; + float power = 5; + float frequency = 6; +} + message IpowdmRuleSet { - string src = 1; - string dst = 2; - string uuid = 3; - string bw = 4; - string unit = 5; + repeated RuleEndpoint src = 1; + repeated RuleEndpoint dst = 2; + int32 bw = 3; + string uuid = 4; } \ No newline at end of file diff --git a/proto/tapi_lsp.proto b/proto/tapi_lsp.proto index f46060048..48d8537b3 100644 --- a/proto/tapi_lsp.proto +++ b/proto/tapi_lsp.proto @@ -20,9 +20,13 @@ message TapiLspRuleSet { string dst = 2; string uuid = 3; string bw = 4; - string direction = 5; + string tenant_uuid = 5; string layer_protocol_name = 6; string layer_protocol_qualifier = 7; string lower_frequency_mhz = 8; string upper_frequency_mhz = 9; + repeated string link_uuid_path = 10; + string granularity = 11; + string grid_type = 12; + string direction = 13; } \ No newline at end of file diff --git a/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json b/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json index 9b30665a4..f1ac12dca 100644 --- a/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json +++ b/src/device/service/drivers/ietf_l3vpn/templates/ipowdm.json @@ -2,42 +2,85 @@ "services": [ { "service_id": { - "context_id": {"context_uuid": {"uuid": "admin"}}, - "service_uuid": {"uuid": "IP-Link2"} + "context_id": { + "context_uuid": { + "uuid": "admin" + } + }, + "service_uuid": { + "uuid": "8078f62d-91f4-55fa-81dd-de369e414be8" + } + }, + "service_type": 12, + "service_status": { + "service_status": 1 }, - "service_type": 8, - "service_status": {"service_status": 1}, "service_endpoint_ids": [ - {"device_id": {"device_uuid": {"uuid": ""}},"endpoint_uuid": {"uuid": ""}}, - {"device_id": {"device_uuid": {"uuid": ""}},"endpoint_uuid": {"uuid": ""}} - ], - "service_constraints": [], - - "service_config": {"config_rules": [ - {"action": 1, "ip_link": { - "endpoint_id": { - "device_id": {"device_uuid": {"uuid": ""}}, - "endpoint_uuid": {"uuid": ""} + { + "device_id": { + "device_uuid": { + "uuid": "IP1" + } }, - "rule_set": { - "ip" : "", - "mask": "", - "vlan": "" + "endpoint_uuid": { + "uuid": "PORT-xe4" } - }}, - {"action": 1, "ip_link": { - "endpoint_id": { - "device_id": {"device_uuid": {"uuid": ""}}, - "endpoint_uuid": {"uuid": ""} + }, + { + "device_id": { + "device_uuid": { + "uuid": "IP2" + } }, - "rule_set": { - "ip" : "", - "mask": "", - "vlan": "" + "endpoint_uuid": { + "uuid": "PORT-xe4" + } + } + ], + "service_constraints": [], + "service_config": { + "config_rules": [ + { + "action": 1, + "ipowdm": { + "endpoint_id": { + "device_id": { + "device_uuid": { + "uuid": "IP1" + } + }, + "endpoint_uuid": { + "uuid": "PORT-xe4" + } + }, + "rule_set": { + "src": [ + { + "uuid": "Phoenix-1", + "ip_address": "10.10.1.1", + "ip_mask": "/24", + "vlan_id": 100, + "power": 0, + "frequency": 194700 + } + ], + "dst": [ + { + "uuid": "Phoenix-2", + "ip_address": "10.10.2.1", + "ip_mask": "/24", + "vlan_id": 100, + "power": 0, + "frequency": 194700 + } + ], + "bw": 100, + "uuid": "95876830-8396-5241-9bbd-7bfa248232e7" + } + } } - }} - - ]} + ] + } } ] } \ No newline at end of file diff --git a/src/device/service/drivers/ietf_l3vpn/templates/tools.py b/src/device/service/drivers/ietf_l3vpn/templates/tools.py index 810a39035..5adda4f45 100644 --- a/src/device/service/drivers/ietf_l3vpn/templates/tools.py +++ b/src/device/service/drivers/ietf_l3vpn/templates/tools.py @@ -16,9 +16,29 @@ import os import json import logging +import requests + LOGGER = logging.getLogger(__name__) def create_request(resource_value): + """ Create and send HTTP request based on a JSON template and provided resource value. + The JSON template is expected to be in the same directory as this script, named 'ipowdm.json'. + Example resource_value: + {"rule_set": { + "uuid": "unique-service-uuid", + "bw": 100, + "src": [{"uuid": "src-device-uuid", "ip_address": "192.168.1.1", "ip_mask": "24", "vlan_id": 100, "power": 10, "frequency": 193100}], + "dst": [{"uuid": "dst-device-uuid", "ip_address": "192.168.3.3", "ip_mask": "24", "vlan_id": 100, "power": 10, "frequency": 193100}] + }} + The src and dst fields are lists to accommodate future extensions for multi-endpoint scenarios. + The request is sent to a predefined URL with appropriate headers. + Returns a response-like object with status_code and text attributes. + In case of error, returns a SimpleNamespace with status_code 500 and the error message in text. + + Note: The actual HTTP request sending is currently mocked for testing purposes. + The URL and headers are hardcoded for demonstration and should be adapted as needed. + """ + LOGGER.info("Creating request for resource_value: %s", resource_value) try: BaseDir = os.path.dirname(os.path.abspath(__file__)) @@ -26,44 +46,102 @@ def create_request(resource_value): with open(json_path, 'r', encoding='utf-8') as f: template = json.load(f) - rule_set = resource_value[1]['rule_set'] - - # Completar los campos en el template cargado desde ipowdm.json - service = template["services"][0] - - # service_id - service["service_id"]["service_uuid"]["uuid"] = rule_set["uuid"] - - # endpoints - service_endpoints = service["service_endpoint_ids"] - service_endpoints[0]["device_id"]["device_uuid"]["uuid"] = rule_set["src"] - service_endpoints[0]["endpoint_uuid"]["uuid"] = rule_set - - service_endpoints[1]["device_id"]["device_uuid"]["uuid"] = rule_set["dst"] - service_endpoints[1]["endpoint_uuid"]["uuid"] = rule_set["dst"] - # config rules - config_rules = service["service_config"]["config_rules"] - - # regla 1 - endpoint origen - config_rules[0]["action"] = 1 - config_rules[0]["ip_link"]["endpoint_id"]["device_id"]["device_uuid"]["uuid"] = rule_set["src"] - config_rules[0]["ip_link"]["endpoint_id"]["endpoint_uuid"]["uuid"] = rule_set["src"] - config_rules[0]["ip_link"]["rule_set"]["ip"] = rule_set["bw"] - config_rules[0]["ip_link"]["rule_set"]["mask"] = rule_set["bw"] - config_rules[0]["ip_link"]["rule_set"]["vlan"] = rule_set["bw"] - - # regla 2 - endpoint destino - config_rules[1]["action"] = 1 - config_rules[1]["ip_link"]["endpoint_id"]["device_id"]["device_uuid"]["uuid"] = rule_set["dst"] - config_rules[1]["ip_link"]["endpoint_id"]["endpoint_uuid"]["uuid"] = rule_set["dst"] - config_rules[1]["ip_link"]["rule_set"]["ip"] = rule_set["bw"] - config_rules[1]["ip_link"]["rule_set"]["mask"] = rule_set["bw"] - config_rules[1]["ip_link"]["rule_set"]["vlan"] = rule_set["bw"] - - LOGGER.info("Sending POST request with payload: %s", json.dumps(template)) - response = requests.post(url, headers=headers, json=template, timeout=10) + # rule_set = resource_value[1]['rule_set'] + + # service = template["services"][0] + + # # service_id + # service["service_id"]["service_uuid"]["uuid"] = rule_set["uuid"] + + # # endpoints + # service_endpoints = service["service_endpoint_ids"] + + # src = rule_set["src"][0] + # service_endpoints[0]["device_id"]["device_uuid"]["uuid"] = src["uuid"] + # service_endpoints[0]["endpoint_uuid"]["uuid"] = "mgmt" + + # dst = rule_set["dst"][0] + # service_endpoints[1]["device_id"]["device_uuid"]["uuid"] = dst["uuid"] + # service_endpoints[1]["endpoint_uuid"]["uuid"] = "mgmt" + + # config_rules = service["service_config"]["config_rules"] + + # # rule 1 - source + # config_rules[0]["action"] = 1 + # config_rules[0]["activate_transceiver"]["endpoint_id"]["device_id"]["device_uuid"]["uuid"] = src["uuid"] + # config_rules[0]["activate_transceiver"]["endpoint_id"]["endpoint_uuid"]["uuid"] = "mgmt" + + # config_rules[0]["activate_transceiver"]["rule_set"]["ip"] = src["ip_address"] + # config_rules[0]["activate_transceiver"]["rule_set"]["mask"] = src["ip_mask"] + # config_rules[0]["activate_transceiver"]["rule_set"]["vlan"] = src["vlan_id"] + # config_rules[0]["activate_transceiver"]["rule_set"]["bw"] = rule_set["bw"] + # config_rules[0]["activate_transceiver"]["rule_set"]["power"] = src.get("power", 0) + # config_rules[0]["activate_transceiver"]["rule_set"]["uuid"] = rule_set["uuid"] + # config_rules[0]["activate_transceiver"]["rule_set"]["frequency"] = src.get("frequency", 0) + + # LOGGER.info("Sending POST SOURCE request with payload: %s", json.dumps(template, indent=2)) + + # # rule 2 - destination + # config_rules[0]["action"] = 1 + # config_rules[0]["activate_transceiver"]["endpoint_id"]["device_id"]["device_uuid"]["uuid"] = dst["uuid"] + # config_rules[0]["activate_transceiver"]["endpoint_id"]["endpoint_uuid"]["uuid"] = "mgmt" + + # config_rules[0]["activate_transceiver"]["rule_set"]["ip"] = dst["ip_address"] + # config_rules[0]["activate_transceiver"]["rule_set"]["mask"] = dst["ip_mask"] + # config_rules[0]["activate_transceiver"]["rule_set"]["vlan"] = dst["vlan_id"] + # config_rules[0]["activate_transceiver"]["rule_set"]["bw"] = rule_set["bw"] + # config_rules[0]["activate_transceiver"]["rule_set"]["power"] = dst.get("power", 0) + # config_rules[0]["activate_transceiver"]["rule_set"]["uuid"] = rule_set["uuid"] + # config_rules[0]["activate_transceiver"]["rule_set"]["frequency"] = dst.get("frequency", 0) + + LOGGER.info("Sending POST DSTINATION request with payload: %s", json.dumps(template, indent=2)) + response = tfs_post(template) + # response = FakeResponse() + return response - except (OSError, json.JSONDecodeError, Exception) as e: + except (OSError, json.JSONDecodeError, KeyError, TypeError) as e: LOGGER.error("Error creating request: %s", str(e)) return SimpleNamespace(status_code=500, text=str(e)) + +class FakeResponse: + """_Fake response object for testing purposes.""" + def __init__(self): + self.ok = True + self.status_code = 200 + self.text = '{"message": "OK"}' + + def json(self): + """Return a sample JSON response.""" + return {"message": "OK"} +def tfs_post(request): + """ + Send a POST request to the TeraFlow Service Orchestrator. + + Args: + ip (str): IP address of the TeraFlow Service Orchestrator. + request (dict): The request payload to be sent. + + Returns: + dict: The response from the TeraFlow Service Orchestrator. + """ + user="admin" + password="admin" + token="" + session = requests.Session() + session.auth = (user, password) + url=f'http://10.95.86.62/webui' + response=session.get(url=url) + for item in response.iter_lines(): + if"csrf_token" in str(item): + string=str(item).split(' dict: + response = {} + # allocationDirection = '??' # String: 00 = Downlink (towards the UE); 01 = Uplink (towards the application/session); 10 = Symmetrical + response['appInsId'] = service.service_id.service_uuid.uuid # String: Application instance identifier + for constraint in service.service_constraints: + if constraint.WhichOneof('constraint') == 'sla_capacity': + # String: Size of requested fixed BW allocation in [bps] + fixed_allocation = Decimal(constraint.sla_capacity.capacity_gbps * 1.e9) + fixed_allocation = fixed_allocation.quantize(Decimal('0.1'), rounding=ROUND_HALF_EVEN) + response['fixedAllocation'] = str(fixed_allocation) + break + + for config_rule in service.service_config.config_rules: + resource_value_json = json.loads(config_rule.custom.resource_value) + if config_rule.custom.resource_key != '/request': + continue + for key in ['allocationDirection', 'fixedBWPriority', 'requestType', 'sourceIp', 'sourcePort', 'dstPort', 'protocol', 'sessionFilter']: + if key not in resource_value_json: + continue + + if key == 'sessionFilter': + response[key] = [resource_value_json[key]] + elif key == 'requestType': + response[key] = str(resource_value_json[key]) + else: + response[key] = resource_value_json[key] + + unixtime = time.time() + response['timeStamp'] = { # Time stamp to indicate when the corresponding information elements are sent + "seconds": int(unixtime), + "nanoseconds": int(unixtime%1*1e9) + } + + return response + +def bwInfo_2_service(client, bw_info: dict) -> Service: + # add description to allocationDirection code + if 'sessionFilter' in bw_info: + bw_info['sessionFilter'] = bw_info['sessionFilter'][0] # Discard other items in sessionFilter field + + service = Service() + + service_config_rules = service.service_config.config_rules + + + request_cr_key = '/request' + request_cr_value = {k:bw_info[k] for k in MEC_CONSIDERED_FIELDS} + + config_rule = ConfigRule() + config_rule.action = ConfigActionEnum.CONFIGACTION_SET + config_rule_custom = ConfigRule_Custom() + config_rule_custom.resource_key = request_cr_key + config_rule_custom.resource_value = json.dumps(request_cr_value) + config_rule.custom.CopyFrom(config_rule_custom) + service_config_rules.append(config_rule) + + if 'sessionFilter' in bw_info: + a_ip = bw_info['sessionFilter']['sourceIp'] + z_ip = bw_info['sessionFilter']['dstAddress'] + + devices = client.ListDevices(Empty()).devices + ip_interface_name_dict = {} + for device in devices: + device_endpoint_uuids = {ep.name:ep.endpoint_id.endpoint_uuid.uuid for ep in device.device_endpoints} + skip_device = True + for cr in device.device_config.config_rules: + if cr.WhichOneof('config_rule') != 'custom': + continue + match_subif = RE_CONFIG_RULE_IF_SUBIF.match(cr.custom.resource_key) + if not match_subif: + continue + address_ip = json.loads(cr.custom.resource_value).get('address_ip') + short_port_name = match_subif.groups(0)[0] + ip_interface_name_dict[address_ip] = short_port_name + if address_ip not in [a_ip, z_ip]: + continue + port_name = 'PORT-' + short_port_name # `PORT-` added as prefix + ep_id = EndPointId() + ep_id.endpoint_uuid.uuid = device_endpoint_uuids[port_name] + ep_id.device_id.device_uuid.uuid = device.device_id.device_uuid.uuid + service.service_endpoint_ids.append(ep_id) + # add interface config rules + endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device.name, port_name, VLAN_TAG) + if address_ip in a_ip: + router_id = ROUTER_ID_A + policy_az = POLICY_AZ + policy_za = POLICY_ZA + neighbor_bgp_interface_address_ip = BGP_NEIGHBOR_IP_Z + self_bgp_interface_address_ip = BGP_NEIGHBOR_IP_A + else: + router_id = ROUTER_ID_Z + policy_az = POLICY_ZA + policy_za = POLICY_AZ + neighbor_bgp_interface_address_ip= BGP_NEIGHBOR_IP_A + self_bgp_interface_address_ip = BGP_NEIGHBOR_IP_Z + endpoint_field_updates = { + 'address_ip': (address_ip, True), + 'address_prefix' : (PREFIX_LENGTH, True), + 'sub_interface_index': (0, True), + } + LOGGER.debug(f'BEFORE UPDATE -> device.device_config.config_rules: {service_config_rules}') + update_config_rule_custom(service_config_rules, endpoint_settings_key, endpoint_field_updates) + LOGGER.debug(f'AFTER UPDATE -> device.device_config.config_rules: {service_config_rules}') + skip_device = False + if skip_device: + continue + device_field_updates = { + 'bgp_as':(BGP_AS, True), + 'route_distinguisher': (ROUTE_DISTINGUISHER, True), + 'router_id': (router_id, True), + 'policy_AZ': (policy_az, True), + 'policy_ZA': (policy_za, True), + 'neighbor_bgp_interface_address_ip': (neighbor_bgp_interface_address_ip, True), + 'self_bgp_interface_name': (ip_interface_name_dict[self_bgp_interface_address_ip], True), + 'self_bgp_interface_address_ip': (self_bgp_interface_address_ip, True), + 'bgp_interface_address_prefix': (PREFIX_LENGTH, True) + } + device_settings_key = DEVICE_SETTINGS_KEY.format(device.name) + LOGGER.debug(f'BEFORE UPDATE -> device.device_config.config_rules: {service_config_rules}') + update_config_rule_custom(service_config_rules, device_settings_key, device_field_updates) + LOGGER.debug(f'AFTER UPDATE -> device.device_config.config_rules: {service_config_rules}') + + settings_cr_key = '/settings' + settings_cr_value = {} + update_config_rule_custom(service_config_rules, settings_cr_key, settings_cr_value) + + service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED + service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM + + if 'appInsId' in bw_info: + service.service_id.service_uuid.uuid = bw_info['appInsId'] + service.service_id.context_id.context_uuid.uuid = 'admin' + service.name = bw_info['appInsId'] + + if 'fixedAllocation' in bw_info: + capacity = Constraint_SLA_Capacity() + capacity.capacity_gbps = float(bw_info['fixedAllocation']) / 1.e9 + constraint = Constraint() + constraint.sla_capacity.CopyFrom(capacity) + service.service_constraints.append(constraint) + + return service + + +def format_grpc_to_json(grpc_reply): + return jsonify(grpc_message_to_json(grpc_reply)) + +def grpc_context_id(context_uuid): + return ContextId(**json_context_id(context_uuid)) + +def grpc_service_id(context_uuid, service_uuid): + return ServiceId(**json_service_id(service_uuid, context_id=json_context_id(context_uuid))) diff --git a/src/nbi/service/e2e_services/__init__.py b/src/nbi/service/e2e_services/__init__.py new file mode 100644 index 000000000..d90f352f3 --- /dev/null +++ b/src/nbi/service/e2e_services/__init__.py @@ -0,0 +1,25 @@ +# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (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 nbi.service.NbiApplication import NbiApplication +from .Resources import E2EInfoDelete + +URL_PREFIX = '/restconf/E2E/v1' + +def register_etsi_api(nbi_app : NbiApplication): + + nbi_app.add_rest_api_resource( + E2EInfoDelete, + URL_PREFIX + '/service/', + endpoint='etsi_E2E.e2e_info_delete' + ) diff --git a/src/service/service/service_handler_api/ServiceHandlerFactory.py b/src/service/service/service_handler_api/ServiceHandlerFactory.py index 45271dd72..b7da0fa35 100644 --- a/src/service/service/service_handler_api/ServiceHandlerFactory.py +++ b/src/service/service/service_handler_api/ServiceHandlerFactory.py @@ -118,7 +118,7 @@ def get_service_handler_class( ) -> Optional['_ServiceHandler']: str_service_key = grpc_message_to_json_string(service.service_id) - + LOGGER.debug('Selecting service handler for service(%s)...', str(service.service_config.config_rules)) # Checks if the service is of type ipowdm if 'ipowdm' in str(service.service_config.config_rules): ipowdm_device_uuid = service.service_config.config_rules[0].ipowdm.endpoint_id.device_id.device_uuid.uuid @@ -129,8 +129,10 @@ def get_service_handler_class( # Checks if the service is of type tapi_lsp elif 'tapi_lsp' in str(service.service_config.config_rules): tapi_lsp_device_uuid = service.service_config.config_rules[0].tapi_lsp.endpoint_id.device_id.device_uuid.uuid + LOGGER.debug('tapi_lsp_device_uuid: %s', tapi_lsp_device_uuid) for device in connection_devices.values(): if device.name == tapi_lsp_device_uuid: + LOGGER.debug('Device(%s) supported drivers: %s', device.name, device.device_drivers) common_device_drivers = device.device_drivers else: diff --git a/src/service/service/service_handlers/tapi_lsp/Tapi_LSPServiceHandler.py b/src/service/service/service_handlers/tapi_lsp/Tapi_LSPServiceHandler.py index 36276e9d1..0697001ef 100644 --- a/src/service/service/service_handlers/tapi_lsp/Tapi_LSPServiceHandler.py +++ b/src/service/service/service_handlers/tapi_lsp/Tapi_LSPServiceHandler.py @@ -23,11 +23,15 @@ from service.service.service_handler_api._ServiceHandler import _ServiceHandler from service.service.service_handler_api.SettingsHandler import SettingsHandler from service.service.task_scheduler.TaskExecutor import TaskExecutor from .ConfigRules import setup_config_rules, teardown_config_rules +from context.client.ContextClient import ContextClient + LOGGER = logging.getLogger(__name__) METRICS_POOL = MetricsPool('Service', 'Handler', labels={'handler': 'l3nm_openconfig'}) +ContextClient = ContextClient() + class Tapi_LSPServiceHandler(_ServiceHandler): def __init__( # pylint: disable=super-init-not-called self, service : Service, task_executor : TaskExecutor, **settings @@ -74,6 +78,9 @@ class Tapi_LSPServiceHandler(_ServiceHandler): json_config_rules = setup_config_rules(endpoint_name, endpoint_tapi_lsp) LOGGER.debug("[SetEndpoint] Generated json_config_rules: %s", json_config_rules) + if 'DELETE' in str(json_config_rules): + LOGGER.debug("[SetEndpoint] Config rules contain DELETE operation.") + # ContextClient.RemoveService() if len(json_config_rules) > 0: LOGGER.info("[SetEndpoint] Applying %d config rules to device %s", len(json_config_rules), device_uuid) diff --git a/src/webui/service/templates/main/home.html b/src/webui/service/templates/main/home.html index 5a5c439e1..1f9cde1ee 100644 --- a/src/webui/service/templates/main/home.html +++ b/src/webui/service/templates/main/home.html @@ -17,7 +17,7 @@ {% extends 'base.html' %} {% block content %} -

ETSI TeraFlowSDN Controller

+

ETSI TeraFlowSDN Controller (End-to-End)

{% for field, message in context_topology_form.errors.items() %}