Commit e14c3f50 authored by Pablo Armingol's avatar Pablo Armingol
Browse files

Merge branch 'develop' of https://labs.etsi.org/rep/tfs/controller into...

Merge branch 'develop' of https://labs.etsi.org/rep/tfs/controller into feat/tid-media-channel-service
parents 3de14e4a 26a57404
Loading
Loading
Loading
Loading
+19 −9
Original line number Diff line number Diff line
@@ -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()}
@@ -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')
@@ -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)
@@ -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},
                    }
                )
+6 −1
Original line number Diff line number Diff line
@@ -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
@@ -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__)
@@ -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
+5 −1
Original line number Diff line number Diff line
@@ -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

@@ -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
+39 −15
Original line number Diff line number Diff line
@@ -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
    )


@@ -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):
@@ -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[
+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