Commit c3bc4eca authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

NBI component - IETF L2VPN connector:

- Fixed L2VPN_SiteNetworkAccesses and Handlers
parent 91fc5331
Loading
Loading
Loading
Loading
+130 −169
Original line number Diff line number Diff line
@@ -12,8 +12,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

import logging, netaddr
from typing import Dict, List, Optional, Tuple
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
@@ -27,9 +27,10 @@ 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 nbi.service._tools.Authentication import HTTP_AUTH
from nbi.service._tools.HttpStatusCodes import HTTP_NOCONTENT, HTTP_SERVERERROR
from .Constants import BEARER_MAPPINGS, DEFAULT_ADDRESS_FAMILIES, DEFAULT_BGP_AS, DEFAULT_BGP_ROUTE_TARGET, DEFAULT_MTU
from .Constants import (
    #DEFAULT_ADDRESS_FAMILIES, DEFAULT_BGP_AS, DEFAULT_BGP_ROUTE_TARGET,
    BEARER_MAPPINGS, DEFAULT_MTU,
)

LOGGER = logging.getLogger(__name__)

@@ -62,8 +63,7 @@ def process_vpn_service(
def process_site_network_access(
    site_id : str, network_access : Dict, errors : List[Dict]
) -> None:
    endpoint_uuid = network_access['site-network-access-id']

    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':
@@ -71,20 +71,21 @@ def process_site_network_access(
            msg = MSG.format(str(network_access['site-network-access-type']))
            raise NotImplementedError(msg)

    device_uuid  = network_access['device-reference']
    service_uuid = network_access['vpn-attachment']['vpn-id']

        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']
@@ -98,70 +99,17 @@ def process_site_network_access(
                MSG = 'Site Network Access QoS Class Id: {:s}'
                raise NotImplementedError(MSG.format(str(qos_profile_class['class-id'])))

        if 'ietf-l2vpn-svc' in qos_profile_class['direction']:
            # replace 'ietf-l2vpn-svc:both' with 'both' for backward compatibility
            qos_profile_class['direction'] = qos_profile_class['direction'].replace('ietf-l2vpn-svc:', '')
        if qos_profile_class['direction'] != 'both':
            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']

    errors.append({'error': str(exc)})

    
    context_uuid : Optional[str] = DEFAULT_CONTEXT_NAME
    context_client = ContextClient()
    service = get_service_by_uuid(context_client, service_uuid, context_uuid=context_uuid, 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
    endpoint_id =  update_endpoint_ids(endpoint_ids, device_uuid, endpoint_uuid)

    constraints  = service.service_constraints
    update_constraint_endpoint_location(constraints, endpoint_id, region=site_id)
    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)

    config_rules = service.service_config.config_rules

    service_settings_key = '/settings'
    service_settings = dict()
    if service_mtu is not None: service_settings['mtu'] = (service_mtu, True)
    update_config_rule_custom(config_rules, service_settings_key, service_settings)

    #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 cvlan_tag_id is not None: field_updates['vlan_tag'] = (cvlan_tag_id, True)
    update_config_rule_custom(config_rules, endpoint_settings_key, field_updates)

    try:
        service_client = ServiceClient()
        service_client.UpdateService(service)
        return None
    except Exception as e: # pylint: disable=broad-except
        LOGGER.exception('Unhandled exception updating Service')
        return e


# TODO: merge with


def process_site_network_access(site_id : str, network_access : Dict) -> Service:
    service_uuid = network_access['vpn-attachment']['vpn-id']

    bearer_reference = network_access['bearer']['bearer-reference']

    access_priority : Optional[int] = network_access.get('availability', {}).get('access-priority')
    single_active   : bool = len(network_access.get('availability', {}).get('single-active', [])) > 0
    all_active      : bool = len(network_access.get('availability', {}).get('all-active', [])) > 0

    diversity_constraints = network_access.get('access-diversity', {}).get('constraints', {}).get('constraint', [])
        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']:([
@@ -172,6 +120,11 @@ def process_site_network_access(site_id : str, network_access : Dict) -> Service
            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.'
@@ -181,42 +134,32 @@ def process_site_network_access(site_id : str, network_access : Dict) -> Service
            address_ip, address_prefix, remote_router, circuit_id
        ) = mapping

    target = get_service_by_uuid(context_client, service_uuid, rw_copy=True)
    if target is None: raise Exception('VPN({:s}) not found in database'.format(str(service_uuid)))
        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 = target.service_endpoint_ids       # pylint: disable=no-member
    config_rules = target.service_config.config_rules # pylint: disable=no-member
    constraints  = target.service_constraints        # pylint: disable=no-member
        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)

    service_settings_key = '/settings'
    update_config_rule_custom(config_rules, service_settings_key, {
        'mtu'             : (DEFAULT_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}]/settings'.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_id       is not None: field_updates['vlan_id'            ] = (cvlan_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)

        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)

    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 single_active or all_active:
        # assume 1 disjoint path per endpoint/location included in service/slice
            # assume 1 disjoint path per endpoint/location included in service
            location_endpoints = {}
            for constraint in constraints:
                if constraint.WhichOneof('constraint') != 'endpoint_location': continue
@@ -227,17 +170,35 @@ def process_site_network_access(site_id : str, network_access : Dict) -> Service
            num_disjoint_paths = max(num_endpoints_per_location)
            update_constraint_sla_availability(constraints, num_disjoint_paths, all_active, 0.0)

    return target








        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:
+138 −25
Original line number Diff line number Diff line
@@ -13,44 +13,157 @@
# limitations under the License.

import logging
from typing import Dict
from typing import Dict, List
from flask import request
from flask.json import jsonify
from flask.wrappers import Response
from flask_restful import Resource
from werkzeug.exceptions import UnsupportedMediaType
from common.proto.context_pb2 import ServiceTypeEnum
from common.tools.context_queries.Service import get_services
from context.client.ContextClient import ContextClient
from nbi.service._tools.Authentication import HTTP_AUTH
from nbi.service._tools.HttpStatusCodes import HTTP_CREATED, HTTP_SERVERERROR
from nbi.service._tools.HttpStatusCodes import (
    HTTP_CREATED, HTTP_NOCONTENT, HTTP_SERVERERROR
)
from .Handlers import process_site_network_access
from .YangValidator import YangValidator

LOGGER = logging.getLogger(__name__)

def process_site_network_accesses(site_id : str) -> Response:
class L2VPN_SiteNetworkAccesses(Resource):
    @HTTP_AUTH.login_required
    def post(self, site_id : str):
        if not request.is_json: raise UnsupportedMediaType('JSON payload is required')
        request_data : Dict = request.json
        LOGGER.debug('Site_Id: {:s}'.format(str(site_id)))
        LOGGER.debug('Request: {:s}'.format(str(request_data)))

    yang_validator = YangValidator('ietf-l2vpn-svc')
    request_data = yang_validator.parse_to_dict(request_data)
    yang_validator.destroy()


    errors = []
    for network_access in request_data['site-network-accesses']['site-network-access']:
        exc = process_site_network_access(site_id, network_access)
        if exc is not None: errors.append({'error': str(exc)})

        errors = self._process_site_network_accesses(site_id, request_data)
        response = jsonify(errors)
        response.status_code = HTTP_CREATED if len(errors) == 0 else HTTP_SERVERERROR
        return response

class L2VPN_SiteNetworkAccesses(Resource):
    @HTTP_AUTH.login_required
    def post(self, site_id : str):
        return process_site_network_accesses(site_id)

    @HTTP_AUTH.login_required
    def put(self, site_id : str):
        return process_site_network_accesses(site_id)
        if not request.is_json: raise UnsupportedMediaType('JSON payload is required')
        request_data : Dict = request.json
        LOGGER.debug('Site_Id: {:s}'.format(str(site_id)))
        LOGGER.debug('Request: {:s}'.format(str(request_data)))
        errors = self._process_site_network_accesses(site_id, request_data)
        response = jsonify(errors)
        response.status_code = HTTP_NOCONTENT if len(errors) == 0 else HTTP_SERVERERROR
        return response

    def _prepare_request_payload(self, site_id : str, request_data : Dict, errors : List[Dict]) -> Dict:
        if 'ietf-l2vpn-svc:l2vpn-svc' in request_data:
            # processing single (standard) request formatted as:
            #{"ietf-l2vpn-svc:l2vpn-svc": {
            #  "sites": {"site": [
            #    {
            #      "site-id": ...,
            #      "site-network-accesses": {"site-network-access": [
            #        {
            #          "network-access-id": ...,
            #          ...
            #        }
            #      ]}
            #    }
            #  ]}
            #}}
            return request_data

        if 'ietf-l2vpn-svc:site-network-access' in request_data:
            # processing OSM-style payload request formatted as:
            #{
            #  "ietf-l2vpn-svc:site-network-access": [
            site_network_accesses = request_data['ietf-l2vpn-svc:site-network-access']

            location_refs = set()
            location_refs.add('fake-location')

            # Add mandatory fields OSM RO driver skips and fix wrong ones
            for site_network_access in site_network_accesses:
                if 'location-reference' in site_network_access:
                    location_refs.add(site_network_access['location-reference'])
                else:
                    site_network_access['location-reference'] = 'fake-location'

                if 'connection' in site_network_access:
                    connection = site_network_access['connection']
                    if 'encapsulation-type' in connection:
                        if connection['encapsulation-type'] == 'dot1q-vlan-tagged':
                            connection['encapsulation-type'] = 'vlan'
                        else:
                            connection['encapsulation-type'] = 'ethernet'
                    if 'tagged-interface' in connection:
                        tagged_interface = connection['tagged-interface']
                        if 'dot1q-vlan-tagged' in tagged_interface:
                            if 'type' not in tagged_interface:
                                tagged_interface['type'] = 'dot1q'

                    if 'oam' not in connection:
                        connection['oam'] = dict()
                    if 'md-name' not in connection['oam']:
                        connection['oam']['md-name'] = 'fake-md-name'
                    if 'md-level' not in connection['oam']:
                        connection['oam']['md-level'] = 0

                if 'service' not in site_network_access:
                    site_network_access['service'] = dict()
                if 'svc-mtu' not in site_network_access['service']:
                    site_network_access['service']['svc-mtu'] = 1500

            context_client = ContextClient()
            vpn_services = list()
            for service in get_services(context_client):
                if service.service_type != ServiceTypeEnum.SERVICETYPE_L2NM: continue

                vpn_ids = [service.service_id.service_uuid.uuid, service.name]
                for vpn_id in vpn_ids:
                    vpn_services.append({
                        'vpn-id': vpn_id,
                        'frame-delivery': {
                            'multicast-gp-port-mapping': 'ietf-l2vpn-svc:static-mapping'
                        },
                        'ce-vlan-preservation': True,
                        'ce-vlan-cos-preservation': True,
                    })

            request_data = {'ietf-l2vpn-svc:l2vpn-svc': {
                'vpn-services': {
                    'vpn-service': vpn_services
                },
                'sites': {'site': [{
                    'site-id': site_id,
                    'default-ce-vlan-id': 1,
                    'management': {'type': 'customer-managed'},
                    'locations': {'location': [
                        {'location-id': location_ref}
                        for location_ref in location_refs
                    ]},
                    'site-network-accesses': {
                        'site-network-access': site_network_accesses
                    }
                }]}
            }}
            return request_data

        errors.append('Unexpected request: {:s}'.format(str(request_data)))
        return None

    def _process_site_network_accesses(self, site_id : str, request_data : Dict) -> List[Dict]:
        errors = list()
        request_data = self._prepare_request_payload(site_id, request_data, errors)
        if len(errors) > 0: return errors

        yang_validator = YangValidator('ietf-l2vpn-svc')
        request_data = yang_validator.parse_to_dict(request_data)
        yang_validator.destroy()

        site_network_accesses = (
            request_data.get('site-network-accesses', dict())
            .get('site-network-access', list())
        )
        for site_network_access in site_network_accesses:
            process_site_network_access(site_id, site_network_access, errors)

        return errors