Loading src/device/service/drivers/gnmi_openconfig/handlers/Acl.py +19 −9 Original line number Diff line number Diff line Loading @@ -30,7 +30,8 @@ _TFS_OC_RULE_TYPE = { _TFS_OC_FWD_ACTION = { 'ACLFORWARDINGACTION_DROP': 'DROP', 'ACLFORWARDINGACTION_ACCEPT': 'ACCEPT', 'ACLFORWARDINGACTION_REJECT': 'REJECT', #'ACLFORWARDINGACTION_REJECT': 'REJECT', # Correct according to OpenConfig. 'ACLFORWARDINGACTION_REJECT': 'DROP', # - Arista EOS only supports ACCEPT/DROP } _OC_TFS_RULE_TYPE = {v: k for k, v in _TFS_OC_RULE_TYPE.items()} Loading Loading @@ -89,7 +90,7 @@ class AclHandler(_Handler): y_entries = y_set.create_path('acl-entries') for entry in rs.get('entries', []): seq = int(entry['sequence_id']) m_ = entry['match'] m_ = entry.get('match', dict()) src_address = m_.get('src_address', '0.0.0.0/0') dst_address = m_.get('dst_address', '0.0.0.0/0') src_port = m_.get('src_port') Loading @@ -110,10 +111,9 @@ class AclHandler(_Handler): if src_port or dst_port: y_trans = y_e.create_path('transport') if src_port: y_trans.create_path("config/source-port", int(src_port)) y_trans.create_path('config/source-port', int(src_port)) if dst_port: y_trans.create_path("config/destination-port", int(dst_port)) y_ipv4.create_path('config/protocol', int(proto)) y_trans.create_path('config/destination-port', int(dst_port)) y_act = y_e.create_path('actions') y_act.create_path('config/forwarding-action', act) Loading Loading @@ -183,14 +183,24 @@ class AclHandler(_Handler): act = ace.get('actions', {}).get('config', {}).get('forwarding-action', 'DROP') fwd_tfs = _OC_TFS_FWD_ACTION[act] ipv4_cfg = ace.get('ipv4', {}).get('config', {}) transport_cfg = ace.get('transport', {}).get('config', {}) match_conditions = dict() if 'source-address' in ipv4_cfg: match_conditions['src_address'] = ipv4_cfg['source-address'] if 'destination-address' in ipv4_cfg: match_conditions['dst_address'] = ipv4_cfg['destination-address'] if 'protocol' in ipv4_cfg: match_conditions['protocol'] = ipv4_cfg['protocol'] if 'source-port' in transport_cfg: match_conditions['src_port'] = transport_cfg['source-port'] if 'destination-port' in transport_cfg: match_conditions['dst_port'] = transport_cfg['destination-port'] rule_set['entries'].append( { 'sequence_id': seq, 'match': { 'src_address': ipv4_cfg.get('source-address', ''), 'dst_address': ipv4_cfg.get('destination-address', ''), }, 'match': match_conditions, 'action': {'forward_action': fwd_tfs}, } ) Loading src/nbi/service/ietf_acl/Acl.py +6 −1 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ # limitations under the License. import json, logging, re from flask import jsonify from flask_restful import Resource from werkzeug.exceptions import NotFound from common.proto.context_pb2 import ConfigActionEnum, ConfigRule Loading @@ -20,6 +21,7 @@ from common.tools.context_queries.Device import get_device from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from nbi.service._tools.Authentication import HTTP_AUTH from nbi.service._tools.HttpStatusCodes import HTTP_NOCONTENT from .ietf_acl_parser import ietf_acl_from_config_rule_resource_value LOGGER = logging.getLogger(__name__) Loading Loading @@ -72,4 +74,7 @@ class Acl(Resource): del device.device_config.config_rules[:] device.device_config.config_rules.extend(delete_config_rules) device_client.ConfigureDevice(device) return None response = jsonify({}) response.status_code = HTTP_NOCONTENT return response src/nbi/service/ietf_acl/Acls.py +5 −1 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ from common.tools.grpc.Tools import grpc_message_to_json_string from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from nbi.service._tools.Authentication import HTTP_AUTH from nbi.service._tools.HttpStatusCodes import HTTP_CREATED from .ietf_acl_parser import AclDirectionEnum, config_rule_from_ietf_acl from .YangValidator import YangValidator Loading Loading @@ -129,4 +130,7 @@ class Acls(Resource): device_client = DeviceClient() device_client.ConfigureDevice(device) return jsonify({}) response = jsonify({}) response.status_code = HTTP_CREATED return response src/nbi/service/ietf_acl/ietf_acl_parser.py +39 −15 Original line number Diff line number Diff line Loading @@ -28,9 +28,12 @@ class AclDirectionEnum(Enum): 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='' protocol: int = 0 source_ipv4_network: Optional[str] = Field( serialization_alias='source-ipv4-network', default=None ) destination_ipv4_network: Optional[str] = Field( serialization_alias='destination-ipv4-network', default=None ) Loading @@ -45,9 +48,15 @@ class Tcp(BaseModel): destination_port: Optional[Port] = Field(serialization_alias='destination-port', default=None) class Udp(BaseModel): source_port: Optional[Port] = Field(serialization_alias='source-port', default=None) destination_port: Optional[Port] = Field(serialization_alias='destination-port', default=None) class Matches(BaseModel): ipv4: Ipv4 = Ipv4() tcp: Optional[Tcp] = None udp: Optional[Udp] = None class Action(BaseModel): Loading Loading @@ -237,27 +246,42 @@ def config_rule_from_ietf_acl( def ietf_acl_from_config_rule_resource_value(config_rule_rv: Dict) -> Dict: rule_set = config_rule_rv['rule_set'] rule_set = config_rule_rv.get('rule_set', dict()) ace = [] for acl_entry in rule_set['entries']: match_ = acl_entry['match'] for acl_entry in rule_set.get('entries', list()): match_ = acl_entry.get('match', dict()) protocol = match_.get('protocol', 0) ipv4 = Ipv4( dscp=match_['dscp'], source_ipv4_network=match_['src_address'], destination_ipv4_network=match_['dst_address'], dscp=match_.get('dscp', 0), protocol=protocol, source_ipv4_network=match_.get('src_address'), destination_ipv4_network=match_.get('dst_address'), ) src_port = match_.get('src_port') src_port = None if src_port is None else Port(port=src_port) dst_port = match_.get('dst_port') dst_port = None if dst_port is None else Port(port=dst_port) tcp = None if match_['tcp_flags']: udp = None if protocol == 6: tcp = Tcp( flags=match_['tcp_flags'], source_port=Port(port=match_['src_port']), destination_port=Port(port=match_['dst_port']), flags=match_.get('tcp_flags', 0), source_port=src_port, destination_port=dst_port, ) matches = Matches(ipv4=ipv4, tcp=tcp) elif protocol == 17: udp = Udp( source_port=src_port, destination_port=dst_port, ) matches = Matches(ipv4=ipv4, tcp=tcp, udp=udp) ace.append( Ace( name=acl_entry['description'], name=acl_entry.get('description', ''), matches=matches, actions=Action( forwarding=TFS_IETF_FORWARDING_ACTION_MAPPING[ Loading src/nbi/service/ietf_l2vpn/Handlers.py 0 → 100644 +265 −0 Original line number Diff line number Diff line # 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 Dict, List, Optional from common.Constants import DEFAULT_CONTEXT_NAME from common.proto.context_pb2 import Service, ServiceStatusEnum, ServiceTypeEnum from common.tools.context_queries.Service import get_service_by_uuid from common.tools.grpc.ConfigRules import update_config_rule_custom from common.tools.grpc.Constraints import ( update_constraint_custom_dict, update_constraint_endpoint_location, update_constraint_endpoint_priority, update_constraint_sla_availability, update_constraint_sla_capacity, update_constraint_sla_latency, ) from common.tools.grpc.EndPointIds import update_endpoint_ids from common.tools.grpc.Tools import grpc_message_to_json_string from context.client.ContextClient import ContextClient from service.client.ServiceClient import ServiceClient from .Constants import ( #DEFAULT_ADDRESS_FAMILIES, DEFAULT_BGP_AS, DEFAULT_BGP_ROUTE_TARGET, BEARER_MAPPINGS, DEFAULT_MTU, ) LOGGER = logging.getLogger(__name__) def create_service( service_uuid : str, context_uuid : Optional[str] = DEFAULT_CONTEXT_NAME ) -> Optional[Exception]: # pylint: disable=no-member service_request = Service() service_request.service_id.context_id.context_uuid.uuid = context_uuid service_request.service_id.service_uuid.uuid = service_uuid service_request.service_type = ServiceTypeEnum.SERVICETYPE_L2NM service_request.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED try: service_client = ServiceClient() service_client.CreateService(service_request) return None except Exception as e: # pylint: disable=broad-except LOGGER.exception('Unhandled exception creating Service') return e def process_vpn_service( vpn_service : Dict, errors : List[Dict] ) -> None: vpn_id = vpn_service['vpn-id'] exc = create_service(vpn_id) if exc is not None: errors.append({'error': str(exc)}) def process_site_network_access( site_id : str, network_access : Dict, errors : List[Dict] ) -> None: try: site_network_access_type = network_access['site-network-access-type'] site_network_access_type = site_network_access_type.replace('ietf-l2vpn-svc:', '') if site_network_access_type != 'multipoint': MSG = 'Site Network Access Type: {:s}' msg = MSG.format(str(network_access['site-network-access-type'])) raise NotImplementedError(msg) access_role : str = network_access['vpn-attachment']['site-role'] access_role = access_role.replace('ietf-l2vpn-svc:', '').replace('-role', '') # hub/spoke if access_role not in {'hub', 'spoke'}: MSG = 'Site VPN Attackment Role: {:s}' raise NotImplementedError(MSG.format(str(network_access['site-network-access-type']))) device_uuid = network_access['device-reference'] endpoint_uuid = network_access['site-network-access-id'] service_uuid = network_access['vpn-attachment']['vpn-id'] encapsulation_type = network_access['connection']['encapsulation-type'] cvlan_tag_id = network_access['connection']['tagged-interface'][encapsulation_type]['cvlan-id'] bearer_reference = network_access['bearer']['bearer-reference'] service_mtu = network_access['service']['svc-mtu'] service_input_bandwidth = network_access['service']['svc-input-bandwidth'] service_output_bandwidth = network_access['service']['svc-output-bandwidth'] service_bandwidth_bps = max(service_input_bandwidth, service_output_bandwidth) service_bandwidth_gbps = service_bandwidth_bps / 1.e9 max_e2e_latency_ms = None availability = None for qos_profile_class in network_access['service']['qos']['qos-profile']['classes']['class']: if qos_profile_class['class-id'] != 'qos-realtime': MSG = 'Site Network Access QoS Class Id: {:s}' raise NotImplementedError(MSG.format(str(qos_profile_class['class-id']))) qos_profile_class_direction = qos_profile_class['direction'] qos_profile_class_direction = qos_profile_class_direction.replace('ietf-l2vpn-svc:', '') if qos_profile_class_direction != 'both': MSG = 'Site Network Access QoS Class Direction: {:s}' raise NotImplementedError(MSG.format(str(qos_profile_class['direction']))) max_e2e_latency_ms = qos_profile_class['latency']['latency-boundary'] availability = qos_profile_class['bandwidth']['guaranteed-bw-percent'] network_access_diversity = network_access.get('access-diversity', {}) diversity_constraints = network_access_diversity.get('constraints', {}).get('constraint', []) raise_if_differs = True diversity_constraints = { constraint['constraint-type']:([ target[0] for target in constraint['target'].items() if len(target[1]) == 1 ][0], raise_if_differs) for constraint in diversity_constraints } network_access_availability = network_access.get('availability', {}) access_priority : Optional[int] = network_access_availability.get('access-priority') single_active : bool = len(network_access_availability.get('single-active', [])) > 0 all_active : bool = len(network_access_availability.get('all-active', [])) > 0 mapping = BEARER_MAPPINGS.get(bearer_reference) if mapping is None: msg = 'Specified Bearer({:s}) is not configured.' raise Exception(msg.format(str(bearer_reference))) ( device_uuid, endpoint_uuid, router_id, route_dist, sub_if_index, address_ip, address_prefix, remote_router, circuit_id ) = mapping context_client = ContextClient() service = get_service_by_uuid( context_client, service_uuid, context_uuid=DEFAULT_CONTEXT_NAME, rw_copy=True ) if service is None: raise Exception('VPN({:s}) not found in database'.format(str(service_uuid))) endpoint_ids = service.service_endpoint_ids config_rules = service.service_config.config_rules constraints = service.service_constraints endpoint_id = update_endpoint_ids(endpoint_ids, device_uuid, endpoint_uuid) update_constraint_endpoint_location(constraints, endpoint_id, region=site_id) if access_priority is not None: update_constraint_endpoint_priority(constraints, endpoint_id, access_priority) if service_bandwidth_gbps is not None: update_constraint_sla_capacity(constraints, service_bandwidth_gbps) if max_e2e_latency_ms is not None: update_constraint_sla_latency(constraints, max_e2e_latency_ms) if availability is not None: update_constraint_sla_availability(constraints, 1, True, availability) if len(diversity_constraints) > 0: update_constraint_custom_dict(constraints, 'diversity', diversity_constraints) if single_active or all_active: # assume 1 disjoint path per endpoint/location included in service location_endpoints = {} for constraint in constraints: if constraint.WhichOneof('constraint') != 'endpoint_location': continue str_endpoint_id = grpc_message_to_json_string(constraint.endpoint_location.endpoint_id) str_location_id = grpc_message_to_json_string(constraint.endpoint_location.location) location_endpoints.setdefault(str_location_id, set()).add(str_endpoint_id) num_endpoints_per_location = {len(endpoints) for endpoints in location_endpoints.values()} num_disjoint_paths = max(num_endpoints_per_location) update_constraint_sla_availability(constraints, num_disjoint_paths, all_active, 0.0) service_settings_key = '/settings' if service_mtu is None: service_mtu = DEFAULT_MTU update_config_rule_custom(config_rules, service_settings_key, { 'mtu' : (service_mtu, True), #'address_families': (DEFAULT_ADDRESS_FAMILIES, True), #'bgp_as' : (DEFAULT_BGP_AS, True), #'bgp_route_target': (DEFAULT_BGP_ROUTE_TARGET, True), }) #ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' #endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device_uuid, endpoint_uuid, cvlan_tag_id) ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/settings' endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device_uuid, endpoint_uuid) field_updates = {} if router_id is not None: field_updates['router_id' ] = (router_id, True) if route_dist is not None: field_updates['route_distinguisher'] = (route_dist, True) if sub_if_index is not None: field_updates['sub_interface_index'] = (sub_if_index, True) if cvlan_tag_id is not None: field_updates['vlan_id' ] = (cvlan_tag_id, True) if address_ip is not None: field_updates['address_ip' ] = (address_ip, True) if address_prefix is not None: field_updates['address_prefix' ] = (address_prefix, True) if remote_router is not None: field_updates['remote_router' ] = (remote_router, True) if circuit_id is not None: field_updates['circuit_id' ] = (circuit_id, True) update_config_rule_custom(config_rules, endpoint_settings_key, field_updates) service_client = ServiceClient() service_client.UpdateService(service) except Exception as exc: LOGGER.exception('Unhandled Exception') errors.append({'error': str(exc)}) def process_site(site : Dict, errors : List[Dict]) -> None: site_id = site['site-id'] # this change is made for ECOC2025 demo purposes if site['management']['type'] != 'provider-managed': # if site['management']['type'] == 'customer-managed': MSG = 'Site Management Type: {:s}' raise NotImplementedError(MSG.format(str(site['management']['type']))) network_accesses : List[Dict] = site['site-network-accesses']['site-network-access'] for network_access in network_accesses: process_site_network_access(site_id, network_access, errors) def update_vpn(site : Dict, errors : List[Dict]) -> None: if site['management']['type'] != 'provider-managed': MSG = 'Site Management Type: {:s}' raise NotImplementedError(MSG.format(str(site['management']['type']))) network_accesses : List[Dict] = site['site-network-accesses']['site-network-access'] for network_access in network_accesses: update_site_network_access(network_access, errors) def update_site_network_access(network_access : Dict, errors : List[Dict]) -> None: try: site_network_access_type = network_access['site-network-access-type'] site_network_access_type = site_network_access_type.replace('ietf-l2vpn-svc:', '') if site_network_access_type != 'multipoint': MSG = 'Site Network Access Type: {:s}' msg = MSG.format(str(network_access['site-network-access-type'])) raise NotImplementedError(msg) service_uuid = network_access['vpn-attachment']['vpn-id'] service_input_bandwidth = network_access['service']['svc-input-bandwidth'] service_output_bandwidth = network_access['service']['svc-output-bandwidth'] service_bandwidth_bps = max(service_input_bandwidth, service_output_bandwidth) service_bandwidth_gbps = service_bandwidth_bps / 1.e9 max_e2e_latency_ms = None availability = None context_client = ContextClient() service = get_service_by_uuid( context_client, service_uuid, context_uuid=DEFAULT_CONTEXT_NAME, rw_copy=True ) if service is None: MSG = 'VPN({:s}) not found in database' raise Exception(MSG.format(str(service_uuid))) constraints = service.service_constraints if service_bandwidth_gbps is not None: update_constraint_sla_capacity(constraints, service_bandwidth_gbps) if max_e2e_latency_ms is not None: update_constraint_sla_latency(constraints, max_e2e_latency_ms) if availability is not None: update_constraint_sla_availability(constraints, 1, True, availability) service_client = ServiceClient() service_client.UpdateService(service) except Exception as e: # pylint: disable=broad-except LOGGER.exception('Unhandled exception updating Service') errors.append({'error': str(e)}) Loading
src/device/service/drivers/gnmi_openconfig/handlers/Acl.py +19 −9 Original line number Diff line number Diff line Loading @@ -30,7 +30,8 @@ _TFS_OC_RULE_TYPE = { _TFS_OC_FWD_ACTION = { 'ACLFORWARDINGACTION_DROP': 'DROP', 'ACLFORWARDINGACTION_ACCEPT': 'ACCEPT', 'ACLFORWARDINGACTION_REJECT': 'REJECT', #'ACLFORWARDINGACTION_REJECT': 'REJECT', # Correct according to OpenConfig. 'ACLFORWARDINGACTION_REJECT': 'DROP', # - Arista EOS only supports ACCEPT/DROP } _OC_TFS_RULE_TYPE = {v: k for k, v in _TFS_OC_RULE_TYPE.items()} Loading Loading @@ -89,7 +90,7 @@ class AclHandler(_Handler): y_entries = y_set.create_path('acl-entries') for entry in rs.get('entries', []): seq = int(entry['sequence_id']) m_ = entry['match'] m_ = entry.get('match', dict()) src_address = m_.get('src_address', '0.0.0.0/0') dst_address = m_.get('dst_address', '0.0.0.0/0') src_port = m_.get('src_port') Loading @@ -110,10 +111,9 @@ class AclHandler(_Handler): if src_port or dst_port: y_trans = y_e.create_path('transport') if src_port: y_trans.create_path("config/source-port", int(src_port)) y_trans.create_path('config/source-port', int(src_port)) if dst_port: y_trans.create_path("config/destination-port", int(dst_port)) y_ipv4.create_path('config/protocol', int(proto)) y_trans.create_path('config/destination-port', int(dst_port)) y_act = y_e.create_path('actions') y_act.create_path('config/forwarding-action', act) Loading Loading @@ -183,14 +183,24 @@ class AclHandler(_Handler): act = ace.get('actions', {}).get('config', {}).get('forwarding-action', 'DROP') fwd_tfs = _OC_TFS_FWD_ACTION[act] ipv4_cfg = ace.get('ipv4', {}).get('config', {}) transport_cfg = ace.get('transport', {}).get('config', {}) match_conditions = dict() if 'source-address' in ipv4_cfg: match_conditions['src_address'] = ipv4_cfg['source-address'] if 'destination-address' in ipv4_cfg: match_conditions['dst_address'] = ipv4_cfg['destination-address'] if 'protocol' in ipv4_cfg: match_conditions['protocol'] = ipv4_cfg['protocol'] if 'source-port' in transport_cfg: match_conditions['src_port'] = transport_cfg['source-port'] if 'destination-port' in transport_cfg: match_conditions['dst_port'] = transport_cfg['destination-port'] rule_set['entries'].append( { 'sequence_id': seq, 'match': { 'src_address': ipv4_cfg.get('source-address', ''), 'dst_address': ipv4_cfg.get('destination-address', ''), }, 'match': match_conditions, 'action': {'forward_action': fwd_tfs}, } ) Loading
src/nbi/service/ietf_acl/Acl.py +6 −1 Original line number Diff line number Diff line Loading @@ -13,6 +13,7 @@ # limitations under the License. import json, logging, re from flask import jsonify from flask_restful import Resource from werkzeug.exceptions import NotFound from common.proto.context_pb2 import ConfigActionEnum, ConfigRule Loading @@ -20,6 +21,7 @@ from common.tools.context_queries.Device import get_device from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from nbi.service._tools.Authentication import HTTP_AUTH from nbi.service._tools.HttpStatusCodes import HTTP_NOCONTENT from .ietf_acl_parser import ietf_acl_from_config_rule_resource_value LOGGER = logging.getLogger(__name__) Loading Loading @@ -72,4 +74,7 @@ class Acl(Resource): del device.device_config.config_rules[:] device.device_config.config_rules.extend(delete_config_rules) device_client.ConfigureDevice(device) return None response = jsonify({}) response.status_code = HTTP_NOCONTENT return response
src/nbi/service/ietf_acl/Acls.py +5 −1 Original line number Diff line number Diff line Loading @@ -23,6 +23,7 @@ from common.tools.grpc.Tools import grpc_message_to_json_string from context.client.ContextClient import ContextClient from device.client.DeviceClient import DeviceClient from nbi.service._tools.Authentication import HTTP_AUTH from nbi.service._tools.HttpStatusCodes import HTTP_CREATED from .ietf_acl_parser import AclDirectionEnum, config_rule_from_ietf_acl from .YangValidator import YangValidator Loading Loading @@ -129,4 +130,7 @@ class Acls(Resource): device_client = DeviceClient() device_client.ConfigureDevice(device) return jsonify({}) response = jsonify({}) response.status_code = HTTP_CREATED return response
src/nbi/service/ietf_acl/ietf_acl_parser.py +39 −15 Original line number Diff line number Diff line Loading @@ -28,9 +28,12 @@ class AclDirectionEnum(Enum): 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='' protocol: int = 0 source_ipv4_network: Optional[str] = Field( serialization_alias='source-ipv4-network', default=None ) destination_ipv4_network: Optional[str] = Field( serialization_alias='destination-ipv4-network', default=None ) Loading @@ -45,9 +48,15 @@ class Tcp(BaseModel): destination_port: Optional[Port] = Field(serialization_alias='destination-port', default=None) class Udp(BaseModel): source_port: Optional[Port] = Field(serialization_alias='source-port', default=None) destination_port: Optional[Port] = Field(serialization_alias='destination-port', default=None) class Matches(BaseModel): ipv4: Ipv4 = Ipv4() tcp: Optional[Tcp] = None udp: Optional[Udp] = None class Action(BaseModel): Loading Loading @@ -237,27 +246,42 @@ def config_rule_from_ietf_acl( def ietf_acl_from_config_rule_resource_value(config_rule_rv: Dict) -> Dict: rule_set = config_rule_rv['rule_set'] rule_set = config_rule_rv.get('rule_set', dict()) ace = [] for acl_entry in rule_set['entries']: match_ = acl_entry['match'] for acl_entry in rule_set.get('entries', list()): match_ = acl_entry.get('match', dict()) protocol = match_.get('protocol', 0) ipv4 = Ipv4( dscp=match_['dscp'], source_ipv4_network=match_['src_address'], destination_ipv4_network=match_['dst_address'], dscp=match_.get('dscp', 0), protocol=protocol, source_ipv4_network=match_.get('src_address'), destination_ipv4_network=match_.get('dst_address'), ) src_port = match_.get('src_port') src_port = None if src_port is None else Port(port=src_port) dst_port = match_.get('dst_port') dst_port = None if dst_port is None else Port(port=dst_port) tcp = None if match_['tcp_flags']: udp = None if protocol == 6: tcp = Tcp( flags=match_['tcp_flags'], source_port=Port(port=match_['src_port']), destination_port=Port(port=match_['dst_port']), flags=match_.get('tcp_flags', 0), source_port=src_port, destination_port=dst_port, ) matches = Matches(ipv4=ipv4, tcp=tcp) elif protocol == 17: udp = Udp( source_port=src_port, destination_port=dst_port, ) matches = Matches(ipv4=ipv4, tcp=tcp, udp=udp) ace.append( Ace( name=acl_entry['description'], name=acl_entry.get('description', ''), matches=matches, actions=Action( forwarding=TFS_IETF_FORWARDING_ACTION_MAPPING[ Loading
src/nbi/service/ietf_l2vpn/Handlers.py 0 → 100644 +265 −0 Original line number Diff line number Diff line # 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 Dict, List, Optional from common.Constants import DEFAULT_CONTEXT_NAME from common.proto.context_pb2 import Service, ServiceStatusEnum, ServiceTypeEnum from common.tools.context_queries.Service import get_service_by_uuid from common.tools.grpc.ConfigRules import update_config_rule_custom from common.tools.grpc.Constraints import ( update_constraint_custom_dict, update_constraint_endpoint_location, update_constraint_endpoint_priority, update_constraint_sla_availability, update_constraint_sla_capacity, update_constraint_sla_latency, ) from common.tools.grpc.EndPointIds import update_endpoint_ids from common.tools.grpc.Tools import grpc_message_to_json_string from context.client.ContextClient import ContextClient from service.client.ServiceClient import ServiceClient from .Constants import ( #DEFAULT_ADDRESS_FAMILIES, DEFAULT_BGP_AS, DEFAULT_BGP_ROUTE_TARGET, BEARER_MAPPINGS, DEFAULT_MTU, ) LOGGER = logging.getLogger(__name__) def create_service( service_uuid : str, context_uuid : Optional[str] = DEFAULT_CONTEXT_NAME ) -> Optional[Exception]: # pylint: disable=no-member service_request = Service() service_request.service_id.context_id.context_uuid.uuid = context_uuid service_request.service_id.service_uuid.uuid = service_uuid service_request.service_type = ServiceTypeEnum.SERVICETYPE_L2NM service_request.service_status.service_status = ServiceStatusEnum.SERVICESTATUS_PLANNED try: service_client = ServiceClient() service_client.CreateService(service_request) return None except Exception as e: # pylint: disable=broad-except LOGGER.exception('Unhandled exception creating Service') return e def process_vpn_service( vpn_service : Dict, errors : List[Dict] ) -> None: vpn_id = vpn_service['vpn-id'] exc = create_service(vpn_id) if exc is not None: errors.append({'error': str(exc)}) def process_site_network_access( site_id : str, network_access : Dict, errors : List[Dict] ) -> None: try: site_network_access_type = network_access['site-network-access-type'] site_network_access_type = site_network_access_type.replace('ietf-l2vpn-svc:', '') if site_network_access_type != 'multipoint': MSG = 'Site Network Access Type: {:s}' msg = MSG.format(str(network_access['site-network-access-type'])) raise NotImplementedError(msg) access_role : str = network_access['vpn-attachment']['site-role'] access_role = access_role.replace('ietf-l2vpn-svc:', '').replace('-role', '') # hub/spoke if access_role not in {'hub', 'spoke'}: MSG = 'Site VPN Attackment Role: {:s}' raise NotImplementedError(MSG.format(str(network_access['site-network-access-type']))) device_uuid = network_access['device-reference'] endpoint_uuid = network_access['site-network-access-id'] service_uuid = network_access['vpn-attachment']['vpn-id'] encapsulation_type = network_access['connection']['encapsulation-type'] cvlan_tag_id = network_access['connection']['tagged-interface'][encapsulation_type]['cvlan-id'] bearer_reference = network_access['bearer']['bearer-reference'] service_mtu = network_access['service']['svc-mtu'] service_input_bandwidth = network_access['service']['svc-input-bandwidth'] service_output_bandwidth = network_access['service']['svc-output-bandwidth'] service_bandwidth_bps = max(service_input_bandwidth, service_output_bandwidth) service_bandwidth_gbps = service_bandwidth_bps / 1.e9 max_e2e_latency_ms = None availability = None for qos_profile_class in network_access['service']['qos']['qos-profile']['classes']['class']: if qos_profile_class['class-id'] != 'qos-realtime': MSG = 'Site Network Access QoS Class Id: {:s}' raise NotImplementedError(MSG.format(str(qos_profile_class['class-id']))) qos_profile_class_direction = qos_profile_class['direction'] qos_profile_class_direction = qos_profile_class_direction.replace('ietf-l2vpn-svc:', '') if qos_profile_class_direction != 'both': MSG = 'Site Network Access QoS Class Direction: {:s}' raise NotImplementedError(MSG.format(str(qos_profile_class['direction']))) max_e2e_latency_ms = qos_profile_class['latency']['latency-boundary'] availability = qos_profile_class['bandwidth']['guaranteed-bw-percent'] network_access_diversity = network_access.get('access-diversity', {}) diversity_constraints = network_access_diversity.get('constraints', {}).get('constraint', []) raise_if_differs = True diversity_constraints = { constraint['constraint-type']:([ target[0] for target in constraint['target'].items() if len(target[1]) == 1 ][0], raise_if_differs) for constraint in diversity_constraints } network_access_availability = network_access.get('availability', {}) access_priority : Optional[int] = network_access_availability.get('access-priority') single_active : bool = len(network_access_availability.get('single-active', [])) > 0 all_active : bool = len(network_access_availability.get('all-active', [])) > 0 mapping = BEARER_MAPPINGS.get(bearer_reference) if mapping is None: msg = 'Specified Bearer({:s}) is not configured.' raise Exception(msg.format(str(bearer_reference))) ( device_uuid, endpoint_uuid, router_id, route_dist, sub_if_index, address_ip, address_prefix, remote_router, circuit_id ) = mapping context_client = ContextClient() service = get_service_by_uuid( context_client, service_uuid, context_uuid=DEFAULT_CONTEXT_NAME, rw_copy=True ) if service is None: raise Exception('VPN({:s}) not found in database'.format(str(service_uuid))) endpoint_ids = service.service_endpoint_ids config_rules = service.service_config.config_rules constraints = service.service_constraints endpoint_id = update_endpoint_ids(endpoint_ids, device_uuid, endpoint_uuid) update_constraint_endpoint_location(constraints, endpoint_id, region=site_id) if access_priority is not None: update_constraint_endpoint_priority(constraints, endpoint_id, access_priority) if service_bandwidth_gbps is not None: update_constraint_sla_capacity(constraints, service_bandwidth_gbps) if max_e2e_latency_ms is not None: update_constraint_sla_latency(constraints, max_e2e_latency_ms) if availability is not None: update_constraint_sla_availability(constraints, 1, True, availability) if len(diversity_constraints) > 0: update_constraint_custom_dict(constraints, 'diversity', diversity_constraints) if single_active or all_active: # assume 1 disjoint path per endpoint/location included in service location_endpoints = {} for constraint in constraints: if constraint.WhichOneof('constraint') != 'endpoint_location': continue str_endpoint_id = grpc_message_to_json_string(constraint.endpoint_location.endpoint_id) str_location_id = grpc_message_to_json_string(constraint.endpoint_location.location) location_endpoints.setdefault(str_location_id, set()).add(str_endpoint_id) num_endpoints_per_location = {len(endpoints) for endpoints in location_endpoints.values()} num_disjoint_paths = max(num_endpoints_per_location) update_constraint_sla_availability(constraints, num_disjoint_paths, all_active, 0.0) service_settings_key = '/settings' if service_mtu is None: service_mtu = DEFAULT_MTU update_config_rule_custom(config_rules, service_settings_key, { 'mtu' : (service_mtu, True), #'address_families': (DEFAULT_ADDRESS_FAMILIES, True), #'bgp_as' : (DEFAULT_BGP_AS, True), #'bgp_route_target': (DEFAULT_BGP_ROUTE_TARGET, True), }) #ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/vlan[{:d}]/settings' #endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device_uuid, endpoint_uuid, cvlan_tag_id) ENDPOINT_SETTINGS_KEY = '/device[{:s}]/endpoint[{:s}]/settings' endpoint_settings_key = ENDPOINT_SETTINGS_KEY.format(device_uuid, endpoint_uuid) field_updates = {} if router_id is not None: field_updates['router_id' ] = (router_id, True) if route_dist is not None: field_updates['route_distinguisher'] = (route_dist, True) if sub_if_index is not None: field_updates['sub_interface_index'] = (sub_if_index, True) if cvlan_tag_id is not None: field_updates['vlan_id' ] = (cvlan_tag_id, True) if address_ip is not None: field_updates['address_ip' ] = (address_ip, True) if address_prefix is not None: field_updates['address_prefix' ] = (address_prefix, True) if remote_router is not None: field_updates['remote_router' ] = (remote_router, True) if circuit_id is not None: field_updates['circuit_id' ] = (circuit_id, True) update_config_rule_custom(config_rules, endpoint_settings_key, field_updates) service_client = ServiceClient() service_client.UpdateService(service) except Exception as exc: LOGGER.exception('Unhandled Exception') errors.append({'error': str(exc)}) def process_site(site : Dict, errors : List[Dict]) -> None: site_id = site['site-id'] # this change is made for ECOC2025 demo purposes if site['management']['type'] != 'provider-managed': # if site['management']['type'] == 'customer-managed': MSG = 'Site Management Type: {:s}' raise NotImplementedError(MSG.format(str(site['management']['type']))) network_accesses : List[Dict] = site['site-network-accesses']['site-network-access'] for network_access in network_accesses: process_site_network_access(site_id, network_access, errors) def update_vpn(site : Dict, errors : List[Dict]) -> None: if site['management']['type'] != 'provider-managed': MSG = 'Site Management Type: {:s}' raise NotImplementedError(MSG.format(str(site['management']['type']))) network_accesses : List[Dict] = site['site-network-accesses']['site-network-access'] for network_access in network_accesses: update_site_network_access(network_access, errors) def update_site_network_access(network_access : Dict, errors : List[Dict]) -> None: try: site_network_access_type = network_access['site-network-access-type'] site_network_access_type = site_network_access_type.replace('ietf-l2vpn-svc:', '') if site_network_access_type != 'multipoint': MSG = 'Site Network Access Type: {:s}' msg = MSG.format(str(network_access['site-network-access-type'])) raise NotImplementedError(msg) service_uuid = network_access['vpn-attachment']['vpn-id'] service_input_bandwidth = network_access['service']['svc-input-bandwidth'] service_output_bandwidth = network_access['service']['svc-output-bandwidth'] service_bandwidth_bps = max(service_input_bandwidth, service_output_bandwidth) service_bandwidth_gbps = service_bandwidth_bps / 1.e9 max_e2e_latency_ms = None availability = None context_client = ContextClient() service = get_service_by_uuid( context_client, service_uuid, context_uuid=DEFAULT_CONTEXT_NAME, rw_copy=True ) if service is None: MSG = 'VPN({:s}) not found in database' raise Exception(MSG.format(str(service_uuid))) constraints = service.service_constraints if service_bandwidth_gbps is not None: update_constraint_sla_capacity(constraints, service_bandwidth_gbps) if max_e2e_latency_ms is not None: update_constraint_sla_latency(constraints, max_e2e_latency_ms) if availability is not None: update_constraint_sla_availability(constraints, 1, True, availability) service_client = ServiceClient() service_client.UpdateService(service) except Exception as e: # pylint: disable=broad-except LOGGER.exception('Unhandled exception updating Service') errors.append({'error': str(e)})