diff --git a/proto/context.proto b/proto/context.proto index ba3e30d678346b183eb4d04091e8fa5cd7f64202..d4253f23fe3789881e23c503715a775c15a692d5 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -19,6 +19,7 @@ package context; import "google/protobuf/any.proto"; import "acl.proto"; +import "ip_link.proto"; import "kpi_sample_types.proto"; service ContextService { @@ -216,7 +217,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; } @@ -347,6 +348,7 @@ enum ServiceTypeEnum { SERVICETYPE_L1NM = 8; SERVICETYPE_INT = 9; SERVICETYPE_ACL = 10; + SERVICETYPE_IP_LINK = 11; } enum ServiceStatusEnum { @@ -560,11 +562,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; } } @@ -595,7 +603,7 @@ message Location { oneof location { string region = 1; GPS_Position gps_position = 2; - + string interface=3; string circuit_pack=4; } @@ -727,7 +735,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/ip_link.proto b/proto/ip_link.proto new file mode 100644 index 0000000000000000000000000000000000000000..a4f00b0de4c2be5f1d3873bc9f1589c576b93f9c --- /dev/null +++ b/proto/ip_link.proto @@ -0,0 +1,22 @@ +// 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 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 2bfe50ccca7459f4ad6d30bfcd7f007460557c4b..5f5c776dc6bbcf3365f3436cbfb1569a32aafc85 100644 --- a/src/common/tools/object_factory/Service.py +++ b/src/common/tools/object_factory/Service.py @@ -92,4 +92,14 @@ 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_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_IP_LINK, 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 11696daa9a68f57a9ab2a3d5d81a4d6ed1462f3c..a448a9a0716c9d1fa254b75a47a4ca3337084c81 100644 --- a/src/common/type_checkers/Assertions.py +++ b/src/common/type_checkers/Assertions.py @@ -113,6 +113,7 @@ def validate_service_type_enum(message): 'SERVICETYPE_TAPI_CONNECTIVITY_SERVICE', 'SERVICETYPE_TE', 'SERVICETYPE_E2E', + 'SERVICETYPE_IP_LINK' 'SERVICETYPE_OPTICAL_CONNECTIVITY', 'SERVICETYPE_QKD', ] @@ -152,6 +153,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 c9db5488c15e57f805cb8b7f87c4f4bde4b2c665..b24b0c6518935b6640475f23ceca02bfb29c5176 100644 --- a/src/context/service/database/ConfigRule.py +++ b/src/context/service/database/ConfigRule.py @@ -71,6 +71,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/Service.py b/src/context/service/database/Service.py index 9076fc025d86457cab48fe321f55926ba728cb6c..ba872c50f6ceac4f0aa56947129604f4a4ac74bc 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_IP_LINK : + 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 5462df672331d8b2ce28756a818d1e6b290aa2ed..f098c1253fef99e8c1e5c55da956811691ed2df2 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 93271c3b94e3ffa2911dc0c8023332a8975ff2b5..29631ce3c27ceee4a949afb68fd36d201594ebeb 100644 --- a/src/context/service/database/models/enums/ServiceType.py +++ b/src/context/service/database/models/enums/ServiceType.py @@ -25,14 +25,15 @@ class ORM_ServiceTypeEnum(enum.Enum): UNKNOWN = ServiceTypeEnum.SERVICETYPE_UNKNOWN L3NM = ServiceTypeEnum.SERVICETYPE_L3NM L2NM = ServiceTypeEnum.SERVICETYPE_L2NM - L1NM = ServiceTypeEnum.SERVICETYPE_L1NM TAPI_CONNECTIVITY_SERVICE = ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE TE = ServiceTypeEnum.SERVICETYPE_TE E2E = ServiceTypeEnum.SERVICETYPE_E2E OPTICAL_CONNECTIVITY = ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY QKD = ServiceTypeEnum.SERVICETYPE_QKD + L1NM = ServiceTypeEnum.SERVICETYPE_L1NM INT = ServiceTypeEnum.SERVICETYPE_INT ACL = ServiceTypeEnum.SERVICETYPE_ACL + IP_LINK = ServiceTypeEnum.SERVICETYPE_IP_LINK 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 384cfc9bb1d1c052f28ba5604e3ec8fae8b23d2b..335955b4948de3d8ed3d5afa85a525fc1034543e 100644 --- a/src/device/service/Tools.py +++ b/src/device/service/Tools.py @@ -395,7 +395,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') @@ -412,6 +418,14 @@ 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_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/ip_link_ruleset' + key_or_path = IP_LINK_KEY_TEMPLATE.format(device_uuid, endpoint_uuid) + 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/device/service/drivers/oc_driver/OCDriver.py b/src/device/service/drivers/oc_driver/OCDriver.py index 05c2d29a16a54a2e046247ce4ab180233f159adc..e9bcf307f9438bd85c253ccb683d33a67f1e8990 100644 --- a/src/device/service/drivers/oc_driver/OCDriver.py +++ b/src/device/service/drivers/oc_driver/OCDriver.py @@ -12,9 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import time import json -import logging, pytz, queue, re, threading -#import lxml.etree as ET +import logging, pytz, re, threading from typing import Any, List, Tuple, Union from apscheduler.executors.pool import ThreadPoolExecutor from apscheduler.jobstores.memory import MemoryJobStore @@ -22,12 +22,11 @@ from apscheduler.schedulers.background import BackgroundScheduler from ncclient.manager import Manager, connect_ssh from common.method_wrappers.Decorator import MetricsPool, metered_subclass_method from common.tools.client.RetryDecorator import delay_exponential -from common.type_checkers.Checkers import chk_type +from common.type_checkers.Checkers import chk_length, chk_string, chk_type from device.service.driver_api.Exceptions import UnsupportedResourceKeyException from device.service.driver_api._Driver import _Driver -from device.service.driver_api.AnyTreeTools import TreeNode +from .templates import compose_config, cli_compose_config, ufi_interface, cisco_interface from .templates.VPN.common import seperate_port_config -#from .Tools import xml_pretty_print, xml_to_dict, xml_to_file from .templates.VPN.roadms import ( create_optical_band, disable_media_channel, delete_optical_band, create_media_channel_v2 ) @@ -36,11 +35,10 @@ from .RetryDecorator import retry from context.client.ContextClient import ContextClient from common.proto.context_pb2 import OpticalConfig from .templates.discovery_tool.transponders import transponder_values_extractor -from .templates.discovery_tool.roadms import roadm_values_extractor, extract_media_channels +from .templates.discovery_tool.roadms import roadm_values_extractor from .templates.discovery_tool.open_roadm import openroadm_values_extractor from .templates.VPN.openroadm import network_media_channel_handler - DEBUG_MODE = False logging.getLogger('ncclient.manager').setLevel(logging.DEBUG if DEBUG_MODE else logging.WARNING) logging.getLogger('ncclient.transport.ssh').setLevel(logging.DEBUG if DEBUG_MODE else logging.WARNING) @@ -51,9 +49,6 @@ logging.getLogger('monitoring-client').setLevel(logging.INFO if DEBUG_MODE else RE_GET_ENDPOINT_FROM_INTERFACE_KEY = re.compile(r'.*interface\[([^\]]+)\].*') RE_GET_ENDPOINT_FROM_INTERFACE_XPATH = re.compile(r".*interface\[oci\:name\='([^\]]+)'\].*") -# Collection of samples through NetConf is very slow and each request collects all the data. -# Populate a cache periodically (when first interface is interrogated). -# Evict data after some seconds, when data is considered as outdated SAMPLE_EVICTION_SECONDS = 30.0 # seconds SAMPLE_RESOURCE_KEY = 'interfaces/interface/state/counters' @@ -200,6 +195,65 @@ def edit_config( #results[i] = True results.append(result) + + if netconf_handler.vendor == "CISCO": + if "L2VSI" in resources[0][1]: + #Configure by CLI + logger.warning("CLI Configuration") + cli_compose_config(resources, delete=delete, host= netconf_handler._NetconfSessionHandler__address, user=netconf_handler._NetconfSessionHandler__username, passw=netconf_handler._NetconfSessionHandler__password) + for i,resource in enumerate(resources): + results.append(True) + else: + logger.warning("CLI Configuration CISCO INTERFACE") + cisco_interface(resources, delete=delete, host= netconf_handler._NetconfSessionHandler__address, user=netconf_handler._NetconfSessionHandler__username, passw=netconf_handler._NetconfSessionHandler__password) + for i,resource in enumerate(resources): + results.append(True) + elif netconf_handler.vendor == "UFISPACE": + #Configure by CLI + logger.warning("CLI Configuration: {:s}".format(resources)) + ufi_interface(resources, delete=delete, host= netconf_handler._NetconfSessionHandler__address, user=netconf_handler._NetconfSessionHandler__username, passw=netconf_handler._NetconfSessionHandler__password) + for i,resource in enumerate(resources): + results.append(True) + else: + for i,resource in enumerate(resources): + str_resource_name = 'resources[#{:d}]'.format(i) + try: + logger.debug('[{:s}] resource = {:s}'.format(str_method, str(resource))) + chk_type(str_resource_name, resource, (list, tuple)) + chk_length(str_resource_name, resource, min_length=2, max_length=2) + resource_key,resource_value = resource + chk_string(str_resource_name + '.key', resource_key, allow_empty=False) + str_config_messages = compose_config( # get template for configuration + resource_key, resource_value, delete=delete, vendor=netconf_handler.vendor, message_renderer=netconf_handler.message_renderer) + for str_config_message in str_config_messages: # configuration of the received templates + if str_config_message is None: raise UnsupportedResourceKeyException(resource_key) + logger.debug('[{:s}] str_config_message[{:d}] = {:s}'.format( + str_method, len(str_config_message), str(str_config_message))) + netconf_handler.edit_config( # configure the device + config=str_config_message, target=target, default_operation=default_operation, + test_option=test_option, error_option=error_option, format=format) + if commit_per_rule: + netconf_handler.commit() # configuration commit + if 'table_connections' in resource_key: + time.sleep(5) # CPU usage might exceed critical level after route redistribution, BGP daemon needs time to reload + + #results[i] = True + results.append(True) + except Exception as e: # pylint: disable=broad-except + str_operation = 'preparing' if target == 'candidate' else ('deleting' if delete else 'setting') + msg = '[{:s}] Exception {:s} {:s}: {:s}' + logger.exception(msg.format(str_method, str_operation, str_resource_name, str(resource))) + #results[i] = e # if validation fails, store the exception + results.append(e) + + if not commit_per_rule: + try: + netconf_handler.commit() + except Exception as e: # pylint: disable=broad-except + msg = '[{:s}] Exception committing: {:s}' + str_operation = 'preparing' if target == 'candidate' else ('deleting' if delete else 'setting') + logger.exception(msg.format(str_method, str_operation, str(resources))) + results = [e for _ in resources] # if commit fails, set exception in each resource return results diff --git a/src/device/service/drivers/oc_driver/templates/IP_LINK/IP_LINK_multivendor.py b/src/device/service/drivers/oc_driver/templates/IP_LINK/IP_LINK_multivendor.py new file mode 100755 index 0000000000000000000000000000000000000000..ba4bbf5114dbe71f5783c5ad738deff2d372d6f7 --- /dev/null +++ b/src/device/service/drivers/oc_driver/templates/IP_LINK/IP_LINK_multivendor.py @@ -0,0 +1,59 @@ +# 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 yattag import Doc, indent + +def ip_link_mgmt(data,vendor, delete): + doc, tag, text = Doc().tagtext() + + ID = data['endpoint_id']['endpoint_uuid']['uuid'] + DATA = data["rule_set"] + + with tag('interfaces', xmlns="http://openconfig.net/yang/interfaces"): + if delete == True: + with tag('interface' ,'xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" nc:operation="delete"'): + with tag('name'):text(ID) + else: + with tag('interface'): + with tag('name'):text(ID) + with tag('config'): + with tag('name'):text(ID) + with tag('type', 'xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type"'):text('ianaift:l3ipvlan') + with tag('enabled'):text('true') + with tag('subinterfaces'): + with tag('subinterface'): + if vendor is None or vendor == 'ADVA': + with tag('index'): text('0') + with tag('config'): + with tag('index'): text('0') + if vendor == 'ADVA' and not 'vlan'in data: + with tag('untagged-allowed', 'xmlns="http://www.advaoptical.com/cim/adva-dnos-oc-interfaces"'):text('true') + with tag('vlan', xmlns="http://openconfig.net/yang/vlan"): + with tag('match'): + with tag('single-tagged'): + with tag('config'): + with tag('vlan-id'):text(DATA['vlan']) + with tag('ipv4', xmlns="http://openconfig.net/yang/interfaces/ip"): + with tag('addresses'): + with tag('address'): + with tag('ip'):text(DATA['ip']) + with tag('config'): + with tag('ip'):text(DATA['ip']) + with tag('prefix-length'):text(DATA['mask']) + result = indent( + doc.getvalue(), + indentation = ' '*2, + newline = '\r\n' + ) + return result diff --git a/src/device/service/drivers/oc_driver/templates/IP_LINK/__init__.py b/src/device/service/drivers/oc_driver/templates/IP_LINK/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3ccc21c7db78aac26daa1f8c5ff8e1ffd3f35460 --- /dev/null +++ b/src/device/service/drivers/oc_driver/templates/IP_LINK/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/device/service/drivers/oc_driver/templates/Tools.py b/src/device/service/drivers/oc_driver/templates/Tools.py index 5d682142ea4098ad6a227a7b1f8e421485e4d22a..bc0567ef2e9b7b7cb9c2688a082b9fc56d9434b6 100644 --- a/src/device/service/drivers/oc_driver/templates/Tools.py +++ b/src/device/service/drivers/oc_driver/templates/Tools.py @@ -13,12 +13,10 @@ # limitations under the License. import re,logging -import json import lxml.etree as ET -from typing import Collection, Dict, Any +from typing import Collection, Dict +from .IP_LINK.IP_LINK_multivendor import ip_link_mgmt -from yattag import Doc, indent -from .VPN.physical import create_optical_channel def add_value_from_tag(target : Dict, field_name: str, field_value : ET.Element, cast=None) -> None: if isinstance(field_value,str) or field_value is None or field_value.text is None: return @@ -49,14 +47,12 @@ def add_value_from_collection(target : Dict, field_name: str, field_value : Coll # Return: [dict] Set of templates generated according to the configuration rule """ -def generate_templates(resource_key: str, resource_value: str, channel:str) -> str: # template management to be configured +def generate_templates(resource_key: str, resource_value: str, delete: bool,vendor:str) -> str: # template management to be configured result_templates = [] - data={} - data['name']=channel - data['resource_key']=resource_key - data['value']=resource_value - #result_templates.append(create_physical_config(data)) + list_resource_key = resource_key.split("/") # the rule resource key management + if "ip_link" in list_resource_key[1]: # network instance rules management + result_templates.append(ip_link_mgmt(resource_value,vendor,delete)) return result_templates diff --git a/src/device/service/drivers/oc_driver/templates/__init__.py b/src/device/service/drivers/oc_driver/templates/__init__.py index 3ccc21c7db78aac26daa1f8c5ff8e1ffd3f35460..411579ecad5d08be065131020cdb2e0d34738fbe 100644 --- a/src/device/service/drivers/oc_driver/templates/__init__.py +++ b/src/device/service/drivers/oc_driver/templates/__init__.py @@ -12,3 +12,288 @@ # See the License for the specific language governing permissions and # limitations under the License. +import json, logging, lxml.etree as ET, re +import time +from typing import Any, Dict, Optional +from jinja2 import Environment, PackageLoader, select_autoescape +import paramiko +from .Tools import generate_templates + +LOGGER = logging.getLogger(__name__) + + + +LOGGER = logging.getLogger(__name__) +RE_REMOVE_FILTERS = re.compile(r'\[[^\]]+\]') +RE_REMOVE_FILTERS_2 = re.compile(r'\/[a-z]+:') +EMPTY_CONFIG = '' +EMPTY_FILTER = '' +JINJA_ENV = Environment(loader=PackageLoader('device.service.drivers.openconfig'), autoescape=select_autoescape()) + +""" +# Method Name: compose_config + +# Parameters: + - resource_key: [str] Variable to identify the rule to be executed. + - resource_value: [str] Variable with the configuration parameters of the rule to be executed. + - delete: [bool] Variable to identify whether to create or delete the rule. + - vendor: [str] Variable to identify the vendor of the equipment to be configured. + - message_renderer [str] Variable to dientify template generation method. Can be "jinja" or "pyangbind". + +# Functionality: + This method calls the function obtains the equipment configuration template according to the value of the variable "message_renderer". + Depending on the value of this variable, it gets the template with "jinja" or "pyangbind". + +# Return: + [dict] Set of templates obtained according to the configuration method +""" + +def compose_config( # template generation + resource_key : str, resource_value : str, delete : bool = False, vendor : Optional[str] = None, message_renderer = str +) -> str: + + if (message_renderer == "pyangbind"): + templates = (generate_templates(resource_key, resource_value, delete, vendor)) + return [ + '{:s}'.format(template) # format correction + for template in templates + ] + + elif (message_renderer == "jinja"): + templates = [] + template_name = '{:s}/edit_config.xml'.format(RE_REMOVE_FILTERS.sub('', resource_key)) + templates.append(JINJA_ENV.get_template(template_name)) + data : Dict[str, Any] = json.loads(resource_value) + + operation = 'delete' if delete else 'merge' # others + #operation = 'delete' if delete else '' # ipinfusion? + + return [ + '{:s}'.format( + template.render(**data, operation=operation, vendor=vendor).strip()) + for template in templates + ] + + else: + raise ValueError('Invalid message_renderer value: {}'.format(message_renderer)) + +""" +# Method Name: cli_compose_config + +# Parameters: + - resource_key: [str] Variable to identify the rule to be executed. + - resource_value: [str] Variable with the configuration parameters of the rule to be executed. + - delete: [bool] Variable to identify whether to create or delete the rule. + - vendor: [str] Variable to identify the vendor of the equipment to be configured. + - message_renderer [str] Variable to dientify template generation method. Can be "jinja" or "pyangbind". + +# Functionality: + This method calls the function obtains the equipment configuration template according to the value of the variable "message_renderer". + Depending on the value of this variable, it gets the template with "jinja" or "pyangbind". + +# Return: + [dict] Set of templates obtained according to the configuration method +""" + +def cli_compose_config(resources, delete: bool, host: str, user: str, passw: str): #Method used for configuring via CLI directly L2VPN in CISCO devices + + key_value_data = {} + + for path, json_str in resources: + key_value_data[path] = json_str + + # Iterate through the resources and extract parameter values dynamically + for path, json_str in resources: + data = json.loads(json_str) + if 'VC_ID' in data: vc_id = data['VC_ID'] + if 'connection_point' in data: connection_point = data['connection_point'] + if 'remote_system' in data: remote_system = data['remote_system'] + if 'interface' in data: + interface = data['interface'] + interface = interface.split("-") #New Line To Avoid Bad Endpoint Name In CISCO + interface = interface[1] + if 'vlan_id' in data: vlan_id = data['vlan_id'] + if 'name' in data: ni_name = data['name'] + if 'type' in data: ni_type = data['type'] + if 'index' in data: subif_index = data['index'] + if 'description' in data: description = data['description'] + else: description = " " + + # initialize the SSH client + ssh_client = paramiko.SSHClient() + ssh_client.load_system_host_keys() + # add to known hosts + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + ssh_client.connect(hostname=host, username=user, password=passw, look_for_keys=False) + #print("Connection successful") + LOGGER.warning("Connection successful") + except: + #print("[!] Cannot connect to the SSH Server") + LOGGER.warning("[!] Cannot connect to the SSH Server") + exit() + + try: + # Open an SSH shell + channel = ssh_client.invoke_shell() + channel.send('enable\n') + time.sleep(1) + channel.send('conf term\n') + time.sleep(0.1) + channel.send(f"interface {interface} l2transport\n") + time.sleep(0.1) + channel.send('description l2vpn_vpws_example\n') + time.sleep(0.1) + channel.send(f"encapsulation dot1q {vlan_id}\n") + time.sleep(0.1) + channel.send('mtu 9088\n') + time.sleep(0.1) + channel.send('commit\n') + time.sleep(0.1) + + channel.send('l2vpn\n') + time.sleep(0.1) + channel.send('load-balancing flow src-dst-ip\n') + time.sleep(0.1) + channel.send('pw-class l2vpn_vpws_profile_example\n') + time.sleep(0.1) + channel.send('encapsulation mpls\n') + time.sleep(0.1) + channel.send('transport-mode vlan passthrough\n') + time.sleep(0.1) + channel.send('control-word\n') + time.sleep(0.1) + channel.send('exit\n') + time.sleep(0.1) + channel.send('l2vpn\n') + time.sleep(0.1) + channel.send('xconnect group l2vpn_vpws_group_example\n') + time.sleep(0.1) + channel.send(f"p2p {ni_name}\n") + time.sleep(0.1) + channel.send(f"interface {interface}\n") #Ignore the VlanID because the interface already includes the vlanid tag + time.sleep(0.1) + channel.send(f"neighbor ipv4 {remote_system} pw-id {vc_id}\n") + time.sleep(0.1) + channel.send('pw-class l2vpn_vpws_profile_example\n') + time.sleep(0.1) + channel.send('exit\n') + time.sleep(0.1) + channel.send(f"description {description}\n") + time.sleep(0.1) + channel.send('commit\n') + time.sleep(0.1) + # Capturar la salida del comando + output = channel.recv(65535).decode('utf-8') + #print(output) + LOGGER.warning(output) + # Close the SSH shell + channel.close() + + except Exception as e: + LOGGER.exception(f"Error with the CLI configuration: {e}") + + # Close the SSH client + ssh_client.close() + +def ufi_interface(resources, delete: bool, host: str, user: str, passw: str): #Method used for configuring via CLI directly L2VPN in CISCO devices + + key_value_data = {} + + for path, json_str in resources: + key_value_data[path] = json_str + + # initialize the SSH client + ssh_client = paramiko.SSHClient() + ssh_client.load_system_host_keys() + # add to known hosts + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + ssh_client.connect(hostname=host, username=user, password=passw, look_for_keys=False) + LOGGER.warning("Connection successful") + except: + LOGGER.warning("[!] Cannot connect to the SSH Server") + exit() + interface = 'ge100-0/0/3/1' + ip = '1.1.1.1' + mask = '24' + vlan = '1212' + try: + # Open an SSH shell + channel = ssh_client.invoke_shell() + time.sleep(5) + channel.send('config\n') + time.sleep(1) + channel.send(f'interfaces {interface} \n') + time.sleep(1) + channel.send('admin-state enabled \n') + time.sleep(1) + channel.send(f'ipv4-address {ip}/{mask} \n') + time.sleep(1) + channel.send(f'vlan-id {vlan} \n') + time.sleep(1) + channel.send('commit\n') + time.sleep(1) + + output = channel.recv(65535).decode('utf-8') + LOGGER.warning(output) + # Close the SSH shell + channel.close() + + except Exception as e: + LOGGER.exception(f"Error with the CLI configuration: {e}") + + # Close the SSH client + ssh_client.close() + +def cisco_interface(resources, delete: bool, host: str, user: str, passw: str): #Method used for configuring via CLI directly L2VPN in CISCO devices + + key_value_data = {} + for path, json_str in resources: + key_value_data[path] = json_str + + # initialize the SSH client + ssh_client = paramiko.SSHClient() + ssh_client.load_system_host_keys() + # add to known hosts + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + ssh_client.connect(hostname=host, username=user, password=passw, look_for_keys=False) + LOGGER.warning("Connection successful") + except: + LOGGER.warning("[!] Cannot connect to the SSH Server") + exit() + interface = 'FourHundredGigE0/0/0/10.1212' + ip = '1.1.1.1' + mask = '24' + vlan = '1212' + try: + # Open an SSH shell + channel = ssh_client.invoke_shell() + time.sleep(1) + channel.send('config\n') + time.sleep(0.1) + channel.send(f'interface {interface} \n') + time.sleep(0.1) + channel.send('no shutdown\n') + time.sleep(0.1) + channel.send(f'ipv4 address {ip}/{mask} \n') + time.sleep(0.1) + channel.send(f'encapsulation dot1q {vlan} \n') + time.sleep(0.1) + channel.send('commit\n') + time.sleep(0.1) + + output = channel.recv(65535).decode('utf-8') + LOGGER.warning(output) + # Close the SSH shell + channel.close() + + except Exception as e: + LOGGER.exception(f"Error with the CLI configuration: {e}") + + # Close the SSH client + ssh_client.close() diff --git a/src/device/service/drivers/openconfig/OpenConfigDriver.py b/src/device/service/drivers/openconfig/OpenConfigDriver.py index 4bf7be22c1d474a4ddf91d23f96f412d631057b2..46931674b10bd765129b90d6fa6c3cf0d8513caa 100644 --- a/src/device/service/drivers/openconfig/OpenConfigDriver.py +++ b/src/device/service/drivers/openconfig/OpenConfigDriver.py @@ -30,7 +30,7 @@ from device.service.driver_api.Exceptions import UnsupportedResourceKeyException from device.service.driver_api._Driver import _Driver from device.service.driver_api.AnyTreeTools import TreeNode, get_subnode, set_subnode_value #dump_subtree #from .Tools import xml_pretty_print, xml_to_dict, xml_to_file -from .templates import ALL_RESOURCE_KEYS, EMPTY_CONFIG, compose_config, get_filter, parse, cli_compose_config +from .templates import ALL_RESOURCE_KEYS, EMPTY_CONFIG, compose_config, get_filter, parse, cli_compose_config, ufi_interface, cisco_interface from .RetryDecorator import retry DEBUG_MODE = False @@ -212,10 +212,22 @@ def edit_config( ): str_method = 'DeleteConfig' if delete else 'SetConfig' results = [] - if "L2VSI" in resources[0][1] and netconf_handler.vendor == "CISCO": + if netconf_handler.vendor == "CISCO": + if "L2VSI" in resources[0][1]: + #Configure by CLI + logger.warning("CLI Configuration") + cli_compose_config(resources, delete=delete, host= netconf_handler._NetconfSessionHandler__address, user=netconf_handler._NetconfSessionHandler__username, passw=netconf_handler._NetconfSessionHandler__password) + for i,resource in enumerate(resources): + results.append(True) + else: + logger.warning("CLI Configuration CISCO INTERFACE") + cisco_interface(resources, delete=delete, host= netconf_handler._NetconfSessionHandler__address, user=netconf_handler._NetconfSessionHandler__username, passw=netconf_handler._NetconfSessionHandler__password) + for i,resource in enumerate(resources): + results.append(True) + elif netconf_handler.vendor == "UFISPACE": #Configure by CLI - logger.warning("CLI Configuration") - cli_compose_config(resources, delete=delete, host= netconf_handler._NetconfSessionHandler__address, user=netconf_handler._NetconfSessionHandler__username, passw=netconf_handler._NetconfSessionHandler__password) + logger.warning("CLI Configuration: {:s}".format(resources)) + ufi_interface(resources, delete=delete) for i,resource in enumerate(resources): results.append(True) else: diff --git a/src/device/service/drivers/openconfig/templates/IP_LINK/IP_LINK_multivendor.py b/src/device/service/drivers/openconfig/templates/IP_LINK/IP_LINK_multivendor.py new file mode 100755 index 0000000000000000000000000000000000000000..b13e3ee3d606c3c2d8ded0d3e5d8c4997bbaa7bf --- /dev/null +++ b/src/device/service/drivers/openconfig/templates/IP_LINK/IP_LINK_multivendor.py @@ -0,0 +1,59 @@ +# 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 yattag import Doc, indent + +def ip_link_mgmt(data,vendor, delete): + doc, tag, text = Doc().tagtext() + + ID = data['endpoint_id']['endpoint_uuid']['uuid'] + DATA = data["rule_set"] + + with tag('interfaces', xmlns="http://openconfig.net/yang/interfaces"): + if delete == True: + with tag('interface' ,'xmlns:nc="urn:ietf:params:xml:ns:netconf:base:1.0" nc:operation="delete"'): + with tag('name'):text(ID) + else: + with tag('interface'): + with tag('name'):text(ID) + with tag('config'): + with tag('name'):text(ID) + with tag('type', 'xmlns:ianaift="urn:ietf:params:xml:ns:yang:iana-if-type"'):text('ianaift:l3ipvlan') + with tag('enabled'):text('true') + with tag('subinterfaces'): + with tag('subinterface'): + if vendor is None or vendor == 'ADVA': + with tag('index'): text('0') + with tag('config'): + with tag('index'): text('0') + if vendor == 'ADVA' and not 'vlan'in data: + with tag('untagged-allowed', 'xmlns="http://www.advaoptical.com/cim/adva-dnos-oc-interfaces"'):text('true') + with tag('vlan', xmlns="http://openconfig.net/yang/vlan"): + with tag('match'): + with tag('single-tagged'): + with tag('config'): + with tag('vlan-id'):text(DATA['vlan']) + with tag('ipv4', xmlns="http://openconfig.net/yang/interfaces/ip"): + with tag('addresses'): + with tag('address'): + with tag('ip'):text(DATA['ip']) + with tag('config'): + with tag('ip'):text(DATA['ip']) + with tag('prefix-length'):text(DATA['mask']) + result = indent( + doc.getvalue(), + indentation = ' '*2, + newline = '\r\n' + ) + return result diff --git a/src/device/service/drivers/openconfig/templates/IP_LINK/__init__.py b/src/device/service/drivers/openconfig/templates/IP_LINK/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3704791d86a003d938d1831af8f15b6480c7166a --- /dev/null +++ b/src/device/service/drivers/openconfig/templates/IP_LINK/__init__.py @@ -0,0 +1,376 @@ +# 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 ast import List, Tuple +import json, logging, lxml.etree as ET, re +import time +from typing import Any, Dict, Optional +from jinja2 import Environment, PackageLoader, select_autoescape +import paramiko +from .Tools import generate_templates +from device.service.driver_api._Driver import ( + RESOURCE_ENDPOINTS, RESOURCE_INTERFACES, RESOURCE_NETWORK_INSTANCES, RESOURCE_ROUTING_POLICIES, RESOURCE_ACL, RESOURCE_INVENTORY) +from .EndPoints import parse as parse_endpoints +from .Interfaces import parse as parse_interfaces, parse_counters +from .NetworkInstances import parse as parse_network_instances +from .RoutingPolicy import parse as parse_routing_policy +from .Acl import parse as parse_acl +from .Inventory import parse as parse_inventory +from .acl.acl_adapter import acl_cr_to_dict +from .acl.acl_adapter_ipinfusion_proprietary import acl_cr_to_dict_ipinfusion_proprietary + +LOGGER = logging.getLogger(__name__) + +ALL_RESOURCE_KEYS = [ + RESOURCE_INVENTORY, + RESOURCE_ENDPOINTS, + RESOURCE_INTERFACES, + RESOURCE_ROUTING_POLICIES, # routing policies should come before network instances + RESOURCE_NETWORK_INSTANCES, + RESOURCE_ACL, +] + +RESOURCE_KEY_MAPPINGS = { + RESOURCE_INVENTORY : 'inventory', + RESOURCE_ENDPOINTS : 'component', + RESOURCE_INTERFACES : 'interface', + RESOURCE_NETWORK_INSTANCES: 'network_instance', + RESOURCE_ROUTING_POLICIES : 'routing_policy', + RESOURCE_ACL : 'acl', +} + +RESOURCE_PARSERS = { + 'inventory' : parse_inventory, + 'component' : parse_endpoints, + 'interface' : parse_interfaces, + 'network_instance': parse_network_instances, + 'routing_policy' : parse_routing_policy, + 'interfaces/interface/state/counters': parse_counters, + 'acl' : parse_acl, +} + +LOGGER = logging.getLogger(__name__) +RE_REMOVE_FILTERS = re.compile(r'\[[^\]]+\]') +RE_REMOVE_FILTERS_2 = re.compile(r'\/[a-z]+:') +EMPTY_CONFIG = '' +EMPTY_FILTER = '' +JINJA_ENV = Environment(loader=PackageLoader('device.service.drivers.openconfig'), autoescape=select_autoescape()) + +def get_filter(resource_key : str): + resource_key = RESOURCE_KEY_MAPPINGS.get(resource_key, resource_key) + resource_key = RE_REMOVE_FILTERS.sub('', resource_key) + resource_key = RE_REMOVE_FILTERS_2.sub('/', resource_key) + resource_key = resource_key.replace('//', '') + template_name = '{:s}/get.xml'.format(resource_key) + template = JINJA_ENV.get_template(template_name) + return '{:s}'.format(template.render().strip()) + +def parse(resource_key : str, xml_data : ET.Element): + resource_key = RESOURCE_KEY_MAPPINGS.get(resource_key, resource_key) + resource_key = RE_REMOVE_FILTERS.sub('', resource_key) + resource_key = RE_REMOVE_FILTERS_2.sub('/', resource_key) + resource_key = resource_key.replace('//', '') + parser = RESOURCE_PARSERS.get(resource_key) + if parser is None: return [(resource_key, xml_data)] + return parser(xml_data) + +""" +# Method Name: compose_config + +# Parameters: + - resource_key: [str] Variable to identify the rule to be executed. + - resource_value: [str] Variable with the configuration parameters of the rule to be executed. + - delete: [bool] Variable to identify whether to create or delete the rule. + - vendor: [str] Variable to identify the vendor of the equipment to be configured. + - message_renderer [str] Variable to dientify template generation method. Can be "jinja" or "pyangbind". + +# Functionality: + This method calls the function obtains the equipment configuration template according to the value of the variable "message_renderer". + Depending on the value of this variable, it gets the template with "jinja" or "pyangbind". + +# Return: + [dict] Set of templates obtained according to the configuration method +""" + +def compose_config( # template generation + resource_key : str, resource_value : str, delete : bool = False, vendor : Optional[str] = None, message_renderer = str +) -> str: + + if (message_renderer == "pyangbind"): + templates = (generate_templates(resource_key, resource_value, delete,vendor)) + return [ + '{:s}'.format(template) # format correction + for template in templates + ] + + elif (message_renderer == "jinja"): + templates = [] + if "acl_ruleset" in resource_key: # MANAGING ACLs + if vendor == 'ipinfusion': # ipinfusion proprietary netconf receipe is used temporarily + enable_ingress_filter_path = 'acl/interfaces/ingress/enable_ingress_filter.xml' + acl_entry_path = 'acl/acl-set/acl-entry/edit_config_ipinfusion_proprietary.xml' + acl_ingress_path = 'acl/interfaces/ingress/edit_config_ipinfusion_proprietary.xml' + data : Dict[str, Any] = acl_cr_to_dict_ipinfusion_proprietary(resource_value, delete=delete) + else: + enable_ingress_filter_path = 'acl/interfaces/ingress/enable_ingress_filter.xml' + acl_entry_path = 'acl/acl-set/acl-entry/edit_config.xml' + acl_ingress_path = 'acl/interfaces/ingress/edit_config.xml' + data : Dict[str, Any] = acl_cr_to_dict(resource_value, delete=delete) + + if delete: # unpair acl and interface before removing acl + templates.append(JINJA_ENV.get_template(acl_ingress_path)) + templates.append(JINJA_ENV.get_template(acl_entry_path)) + templates.append(JINJA_ENV.get_template(enable_ingress_filter_path)) + else: + templates.append(JINJA_ENV.get_template(enable_ingress_filter_path)) + templates.append(JINJA_ENV.get_template(acl_entry_path)) + templates.append(JINJA_ENV.get_template(acl_ingress_path)) + else: + template_name = '{:s}/edit_config.xml'.format(RE_REMOVE_FILTERS.sub('', resource_key)) + templates.append(JINJA_ENV.get_template(template_name)) + data : Dict[str, Any] = json.loads(resource_value) + + operation = 'delete' if delete else 'merge' # others + #operation = 'delete' if delete else '' # ipinfusion? + + return [ + '{:s}'.format( + template.render(**data, operation=operation, vendor=vendor).strip()) + for template in templates + ] + + else: + raise ValueError('Invalid message_renderer value: {}'.format(message_renderer)) + +""" +# Method Name: cli_compose_config + +# Parameters: + - resource_key: [str] Variable to identify the rule to be executed. + - resource_value: [str] Variable with the configuration parameters of the rule to be executed. + - delete: [bool] Variable to identify whether to create or delete the rule. + - vendor: [str] Variable to identify the vendor of the equipment to be configured. + - message_renderer [str] Variable to dientify template generation method. Can be "jinja" or "pyangbind". + +# Functionality: + This method calls the function obtains the equipment configuration template according to the value of the variable "message_renderer". + Depending on the value of this variable, it gets the template with "jinja" or "pyangbind". + +# Return: + [dict] Set of templates obtained according to the configuration method +""" + +def cli_compose_config(resources, delete: bool, host: str, user: str, passw: str): #Method used for configuring via CLI directly L2VPN in CISCO devices + + key_value_data = {} + + for path, json_str in resources: + key_value_data[path] = json_str + + # Iterate through the resources and extract parameter values dynamically + for path, json_str in resources: + data = json.loads(json_str) + if 'VC_ID' in data: vc_id = data['VC_ID'] + if 'connection_point' in data: connection_point = data['connection_point'] + if 'remote_system' in data: remote_system = data['remote_system'] + if 'interface' in data: + interface = data['interface'] + interface = interface.split("-") #New Line To Avoid Bad Endpoint Name In CISCO + interface = interface[1] + if 'vlan_id' in data: vlan_id = data['vlan_id'] + if 'name' in data: ni_name = data['name'] + if 'type' in data: ni_type = data['type'] + if 'index' in data: subif_index = data['index'] + if 'description' in data: description = data['description'] + else: description = " " + + # initialize the SSH client + ssh_client = paramiko.SSHClient() + ssh_client.load_system_host_keys() + # add to known hosts + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + ssh_client.connect(hostname=host, username=user, password=passw, look_for_keys=False) + #print("Connection successful") + LOGGER.warning("Connection successful") + except: + #print("[!] Cannot connect to the SSH Server") + LOGGER.warning("[!] Cannot connect to the SSH Server") + exit() + + try: + # Open an SSH shell + channel = ssh_client.invoke_shell() + channel.send('enable\n') + time.sleep(1) + channel.send('conf term\n') + time.sleep(0.1) + channel.send(f"interface {interface} l2transport\n") + time.sleep(0.1) + channel.send('description l2vpn_vpws_example\n') + time.sleep(0.1) + channel.send(f"encapsulation dot1q {vlan_id}\n") + time.sleep(0.1) + channel.send('mtu 9088\n') + time.sleep(0.1) + channel.send('commit\n') + time.sleep(0.1) + + channel.send('l2vpn\n') + time.sleep(0.1) + channel.send('load-balancing flow src-dst-ip\n') + time.sleep(0.1) + channel.send('pw-class l2vpn_vpws_profile_example\n') + time.sleep(0.1) + channel.send('encapsulation mpls\n') + time.sleep(0.1) + channel.send('transport-mode vlan passthrough\n') + time.sleep(0.1) + channel.send('control-word\n') + time.sleep(0.1) + channel.send('exit\n') + time.sleep(0.1) + channel.send('l2vpn\n') + time.sleep(0.1) + channel.send('xconnect group l2vpn_vpws_group_example\n') + time.sleep(0.1) + channel.send(f"p2p {ni_name}\n") + time.sleep(0.1) + channel.send(f"interface {interface}\n") #Ignore the VlanID because the interface already includes the vlanid tag + time.sleep(0.1) + channel.send(f"neighbor ipv4 {remote_system} pw-id {vc_id}\n") + time.sleep(0.1) + channel.send('pw-class l2vpn_vpws_profile_example\n') + time.sleep(0.1) + channel.send('exit\n') + time.sleep(0.1) + channel.send(f"description {description}\n") + time.sleep(0.1) + channel.send('commit\n') + time.sleep(0.1) + # Capturar la salida del comando + output = channel.recv(65535).decode('utf-8') + #print(output) + LOGGER.warning(output) + # Close the SSH shell + channel.close() + + except Exception as e: + LOGGER.exception(f"Error with the CLI configuration: {e}") + + # Close the SSH client + ssh_client.close() + +def ufi_interface(resources, delete: bool): #Method used for configuring via CLI directly L2VPN in CISCO devices + + key_value_data = {} + + for path, json_str in resources: + key_value_data[path] = json_str + + # initialize the SSH client + ssh_client = paramiko.SSHClient() + ssh_client.load_system_host_keys() + # add to known hosts + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + ssh_client.connect(hostname='10.95.90.75', username='dnroot', password='dnroot', look_for_keys=False) + LOGGER.warning("Connection successful") + except: + LOGGER.warning("[!] Cannot connect to the SSH Server") + exit() + interface = 'ge100-0/0/3/1' + ip = '1.1.1.1' + mask = '24' + vlan = '1212' + try: + # Open an SSH shell + channel = ssh_client.invoke_shell() + time.sleep(5) + channel.send('config\n') + time.sleep(1) + channel.send(f'interfaces {interface} \n') + time.sleep(1) + channel.send('admin-state enabled \n') + time.sleep(1) + channel.send(f'ipv4-address {ip}/{mask} \n') + time.sleep(1) + channel.send(f'vlan-id {vlan} \n') + time.sleep(1) + channel.send('commit\n') + time.sleep(1) + + output = channel.recv(65535).decode('utf-8') + LOGGER.warning(output) + # Close the SSH shell + channel.close() + + except Exception as e: + LOGGER.exception(f"Error with the CLI configuration: {e}") + + # Close the SSH client + ssh_client.close() + +def cisco_interface(resources, delete: bool, host: str, user: str, passw: str): #Method used for configuring via CLI directly L2VPN in CISCO devices + + key_value_data = {} + + for path, json_str in resources: + key_value_data[path] = json_str + + # initialize the SSH client + ssh_client = paramiko.SSHClient() + ssh_client.load_system_host_keys() + # add to known hosts + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + ssh_client.connect(hostname='10.90.95.150', username='cisco', password='cisco123', look_for_keys=False) + LOGGER.warning("Connection successful") + except: + LOGGER.warning("[!] Cannot connect to the SSH Server") + exit() + interface = 'FourHundredGigE0/0/0/10.1212' + ip = '1.1.1.1' + mask = '24' + vlan = '1212' + try: + # Open an SSH shell + channel = ssh_client.invoke_shell() + time.sleep(1) + channel.send('config\n') + time.sleep(0.1) + channel.send(f'interface {interface} \n') + time.sleep(0.1) + channel.send('no shutdown\n') + time.sleep(0.1) + channel.send(f'ipv4 address {ip}/{mask} \n') + time.sleep(0.1) + channel.send(f'encapsulation dot1q {vlan} \n') + time.sleep(0.1) + channel.send('commit\n') + time.sleep(0.1) + + output = channel.recv(65535).decode('utf-8') + LOGGER.warning(output) + # Close the SSH shell + channel.close() + + except Exception as e: + LOGGER.exception(f"Error with the CLI configuration: {e}") + + # Close the SSH client + ssh_client.close() diff --git a/src/device/service/drivers/openconfig/templates/Tools.py b/src/device/service/drivers/openconfig/templates/Tools.py index 62a0ab96299acd0054289b3559d3b3775c0a4334..2bf9535b3965aedf372a122b3d2c66678a419761 100644 --- a/src/device/service/drivers/openconfig/templates/Tools.py +++ b/src/device/service/drivers/openconfig/templates/Tools.py @@ -15,9 +15,10 @@ import json import lxml.etree as ET from typing import Collection, Dict, Any -from .ACL.ACL_multivendor import acl_mgmt +from .ACL.ACL_multivendor import acl_mgmt +from .IP_LINK.IP_LINK_multivendor import ip_link_mgmt from .VPN.Network_instance_multivendor import create_NI, associate_virtual_circuit, associate_RP_to_NI, add_protocol_NI, create_table_conns, associate_If_to_NI -from .VPN.Interfaces_multivendor import create_If_SubIf +from .VPN.Interfaces_multivendor import create_If_SubIf from .VPN.Routing_policy import create_rp_def, create_rp_statement def add_value_from_tag(target : Dict, field_name: str, field_value : ET.Element, cast=None) -> None: @@ -70,7 +71,7 @@ def generate_templates(resource_key: str, resource_value: str, delete: bool,vend else: result_templates.append(create_NI(data,vendor,delete)) - if "interface" in list_resource_key[1]: # interface rules management + elif "interface" in list_resource_key[1]: # interface rules management data: Dict[str, Any] = json.loads(resource_value) #data['DEL'] = delete if "subinterface" in resource_key: @@ -83,8 +84,10 @@ def generate_templates(resource_key: str, resource_value: str, delete: bool,vend result_templates.append(create_rp_def(data, delete)) else: result_templates.append(create_rp_statement(data, delete)) - else: - if "acl_ruleset" in resource_key: # acl rules management + elif "acl_ruleset" in resource_key: # acl rules management result_templates.extend(acl_mgmt(resource_value,vendor, delete)) + else: + if "ip_link" in resource_key: + result_templates.append(ip_link_mgmt(resource_value,vendor,delete)) - return result_templates \ No newline at end of file + return result_templates diff --git a/src/device/service/drivers/openconfig/templates/__init__.py b/src/device/service/drivers/openconfig/templates/__init__.py index 47559efda07a08e9e56a980436d7dd1470556ecf..3704791d86a003d938d1831af8f15b6480c7166a 100644 --- a/src/device/service/drivers/openconfig/templates/__init__.py +++ b/src/device/service/drivers/openconfig/templates/__init__.py @@ -272,4 +272,105 @@ def cli_compose_config(resources, delete: bool, host: str, user: str, passw: str # Close the SSH client ssh_client.close() - \ No newline at end of file + +def ufi_interface(resources, delete: bool): #Method used for configuring via CLI directly L2VPN in CISCO devices + + key_value_data = {} + + for path, json_str in resources: + key_value_data[path] = json_str + + # initialize the SSH client + ssh_client = paramiko.SSHClient() + ssh_client.load_system_host_keys() + # add to known hosts + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + ssh_client.connect(hostname='10.95.90.75', username='dnroot', password='dnroot', look_for_keys=False) + LOGGER.warning("Connection successful") + except: + LOGGER.warning("[!] Cannot connect to the SSH Server") + exit() + interface = 'ge100-0/0/3/1' + ip = '1.1.1.1' + mask = '24' + vlan = '1212' + try: + # Open an SSH shell + channel = ssh_client.invoke_shell() + time.sleep(5) + channel.send('config\n') + time.sleep(1) + channel.send(f'interfaces {interface} \n') + time.sleep(1) + channel.send('admin-state enabled \n') + time.sleep(1) + channel.send(f'ipv4-address {ip}/{mask} \n') + time.sleep(1) + channel.send(f'vlan-id {vlan} \n') + time.sleep(1) + channel.send('commit\n') + time.sleep(1) + + output = channel.recv(65535).decode('utf-8') + LOGGER.warning(output) + # Close the SSH shell + channel.close() + + except Exception as e: + LOGGER.exception(f"Error with the CLI configuration: {e}") + + # Close the SSH client + ssh_client.close() + +def cisco_interface(resources, delete: bool, host: str, user: str, passw: str): #Method used for configuring via CLI directly L2VPN in CISCO devices + + key_value_data = {} + + for path, json_str in resources: + key_value_data[path] = json_str + + # initialize the SSH client + ssh_client = paramiko.SSHClient() + ssh_client.load_system_host_keys() + # add to known hosts + ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + + try: + ssh_client.connect(hostname='10.90.95.150', username='cisco', password='cisco123', look_for_keys=False) + LOGGER.warning("Connection successful") + except: + LOGGER.warning("[!] Cannot connect to the SSH Server") + exit() + interface = 'FourHundredGigE0/0/0/10.1212' + ip = '1.1.1.1' + mask = '24' + vlan = '1212' + try: + # Open an SSH shell + channel = ssh_client.invoke_shell() + time.sleep(1) + channel.send('config\n') + time.sleep(0.1) + channel.send(f'interface {interface} \n') + time.sleep(0.1) + channel.send('no shutdown\n') + time.sleep(0.1) + channel.send(f'ipv4 address {ip}/{mask} \n') + time.sleep(0.1) + channel.send(f'encapsulation dot1q {vlan} \n') + time.sleep(0.1) + channel.send('commit\n') + time.sleep(0.1) + + output = channel.recv(65535).decode('utf-8') + LOGGER.warning(output) + # Close the SSH shell + channel.close() + + except Exception as e: + LOGGER.exception(f"Error with the CLI configuration: {e}") + + # Close the SSH client + ssh_client.close() diff --git a/src/pathcomp/frontend/service/algorithms/_Algorithm.py b/src/pathcomp/frontend/service/algorithms/_Algorithm.py index 5e1a5e3a610928fcd8b1e6180d54e2f60d907deb..7f1a28895cd4af1878b8d6879ba86e2ec14e3a3a 100644 --- a/src/pathcomp/frontend/service/algorithms/_Algorithm.py +++ b/src/pathcomp/frontend/service/algorithms/_Algorithm.py @@ -29,6 +29,7 @@ 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, + compose_iplink_config_rules ) from .tools.ComposeRequest import compose_device, compose_link, compose_service from .tools.ComputeSubServices import ( @@ -150,7 +151,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( @@ -203,6 +204,9 @@ 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_IP_LINK: + compose_iplink_config_rules(config_rules, service.service_config.config_rules) + self.logger.info("Installing default rules for IP-LINK service") else: MSG = 'Unhandled generic Config Rules for service {:s} {:s}' self.logger.warning(MSG.format(str(service_uuid), str(ServiceTypeEnum.Name(service_type)))) @@ -376,7 +380,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 073c3474fe888a0daf0e20187f46809bbd859b0a..383aa8a8b16b0e72d6ac637f0fda0279b0194355 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py @@ -54,6 +54,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: @@ -108,6 +112,11 @@ 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] @@ -153,10 +162,34 @@ 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) + 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))) + 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') @@ -292,49 +325,55 @@ 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'] + 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: - 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) + 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['ip_link'] = 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))) LOGGER.debug('[generate_neighbor_endpoint_config_rules] end') diff --git a/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py index 5943bf1ba245f7fe3997492f88bcc5787dc830cb..2738bb0bec871d3d81c4dc6e15108d7adb51b5bf 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ServiceTypes.py @@ -43,10 +43,11 @@ 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_IP_LINK = {ServiceTypeEnum.SERVICETYPE_IP_LINK} def get_service_type( device_type : DeviceTypeEnum, prv_service_type : ServiceTypeEnum @@ -57,6 +58,10 @@ def get_service_type( 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_IP_LINK + ): 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 dc2b83781c4ec30603de89073fd3cd35b7c70f13..002be371520c2e414df544e60dec8c4004700f15 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,8 @@ public class Serializer { return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_L3NM; case TAPI_CONNECTIVITY_SERVICE: return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_TAPI_CONNECTIVITY_SERVICE; + case IP_LINK: + return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_IP_LINK; case UNKNOWN: return ContextOuterClass.ServiceTypeEnum.SERVICETYPE_UNKNOWN; default: @@ -1181,6 +1183,8 @@ public class Serializer { return ServiceTypeEnum.L3NM; case SERVICETYPE_TAPI_CONNECTIVITY_SERVICE: return ServiceTypeEnum.TAPI_CONNECTIVITY_SERVICE; + case SERVICETYPE_IP_LINK: + return ServiceTypeEnum.IP_LINK; case SERVICETYPE_UNKNOWN: case UNRECOGNIZED: default: diff --git a/src/service/service/service_handler_api/FilterFields.py b/src/service/service/service_handler_api/FilterFields.py index caeb1cbf8c77275487019535198098ef8a36d055..2406ca5f042c516876f328e614db44c097a6f7ec 100644 --- a/src/service/service/service_handler_api/FilterFields.py +++ b/src/service/service/service_handler_api/FilterFields.py @@ -29,6 +29,7 @@ SERVICE_TYPE_VALUES = { ServiceTypeEnum.SERVICETYPE_E2E, ServiceTypeEnum.SERVICETYPE_OPTICAL_CONNECTIVITY, ServiceTypeEnum.SERVICETYPE_QKD, + ServiceTypeEnum.SERVICETYPE_IP_LINK ServiceTypeEnum.SERVICETYPE_INT, ServiceTypeEnum.SERVICETYPE_ACL, } diff --git a/src/service/service/service_handler_api/SettingsHandler.py b/src/service/service/service_handler_api/SettingsHandler.py index 5c9bc79a76215003a67d3fb7563b0102148b8e97..762339065c201b126aa52a7664cac5d8685669f4 100644 --- a/src/service/service/service_handler_api/SettingsHandler.py +++ b/src/service/service/service_handler_api/SettingsHandler.py @@ -47,6 +47,13 @@ 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 + endpoint_name, endpoint_index = extract_endpoint_index(endpoint_uuid) + IP_LINK_KEY_TEMPLATE = '/device[{:s}]/endpoint[{:s}]/subindex[{:d}]/ip_link' + key_or_path = IP_LINK_KEY_TEMPLATE.format(device_uuid, endpoint_name, endpoint_index) + 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))) @@ -111,6 +118,28 @@ 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)) + iplink_index = extract_index(res_value) + if not 'subindex[{:d}]'.format(iplink_index) in res_key: continue + ip_links.append((res_key, res_value)) + return ip_links 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 fa132909a5303554ad599c26f87803d64fb7203a..20a2fa3016b1accbb6a61c1593e845e091166c32 100644 --- a/src/service/service/service_handler_api/Tools.py +++ b/src/service/service/service_handler_api/Tools.py @@ -61,16 +61,16 @@ 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() if index is not None: index = int(index) 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': '") +def extract_index(res_value : Any) -> int: + res_value = grpc_message_to_json(res_value,use_integers_for_enums=True) + endpoint = res_value['endpoint_id']['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 2b06a1c3484131fa715ea1414a49d83e52c43f03..10a7effc225858ae966e7f4543b1711789ccfb54 100644 --- a/src/service/service/service_handlers/__init__.py +++ b/src/service/service/service_handlers/__init__.py @@ -34,6 +34,7 @@ from .tapi_tapi.TapiServiceHandler import TapiServiceHandler from .tapi_xr.TapiXrServiceHandler import TapiXrServiceHandler from .optical_tfs.OpticalTfsServiceHandler import OpticalTfsServiceHandler from .oc.OCServiceHandler import OCServiceHandler +from .ip_link.IP_LinkServiceHandler import IP_LinkServiceHandler from .qkd.qkd_service_handler import QKDServiceHandler from .l3nm_ryu.L3NMRyuServiceHandler import L3NMRyuServiceHandler @@ -167,6 +168,15 @@ SERVICE_HANDLERS = [ FilterFieldEnum.DEVICE_DRIVER : DeviceDriverEnum.DEVICEDRIVER_OC, } ]), + (IP_LinkServiceHandler, [ + { + FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_IP_LINK, + FilterFieldEnum.DEVICE_DRIVER : [ + DeviceDriverEnum.DEVICEDRIVER_OPENCONFIG, + DeviceDriverEnum.DEVICEDRIVER_OC, + ], + } + ]), (QKDServiceHandler, [ { FilterFieldEnum.SERVICE_TYPE : ServiceTypeEnum.SERVICETYPE_QKD, 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..7f41333e3e980090e2dfa550cc9e4845a91d85db --- /dev/null +++ b/src/service/service/service_handlers/ip_link/ConfigRules.py @@ -0,0 +1,58 @@ +# 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 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_ip_link : List [Tuple] +) -> List[Dict]: + + json_config_rules = [ + ] + + for res_key, res_value in endpoint_ip_link: + json_config_rules.append( + {'action': 1, 'ip_link': 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/ip_link/IP_LinkServiceHandler.py b/src/service/service/service_handlers/ip_link/IP_LinkServiceHandler.py new file mode 100644 index 0000000000000000000000000000000000000000..c844552cddf1dca9561460fd297b366c1bb11fc5 --- /dev/null +++ b/src/service/service/service_handlers/ip_link/IP_LinkServiceHandler.py @@ -0,0 +1,157 @@ +# 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, 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 [] + + 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))) + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + 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( + endpoint_name, 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..3ccc21c7db78aac26daa1f8c5ff8e1ffd3f35460 --- /dev/null +++ b/src/service/service/service_handlers/ip_link/__init__.py @@ -0,0 +1,14 @@ +# 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. + diff --git a/src/service/service/service_handlers/oc/ConfigRules.py b/src/service/service/service_handlers/oc/ConfigRules.py new file mode 100644 index 0000000000000000000000000000000000000000..99ab99e43a42e131adc179b0a898a13b608b80fe --- /dev/null +++ b/src/service/service/service_handlers/oc/ConfigRules.py @@ -0,0 +1,57 @@ +# 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 logging +from typing import Any, Dict, List, Optional, Tuple +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_ip_link : List [Tuple] +) -> List[Dict]: + + json_config_rules = [ + ] + + for res_key, res_value in endpoint_ip_link: + json_config_rules.append( + {'action': 1, 'ip_link': 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/oc/OCServiceHandler.py b/src/service/service/service_handlers/oc/OCServiceHandler.py index f2aca03c1b7c11933a33c685f40463c758882926..3235b4046d344a499d7cfc0cf577ab519b63831a 100644 --- a/src/service/service/service_handlers/oc/OCServiceHandler.py +++ b/src/service/service/service_handlers/oc/OCServiceHandler.py @@ -15,16 +15,17 @@ 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 DeviceId, Service +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 common.DeviceTypes import DeviceTypeEnum -from service.service.service_handler_api.Tools import get_endpoint_matching +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 from .OCTools import ( - convert_endpoints_to_flows, endpoints_to_flows, + convert_endpoints_to_flows #handle_flows_names, check_media_channel_existance ) @@ -68,6 +69,28 @@ class OCServiceHandler(_ServiceHandler): #handled_flows=handle_flows_names(flows=flows,task_executor=self.__task_executor) 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))) + endpoint_obj = get_endpoint_matching(device_obj, endpoint_uuid) + 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( + endpoint_name, 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) + LOGGER.info(f"flows {flows} ") LOGGER.info(f"settings {settings} ") @@ -102,6 +125,32 @@ class OCServiceHandler(_ServiceHandler): 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) + for device_uuid, dev_flows in flows.items(): try: channel_indexes= []