diff --git a/proto/context.proto b/proto/context.proto index 87f69132df022e2aa4a0766dc9f0a7a7fae36d59..40f54b13588ec40b244a5cfed0171069796faa59 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -16,6 +16,7 @@ syntax = "proto3"; package context; import "acl.proto"; +import "ip_link.proto"; import "kpi_sample_types.proto"; service ContextService { @@ -300,6 +301,7 @@ enum ServiceTypeEnum { SERVICETYPE_TE = 4; SERVICETYPE_E2E = 5; SERVICETYPE_OPTICAL_CONNECTIVITY = 6; + SERVICETYPE_IPLINK = 7; } enum ServiceStatusEnum { @@ -512,11 +514,17 @@ message ConfigRule_ACL { acl.AclRuleSet rule_set = 2; } +message ConfigRule_IP_LINK { + EndPointId endpoint_id = 1; + ip_link.IpLinkRuleSet rule_set = 2; +} + message ConfigRule { ConfigActionEnum action = 1; oneof config_rule { - ConfigRule_Custom custom = 2; - ConfigRule_ACL acl = 3; + ConfigRule_Custom custom = 2; + ConfigRule_ACL acl = 3; + ConfigRule_IP_LINK ip_link = 4; } } diff --git a/proto/ip_link.proto b/proto/ip_link.proto new file mode 100644 index 0000000000000000000000000000000000000000..79a5bed5adbc749ba3e200b44d2cafea6bac7615 --- /dev/null +++ b/proto/ip_link.proto @@ -0,0 +1,24 @@ +// Copyright 2022-2024 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 ip_link; + + + +message IpLinkRuleSet { + string ip = 1; + string mask = 3; + string vlan = 4; +} diff --git a/src/common/tools/object_factory/Service.py b/src/common/tools/object_factory/Service.py index 32b99a31f22072874ab894de2a87ce2b7d56ba85..642aa5a032b88bc10eea99cb174f76d5b2884d0f 100644 --- a/src/common/tools/object_factory/Service.py +++ b/src/common/tools/object_factory/Service.py @@ -80,4 +80,14 @@ def json_service_p4_planned( return json_service( service_uuid, ServiceTypeEnum.SERVICETYPE_L2NM, context_id=json_context_id(context_uuid), status=ServiceStatusEnum.SERVICESTATUS_PLANNED, endpoint_ids=endpoint_ids, constraints=constraints, + config_rules=config_rules) + +def json_service_iplink_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_IPLINK, 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 diff --git a/src/common/type_checkers/Assertions.py b/src/common/type_checkers/Assertions.py index 90b7d976b0f6fff9d478ce7b40188240a8eea2d6..94a17f4badeded0c864050a73b8516231782f3d2 100644 --- a/src/common/type_checkers/Assertions.py +++ b/src/common/type_checkers/Assertions.py @@ -79,6 +79,7 @@ def validate_service_type_enum(message): 'SERVICETYPE_TAPI_CONNECTIVITY_SERVICE', 'SERVICETYPE_TE', 'SERVICETYPE_E2E', + 'SERVICETYPE_IPLINK' ] def validate_service_state_enum(message): @@ -116,6 +117,7 @@ def validate_uuid(message, allow_empty=False): CONFIG_RULE_TYPES = { 'custom', 'acl', + 'ip_link' } 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 7d816b3e87803f71678511f4fadc6bbe7eba548e..e2b19420a617617cf1d4369befce20b9d45ff842 100644 --- a/src/context/service/database/ConfigRule.py +++ b/src/context/service/database/ConfigRule.py @@ -68,6 +68,9 @@ 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.IP_LINK: + _, _, endpoint_uuid = endpoint_get_uuid(config_rule.ip_link.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/Link.py b/src/context/service/database/Link.py index 8aa2563e4b3bb5b46ffdefe4644cdfe321f1e734..87ec27eb8bc46945807c5b902638fd8414b027ee 100644 --- a/src/context/service/database/Link.py +++ b/src/context/service/database/Link.py @@ -74,12 +74,12 @@ def link_set(db_engine : Engine, messagebroker : MessageBroker, request : Link) related_topologies : List[Dict] = list() # By default, always add link to default Context/Topology - _,topology_uuid = topology_get_uuid(TopologyId(), allow_random=False, allow_default=True) - related_topologies.append({ - 'topology_uuid': topology_uuid, - 'link_uuid' : link_uuid, - }) - topology_uuids.add(topology_uuid) + # _,topology_uuid = topology_get_uuid(TopologyId(), allow_random=False, allow_default=True) + # related_topologies.append({ + # 'topology_uuid': topology_uuid, + # 'link_uuid' : link_uuid, + # }) + # topology_uuids.add(topology_uuid) link_endpoints_data : List[Dict] = list() for i,endpoint_id in enumerate(request.link_endpoint_ids): diff --git a/src/context/service/database/Service.py b/src/context/service/database/Service.py index ba042fe8fd079172df66526732edbea660d2d9fa..2da31d58d2870f36a11c97378a29a875efca8725 100644 --- a/src/context/service/database/Service.py +++ b/src/context/service/database/Service.py @@ -88,6 +88,8 @@ 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_IPLINK : + service_type = "IP_LINK" 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 f57c90b82b950e68103e1381c6ff0b118e6307df..5799934a76c0240535984ecf297b0baf54426450 100644 --- a/src/context/service/database/models/ConfigRuleModel.py +++ b/src/context/service/database/models/ConfigRuleModel.py @@ -21,8 +21,9 @@ from ._Base import _Base # Enum values should match name of field in ConfigRule message class ConfigRuleKindEnum(enum.Enum): - CUSTOM = 'custom' - ACL = 'acl' + CUSTOM = 'custom' + ACL = 'acl' + IP_LINK = 'ip_link' 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 62d5380b56803b3cc21dd1456292ec9df470cb15..bcc5aff09da2899470d6c56e82cad5a6d0f00f95 100644 --- a/src/context/service/database/models/enums/ServiceType.py +++ b/src/context/service/database/models/enums/ServiceType.py @@ -29,6 +29,7 @@ class ORM_ServiceTypeEnum(enum.Enum): TE = ServiceTypeEnum.SERVICETYPE_TE E2E = ServiceTypeEnum.SERVICETYPE_E2E OPTICAL_CONNECTIVITY = ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY + IP_LINK = ServiceTypeEnum.SERVICETYPE_IPLINK grpc_to_enum__service_type = functools.partial( grpc_to_enum, ServiceTypeEnum, ORM_ServiceTypeEnum) diff --git a/src/device/requirements.in b/src/device/requirements.in index 73ea741d16dcdafd7a9be87ad79b457ccb6c5d5e..6f20b0de1c62eee000a22244f38b2ab0fd4aefd5 100644 --- a/src/device/requirements.in +++ b/src/device/requirements.in @@ -23,7 +23,7 @@ Flask==2.1.3 Flask-HTTPAuth==4.5.0 Flask-RESTful==0.3.9 Jinja2==3.0.3 -ncclient==0.6.13 +ncclient==0.6.15 p4runtime==1.3.0 pandas==1.5.* paramiko==2.9.2 diff --git a/src/device/service/Tools.py b/src/device/service/Tools.py index 91926b9e59cccac2e233ac14bbac497bbb0ac15c..8eb25578488dd0fe5b1781a0e129709b86df69fa 100644 --- a/src/device/service/Tools.py +++ b/src/device/service/Tools.py @@ -306,7 +306,13 @@ def compute_rules_to_add_delete( ACL_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/acl_ruleset[{:s}]' 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 == 'ip_link': + device_uuid = config_rule.ip_link.endpoint_id.device_id.device_uuid.uuid # get the device name + endpoint_uuid = config_rule.ip_link.endpoint_id.endpoint_uuid.uuid # get the endpoint name request_config_rules = [] + ip_link_ruleset_name = config_rule.ip_link.rule_set.name # get the ip_link name + IP_LINK_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/ip_link_ruleset[{:s}]' + key_or_path = IP_LINK_KEY_TEMPLATE.format(device_uuid, endpoint_uuid, ip_link_ruleset_name) + context_config_rules[key_or_path] = grpc_message_to_json(config_rule.ip_link) # get the resource value of the ip_link request_config_rules = [] for config_rule in request.device_config.config_rules: config_rule_kind = config_rule.WhichOneof('config_rule') @@ -323,6 +329,15 @@ def compute_rules_to_add_delete( request_config_rules.append(( config_rule.action, key_or_path, grpc_message_to_json(config_rule.acl) )) + elif config_rule_kind == 'ip_link': # resource management of "ip_link" rule + device_uuid = config_rule.ip_link.endpoint_id.device_id.device_uuid.uuid + endpoint_uuid = config_rule.ip_link.endpoint_id.endpoint_uuid.uuid + ip_link_ruleset_name = config_rule.ip_link.rule_set.name + IP_LINK_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/ip_link_ruleset[{:s}]' + key_or_path = IP_LINK_KEY_TEMPLATE.format(device_uuid, endpoint_uuid, ip_link_ruleset_name) + request_config_rules.append(( + config_rule.action, key_or_path, grpc_message_to_json(config_rule.ip_link) + )) resources_to_set : List[Tuple[str, Any]] = [] # key, value resources_to_delete : List[Tuple[str, Any]] = [] # key, value diff --git a/src/pathcomp/frontend/service/algorithms/_Algorithm.py b/src/pathcomp/frontend/service/algorithms/_Algorithm.py index 3ed2b13fb33ae06faeacc4286959a8016ca995d1..d4e4194497be44bf9eb0176bcc9014e9402e582c 100644 --- a/src/pathcomp/frontend/service/algorithms/_Algorithm.py +++ b/src/pathcomp/frontend/service/algorithms/_Algorithm.py @@ -23,7 +23,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_iplink_config_rules ) from .tools.ComposeRequest import compose_device, compose_link, compose_service from .tools.ComputeSubServices import ( @@ -182,6 +182,8 @@ class _Algorithm: compose_l3nm_config_rules(config_rules, service.service_config.config_rules) elif service_type == ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE: compose_tapi_config_rules(config_rules, service.service_config.config_rules) + if service_type == ServiceTypeEnum.SERVICETYPE_IPLINK: + compose_iplink_config_rules(config_rules, service.service_config.config_rules) else: MSG = 'Unhandled generic Config Rules for service {:s} {:s}' self.logger.warning(MSG.format(str(service_uuid), str(ServiceTypeEnum.Name(service_type)))) diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py index f92f9b2fff11ab585813ab59e07c463f361413d2..f96291e20b0d93fbf8102295d9ed8347e301760a 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py @@ -52,6 +52,10 @@ TAPI_SETTINGS_FIELD_DEFAULTS = { 'direction' : 'UNIDIRECTIONAL', } +IPLINK_SETTINGS_FIELD_DEFAULTS = { + 'mtu' : 1450, +} + def find_custom_config_rule(config_rules : List, resource_name : str) -> Optional[Dict]: resource_value : Optional[Dict] = None for config_rule in config_rules: @@ -103,7 +107,12 @@ def compose_tapi_config_rules(main_service_config_rules : List, subservice_confi ] for rule_name, defaults in CONFIG_RULES: compose_config_rules(main_service_config_rules, subservice_config_rules, rule_name, defaults) - + +def compose_iplink_config_rules(main_service_config_rules : List, subservice_config_rules : List) -> None: + CONFIG_RULES: List[Tuple[str, dict]] = [(SETTINGS_RULE_NAME, IPLINK_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_device_config_rules( config_rules : List, subservice_config_rules : List, path_hops : List, device_name_mapping : Dict[str, str], endpoint_name_mapping : Dict[Tuple[str, str], str] @@ -152,6 +161,31 @@ def compose_device_config_rules( LOGGER.debug('[compose_device_config_rules] adding acl config rule') subservice_config_rules.append(config_rule) + + elif config_rule.WhichOneof('config_rule') == 'ip_link': + LOGGER.debug('[compose_device_config_rules] is ip_link') + endpoint_id = config_rule.ip_link.endpoint_id + device_uuid_or_name = endpoint_id.device_id.device_uuid.uuid + LOGGER.debug('[compose_device_config_rules] device_uuid_or_name={:s}'.format(str(device_uuid_or_name))) + device_name_or_uuid = device_name_mapping.get(device_uuid_or_name, device_uuid_or_name) + LOGGER.debug('[compose_device_config_rules] device_name_or_uuid={:s}'.format(str(device_name_or_uuid))) + device_keys = {device_uuid_or_name, device_name_or_uuid} + if len(device_keys.intersection(devices_traversed)) == 0: continue + + endpoint_uuid = endpoint_id.endpoint_uuid.uuid + LOGGER.debug('[compose_device_config_rules] endpoint_uuid={:s}'.format(str(endpoint_uuid))) + # given endpoint uuids link 'eth-1/0/20.533', remove last part after the '.' + endpoint_uuid_or_name = (endpoint_uuid[::-1].split('.', maxsplit=1)[-1])[::-1] + LOGGER.debug('[compose_device_config_rules] endpoint_uuid_or_name={:s}'.format(str(endpoint_uuid_or_name))) + endpoint_name_or_uuid_1 = endpoint_name_mapping[(device_uuid_or_name, endpoint_uuid_or_name)] + endpoint_name_or_uuid_2 = endpoint_name_mapping[(device_name_or_uuid, endpoint_uuid_or_name)] + endpoint_keys = {endpoint_uuid_or_name, endpoint_name_or_uuid_1, endpoint_name_or_uuid_2} + + 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 ip_link config rule') + subservice_config_rules.append(config_rule) elif config_rule.WhichOneof('config_rule') == 'custom': LOGGER.debug('[compose_device_config_rules] is custom') @@ -288,48 +322,54 @@ def generate_neighbor_endpoint_config_rules( for config_rule in config_rules: # Only applicable, by now, to Custom Config Rules for endpoint settings - if 'custom' not in config_rule: continue - match = RE_ENDPOINT_SETTINGS.match(config_rule['custom']['resource_key']) - if match is None: - match = RE_ENDPOINT_VLAN_SETTINGS.match(config_rule['custom']['resource_key']) - if match is None: continue - - resource_key_values = match.groups() - if resource_key_values[0:2] in device_endpoint_keys_a: - resource_key_values = list(resource_key_values) - resource_key_values[0] = link_endpoint_b['device'] - resource_key_values[1] = link_endpoint_b['ingress_ep'] - elif resource_key_values[0:2] in device_endpoint_keys_b: - resource_key_values = list(resource_key_values) - resource_key_values[0] = link_endpoint_a['device'] - resource_key_values[1] = link_endpoint_a['egress_ep'] - else: - continue - - device_keys = compute_device_keys(resource_key_values[0], device_name_mapping) - device_names = {device_key for device_key in device_keys if RE_UUID.match(device_key) is None} - if len(device_names) != 1: - MSG = 'Unable to identify name for Device({:s}): device_keys({:s})' - raise Exception(MSG.format(str(resource_key_values[0]), str(device_keys))) - resource_key_values[0] = device_names.pop() - - endpoint_keys = compute_endpoint_keys(device_keys, resource_key_values[1], endpoint_name_mapping) - endpoint_names = {endpoint_key for endpoint_key in endpoint_keys if RE_UUID.match(endpoint_key) is None} - if len(endpoint_names) != 1: - MSG = 'Unable to identify name for Endpoint({:s}): endpoint_keys({:s})' - raise Exception(MSG.format(str(resource_key_values[1]), str(endpoint_keys))) - resource_key_values[1] = endpoint_names.pop() - - resource_value : Dict = json.loads(config_rule['custom']['resource_value']) - if 'neighbor_address' not in resource_value: continue - resource_value['ip_address'] = resource_value.pop('neighbor_address') - - # remove neighbor_address also from original rule as it is already consumed - - resource_key_template = TMPL_ENDPOINT_VLAN_SETTINGS if len(match.groups()) == 3 else TMPL_ENDPOINT_SETTINGS + if 'custom' not in config_rule or 'ip_link' not in config_rule: continue + if 'custom' in config_rule: + match = RE_ENDPOINT_SETTINGS.match(config_rule['custom']['resource_key']) + if match is None: + match = RE_ENDPOINT_VLAN_SETTINGS.match(config_rule['custom']['resource_key']) + if match is None: continue + resource_key_values = match.groups() + if resource_key_values[0:2] in device_endpoint_keys_a: + resource_key_values = list(resource_key_values) + resource_key_values[0] = link_endpoint_b['device'] + resource_key_values[1] = link_endpoint_b['ingress_ep'] + elif resource_key_values[0:2] in device_endpoint_keys_b: + resource_key_values = list(resource_key_values) + resource_key_values[0] = link_endpoint_a['device'] + resource_key_values[1] = link_endpoint_a['egress_ep'] + else: + continue + + device_keys = compute_device_keys(resource_key_values[0], device_name_mapping) + device_names = {device_key for device_key in device_keys if RE_UUID.match(device_key) is None} + if len(device_names) != 1: + MSG = 'Unable to identify name for Device({:s}): device_keys({:s})' + raise Exception(MSG.format(str(resource_key_values[0]), str(device_keys))) + resource_key_values[0] = device_names.pop() + + endpoint_keys = compute_endpoint_keys(device_keys, resource_key_values[1], endpoint_name_mapping) + endpoint_names = {endpoint_key for endpoint_key in endpoint_keys if RE_UUID.match(endpoint_key) is None} + if len(endpoint_names) != 1: + MSG = 'Unable to identify name for Endpoint({:s}): endpoint_keys({:s})' + raise Exception(MSG.format(str(resource_key_values[1]), str(endpoint_keys))) + resource_key_values[1] = endpoint_names.pop() + + resource_value : Dict = json.loads(config_rule['custom']['resource_value']) + if 'neighbor_address' not in resource_value: continue + resource_value['ip_address'] = resource_value.pop('neighbor_address') + + # remove neighbor_address also from original rule as it is already consumed + + resource_key_template = TMPL_ENDPOINT_VLAN_SETTINGS if len(match.groups()) == 3 else TMPL_ENDPOINT_SETTINGS + generated_config_rule = copy.deepcopy(config_rule) + generated_config_rule['custom']['resource_key'] = resource_key_template.format(*resource_key_values) + generated_config_rule['custom']['resource_value'] = json.dumps(resource_value) + generated_config_rules.append(generated_config_rule) + else: + LOGGER.debug('[generate_neighbor_endpoint_config_rules] IP_LINK: {:s}'.format(str(config_rule))) + resource_value : Dict = config_rule['ip_link'] generated_config_rule = copy.deepcopy(config_rule) - generated_config_rule['custom']['resource_key'] = resource_key_template.format(*resource_key_values) - generated_config_rule['custom']['resource_value'] = json.dumps(resource_value) + generated_config_rule['ip_link'] = json.dumps(resource_value) generated_config_rules.append(generated_config_rule) LOGGER.debug('[generate_neighbor_endpoint_config_rules] generated_config_rules={:s}'.format(str(generated_config_rules))) diff --git a/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py index 2792a86639fbd6852a41499e928de7a4131ed408..c47e1995e714bf35745ffecf2c908aae6b2bdb27 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py @@ -41,13 +41,15 @@ OPTICAL_DEVICE_TYPES = { DeviceTypeEnum.OPTICAL_TRANSPONDER, DeviceTypeEnum.EMULATED_OPTICAL_TRANSPONDER, } -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_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_IPLINK = {ServiceTypeEnum.SERVICETYPE_IPLINK} + 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_LXNM or prv_service_type in SERVICE_TYPE_IPLINK ): 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 654e7b6ce1d68f6facaec8c772e16dde68e710f0..5ee68f2eda28c7445d2da7b05415c91c7d177d45 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 @@ -1177,6 +1177,8 @@ public class Serializer { return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_L3NM; case TAPI_CONNECTIVITY_SERVICE: return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE; + case IPLINK: + return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_IPLINK; case UNKNOWN: return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_UNKNOWN; default: @@ -1192,6 +1194,8 @@ public class Serializer { return ServiceTypeEnum.L3NM; case SERVICETYPE_TAPI_CONNECTIVITY_SERVICE: return ServiceTypeEnum.TAPI_CONNECTIVITY_SERVICE; + case SERVICETYPE_IPLINK: + return ServiceTypeEnum.IPLINK; case SERVICETYPE_UNKNOWN: case UNRECOGNIZED: default: diff --git a/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java b/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java index 4593770498216267b8d2f95dd728fccfbb9dc134..8eea2d6d82380b60f746942a2314057a7d3b29c2 100644 --- a/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java +++ b/src/policy/target/generated-sources/grpc/context/ContextOuterClass.java @@ -504,6 +504,10 @@ public final class ContextOuterClass { * SERVICETYPE_OPTICAL_CONNECTIVITY = 6; */ SERVICETYPE_OPTICAL_CONNECTIVITY(6), + /** + * SERVICETYPE_IPLINK = 7; + */ + SERVICETYPE_IPLINK(7), UNRECOGNIZED(-1); /** @@ -541,6 +545,11 @@ public final class ContextOuterClass { */ public static final int SERVICETYPE_OPTICAL_CONNECTIVITY_VALUE = 6; + /** + * SERVICETYPE_IPLINK = 7; + */ + public static final int SERVICETYPE_IPLINK_VALUE = 7; + public final int getNumber() { if (this == UNRECOGNIZED) { throw new java.lang.IllegalArgumentException("Can't get the number of an unknown enum value."); @@ -578,6 +587,8 @@ public final class ContextOuterClass { return SERVICETYPE_E2E; case 6: return SERVICETYPE_OPTICAL_CONNECTIVITY; + case 7: + return SERVICETYPE_IPLINK; default: return null; } diff --git a/src/service/service/service_handler_api/FilterFields.py b/src/service/service/service_handler_api/FilterFields.py index ca70fa9386e356e7e49397365701013e1d3a1697..e0811c0a9c53cb14fbd654797faf5f5e0522c4ac 100644 --- a/src/service/service/service_handler_api/FilterFields.py +++ b/src/service/service/service_handler_api/FilterFields.py @@ -26,7 +26,8 @@ SERVICE_TYPE_VALUES = { ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE, ServiceTypeEnum.SERVICETYPE_TE, ServiceTypeEnum.SERVICETYPE_E2E, - ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY + ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY, + ServiceTypeEnum.SERVICETYPE_IPLINK } DEVICE_DRIVER_VALUES = { diff --git a/src/service/service/service_handler_api/SettingsHandler.py b/src/service/service/service_handler_api/SettingsHandler.py index 293de54aa84be11f3c31bc1b47fce852df19a16a..8bfa1cd8a983ad20323525b38ab8d898c99dbde1 100644 --- a/src/service/service/service_handler_api/SettingsHandler.py +++ b/src/service/service/service_handler_api/SettingsHandler.py @@ -47,6 +47,12 @@ 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 == 'ip_link': + device_uuid = config_rule.ip_link.endpoint_id.device_id.device_uuid.uuid + endpoint_uuid = config_rule.ip_link.endpoint_id.endpoint_uuid.uuid + IP_LINK_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/ip_link' + key_or_path = IP_LINK_KEY_TEMPLATE.format(device_uuid, endpoint_uuid,) + value = config_rule.ip_link else: MSG = 'Unsupported Kind({:s}) in ConfigRule({:s})' LOGGER.warning(MSG.format(str(kind), grpc_message_to_json_string(config_rule))) @@ -100,6 +106,25 @@ class SettingsHandler: if not 'index[{:d}]'.format(acl_index) in res_key: continue acl_rules.append((res_key, res_value)) return acl_rules + + def get_endpoint_ip_link(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 + ip_links = [] + 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 + IP_LINK_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(IP_LINK_KEY_TEMPLATE): continue + if not "ip_link" in res_key: continue + ip_links.append((res_key, res_value)) + return None 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_handlers/__init__.py b/src/service/service/service_handlers/__init__.py index 8b5e2b2834f37dc3d616907e46cf1fa5b2f1274f..a731e8176d51b32305bb13bd223e19bfb44c2b6e 100644 --- a/src/service/service/service_handlers/__init__.py +++ b/src/service/service/service_handlers/__init__.py @@ -27,6 +27,7 @@ from .tapi_tapi.TapiServiceHandler import TapiServiceHandler from .tapi_xr.TapiXrServiceHandler import TapiXrServiceHandler from .e2e_orch.E2EOrchestratorServiceHandler import E2EOrchestratorServiceHandler from .oc.OCServiceHandler import OCServiceHandler +from .ip_link.IP_LinkServiceHandler import IP_LinkServiceHandler SERVICE_HANDLERS = [ (L2NMEmulatedServiceHandler, [ @@ -106,5 +107,11 @@ SERVICE_HANDLERS = [ FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY, FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_OC, } - ]) + ]), + (IP_LinkServiceHandler, [ + { + FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_IPLINK, + FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_OPENCONFIG, + } + ]), ] diff --git a/src/service/service/service_handlers/ip_link/ConfigRules.py b/src/service/service/service_handlers/ip_link/ConfigRules.py new file mode 100644 index 0000000000000000000000000000000000000000..46c7877fed1044e3d029a5c9500fd5ac23ef6a3f --- /dev/null +++ b/src/service/service/service_handlers/ip_link/ConfigRules.py @@ -0,0 +1,81 @@ +# Copyright 2022-2024 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. + +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 + +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( + service_uuid : str, connection_uuid : str, device_uuid : str, endpoint_uuid : str, endpoint_name : str, + service_settings : TreeNode, device_settings : TreeNode, endpoint_settings : TreeNode, endpoint_acls : List [Tuple], endpoint_ip_link : List [Tuple] +) -> 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) + + mtu = get_value('mtu', *settings, default=1450) # 1512 + + json_config_rules = [] + 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) + + service_short_uuid = service_uuid.split('-')[-1] + network_instance_name = '{:s}-NetInst'.format(service_short_uuid) + #network_interface_desc = '{:s}-NetIf'.format(service_uuid) + #network_subinterface_desc = '{:s}-NetSubIf'.format(service_uuid) + + #mtu = get_value('mtu', *settings, default=1450) # 1512 + #address_families = json_settings.get('address_families', [] ) # ['IPV4'] + #bgp_as = get_value('bgp_as', *settings, default=65000) # 65000 + route_distinguisher = json_settings.get('route_distinguisher', '0:0' ) # '60001:801' + #sub_interface_index = json_endpoint_settings.get('sub_interface_index', 0 ) # 1 + #router_id = json_endpoint_settings.get('router_id', '0.0.0.0') # '10.95.0.10' + vlan_id = json_endpoint_settings.get('vlan_id', 1 ) # 400 + #address_ip = json_endpoint_settings.get('address_ip', '0.0.0.0') # '2.2.2.1' + #address_prefix = json_endpoint_settings.get('address_prefix', 24 ) # 30 + policy_import = json_endpoint_settings.get('policy_AZ', '2' ) # 2 + policy_export = json_endpoint_settings.get('policy_ZA', '7' ) # 30 + + if_subif_name = '{:s}.{:d}'.format(endpoint_name, vlan_id) + + json_config_rules = [] + return json_config_rules diff --git a/src/service/service/service_handlers/ip_link/IP_LinkServiceHandler.py b/src/service/service/service_handlers/ip_link/IP_LinkServiceHandler.py new file mode 100644 index 0000000000000000000000000000000000000000..832b386c54694d652400238b58333f0721a2bfd1 --- /dev/null +++ b/src/service/service/service_handlers/ip_link/IP_LinkServiceHandler.py @@ -0,0 +1,164 @@ +# Copyright 2022-2024 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 IP_LinkServiceHandler(_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) + 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_acls = self.__settings_handler.get_endpoint_acls(device_obj, endpoint_obj) + endpoint_ip_link = self.__settings_handler.get_endpoint_ip_link(device_obj, endpoint_obj) + endpoint_name = endpoint_obj.name + json_config_rules = setup_config_rules( + service_uuid, connection_uuid, device_uuid, endpoint_uuid, endpoint_name, + settings, device_settings, endpoint_settings, endpoint_acls, endpoint_ip_link) + + 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 SetEndpoint({:s})'.format(str(endpoint))) + results.append(e) + + 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/ip_link/__init__.py b/src/service/service/service_handlers/ip_link/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3ee6f7071f145e06c3aeaefc09a43ccd88e619e3 --- /dev/null +++ b/src/service/service/service_handlers/ip_link/__init__.py @@ -0,0 +1,14 @@ +# Copyright 2022-2024 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/ztp/target/generated-sources/grpc/context/ContextOuterClass.java b/src/ztp/target/generated-sources/grpc/context/ContextOuterClass.java index 4593770498216267b8d2f95dd728fccfbb9dc134..8eea2d6d82380b60f746942a2314057a7d3b29c2 100644 --- a/src/ztp/target/generated-sources/grpc/context/ContextOuterClass.java +++ b/src/ztp/target/generated-sources/grpc/context/ContextOuterClass.java @@ -504,6 +504,10 @@ public final class ContextOuterClass { * SERVICETYPE_OPTICAL_CONNECTIVITY = 6; */ SERVICETYPE_OPTICAL_CONNECTIVITY(6), + /** + * SERVICETYPE_IPLINK = 7; + */ + SERVICETYPE_IPLINK(7), UNRECOGNIZED(-1); /** @@ -541,6 +545,11 @@ public final class ContextOuterClass { */ public static final int SERVICETYPE_OPTICAL_CONNECTIVITY_VALUE = 6; + /** + * SERVICETYPE_IPLINK = 7; + */ + public static final int SERVICETYPE_IPLINK_VALUE = 7; + public final int getNumber() { if (this == UNRECOGNIZED) { throw new java.lang.IllegalArgumentException("Can't get the number of an unknown enum value."); @@ -578,6 +587,8 @@ public final class ContextOuterClass { return SERVICETYPE_E2E; case 6: return SERVICETYPE_OPTICAL_CONNECTIVITY; + case 7: + return SERVICETYPE_IPLINK; default: return null; }