Commit 1f0b8016 authored by Carlos Manso's avatar Carlos Manso
Browse files

ETSI MEC BW Management API initial support

parent b6bfee3d
Loading
Loading
Loading
Loading
+3 −0
Original line number Diff line number Diff line
@@ -23,6 +23,8 @@ from .rest_server.RestServer import RestServer
from .rest_server.nbi_plugins.debug_api import register_debug_api
from .rest_server.nbi_plugins.ietf_l2vpn import register_ietf_l2vpn
from .rest_server.nbi_plugins.ietf_network_slice import register_ietf_nss
from .rest_server.nbi_plugins.etsi_mec_015 import register_mec_015_api


terminate = threading.Event()
LOGGER = None
@@ -62,6 +64,7 @@ def main():
    register_ietf_l2vpn(rest_server)  # Registering L2VPN entrypoint
    register_ietf_nss(rest_server)  # Registering NSS entrypoint
    register_debug_api(rest_server)
    register_mec_015_api(rest_server)
    rest_server.start()

    # Wait for Ctrl+C or termination signal
+74 −0
Original line number Diff line number Diff line
# 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, request
from context.client.ContextClient import ContextClient
from service.client.ServiceClient import ServiceClient
from .Tools import (
    format_grpc_to_json, grpc_context_id, grpc_service_id, bwInfo_2_service, service_2_bwInfo)
import copy
from common.Constants import DEFAULT_CONTEXT_NAME


class _Resource(Resource):
    def __init__(self) -> None:
        super().__init__()
        self.client = ContextClient()
        self.service_client = ServiceClient()


class BwInfo(_Resource):
    def get(self):
        service_list = self.client.ListServices(grpc_context_id(DEFAULT_CONTEXT_NAME))
        bw_allocations = [service_2_bwInfo(service) for service in service_list.services]
        return bw_allocations

    def post(self):
        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')

        response = format_grpc_to_json(self.service_client.CreateService(stripped_service))
        response = format_grpc_to_json(self.service_client.UpdateService(service))

        return response


class BwInfoId(_Resource):

    def get(self, allocationId: str):
        service = self.client.GetService(grpc_service_id(DEFAULT_CONTEXT_NAME, allocationId))
        return service_2_bwInfo(service)

    def put(self, allocationId: str):
        json_data = request.get_json()
        service = bwInfo_2_service(self.client, json_data)
        response = self.service_client.UpdateService(service)
        return format_grpc_to_json(response)

    def patch(self, allocationId: str):
        json_data = request.get_json()
        if not 'appInsId' in json_data:
            json_data['appInsId'] = allocationId
        service = bwInfo_2_service(self.client, json_data)
        response = self.service_client.UpdateService(service)
        return format_grpc_to_json(response)

    def delete(self, allocationId: str):
        self.service_client.DeleteService(grpc_service_id(DEFAULT_CONTEXT_NAME, allocationId))
        return
+114 −0
Original line number Diff line number Diff line
# 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.json import jsonify
from common.proto.context_pb2 import 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.object_factory.Context import json_context_id
from common.tools.object_factory.Service import json_service_id
import time
import json
import logging

LOGGER = logging.getLogger(__name__)


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.context_id.context_uuid.uuid # String: Application instance identifier
    for constraint in service.service_constraints:
        if constraint.WhichOneof('constraint') == 'sla_capacity':
            response['fixedAllocation'] = str(constraint.sla_capacity.capacity_gbps*1000) # String: Size of requested fixed BW allocation in [bps]
            break


    for config_rule in service.service_config.config_rules:
        for key in ['allocationDirection', 'fixedBWPriority', 'requestType', 'sourceIp', 'sourcePort', 'dstPort', 'protocol', 'sessionFilter']:
            if config_rule.custom.resource_key == key:
                if key != 'sessionFilter':
                    response[key] = config_rule.custom.resource_value
                else:
                    response[key] = json.loads(config_rule.custom.resource_value)

    
    unixtime = time.time()
    response['timeStamp'] = { # Time stamp to indicate when the corresponding information elements are sent
        "seconds": int(unixtime),
        "nanoseconds": int(unixtime%1*1e9)
    }

    return response

def bwInfo_2_service(client, bwInfo: dict) -> Service:
    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']

        devices = client.ListDevices(Empty()).devices
        for device in devices:
            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 len(service.service_endpoint_ids) < 2:
            LOGGER.error('No endpoints matched')
            return None

    service.service_type = ServiceTypeEnum.SERVICETYPE_L3NM

    if 'appInsId' in bwInfo:
        service.service_id.service_uuid.uuid = bwInfo['appInsId']
        service.service_id.context_id.context_uuid.uuid = 'admin'
        service.name = bwInfo['appInsId']

    if 'fixedAllocation' in bwInfo:
        capacity = Constraint_SLA_Capacity()
        capacity.capacity_gbps = float(bwInfo['fixedAllocation'])
        constraint = Constraint()
        constraint.sla_capacity.CopyFrom(capacity)
        service.service_constraints.append(constraint)

    return service


def format_grpc_to_json(grpc_reply):
    return jsonify(grpc_message_to_json(grpc_reply))

def grpc_context_id(context_uuid):
    return ContextId(**json_context_id(context_uuid))

def grpc_service_id(context_uuid, service_uuid):
    return ServiceId(**json_service_id(service_uuid, context_id=json_context_id(context_uuid)))
+29 −0
Original line number Diff line number Diff line
# 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 compute.service.rest_server.RestServer import RestServer
from .Resources import (BwInfo, BwInfoId)

URL_PREFIX = '/bwm/v1'

# Use 'path' type since some identifiers might contain char '/' and Flask is unable to recognize them in 'string' type.
RESOURCES = [
    # (endpoint_name, resource_class, resource_url)
    ('api.bw_info',         BwInfo,     '/bw_allocations'),
    ('api.bw_info_id',      BwInfoId,   '/bw_allocations/<path:allocationId>'),
]

def register_mec_015_api(rest_server : RestServer):
    for endpoint_name, resource_class, resource_url in RESOURCES:
        rest_server.add_resource(resource_class, URL_PREFIX + resource_url, endpoint=endpoint_name)