From f10241a6c1228d1100750f5e79bdff1e88fbce02 Mon Sep 17 00:00:00 2001 From: hajipour Date: Mon, 12 Feb 2024 12:24:01 +0000 Subject: [PATCH 1/5] bug fix: temporary fix for MEC PoC demo. Config rules and OpenConfig driver for L3VPN should change based on telefonica recipe. --- .../drivers/openconfig/OpenConfigDriver.py | 5 +- .../drivers/openconfig/templates/Tools.py | 3 +- .../VPN/Network_instance_multivendor.py | 36 ++++- .../nbi_plugins/etsi_bwm/Resources.py | 15 +- .../rest_server/nbi_plugins/etsi_bwm/Tools.py | 140 +++++++++++++----- .../algorithms/tools/ComposeConfigRules.py | 18 +++ .../l3nm_openconfig/ConfigRules.py | 66 +++++++-- 7 files changed, 229 insertions(+), 54 deletions(-) diff --git a/src/device/service/drivers/openconfig/OpenConfigDriver.py b/src/device/service/drivers/openconfig/OpenConfigDriver.py index 8c6e07b3f..99ae1c8db 100644 --- a/src/device/service/drivers/openconfig/OpenConfigDriver.py +++ b/src/device/service/drivers/openconfig/OpenConfigDriver.py @@ -226,8 +226,11 @@ def edit_config( 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) + resource_key, resource_value, delete=delete, vendor=netconf_handler.vendor, message_renderer='pyangbind') + # 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( diff --git a/src/device/service/drivers/openconfig/templates/Tools.py b/src/device/service/drivers/openconfig/templates/Tools.py index 79bebef51..3e8043680 100644 --- a/src/device/service/drivers/openconfig/templates/Tools.py +++ b/src/device/service/drivers/openconfig/templates/Tools.py @@ -61,7 +61,8 @@ def generate_templates(resource_key: str, resource_value: str, delete: bool,vend elif "inter_instance_policies" in resource_key: result_templates.append(associate_RP_to_NI(data)) elif "protocols" in resource_key: - if vendor == "ADVA": result_templates.append(add_protocol_NI(data, vendor, delete)) + result_templates.append(add_protocol_NI(data, vendor, delete)) + # if vendor == "ADVA": result_templates.append(add_protocol_NI(data, vendor, delete)) elif "table_connections" in resource_key: result_templates.append(create_table_conns(data, delete)) elif "interface" in resource_key: diff --git a/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py b/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py index c4d494ea6..e36955a0d 100644 --- a/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py +++ b/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py @@ -116,6 +116,9 @@ def add_protocol_NI(parameters,vendor, DEL): else: with tag('network-instance'): with tag('name'):text(parameters['name']) + with tag('config'): + with tag('name'): text(parameters['name']) + with tag('type', 'xmlns:oc-ni-types="http://openconfig.net/yang/network-instance-types"'): text('oc-ni-types:DEFAULT_INSTANCE') with tag('protocols'): with tag('protocol'): with tag('identifier', 'xmlns:oc-pol-types="http://openconfig.net/yang/policy-types"'):text('oc-pol-types:',parameters['identifier']) @@ -123,14 +126,41 @@ def add_protocol_NI(parameters,vendor, DEL): with tag('config'): with tag('identifier', 'xmlns:oc-pol-types="http://openconfig.net/yang/policy-types"'):text('oc-pol-types:',parameters['identifier']) with tag('name') :text(parameters['protocol_name']) + with tag('enabled'): text('true') if "BGP" in parameters['identifier']: with tag('bgp'): with tag('global'): + with tag('afi-safis'): + with tag('afi-safi'): + with tag('afi-safi-name', 'xmlns:oc-bgp-types="http://openconfig.net/yang/bgp-types"'): text('oc-bgp-types:IPV4_UNICAST') + with tag('config'): + with tag('afi-safi-name', 'xmlns:oc-bgp-types="http://openconfig.net/yang/bgp-types"'): text('oc-bgp-types:IPV4_UNICAST') + with tag('enabled'): text('true') with tag('config'): with tag('as') :text(parameters['as']) - if "router-id" in parameters: - with tag('router-id'):text(parameters['router-id']) - if vendor == "ADVA": + with tag('peer-groups'): + with tag('peer-group'): + with tag('peer-group-name'): text('IBGP') + with tag('config'): + with tag('peer-group-name'): text('IBGP') + with tag('peer-as'): text(parameters['protocol_name']) + with tag('afi-safis'): + with tag('afi-safi'): + with tag('afi-safi-name', 'xmlns:oc-bgp-types="http://openconfig.net/yang/bgp-types"'): text('oc-bgp-types:IPV4_UNICAST') + with tag('config'): + with tag('afi-safi-name', 'xmlns:oc-bgp-types="http://openconfig.net/yang/bgp-types"'): text('oc-bgp-types:IPV4_UNICAST') + with tag('enabled'): text('true') + + if 'neighbors' in parameters: + with tag('neighbors'): + for neighbor in parameters['neighbors']: + with tag('neighbor'): + with tag('neighbor-address'): text(neighbor['ip_address']) + with tag('config'): + with tag('neighbor-address'): text(neighbor['ip_address']) + with tag('peer-group'): text('IBGP') + # if vendor == "ADVA": + if True: with tag('tables'): with tag('table'): with tag('protocol', 'xmlns:oc-pol-types="http://openconfig.net/yang/policy-types"'):text('oc-pol-types:',parameters['identifier']) diff --git a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py index 3fccbbb55..394b50de8 100644 --- a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py +++ b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py @@ -13,7 +13,9 @@ # limitations under the License. import copy, deepmerge, json, logging +from typing import Dict from common.Constants import DEFAULT_CONTEXT_NAME +from werkzeug.exceptions import UnsupportedMediaType from context.client.ContextClient import ContextClient from flask_restful import Resource, request from service.client.ServiceClient import ServiceClient @@ -37,15 +39,20 @@ class BwInfo(_Resource): return bw_allocations def post(self): - bwinfo = request.get_json() - service = bwInfo_2_service(self.client, bwinfo) + if not request.is_json: + raise UnsupportedMediaType('JSON payload is required') + request_data: Dict = request.get_json() + service = bwInfo_2_service(self.client, request_data) stripped_service = copy.deepcopy(service) stripped_service.ClearField('service_endpoint_ids') stripped_service.ClearField('service_constraints') stripped_service.ClearField('service_config') - response = format_grpc_to_json(self.service_client.CreateService(stripped_service)) - response = format_grpc_to_json(self.service_client.UpdateService(service)) + try: + response = format_grpc_to_json(self.service_client.CreateService(stripped_service)) + response = format_grpc_to_json(self.service_client.UpdateService(service)) + except Exception as e: # pylint: disable=broad-except + return e return response diff --git a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py index a78d28193..d3be769c9 100644 --- a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py +++ b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py @@ -14,22 +14,35 @@ import json import logging +import re import time from decimal import ROUND_HALF_EVEN, Decimal from flask.json import jsonify from common.proto.context_pb2 import ( - ContextId, Empty, EndPointId, ServiceId, ServiceTypeEnum, Service, Constraint, Constraint_SLA_Capacity, + ContextId, Empty, EndPointId, ServiceId, ServiceTypeEnum, Service, ServiceStatusEnum, Constraint, Constraint_SLA_Capacity, ConfigRule, ConfigRule_Custom, ConfigActionEnum) from common.tools.grpc.Tools import grpc_message_to_json +from common.tools.grpc.ConfigRules import update_config_rule_custom from common.tools.object_factory.Context import json_context_id from common.tools.object_factory.Service import json_service_id LOGGER = logging.getLogger(__name__) +ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' +RE_CONFIG_RULE_IF_SUBIF = re.compile(r'^\/interface\[([^\]]+)\]\/subinterface\[([^\]]+)\]$') +MEC_CONSIDERED_FIELDS = ['requestType', 'sessionFilter', 'fixedAllocation', 'allocationDirection'] +ALLOCATION_DIRECTION_DESCRIPTIONS = { + '00' : 'Downlink (towards the UE)', + '01' : 'Uplink (towards the application/session)', + '10' : 'Symmetrical'} +VLAN_TAG = 0 +PREFIX_LENGTH = 24 +BGP_AS = 65000 +policy_AZ = 'srv_{:d}_a'.format(VLAN_TAG) +policy_ZA = 'srv_{:d}_b'.format(VLAN_TAG) def service_2_bwInfo(service: Service) -> dict: response = {} - # allocationDirection = '??' # String: 00 = Downlink (towards the UE); 01 = Uplink (towards the application/session); 10 = Symmetrical response['appInsId'] = service.service_id.service_uuid.uuid # String: Application instance identifier for constraint in service.service_constraints: if constraint.WhichOneof('constraint') == 'sla_capacity': @@ -55,47 +68,108 @@ def service_2_bwInfo(service: Service) -> dict: return response -def bwInfo_2_service(client, bwInfo: dict) -> Service: +def bwInfo_2_service(client, bw_info: dict) -> Service: + # add description to allocationDirection code + if ad_code := bw_info.get('allocationDirection'): + bw_info['allocationDirection'] = {'code': ad_code, 'description': ALLOCATION_DIRECTION_DESCRIPTIONS[ad_code]} + if 'sessionFilter' in bw_info: + bw_info['sessionFilter'] = bw_info['sessionFilter'][0] # Discard other items in sessionFilter field + service = Service() - for key in ['allocationDirection', 'fixedBWPriority', 'requestType', 'timeStamp', 'sessionFilter']: - if key not in bwInfo: - continue - config_rule = ConfigRule() - config_rule.action = ConfigActionEnum.CONFIGACTION_SET - config_rule_custom = ConfigRule_Custom() - config_rule_custom.resource_key = key - if key != 'sessionFilter': - config_rule_custom.resource_value = str(bwInfo[key]) - else: - config_rule_custom.resource_value = json.dumps(bwInfo[key]) - config_rule.custom.CopyFrom(config_rule_custom) - service.service_config.config_rules.append(config_rule) - - if 'sessionFilter' in bwInfo: - a_ip = bwInfo['sessionFilter'][0]['sourceIp'] - z_ip = bwInfo['sessionFilter'][0]['dstAddress'] + + service_config_rules = service.service_config.config_rules + + route_distinguisher = '{:5d}:{:03d}'.format(BGP_AS, VLAN_TAG) + settings_cr_key = '/settings' + settings_cr_value = {'bgp_as':(BGP_AS, True), 'route_distinguisher': (route_distinguisher, True)} + update_config_rule_custom(service_config_rules, settings_cr_key, settings_cr_value) + + request_cr_key = '/request' + request_cr_value = {k:bw_info[k] for k in MEC_CONSIDERED_FIELDS} + + config_rule = ConfigRule() + config_rule.action = ConfigActionEnum.CONFIGACTION_SET + config_rule_custom = ConfigRule_Custom() + config_rule_custom.resource_key = request_cr_key + config_rule_custom.resource_value = json.dumps(request_cr_value) + config_rule.custom.CopyFrom(config_rule_custom) + service_config_rules.append(config_rule) + + if 'sessionFilter' in bw_info: + a_ip = bw_info['sessionFilter']['sourceIp'] + z_ip = bw_info['sessionFilter']['dstAddress'] devices = client.ListDevices(Empty()).devices + router_id_counter = 1 for device in devices: + device_endpoint_uuids = {ep.name:ep.endpoint_id.endpoint_uuid.uuid for ep in device.device_endpoints} for cr in device.device_config.config_rules: - if cr.WhichOneof('config_rule') == 'custom' and cr.custom.resource_key == '_connect/settings': - for ep in json.loads(cr.custom.resource_value)['endpoints']: - if 'ip' in ep and (ep['ip'] == a_ip or ep['ip'] == z_ip): - ep_id = EndPointId() - ep_id.endpoint_uuid.uuid = ep['uuid'] - ep_id.device_id.device_uuid.uuid = device.device_id.device_uuid.uuid - service.service_endpoint_ids.append(ep_id) - + if cr.WhichOneof('config_rule') != 'custom': + continue + match_subif = RE_CONFIG_RULE_IF_SUBIF.match(cr.custom.resource_key) + if not match_subif: + continue + address_ip = json.loads(cr.custom.resource_value).get('address_ip') + if address_ip not in [a_ip, z_ip]: + continue + port_name = 'PORT-' + match_subif.groups(0)[0] # `PORT-` added as prefix + ep_id = EndPointId() + ep_id.endpoint_uuid.uuid = device_endpoint_uuids[port_name] + ep_id.device_id.device_uuid.uuid = device.device_id.device_uuid.uuid + service.service_endpoint_ids.append(ep_id) + + # add interface config rules + endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device.name, port_name, VLAN_TAG) + if address_ip == a_ip: + field_updates = { + 'address_ip': (address_ip, True), + # 'router_id': ('.'.join([str(router_id_counter)]*4), True), + 'router_id': ('200.1.1.1', True), + 'neighbor_address_ip': ('192.168.150.2', True), + 'route_distinguisher': (route_distinguisher, True), + 'sub_interface_index': (0, True), + 'vlan_id' : (VLAN_TAG, True), + # 'bgp_as': (BGP_AS+router_id_counter, True), + 'bgp_as': (BGP_AS, True), + 'ip_address': (address_ip, True), + 'prefix_length': (PREFIX_LENGTH, True), + 'policy_AZ' : (policy_AZ, True), + 'policy_ZA' : (policy_ZA, True), + 'address_prefix' : (PREFIX_LENGTH, True), + } + elif address_ip == z_ip: + field_updates = { + 'address_ip': (address_ip, True), + # 'router_id': ('.'.join([str(router_id_counter)]*4), True), + 'router_id': ('200.1.1.2', True), + 'neighbor_address_ip': ('192.168.150.1', True), + 'route_distinguisher': (route_distinguisher, True), + 'sub_interface_index': (0, True), + 'vlan_id' : (VLAN_TAG, True), + # 'bgp_as': (BGP_AS+router_id_counter, True), + 'bgp_as': (BGP_AS, True), + 'ip_address': (address_ip, True), + 'prefix_length': (PREFIX_LENGTH, True), + 'policy_AZ' : (policy_ZA, True), + 'policy_ZA' : (policy_AZ, True), + 'address_prefix' : (PREFIX_LENGTH, True), + } + router_id_counter += 1 + LOGGER.debug(f'BEFORE UPDATE -> device.device_config.config_rules: {service_config_rules}') + update_config_rule_custom(service_config_rules, endpoint_settings_key, field_updates) + LOGGER.debug(f'AFTER UPDATE -> device.device_config.config_rules: {service_config_rules}') + + service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM - if 'appInsId' in bwInfo: - service.service_id.service_uuid.uuid = bwInfo['appInsId'] + if 'appInsId' in bw_info: + service.service_id.service_uuid.uuid = bw_info['appInsId'] service.service_id.context_id.context_uuid.uuid = 'admin' - service.name = bwInfo['appInsId'] + service.name = bw_info['appInsId'] - if 'fixedAllocation' in bwInfo: + if 'fixedAllocation' in bw_info: capacity = Constraint_SLA_Capacity() - capacity.capacity_gbps = float(bwInfo['fixedAllocation']) / 1.e9 + capacity.capacity_gbps = float(bw_info['fixedAllocation']) / 1.e9 constraint = Constraint() constraint.sla_capacity.CopyFrom(capacity) service.service_constraints.append(constraint) diff --git a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py index 329552a91..498db7dcd 100644 --- a/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py +++ b/src/pathcomp/frontend/service/algorithms/tools/ComposeConfigRules.py @@ -24,6 +24,7 @@ SETTINGS_RULE_NAME = '/settings' DEVICE_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/settings') ENDPOINT_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/endpoint\[([^\]]+)\]\/settings') +RE_ENDPOINT_VLAN_SETTINGS = re.compile(r'\/device\[([^\]]+)\]\/endpoint\[([^\]]+)\]\/vlan\[([^\]]+)\]\/settings') L2NM_SETTINGS_FIELD_DEFAULTS = { 'encapsulation_type': 'dot1q', @@ -150,6 +151,23 @@ 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 subservice_config_rules.append(config_rule) + + match = RE_ENDPOINT_VLAN_SETTINGS.match(config_rule.custom.resource_key) + if match is not None: + device_uuid_or_name = match.group(1) + device_name_or_uuid = device_name_mapping[device_uuid_or_name] + device_keys = {device_uuid_or_name, device_name_or_uuid} + + endpoint_uuid_or_name = match.group(2) + 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 + # ! check later: vlan removed from config_rule + config_rule.custom.resource_key = re.sub('\/vlan\[[^\]]+\]', '', config_rule.custom.resource_key) + subservice_config_rules.append(config_rule) else: continue diff --git a/src/service/service/service_handlers/l3nm_openconfig/ConfigRules.py b/src/service/service/service_handlers/l3nm_openconfig/ConfigRules.py index 1e4425cdb..0369d6207 100644 --- a/src/service/service/service_handlers/l3nm_openconfig/ConfigRules.py +++ b/src/service/service/service_handlers/l3nm_openconfig/ConfigRules.py @@ -37,6 +37,7 @@ def setup_config_rules( 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 + neighbor_address_ip = json_endpoint_settings.get('neighbor_address_ip', '0.0.0.0') # '2.2.2.1' policy_import = json_endpoint_settings.get('policy_AZ', '2' ) # 2 policy_export = json_endpoint_settings.get('policy_ZA', '7' ) # 30 @@ -46,18 +47,21 @@ def setup_config_rules( network_subinterface_desc = json_endpoint_settings.get('subif_description','') #service_short_uuid = service_uuid.split('-')[-1] #network_instance_name = '{:s}-NetInst'.format(service_short_uuid) - network_instance_name = json_endpoint_settings.get('ni_name', service_uuid.split('-')[-1]) #ELAN-AC:1 + # network_instance_name = json_endpoint_settings.get('ni_name', service_uuid.split('-')[-1]) #ELAN-AC:1 + network_instance_name = 'default' - if_subif_name = '{:s}.{:d}'.format(endpoint_name, vlan_id) + ''' + # if_subif_name = '{:s}.{:d}'.format(endpoint_name, vlan_id) + if_subif_name = '{:s}'.format(endpoint_name[5:]) json_config_rules = [ - # Configure Interface (not used) - #json_config_rule_set( + # # Configure Interface (not used) + # json_config_rule_set( # '/interface[{:s}]'.format(endpoint_name), { # 'name': endpoint_name, # 'description': network_interface_desc, # 'mtu': mtu, - #}), + # }), #Create network instance json_config_rule_set( @@ -74,8 +78,10 @@ def setup_config_rules( json_config_rule_set( '/network_instance[{:s}]/protocols[BGP]'.format(network_instance_name), { 'name': network_instance_name, - 'protocol_name': 'BGP', + # 'protocol_name': 'BGP', + 'protocol_name': bgp_as, 'identifier': 'BGP', + # 'identifier': bgp_as, 'as': bgp_as, 'router_id': router_id, }), @@ -101,7 +107,8 @@ def setup_config_rules( json_config_rule_set( '/interface[{:s}]/subinterface[{:d}]'.format(if_subif_name, sub_interface_index), { 'name' : if_subif_name, - 'type' :'l3ipvlan', + # 'type' :'l3ipvlan', + 'type' :'ethernetCsmacd', 'mtu' : mtu, 'index' : sub_interface_index, 'description' : network_subinterface_desc, @@ -183,6 +190,40 @@ def setup_config_rules( }), ] + ''' + if_subif_name = '{:s}'.format(endpoint_name[5:]) + + json_config_rules = [ + + #Add DIRECTLY CONNECTED protocol to network instance + json_config_rule_set( + '/network_instance[{:s}]/protocols[DIRECTLY_CONNECTED]'.format(network_instance_name), { + 'name': network_instance_name, + 'identifier': 'DIRECTLY_CONNECTED', + 'protocol_name': 'DIRECTLY_CONNECTED', + }), + + # Add BGP neighbors + json_config_rule_set( + '/network_instance[{:s}]/protocols[BGP]'.format(network_instance_name), { + 'name': network_instance_name, + 'protocol_name': bgp_as, + 'identifier': 'BGP', + 'as': bgp_as, + 'router_id': router_id, + 'neighbors': [{'ip_address': neighbor_address_ip, 'remote_as': bgp_as}] + }), + json_config_rule_set( + '/network_instance[{:s}]/table_connections[DIRECTLY_CONNECTED][BGP][IPV4]'.format(network_instance_name), { + 'name' : network_instance_name, + 'src_protocol' : 'DIRECTLY_CONNECTED', + 'dst_protocol' : 'BGP', + 'address_family' : 'IPV4', + 'default_import_policy': 'ACCEPT_ROUTE', + }), + + ] + for res_key, res_value in endpoint_acls: json_config_rules.append( {'action': 1, 'acl': res_value} @@ -201,7 +242,8 @@ def teardown_config_rules( json_endpoint_settings : Dict = endpoint_settings.value service_short_uuid = service_uuid.split('-')[-1] - network_instance_name = '{:s}-NetInst'.format(service_short_uuid) + # network_instance_name = '{:s}-NetInst'.format(service_short_uuid) + network_instance_name = json_endpoint_settings.get('ni_name', service_short_uuid) #ELAN-AC:1 #network_interface_desc = '{:s}-NetIf'.format(service_uuid) #network_subinterface_desc = '{:s}-NetSubIf'.format(service_uuid) @@ -262,10 +304,10 @@ def teardown_config_rules( #Delete interface; automatically deletes: # - /interface[]/subinterface[] - json_config_rule_delete('/interface[{:s}]/subinterface[0]'.format(if_subif_name), - { - 'name': if_subif_name, - }), + # json_config_rule_delete('/interface[{:s}]/subinterface[0]'.format(if_subif_name), + # { + # 'name': if_subif_name, + # }), #Delete network instance; automatically deletes: # - /network_instance[]/interface[] -- GitLab From afc009ba8234cc7d6d2f291d92a0f35679e2e492 Mon Sep 17 00:00:00 2001 From: hajipour Date: Tue, 12 Mar 2024 11:45:33 +0000 Subject: [PATCH 2/5] feat: e2e acl installation added: 1.post, get, delete endpoints of acl added to nbi 2.ipinfusion netconf proprietary acl recipe added since openconfig recipe does not create acl entries in the router --- proto/acl.proto | 1 + proto/context.proto | 1 + .../drivers/openconfig/templates/__init__.py | 29 +++- .../openconfig/templates/acl/__init__.py | 13 ++ .../edit_config_ipinfusion_proprietary.xml | 34 ++++ .../openconfig/templates/acl/acl_adapter.py | 75 ++++++++ .../acl/acl_adapter_ipinfusion_proprietary.py | 65 +++++++ .../edit_config_ipinfusion_proprietary.xml | 26 +++ src/nbi/requirements.in | 1 + src/nbi/service/__main__.py | 2 + .../nbi_plugins/ietf_acl/__init__.py | 42 +++++ .../nbi_plugins/ietf_acl/acl_service.py | 98 +++++++++++ .../nbi_plugins/ietf_acl/acl_services.py | 68 ++++++++ .../nbi_plugins/ietf_acl/ietf_acl_parser.py | 164 ++++++++++++++++++ src/nbi/tests/data/ietf_acl.json | 56 ++++++ 15 files changed, 667 insertions(+), 8 deletions(-) create mode 100644 src/device/service/drivers/openconfig/templates/acl/__init__.py create mode 100644 src/device/service/drivers/openconfig/templates/acl/acl-set/acl-entry/edit_config_ipinfusion_proprietary.xml create mode 100644 src/device/service/drivers/openconfig/templates/acl/acl_adapter.py create mode 100644 src/device/service/drivers/openconfig/templates/acl/acl_adapter_ipinfusion_proprietary.py create mode 100644 src/device/service/drivers/openconfig/templates/acl/interfaces/ingress/edit_config_ipinfusion_proprietary.xml create mode 100644 src/nbi/service/rest_server/nbi_plugins/ietf_acl/__init__.py create mode 100644 src/nbi/service/rest_server/nbi_plugins/ietf_acl/acl_service.py create mode 100644 src/nbi/service/rest_server/nbi_plugins/ietf_acl/acl_services.py create mode 100644 src/nbi/service/rest_server/nbi_plugins/ietf_acl/ietf_acl_parser.py create mode 100644 src/nbi/tests/data/ietf_acl.json diff --git a/proto/acl.proto b/proto/acl.proto index 3dba735dc..f691c3dbd 100644 --- a/proto/acl.proto +++ b/proto/acl.proto @@ -46,6 +46,7 @@ message AclMatch { uint32 dst_port = 6; uint32 start_mpls_label = 7; uint32 end_mpls_label = 8; + string flags = 9; } message AclAction { diff --git a/proto/context.proto b/proto/context.proto index d5022ac29..1f8b0ce19 100644 --- a/proto/context.proto +++ b/proto/context.proto @@ -498,6 +498,7 @@ message ConfigRule_Custom { message ConfigRule_ACL { EndPointId endpoint_id = 1; acl.AclRuleSet rule_set = 2; + string interface = 3; } message ConfigRule { diff --git a/src/device/service/drivers/openconfig/templates/__init__.py b/src/device/service/drivers/openconfig/templates/__init__.py index 87eea1f0b..dcd3cf683 100644 --- a/src/device/service/drivers/openconfig/templates/__init__.py +++ b/src/device/service/drivers/openconfig/templates/__init__.py @@ -27,6 +27,9 @@ 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 = [ @@ -113,16 +116,26 @@ def compose_config( # template generation 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)) - if "acl_ruleset" in resource_key: # MANAGING ACLs - templates =[] - templates.append(JINJA_ENV.get_template('acl/acl-set/acl-entry/edit_config.xml')) - templates.append(JINJA_ENV.get_template('acl/interfaces/ingress/edit_config.xml')) - data : Dict[str, Any] = json.loads(resource_value) + if True: #vendor == 'ipinfusion': #! ipinfusion proprietary netconf receipe is used temporarily + 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: + 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)) + else: + 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' - return [ '{:s}'.format( template.render(**data, operation=operation, vendor=vendor).strip()) diff --git a/src/device/service/drivers/openconfig/templates/acl/__init__.py b/src/device/service/drivers/openconfig/templates/acl/__init__.py new file mode 100644 index 000000000..f80ccfd52 --- /dev/null +++ b/src/device/service/drivers/openconfig/templates/acl/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. \ No newline at end of file diff --git a/src/device/service/drivers/openconfig/templates/acl/acl-set/acl-entry/edit_config_ipinfusion_proprietary.xml b/src/device/service/drivers/openconfig/templates/acl/acl-set/acl-entry/edit_config_ipinfusion_proprietary.xml new file mode 100644 index 000000000..d0210a66c --- /dev/null +++ b/src/device/service/drivers/openconfig/templates/acl/acl-set/acl-entry/edit_config_ipinfusion_proprietary.xml @@ -0,0 +1,34 @@ + + + + {{name}} + {% if type is defined %}{{type}}{% endif %} + + {{name}} + {% if type is defined %}{{type}}{% endif %} + + {% if operation != 'delete' %} + + + {{sequence_id}} + + {{sequence_id}} + + + + {{source_address}} + {{destination_address}} + {{dscp}} + + {{source_port}} + {{destination_port}} + {{tcp_flags}} + {{forwarding_action}} + + + + + {% endif %} + + + \ No newline at end of file diff --git a/src/device/service/drivers/openconfig/templates/acl/acl_adapter.py b/src/device/service/drivers/openconfig/templates/acl/acl_adapter.py new file mode 100644 index 000000000..244c4b616 --- /dev/null +++ b/src/device/service/drivers/openconfig/templates/acl/acl_adapter.py @@ -0,0 +1,75 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, TypedDict + +from ..ACL.ACL_multivendor import RULE_TYPE_MAPPING, FORWARDING_ACTION_MAPPING, LOG_ACTION_MAPPING + +class ACLRequestData(TypedDict): + name: str # acl-set name + type: str # acl-set type + sequence_id: int # acl-entry sequence-id + source_address: str + destination_address: str + forwarding_action: str + id: str # interface id + interface: str + subinterface: int + set_name_ingress: str # ingress-acl-set name + type_ingress: str # ingress-acl-set type + all: bool + dscp: int + protocol: int + tcp_flags: str + source_port: int + destination_port: int + +def acl_cr_to_dict(acl_cr_dict: Dict, subinterface:int = 0) -> Dict: + rule_set = acl_cr_dict['rule_set'] + rule_set_entry = rule_set['entries'][0] + rule_set_entry_match = rule_set_entry['match'] + rule_set_entry_action = rule_set_entry['action'] + + name: str = rule_set['name'] + type: str = RULE_TYPE_MAPPING[rule_set["type"]] + sequence_id = rule_set_entry['sequence_id'] + source_address = rule_set_entry_match['src_address'] + destination_address = rule_set_entry_match['dst_address'] + forwarding_action: str = FORWARDING_ACTION_MAPPING[rule_set_entry_action['forward_action']] + interface_id = acl_cr_dict['interface'] + interface = interface_id + set_name_ingress = name + type_ingress = type + + return ACLRequestData( + name=name, + type=type, + sequence_id=sequence_id, + source_address=source_address, + destination_address=destination_address, + forwarding_action=forwarding_action, + id=interface_id, + interface=interface, + # subinterface=subinterface, + set_name_ingress=set_name_ingress, + type_ingress=type_ingress, + all=True, + dscp=18, + protocol=6, + tcp_flags='TCP_SYN', + source_port=22, + destination_port=80 + ) + + \ No newline at end of file diff --git a/src/device/service/drivers/openconfig/templates/acl/acl_adapter_ipinfusion_proprietary.py b/src/device/service/drivers/openconfig/templates/acl/acl_adapter_ipinfusion_proprietary.py new file mode 100644 index 000000000..79db6ad98 --- /dev/null +++ b/src/device/service/drivers/openconfig/templates/acl/acl_adapter_ipinfusion_proprietary.py @@ -0,0 +1,65 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, TypedDict + + +RULE_TYPE_MAPPING = { + 'ACLRULETYPE_IPV4' : 'ip', +} + +FORWARDING_ACTION_MAPPING = { + 'ACLFORWARDINGACTION_DROP' : 'deny', + 'ACLFORWARDINGACTION_ACCEPT' : 'permit', +} + +class ACLRequestData(TypedDict): + name: str # acl-set name + type: str # acl-set type + sequence_id: int # acl-entry sequence-id + source_address: str + destination_address: str + forwarding_action: str + interface: str + dscp: int + tcp_flags: str + source_port: int + destination_port: int + +def acl_cr_to_dict_ipinfusion_proprietary(acl_cr_dict: Dict, delete: bool = False) -> Dict: + rule_set = acl_cr_dict['rule_set'] + name: str = rule_set['name'] + type: str = RULE_TYPE_MAPPING[rule_set["type"]] + interface = acl_cr_dict['interface'][5:] # remove preceding `PORT-` characters + if delete: + return ACLRequestData(name=name, type=type, interface=interface) + rule_set_entry = rule_set['entries'][0] + rule_set_entry_match = rule_set_entry['match'] + rule_set_entry_action = rule_set_entry['action'] + + return ACLRequestData( + name=name, + type=type, + sequence_id=rule_set_entry['sequence_id'], + source_address=rule_set_entry_match['src_address'], + destination_address=rule_set_entry_match['dst_address'], + forwarding_action=FORWARDING_ACTION_MAPPING[rule_set_entry_action['forward_action']], + interface=interface, + dscp=rule_set_entry_match["dscp"], + tcp_flags=rule_set_entry_match["flags"], + source_port=rule_set_entry_match['src_port'], + destination_port=rule_set_entry_match['dst_port'] + ) + + \ No newline at end of file diff --git a/src/device/service/drivers/openconfig/templates/acl/interfaces/ingress/edit_config_ipinfusion_proprietary.xml b/src/device/service/drivers/openconfig/templates/acl/interfaces/ingress/edit_config_ipinfusion_proprietary.xml new file mode 100644 index 000000000..6e502154f --- /dev/null +++ b/src/device/service/drivers/openconfig/templates/acl/interfaces/ingress/edit_config_ipinfusion_proprietary.xml @@ -0,0 +1,26 @@ + + + + {{interface}} + + {{interface}} + + + + {% if type is defined %}{{type}}{% endif %} + + + {{name}} + + {{name}} + + + + + {% if type is defined %}{{type}}{% endif %} + + + + + + \ No newline at end of file diff --git a/src/nbi/requirements.in b/src/nbi/requirements.in index 6e3eb9440..7a7b1cffb 100644 --- a/src/nbi/requirements.in +++ b/src/nbi/requirements.in @@ -24,3 +24,4 @@ pyang==2.6.0 git+https://github.com/robshakir/pyangbind.git requests==2.27.1 werkzeug==2.3.7 +pydantic==2.6.3 diff --git a/src/nbi/service/__main__.py b/src/nbi/service/__main__.py index 8834e45a2..2a8a2251d 100644 --- a/src/nbi/service/__main__.py +++ b/src/nbi/service/__main__.py @@ -26,6 +26,7 @@ from .rest_server.nbi_plugins.ietf_l2vpn import register_ietf_l2vpn from .rest_server.nbi_plugins.ietf_l3vpn import register_ietf_l3vpn from .rest_server.nbi_plugins.ietf_network import register_ietf_network from .rest_server.nbi_plugins.ietf_network_slice import register_ietf_nss +from .rest_server.nbi_plugins.ietf_acl import register_ietf_acl terminate = threading.Event() LOGGER = None @@ -68,6 +69,7 @@ def main(): register_ietf_l3vpn(rest_server) # Registering L3VPN entrypoint register_ietf_network(rest_server) register_ietf_nss(rest_server) # Registering NSS entrypoint + register_ietf_acl(rest_server) rest_server.start() # Wait for Ctrl+C or termination signal diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_acl/__init__.py b/src/nbi/service/rest_server/nbi_plugins/ietf_acl/__init__.py new file mode 100644 index 000000000..6c1353bff --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_acl/__init__.py @@ -0,0 +1,42 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from flask_restful import Resource + +from nbi.service.rest_server.nbi_plugins.ietf_acl.acl_service import ACL +from nbi.service.rest_server.nbi_plugins.ietf_acl.acl_services import ACLs +from nbi.service.rest_server.RestServer import RestServer + +URL_PREFIX = "/restconf/data" + + +def __add_resource(rest_server: RestServer, resource: Resource, *urls, **kwargs): + urls = [(URL_PREFIX + url) for url in urls] + rest_server.add_resource(resource, *urls, **kwargs) + + +def register_ietf_acl(rest_server: RestServer): + __add_resource( + rest_server, + ACLs, + "/device=/ietf-access-control-list:acls", + "/device=/ietf-access-control-list:acls", + ) + + __add_resource( + rest_server, + ACL, + "/device=/ietf-access-control-list:acl=", + "/device=/ietf-access-control-list:acl=/", + ) diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_acl/acl_service.py b/src/nbi/service/rest_server/nbi_plugins/ietf_acl/acl_service.py new file mode 100644 index 000000000..466a68efc --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_acl/acl_service.py @@ -0,0 +1,98 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +import re +import json + +from flask_restful import Resource +from werkzeug.exceptions import NotFound + +from nbi.service.rest_server.nbi_plugins.tools.Authentication import HTTP_AUTH +from common.proto.acl_pb2 import AclRuleTypeEnum +from common.proto.context_pb2 import ( + ConfigActionEnum, + ConfigRule, + Device, + DeviceId, +) +from common.tools.object_factory.Device import json_device_id +from common.tools.grpc.Tools import grpc_message_to_json_string +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient + + +from .ietf_acl_parser import ietf_acl_from_config_rule_resource_value + +LOGGER = logging.getLogger(__name__) + +ACL_CONIG_RULE_KEY = r'\/device\[.+\]\/endpoint\[(.+)\]/acl_ruleset\[{}\]' + + +class ACL(Resource): + # @HTTP_AUTH.login_required + def get(self, device_uuid: str, acl_name: str): + RE_ACL_CONIG_RULE_KEY = re.compile(ACL_CONIG_RULE_KEY.format(acl_name)) + + context_client = ContextClient() + device_client = DeviceClient() + + _device = context_client.GetDevice(DeviceId(**json_device_id(device_uuid))) + + + for cr in _device.device_config.config_rules: + if cr.WhichOneof('config_rule') == 'custom': + if ep_uuid_match := RE_ACL_CONIG_RULE_KEY.match(cr.custom.resource_key): + endpoint_uuid = ep_uuid_match.groups(0)[0] + resource_value_dict = json.loads(cr.custom.resource_value) + LOGGER.debug(f'P99: {resource_value_dict}') + return ietf_acl_from_config_rule_resource_value(resource_value_dict) + else: + raise NotFound(f'ACL not found') + + # @HTTP_AUTH.login_required + def delete(self, device_uuid: str, acl_name: str): + RE_ACL_CONIG_RULE_KEY = re.compile(ACL_CONIG_RULE_KEY.format(acl_name)) + + context_client = ContextClient() + device_client = DeviceClient() + + _device = context_client.GetDevice(DeviceId(**json_device_id(device_uuid))) + + + for cr in _device.device_config.config_rules: + if cr.WhichOneof('config_rule') == 'custom': + if ep_uuid_match := RE_ACL_CONIG_RULE_KEY.match(cr.custom.resource_key): + endpoint_uuid = ep_uuid_match.groups(0)[0] + resource_value_dict = json.loads(cr.custom.resource_value) + type_str = resource_value_dict['rule_set']['type'] + interface = resource_value_dict['interface'] + break + else: + raise NotFound(f'ACL not found') + + acl_config_rule = ConfigRule() + acl_config_rule.action = ConfigActionEnum.CONFIGACTION_DELETE + acl_config_rule.acl.rule_set.name = acl_name + acl_config_rule.acl.interface = interface + acl_config_rule.acl.rule_set.type = getattr(AclRuleTypeEnum, type_str) + acl_config_rule.acl.endpoint_id.device_id.device_uuid.uuid = device_uuid + acl_config_rule.acl.endpoint_id.endpoint_uuid.uuid = endpoint_uuid + + device = Device() + device.CopyFrom(_device) + del device.device_config.config_rules[:] + device.device_config.config_rules.append(acl_config_rule) + response = device_client.ConfigureDevice(device) + return (response.device_uuid.uuid).strip("\"\n") diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_acl/acl_services.py b/src/nbi/service/rest_server/nbi_plugins/ietf_acl/acl_services.py new file mode 100644 index 000000000..2d03e61b6 --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_acl/acl_services.py @@ -0,0 +1,68 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import logging +from typing import Dict + +from flask import request +from flask_restful import Resource +from werkzeug.exceptions import NotFound + +from common.proto.context_pb2 import Device, DeviceId +from common.tools.grpc.Tools import grpc_message_to_json_string +from common.tools.object_factory.Device import json_device_id +from context.client.ContextClient import ContextClient +from device.client.DeviceClient import DeviceClient + +from nbi.service.rest_server.nbi_plugins.tools.Authentication import HTTP_AUTH + +from .ietf_acl_parser import config_rule_from_ietf_acl + +LOGGER = logging.getLogger(__name__) + +class ACLs(Resource): + # @HTTP_AUTH.login_required + def get(self): + return {} + + # @HTTP_AUTH.login_required + def post(self, device_uuid: str): + if not request.is_json: + raise UnsupportedMediaType("JSON pyload is required") + request_data: Dict = request.json + LOGGER.debug("Request: {:s}".format(str(request_data))) + attached_interface = request_data["ietf-access-control-list"]["acls"]['attachment-points']['interface'][0]['interface-id'] + + context_client = ContextClient() + device_client = DeviceClient() + + _device = context_client.GetDevice(DeviceId(**json_device_id(device_uuid))) + + for ep in _device.device_endpoints: + if ep.name == attached_interface: + endpoint_uuid = ep.endpoint_id.endpoint_uuid.uuid + break + else: + raise NotFound(f'interface {attached_interface} not found in device {device_uuid}') + + acl_config_rule = config_rule_from_ietf_acl(request_data, device_uuid, endpoint_uuid, sequence_id=1, subinterface=0) + + LOGGER.info(f"ACL Config Rule: {grpc_message_to_json_string(acl_config_rule)}") + + device = Device() + device.CopyFrom(_device) + del device.device_config.config_rules[:] + device.device_config.config_rules.append(acl_config_rule) + response = device_client.ConfigureDevice(device) + return (response.device_uuid.uuid).strip("\"\n") diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_acl/ietf_acl_parser.py b/src/nbi/service/rest_server/nbi_plugins/ietf_acl/ietf_acl_parser.py new file mode 100644 index 000000000..b378153f8 --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_acl/ietf_acl_parser.py @@ -0,0 +1,164 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import List, Dict, Optional, TypedDict +from pydantic import BaseModel, Field + +from common.proto.acl_pb2 import AclForwardActionEnum, AclRuleTypeEnum, AclEntry +from common.proto.context_pb2 import ConfigActionEnum, ConfigRule + +class Ipv4(BaseModel): + dscp: int = 0 + source_ipv4_network: str = Field(serialization_alias="source-ipv4-network", default="") + destination_ipv4_network: str = Field(serialization_alias="destination-ipv4-network", default="") + +class Port(BaseModel): + port: int = 0 + operator: str = "eq" + +class Tcp(BaseModel): + flags: str = "" + source_port: Port = Field(serialization_alias="source-port", default_factory=lambda: Port()) + destination_port: Port = Field(serialization_alias="destination-port", default_factory=lambda: Port()) + +class Matches(BaseModel): + ipv4: Ipv4 = Ipv4() + tcp: Tcp = Tcp() + +class Action(BaseModel): + forwarding: str = "" + +class Ace(BaseModel): + name: str = "custom_rule" + matches: Matches = Matches() + actions: Action = Action() + +class Aces(BaseModel): + ace: List[Ace] = [Ace()] + +class Acl(BaseModel): + name: str = "" + type: str = "" + aces: Aces = Aces() + +class Name(BaseModel): + name: str = "" + +class AclSet(BaseModel): + acl_set: List[Name] = Field(serialization_alias="acl-set", default=[Name()]) + +class AclSets(BaseModel): + acl_sets: AclSet = Field(serialization_alias="acl-sets", default=AclSet()) + +class Ingress(BaseModel): + ingress: AclSets = AclSets() + +class Interface(BaseModel): + interface_id: str = Field(serialization_alias="interface-id", default="") + ingress: Ingress = Ingress() + +class Interfaces(BaseModel): + interface: List[Interface] = [Interface()] + +class AttachmentPoints(BaseModel): + attachment_points: Interfaces = Field(serialization_alias="attachment-points", default=Interfaces()) + +class Acls(BaseModel): + acl: List[Acl] = [Acl()] + attachment_points: AttachmentPoints = Field(serialization_alias="attachment-points", default=AttachmentPoints()) + +class IETF_ACL(BaseModel): + acls: Acls = Acls() + + +IETF_TFS_RULE_TYPE_MAPPING = { + "ipv4-acl-type": "ACLRULETYPE_IPV4", + "ipv6-acl-type": "ACLRULETYPE_IPV6", +} + +IETF_TFS_FORWARDING_ACTION_MAPPING = { + "drop": "ACLFORWARDINGACTION_DROP", + "accept": "ACLFORWARDINGACTION_ACCEPT", +} + +TFS_IETF_RULE_TYPE_MAPPING = { + "ACLRULETYPE_IPV4": "ipv4-acl-type", + "ACLRULETYPE_IPV6": "ipv6-acl-type", +} + +TFS_IETF_FORWARDING_ACTION_MAPPING = { + "ACLFORWARDINGACTION_DROP": "drop", + "ACLFORWARDINGACTION_ACCEPT": "accept", +} + +def config_rule_from_ietf_acl( + request: Dict, + device_uuid: str, + endpoint_uuid: str, + sequence_id: int, + subinterface: int, +) -> ConfigRule: + the_acl = request["ietf-access-control-list"]["acls"]["acl"][0] + acl_ip_data = the_acl["aces"]["ace"][0]["matches"]["ipv4"] + acl_tcp_data = the_acl["aces"]["ace"][0]["matches"]["tcp"] + attachemnt_interface = request["ietf-access-control-list"]["acls"]['attachment-points']['interface'][0] + source_address = acl_ip_data["source-ipv4-network"] + destination_address = acl_ip_data["destination-ipv4-network"] + source_port = acl_tcp_data['source-port']['port'] + destination_port = acl_tcp_data['destination-port']['port'] + ietf_action = the_acl["aces"]["ace"][0]["actions"]["forwarding"] + interface_id = attachemnt_interface['interface-id'] + + acl_config_rule = ConfigRule() + acl_config_rule.action = ConfigActionEnum.CONFIGACTION_SET + acl_config_rule.acl.interface = interface_id + acl_endpoint_id = acl_config_rule.acl.endpoint_id + acl_endpoint_id.device_id.device_uuid.uuid = device_uuid + acl_endpoint_id.endpoint_uuid.uuid = endpoint_uuid + acl_rule_set = acl_config_rule.acl.rule_set + acl_rule_set.name = the_acl["name"] + acl_rule_set.type = getattr(AclRuleTypeEnum, IETF_TFS_RULE_TYPE_MAPPING[the_acl['type']]) + acl_rule_set.description = ( + f'{ietf_action} {the_acl["type"]}: {source_address}:{source_port}->{destination_address}:{destination_port}' + ) + acl_entry = AclEntry() + acl_entry.sequence_id = sequence_id + acl_entry.match.src_address = source_address + acl_entry.match.dst_address = destination_address + acl_entry.match.src_port = source_port + acl_entry.match.dst_port = destination_port + acl_entry.match.dscp = acl_ip_data["dscp"] + acl_entry.match.flags = acl_tcp_data["flags"] + acl_entry.action.forward_action = getattr(AclForwardActionEnum, IETF_TFS_FORWARDING_ACTION_MAPPING[ietf_action]) + acl_rule_set.entries.append(acl_entry) + + return acl_config_rule + +def ietf_acl_from_config_rule_resource_value(config_rule_rv: Dict) -> Dict: + rule_set = config_rule_rv['rule_set'] + acl_entry = rule_set['entries'][0] + match_ = acl_entry['match'] + + ipv4 = Ipv4(dscp=match_["dscp"], source_ipv4_network=match_["src_address"], destination_ipv4_network=match_["dst_address"]) + tcp = Tcp(flags=match_["flags"], source_port=Port(port=match_["src_port"]), destination_port=Port(port=match_["dst_port"])) + matches = Matches(ipvr=ipv4, tcp=tcp) + aces = Aces(ace=[Ace(matches=matches, actions=Action(forwarding=TFS_IETF_FORWARDING_ACTION_MAPPING[acl_entry["action"]["forward_action"]]))]) + acl = Acl(name=rule_set["name"], type=TFS_IETF_RULE_TYPE_MAPPING[rule_set["type"]], aces=aces) + acl_sets = AclSets(acl_sets=AclSet(acl_set=[Name(name=rule_set["name"])])) + ingress = Ingress(ingress=acl_sets) + interfaces = Interfaces(interface=[Interface(interface_id=config_rule_rv["interface"], ingress=ingress)]) + acls = Acls(acl=[acl], attachment_points=AttachmentPoints(attachment_points=interfaces)) + ietf_acl = IETF_ACL(acls=acls) + + return ietf_acl.model_dump(by_alias=True) \ No newline at end of file diff --git a/src/nbi/tests/data/ietf_acl.json b/src/nbi/tests/data/ietf_acl.json new file mode 100644 index 000000000..3cbdd0c67 --- /dev/null +++ b/src/nbi/tests/data/ietf_acl.json @@ -0,0 +1,56 @@ +{ + "ietf-access-control-list": { + "acls": { + "acl": [ + { + "name": "sample-ipv4-acl", + "type": "ipv4-acl-type", + "aces": { + "ace": [ + { + "name": "rule1", + "matches": { + "ipv4": { + "dscp": 18, + "source-ipv4-network": "192.168.10.6/24", + "destination-ipv4-network": "192.168.20.6/24" + }, + "tcp": { + "flags": "syn", + "source-port": { + "port": 1444, + "operator": "eq" + }, + "destination-port": { + "port": 1333, + "operator": "eq" + } + } + }, + "actions": { + "forwarding": "drop" + } + } + ] + } + } + ], + "attachment-points": { + "interface": [ + { + "interface-id": "PORT-ce1", + "ingress": { + "acl-sets": { + "acl-set": [ + { + "name": "sample-ipv4-acl" + } + ] + } + } + } + ] + } + } + } +} \ No newline at end of file -- GitLab From 14072dc13e8fae70853fd5edbbfe4c811a322690 Mon Sep 17 00:00:00 2001 From: hajipour Date: Tue, 12 Mar 2024 12:15:39 +0000 Subject: [PATCH 3/5] ietf acl client for tfs nbi interaction added --- .../nbi_plugins/ietf_acl/ietf_acl_client.py | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/nbi/service/rest_server/nbi_plugins/ietf_acl/ietf_acl_client.py diff --git a/src/nbi/service/rest_server/nbi_plugins/ietf_acl/ietf_acl_client.py b/src/nbi/service/rest_server/nbi_plugins/ietf_acl/ietf_acl_client.py new file mode 100644 index 000000000..79ec388a2 --- /dev/null +++ b/src/nbi/service/rest_server/nbi_plugins/ietf_acl/ietf_acl_client.py @@ -0,0 +1,69 @@ +# Copyright 2022-2023 ETSI TeraFlowSDN - TFS OSG (https://tfs.etsi.org/) +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import requests +import json +import time + +BASE_URL = "/restconf/data" +POST_URL = "/device={}/ietf-access-control-list:acls" +DELETE_URL = "/device={}/ietf-access-control-list:acl={}" + +class IetfTfsClient: + def __init__(self, + tfs_host: str = "10.1.1.119", + tfs_port: int = 80, + username: str = "admin", + password: str = "admin", + timeout: int = 10, + allow_redirects: bool = True, + ) -> None: + self.host = tfs_host + self.port = tfs_port + self.username = username + self.password = password + self.timeout = timeout + self.allow_redirects = allow_redirects + + def post(self, device_uuid: str, ietf_acl_data: dict) -> str: + request_url = "http://{:s}:{:d}{:s}{:s}".format(self.host, self.port, BASE_URL, POST_URL.format(device_uuid)) + reply = requests.request("post", request_url, timeout=self.timeout, json=ietf_acl_data, allow_redirects=self.allow_redirects) + return reply.text + + def get(self, device_uuid: str, acl_name: str) -> str: + request_url = "http://{:s}:{:d}{:s}{:s}".format(self.host, self.port, BASE_URL, DELETE_URL.format(device_uuid, acl_name)) + reply = requests.request("get", request_url, timeout=self.timeout, allow_redirects=self.allow_redirects) + return reply.text + + def delete(self, device_uuid: str, acl_name: str) -> str: + request_url = "http://{:s}:{:d}{:s}{:s}".format(self.host, self.port, BASE_URL, DELETE_URL.format(device_uuid, acl_name)) + reply = requests.request("delete", request_url, timeout=self.timeout, allow_redirects=self.allow_redirects) + return reply.text + +if __name__ == "__main__": + csg1_device_uuid = 'b71fd62f-e3d4-5956-93b9-3139094836cf' + acl_name = 'sample-ipv4-acl' + acl_request_path = 'src/nbi/tests/data/ietf_acl.json' + with open(acl_request_path, 'r') as afile: + acl_request_data = json.load(afile) + + ietf_tfs_client = IetfTfsClient() + post_response = ietf_tfs_client.post(csg1_device_uuid, acl_request_data) + print(f"post response: {post_response}") + time.sleep(.5) + get_response = ietf_tfs_client.get(csg1_device_uuid, acl_name) + print(f"get response: {get_response}") + time.sleep(.5) + delete_response = ietf_tfs_client.delete(csg1_device_uuid, acl_name) + print(f"delete response: {delete_response}") \ No newline at end of file -- GitLab From ef19aedcd200b0d00a715cc0696c506062a793e6 Mon Sep 17 00:00:00 2001 From: hajipour Date: Tue, 12 Mar 2024 14:45:51 +0000 Subject: [PATCH 4/5] enabling ipv4 ingress filter for acl support xml in ipinfusion added to acl recipe and xml renderer changed to jinja2 --- .../service/drivers/openconfig/OpenConfigDriver.py | 6 +++--- .../service/drivers/openconfig/templates/__init__.py | 4 ++++ .../acl/interfaces/ingress/enable_ingress_filter.xml | 9 +++++++++ 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 src/device/service/drivers/openconfig/templates/acl/interfaces/ingress/enable_ingress_filter.xml diff --git a/src/device/service/drivers/openconfig/OpenConfigDriver.py b/src/device/service/drivers/openconfig/OpenConfigDriver.py index 99ae1c8db..793b292c7 100644 --- a/src/device/service/drivers/openconfig/OpenConfigDriver.py +++ b/src/device/service/drivers/openconfig/OpenConfigDriver.py @@ -227,10 +227,10 @@ def edit_config( 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='pyangbind') # 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) + # resource_key, resource_value, delete=delete, vendor=netconf_handler.vendor, message_renderer='pyangbind') + 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( diff --git a/src/device/service/drivers/openconfig/templates/__init__.py b/src/device/service/drivers/openconfig/templates/__init__.py index dcd3cf683..8df8fa28f 100644 --- a/src/device/service/drivers/openconfig/templates/__init__.py +++ b/src/device/service/drivers/openconfig/templates/__init__.py @@ -118,17 +118,21 @@ def compose_config( # template generation templates =[] if "acl_ruleset" in resource_key: # MANAGING ACLs if True: #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: diff --git a/src/device/service/drivers/openconfig/templates/acl/interfaces/ingress/enable_ingress_filter.xml b/src/device/service/drivers/openconfig/templates/acl/interfaces/ingress/enable_ingress_filter.xml new file mode 100644 index 000000000..274028657 --- /dev/null +++ b/src/device/service/drivers/openconfig/templates/acl/interfaces/ingress/enable_ingress_filter.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file -- GitLab From 582d324933ae6b5acf89c842fd145e4521c045ed Mon Sep 17 00:00:00 2001 From: hajipour Date: Tue, 12 Mar 2024 16:09:11 +0000 Subject: [PATCH 5/5] removin changes of commit f10241a6 as it is related to MEC-PoC --- .../drivers/openconfig/OpenConfigDriver.py | 3 - .../drivers/openconfig/templates/Tools.py | 3 +- .../VPN/Network_instance_multivendor.py | 36 +---- .../nbi_plugins/etsi_bwm/Resources.py | 15 +- .../rest_server/nbi_plugins/etsi_bwm/Tools.py | 140 +++++------------- .../l3nm_openconfig/ConfigRules.py | 66 ++------- 6 files changed, 53 insertions(+), 210 deletions(-) diff --git a/src/device/service/drivers/openconfig/OpenConfigDriver.py b/src/device/service/drivers/openconfig/OpenConfigDriver.py index 793b292c7..8c6e07b3f 100644 --- a/src/device/service/drivers/openconfig/OpenConfigDriver.py +++ b/src/device/service/drivers/openconfig/OpenConfigDriver.py @@ -226,9 +226,6 @@ def edit_config( 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='pyangbind') 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 diff --git a/src/device/service/drivers/openconfig/templates/Tools.py b/src/device/service/drivers/openconfig/templates/Tools.py index 3e8043680..79bebef51 100644 --- a/src/device/service/drivers/openconfig/templates/Tools.py +++ b/src/device/service/drivers/openconfig/templates/Tools.py @@ -61,8 +61,7 @@ def generate_templates(resource_key: str, resource_value: str, delete: bool,vend elif "inter_instance_policies" in resource_key: result_templates.append(associate_RP_to_NI(data)) elif "protocols" in resource_key: - result_templates.append(add_protocol_NI(data, vendor, delete)) - # if vendor == "ADVA": result_templates.append(add_protocol_NI(data, vendor, delete)) + if vendor == "ADVA": result_templates.append(add_protocol_NI(data, vendor, delete)) elif "table_connections" in resource_key: result_templates.append(create_table_conns(data, delete)) elif "interface" in resource_key: diff --git a/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py b/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py index e36955a0d..c4d494ea6 100644 --- a/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py +++ b/src/device/service/drivers/openconfig/templates/VPN/Network_instance_multivendor.py @@ -116,9 +116,6 @@ def add_protocol_NI(parameters,vendor, DEL): else: with tag('network-instance'): with tag('name'):text(parameters['name']) - with tag('config'): - with tag('name'): text(parameters['name']) - with tag('type', 'xmlns:oc-ni-types="http://openconfig.net/yang/network-instance-types"'): text('oc-ni-types:DEFAULT_INSTANCE') with tag('protocols'): with tag('protocol'): with tag('identifier', 'xmlns:oc-pol-types="http://openconfig.net/yang/policy-types"'):text('oc-pol-types:',parameters['identifier']) @@ -126,41 +123,14 @@ def add_protocol_NI(parameters,vendor, DEL): with tag('config'): with tag('identifier', 'xmlns:oc-pol-types="http://openconfig.net/yang/policy-types"'):text('oc-pol-types:',parameters['identifier']) with tag('name') :text(parameters['protocol_name']) - with tag('enabled'): text('true') if "BGP" in parameters['identifier']: with tag('bgp'): with tag('global'): - with tag('afi-safis'): - with tag('afi-safi'): - with tag('afi-safi-name', 'xmlns:oc-bgp-types="http://openconfig.net/yang/bgp-types"'): text('oc-bgp-types:IPV4_UNICAST') - with tag('config'): - with tag('afi-safi-name', 'xmlns:oc-bgp-types="http://openconfig.net/yang/bgp-types"'): text('oc-bgp-types:IPV4_UNICAST') - with tag('enabled'): text('true') with tag('config'): with tag('as') :text(parameters['as']) - with tag('peer-groups'): - with tag('peer-group'): - with tag('peer-group-name'): text('IBGP') - with tag('config'): - with tag('peer-group-name'): text('IBGP') - with tag('peer-as'): text(parameters['protocol_name']) - with tag('afi-safis'): - with tag('afi-safi'): - with tag('afi-safi-name', 'xmlns:oc-bgp-types="http://openconfig.net/yang/bgp-types"'): text('oc-bgp-types:IPV4_UNICAST') - with tag('config'): - with tag('afi-safi-name', 'xmlns:oc-bgp-types="http://openconfig.net/yang/bgp-types"'): text('oc-bgp-types:IPV4_UNICAST') - with tag('enabled'): text('true') - - if 'neighbors' in parameters: - with tag('neighbors'): - for neighbor in parameters['neighbors']: - with tag('neighbor'): - with tag('neighbor-address'): text(neighbor['ip_address']) - with tag('config'): - with tag('neighbor-address'): text(neighbor['ip_address']) - with tag('peer-group'): text('IBGP') - # if vendor == "ADVA": - if True: + if "router-id" in parameters: + with tag('router-id'):text(parameters['router-id']) + if vendor == "ADVA": with tag('tables'): with tag('table'): with tag('protocol', 'xmlns:oc-pol-types="http://openconfig.net/yang/policy-types"'):text('oc-pol-types:',parameters['identifier']) diff --git a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py index 394b50de8..3fccbbb55 100644 --- a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py +++ b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Resources.py @@ -13,9 +13,7 @@ # limitations under the License. import copy, deepmerge, json, logging -from typing import Dict from common.Constants import DEFAULT_CONTEXT_NAME -from werkzeug.exceptions import UnsupportedMediaType from context.client.ContextClient import ContextClient from flask_restful import Resource, request from service.client.ServiceClient import ServiceClient @@ -39,20 +37,15 @@ class BwInfo(_Resource): return bw_allocations def post(self): - if not request.is_json: - raise UnsupportedMediaType('JSON payload is required') - request_data: Dict = request.get_json() - service = bwInfo_2_service(self.client, request_data) + bwinfo = request.get_json() + service = bwInfo_2_service(self.client, bwinfo) stripped_service = copy.deepcopy(service) stripped_service.ClearField('service_endpoint_ids') stripped_service.ClearField('service_constraints') stripped_service.ClearField('service_config') - try: - response = format_grpc_to_json(self.service_client.CreateService(stripped_service)) - response = format_grpc_to_json(self.service_client.UpdateService(service)) - except Exception as e: # pylint: disable=broad-except - return e + response = format_grpc_to_json(self.service_client.CreateService(stripped_service)) + response = format_grpc_to_json(self.service_client.UpdateService(service)) return response diff --git a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py index d3be769c9..a78d28193 100644 --- a/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py +++ b/src/nbi/service/rest_server/nbi_plugins/etsi_bwm/Tools.py @@ -14,35 +14,22 @@ import json import logging -import re import time from decimal import ROUND_HALF_EVEN, Decimal from flask.json import jsonify from common.proto.context_pb2 import ( - ContextId, Empty, EndPointId, ServiceId, ServiceTypeEnum, Service, ServiceStatusEnum, Constraint, Constraint_SLA_Capacity, + ContextId, Empty, EndPointId, ServiceId, ServiceTypeEnum, Service, Constraint, Constraint_SLA_Capacity, ConfigRule, ConfigRule_Custom, ConfigActionEnum) from common.tools.grpc.Tools import grpc_message_to_json -from common.tools.grpc.ConfigRules import update_config_rule_custom from common.tools.object_factory.Context import json_context_id from common.tools.object_factory.Service import json_service_id LOGGER = logging.getLogger(__name__) -ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' -RE_CONFIG_RULE_IF_SUBIF = re.compile(r'^\/interface\[([^\]]+)\]\/subinterface\[([^\]]+)\]$') -MEC_CONSIDERED_FIELDS = ['requestType', 'sessionFilter', 'fixedAllocation', 'allocationDirection'] -ALLOCATION_DIRECTION_DESCRIPTIONS = { - '00' : 'Downlink (towards the UE)', - '01' : 'Uplink (towards the application/session)', - '10' : 'Symmetrical'} -VLAN_TAG = 0 -PREFIX_LENGTH = 24 -BGP_AS = 65000 -policy_AZ = 'srv_{:d}_a'.format(VLAN_TAG) -policy_ZA = 'srv_{:d}_b'.format(VLAN_TAG) def service_2_bwInfo(service: Service) -> dict: response = {} + # allocationDirection = '??' # String: 00 = Downlink (towards the UE); 01 = Uplink (towards the application/session); 10 = Symmetrical response['appInsId'] = service.service_id.service_uuid.uuid # String: Application instance identifier for constraint in service.service_constraints: if constraint.WhichOneof('constraint') == 'sla_capacity': @@ -68,108 +55,47 @@ def service_2_bwInfo(service: Service) -> dict: return response -def bwInfo_2_service(client, bw_info: dict) -> Service: - # add description to allocationDirection code - if ad_code := bw_info.get('allocationDirection'): - bw_info['allocationDirection'] = {'code': ad_code, 'description': ALLOCATION_DIRECTION_DESCRIPTIONS[ad_code]} - if 'sessionFilter' in bw_info: - bw_info['sessionFilter'] = bw_info['sessionFilter'][0] # Discard other items in sessionFilter field - +def bwInfo_2_service(client, bwInfo: dict) -> Service: service = Service() - - service_config_rules = service.service_config.config_rules - - route_distinguisher = '{:5d}:{:03d}'.format(BGP_AS, VLAN_TAG) - settings_cr_key = '/settings' - settings_cr_value = {'bgp_as':(BGP_AS, True), 'route_distinguisher': (route_distinguisher, True)} - update_config_rule_custom(service_config_rules, settings_cr_key, settings_cr_value) - - request_cr_key = '/request' - request_cr_value = {k:bw_info[k] for k in MEC_CONSIDERED_FIELDS} - - config_rule = ConfigRule() - config_rule.action = ConfigActionEnum.CONFIGACTION_SET - config_rule_custom = ConfigRule_Custom() - config_rule_custom.resource_key = request_cr_key - config_rule_custom.resource_value = json.dumps(request_cr_value) - config_rule.custom.CopyFrom(config_rule_custom) - service_config_rules.append(config_rule) - - if 'sessionFilter' in bw_info: - a_ip = bw_info['sessionFilter']['sourceIp'] - z_ip = bw_info['sessionFilter']['dstAddress'] + for key in ['allocationDirection', 'fixedBWPriority', 'requestType', 'timeStamp', 'sessionFilter']: + if key not in bwInfo: + continue + config_rule = ConfigRule() + config_rule.action = ConfigActionEnum.CONFIGACTION_SET + config_rule_custom = ConfigRule_Custom() + config_rule_custom.resource_key = key + if key != 'sessionFilter': + config_rule_custom.resource_value = str(bwInfo[key]) + else: + config_rule_custom.resource_value = json.dumps(bwInfo[key]) + config_rule.custom.CopyFrom(config_rule_custom) + service.service_config.config_rules.append(config_rule) + + if 'sessionFilter' in bwInfo: + a_ip = bwInfo['sessionFilter'][0]['sourceIp'] + z_ip = bwInfo['sessionFilter'][0]['dstAddress'] devices = client.ListDevices(Empty()).devices - router_id_counter = 1 for device in devices: - device_endpoint_uuids = {ep.name:ep.endpoint_id.endpoint_uuid.uuid for ep in device.device_endpoints} for cr in device.device_config.config_rules: - if cr.WhichOneof('config_rule') != 'custom': - continue - match_subif = RE_CONFIG_RULE_IF_SUBIF.match(cr.custom.resource_key) - if not match_subif: - continue - address_ip = json.loads(cr.custom.resource_value).get('address_ip') - if address_ip not in [a_ip, z_ip]: - continue - port_name = 'PORT-' + match_subif.groups(0)[0] # `PORT-` added as prefix - ep_id = EndPointId() - ep_id.endpoint_uuid.uuid = device_endpoint_uuids[port_name] - ep_id.device_id.device_uuid.uuid = device.device_id.device_uuid.uuid - service.service_endpoint_ids.append(ep_id) - - # add interface config rules - endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device.name, port_name, VLAN_TAG) - if address_ip == a_ip: - field_updates = { - 'address_ip': (address_ip, True), - # 'router_id': ('.'.join([str(router_id_counter)]*4), True), - 'router_id': ('200.1.1.1', True), - 'neighbor_address_ip': ('192.168.150.2', True), - 'route_distinguisher': (route_distinguisher, True), - 'sub_interface_index': (0, True), - 'vlan_id' : (VLAN_TAG, True), - # 'bgp_as': (BGP_AS+router_id_counter, True), - 'bgp_as': (BGP_AS, True), - 'ip_address': (address_ip, True), - 'prefix_length': (PREFIX_LENGTH, True), - 'policy_AZ' : (policy_AZ, True), - 'policy_ZA' : (policy_ZA, True), - 'address_prefix' : (PREFIX_LENGTH, True), - } - elif address_ip == z_ip: - field_updates = { - 'address_ip': (address_ip, True), - # 'router_id': ('.'.join([str(router_id_counter)]*4), True), - 'router_id': ('200.1.1.2', True), - 'neighbor_address_ip': ('192.168.150.1', True), - 'route_distinguisher': (route_distinguisher, True), - 'sub_interface_index': (0, True), - 'vlan_id' : (VLAN_TAG, True), - # 'bgp_as': (BGP_AS+router_id_counter, True), - 'bgp_as': (BGP_AS, True), - 'ip_address': (address_ip, True), - 'prefix_length': (PREFIX_LENGTH, True), - 'policy_AZ' : (policy_ZA, True), - 'policy_ZA' : (policy_AZ, True), - 'address_prefix' : (PREFIX_LENGTH, True), - } - router_id_counter += 1 - LOGGER.debug(f'BEFORE UPDATE -> device.device_config.config_rules: {service_config_rules}') - update_config_rule_custom(service_config_rules, endpoint_settings_key, field_updates) - LOGGER.debug(f'AFTER UPDATE -> device.device_config.config_rules: {service_config_rules}') - - service.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED + if cr.WhichOneof('config_rule') == 'custom' and cr.custom.resource_key == '_connect/settings': + for ep in json.loads(cr.custom.resource_value)['endpoints']: + if 'ip' in ep and (ep['ip'] == a_ip or ep['ip'] == z_ip): + ep_id = EndPointId() + ep_id.endpoint_uuid.uuid = ep['uuid'] + ep_id.device_id.device_uuid.uuid = device.device_id.device_uuid.uuid + service.service_endpoint_ids.append(ep_id) + service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM - if 'appInsId' in bw_info: - service.service_id.service_uuid.uuid = bw_info['appInsId'] + if 'appInsId' in bwInfo: + service.service_id.service_uuid.uuid = bwInfo['appInsId'] service.service_id.context_id.context_uuid.uuid = 'admin' - service.name = bw_info['appInsId'] + service.name = bwInfo['appInsId'] - if 'fixedAllocation' in bw_info: + if 'fixedAllocation' in bwInfo: capacity = Constraint_SLA_Capacity() - capacity.capacity_gbps = float(bw_info['fixedAllocation']) / 1.e9 + capacity.capacity_gbps = float(bwInfo['fixedAllocation']) / 1.e9 constraint = Constraint() constraint.sla_capacity.CopyFrom(capacity) service.service_constraints.append(constraint) diff --git a/src/service/service/service_handlers/l3nm_openconfig/ConfigRules.py b/src/service/service/service_handlers/l3nm_openconfig/ConfigRules.py index 0369d6207..1e4425cdb 100644 --- a/src/service/service/service_handlers/l3nm_openconfig/ConfigRules.py +++ b/src/service/service/service_handlers/l3nm_openconfig/ConfigRules.py @@ -37,7 +37,6 @@ def setup_config_rules( 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 - neighbor_address_ip = json_endpoint_settings.get('neighbor_address_ip', '0.0.0.0') # '2.2.2.1' policy_import = json_endpoint_settings.get('policy_AZ', '2' ) # 2 policy_export = json_endpoint_settings.get('policy_ZA', '7' ) # 30 @@ -47,21 +46,18 @@ def setup_config_rules( network_subinterface_desc = json_endpoint_settings.get('subif_description','') #service_short_uuid = service_uuid.split('-')[-1] #network_instance_name = '{:s}-NetInst'.format(service_short_uuid) - # network_instance_name = json_endpoint_settings.get('ni_name', service_uuid.split('-')[-1]) #ELAN-AC:1 - network_instance_name = 'default' + network_instance_name = json_endpoint_settings.get('ni_name', service_uuid.split('-')[-1]) #ELAN-AC:1 - ''' - # if_subif_name = '{:s}.{:d}'.format(endpoint_name, vlan_id) - if_subif_name = '{:s}'.format(endpoint_name[5:]) + if_subif_name = '{:s}.{:d}'.format(endpoint_name, vlan_id) json_config_rules = [ - # # Configure Interface (not used) - # json_config_rule_set( + # Configure Interface (not used) + #json_config_rule_set( # '/interface[{:s}]'.format(endpoint_name), { # 'name': endpoint_name, # 'description': network_interface_desc, # 'mtu': mtu, - # }), + #}), #Create network instance json_config_rule_set( @@ -78,10 +74,8 @@ def setup_config_rules( json_config_rule_set( '/network_instance[{:s}]/protocols[BGP]'.format(network_instance_name), { 'name': network_instance_name, - # 'protocol_name': 'BGP', - 'protocol_name': bgp_as, + 'protocol_name': 'BGP', 'identifier': 'BGP', - # 'identifier': bgp_as, 'as': bgp_as, 'router_id': router_id, }), @@ -107,8 +101,7 @@ def setup_config_rules( json_config_rule_set( '/interface[{:s}]/subinterface[{:d}]'.format(if_subif_name, sub_interface_index), { 'name' : if_subif_name, - # 'type' :'l3ipvlan', - 'type' :'ethernetCsmacd', + 'type' :'l3ipvlan', 'mtu' : mtu, 'index' : sub_interface_index, 'description' : network_subinterface_desc, @@ -190,40 +183,6 @@ def setup_config_rules( }), ] - ''' - if_subif_name = '{:s}'.format(endpoint_name[5:]) - - json_config_rules = [ - - #Add DIRECTLY CONNECTED protocol to network instance - json_config_rule_set( - '/network_instance[{:s}]/protocols[DIRECTLY_CONNECTED]'.format(network_instance_name), { - 'name': network_instance_name, - 'identifier': 'DIRECTLY_CONNECTED', - 'protocol_name': 'DIRECTLY_CONNECTED', - }), - - # Add BGP neighbors - json_config_rule_set( - '/network_instance[{:s}]/protocols[BGP]'.format(network_instance_name), { - 'name': network_instance_name, - 'protocol_name': bgp_as, - 'identifier': 'BGP', - 'as': bgp_as, - 'router_id': router_id, - 'neighbors': [{'ip_address': neighbor_address_ip, 'remote_as': bgp_as}] - }), - json_config_rule_set( - '/network_instance[{:s}]/table_connections[DIRECTLY_CONNECTED][BGP][IPV4]'.format(network_instance_name), { - 'name' : network_instance_name, - 'src_protocol' : 'DIRECTLY_CONNECTED', - 'dst_protocol' : 'BGP', - 'address_family' : 'IPV4', - 'default_import_policy': 'ACCEPT_ROUTE', - }), - - ] - for res_key, res_value in endpoint_acls: json_config_rules.append( {'action': 1, 'acl': res_value} @@ -242,8 +201,7 @@ def teardown_config_rules( json_endpoint_settings : Dict = endpoint_settings.value service_short_uuid = service_uuid.split('-')[-1] - # network_instance_name = '{:s}-NetInst'.format(service_short_uuid) - network_instance_name = json_endpoint_settings.get('ni_name', service_short_uuid) #ELAN-AC: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) @@ -304,10 +262,10 @@ def teardown_config_rules( #Delete interface; automatically deletes: # - /interface[]/subinterface[] - # json_config_rule_delete('/interface[{:s}]/subinterface[0]'.format(if_subif_name), - # { - # 'name': if_subif_name, - # }), + json_config_rule_delete('/interface[{:s}]/subinterface[0]'.format(if_subif_name), + { + 'name': if_subif_name, + }), #Delete network instance; automatically deletes: # - /network_instance[]/interface[] -- GitLab