Commit 8586676f authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Merge branch...

Merge branch 'feat/411-cttc-consume-optical-spectrum-pre-reservation-in-flex-grid-optical-connectivity-services' into 'develop'

Resolve "(CTTC) Consume optical spectrum pre-reservation in flex-grid optical connectivity services"

See merge request !475
parents 72ecbf07 a1256eeb
Loading
Loading
Loading
Loading
+83 −3
Original line number Diff line number Diff line
@@ -240,7 +240,10 @@ class Services(_Resource):

class Service(_Resource):
    def get(self, context_uuid : str, service_uuid : str):
        try:
            return format_grpc_to_json(self.context_client.GetService(grpc_service_id(context_uuid, service_uuid)))
        except grpc.RpcError as exc:
            return _format_grpc_error(exc)

    def put(self, context_uuid : str, service_uuid : str):
        service = request.get_json()
@@ -249,10 +252,87 @@ class Service(_Resource):
        if service_uuid != service['service_id']['service_uuid']['uuid']:
            raise BadRequest('Mismatching service_uuid')
        svc = format_service_custom_config_rules(service)
        try:
            return format_grpc_to_json(self.service_client.UpdateService(grpc_service(svc)))
        except grpc.RpcError as exc:
            return _format_grpc_error(exc)

    def delete(self, context_uuid : str, service_uuid : str):
        try:
            return format_grpc_to_json(self.service_client.DeleteService(grpc_service_id(context_uuid, service_uuid)))
        except grpc.RpcError as exc:
            return _format_grpc_error(exc)

def _decode_custom_value(value):
    if not isinstance(value, str):
        return value
    try:
        return json.loads(value)
    except json.JSONDecodeError:
        return value

def _service_custom_settings(service):
    settings = {}
    for config_rule in service.get('service_config', {}).get('config_rules', []):
        custom = config_rule.get('custom')
        if custom is None:
            continue
        resource_key = custom.get('resource_key')
        if resource_key is None:
            continue
        settings[resource_key] = _decode_custom_value(custom.get('resource_value'))
    return settings

def _slot_range(slots):
    if not isinstance(slots, list) or len(slots) == 0:
        return None
    slot_values = sorted([int(slot) for slot in slots])
    if slot_values == list(range(slot_values[0], slot_values[-1] + 1)):
        return {
            'n_start': slot_values[0],
            'n_end': slot_values[-1],
        }
    return None

class OpticalServiceAllocation(_Resource):
    def get(self, context_uuid : str, service_uuid : str):
        service_id = grpc_service_id(context_uuid, service_uuid)
        try:
            service = grpc_message_to_json(self.context_client.GetService(service_id))
            connections = grpc_message_to_json(self.context_client.ListConnections(service_id))
        except grpc.RpcError as exc:
            return _format_grpc_error(exc)
        settings = _service_custom_settings(service)
        allocation_settings = settings.get('/settings')
        if not isinstance(allocation_settings, dict):
            return {
                'error': 'OPTICAL_ALLOCATION_NOT_FOUND',
                'message': 'Service does not expose optical allocation settings',
            }, 404
        if 'flow_id' not in allocation_settings:
            return {
                'error': 'OPTICAL_ALLOCATION_NOT_FOUND',
                'message': 'Service settings do not include an optical flow identifier',
            }, 404

        slots = allocation_settings.get('slots')
        allocation = {
            'service_id': service.get('service_id'),
            'service_status': service.get('service_status'),
            'service_type': service.get('service_type'),
            'flow_id': allocation_settings.get('flow_id'),
            'optical_band_id': allocation_settings.get('ob_id'),
            'band_type': allocation_settings.get('band_type'),
            'frequency': allocation_settings.get('frequency'),
            'bandwidth': allocation_settings.get('band'),
            'slots': slots,
            'slot_range': _slot_range(slots),
            'path': allocation_settings.get('path'),
            'links': allocation_settings.get('links'),
            'bidirectional': bool(allocation_settings.get('bidir', 0)),
            'connections': connections.get('connections', []),
        }
        return jsonify(allocation)

class SliceIds(_Resource):
    def get(self, context_uuid : str):
+3 −1
Original line number Diff line number Diff line
@@ -24,7 +24,7 @@ from .Resources import (
    OpticalSpectrumReservationRelease, OpticalSpectrumReservations,
    OpticalConnectivityCandidates,
    PolicyRule, PolicyRuleIds, PolicyRules,
    Service, ServiceIds, Services,
    Service, ServiceIds, Services, OpticalServiceAllocation,
    Slice, SliceIds, Slices,
    Topologies, Topology, TopologyDetails, TopologyIds,
    E2epathcomp
@@ -51,6 +51,8 @@ _RESOURCES = [
    ('api.service_ids',      ServiceIds,      '/context/<path:context_uuid>/service_ids'),
    ('api.services',         Services,        '/context/<path:context_uuid>/services'),
    ('api.service',          Service,         '/context/<path:context_uuid>/service/<path:service_uuid>'),
    ('api.optical_service_allocation', OpticalServiceAllocation,
     '/context/<path:context_uuid>/service/<path:service_uuid>/optical_allocation'),

    ('api.slice_ids',        SliceIds,        '/context/<path:context_uuid>/slice_ids'),
    ('api.slices',           Slices,          '/context/<path:context_uuid>/slices'),
+62 −0
Original line number Diff line number Diff line
# Copyright 2022-2026 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 grpc

from nbi.service.tfs_api.Resources import _format_grpc_error, _service_custom_settings, _slot_range


class _RpcError(grpc.RpcError):
    def __init__(self, code, details):
        super().__init__()
        self._code = code
        self._details = details

    def code(self):
        return self._code

    def details(self):
        return self._details


def test_slot_range_compacts_contiguous_slots():
    assert _slot_range([53, 51, 52]) == {'n_start': 51, 'n_end': 53}


def test_slot_range_returns_none_for_sparse_slots():
    assert _slot_range([51, 53]) is None


def test_service_custom_settings_decodes_json_values():
    service = {
        'service_config': {
            'config_rules': [{
                'custom': {
                    'resource_key': '/settings',
                    'resource_value': '{"flow_id": 1, "slots": [51, 52]}',
                },
            }],
        },
    }

    settings = _service_custom_settings(service)

    assert settings['/settings'] == {'flow_id': 1, 'slots': [51, 52]}


def test_grpc_not_found_maps_to_http_404():
    body, status = _format_grpc_error(_RpcError(grpc.StatusCode.NOT_FOUND, 'service not found'))

    assert status == 404
    assert body['error'] == 'NOT_FOUND'
+14 −1
Original line number Diff line number Diff line
@@ -70,8 +70,21 @@ class AddLightpath(Resource):
            rsa.g.printGraph()

        if rsa is not None:
            flow_id = rsa.rsa_computation(src, dst, bitrate, bidir)
            reservation = request.args.get("reservation")
            if reservation is None and request.args.get("reservation_band") is not None:
                reservation = {
                    "band": request.args.get("reservation_band"),
                    "n_start": request.args.get("reservation_start"),
                    "n_end": request.args.get("reservation_end"),
                }
            try:
                flow_id = rsa.rsa_computation(src, dst, bitrate, bidir, reservation=reservation)
            except ValueError as exc:
                LOGGER.warning("Invalid optical spectrum reservation: %s", str(exc))
                return str(exc), 400
            if rsa.db_flows[flow_id]["op-mode"] == 0:
                if reservation is not None:
                    return 'Requested optical spectrum is unavailable', 409
                return 'No path found', 404
            t1 = time.time() * 1000.0
            elapsed = t1 - t0
+7 −6
Original line number Diff line number Diff line
@@ -904,7 +904,7 @@ class RSA():

        return t_flows, band, slots, {}, {}

    def rsa_computation(self, src, dst, rate, bidir):
    def rsa_computation(self, src, dst, rate, bidir, reservation=None):
        if self.flow_id == 0:
            self.flow_id += 1
        else:
@@ -934,15 +934,16 @@ class RSA():
            print(s_slots)
        #@Chafy
        if len(c_slots) > 0 or len(l_slots) > 0 or len(s_slots) > 0:
            flow_list, band_range, slots, fiber_f, fiber_b = self.select_slots_and_ports(links, num_slots, c_slots,
                                                                                         l_slots, s_slots, bidir)
            flow_list, band_range, slots, fiber_f, fiber_b = self.select_slots_and_ports(
                links, num_slots, c_slots, l_slots, s_slots, bidir, reservation=reservation
            )
            if flow_list is None:
                self.null_values(self.flow_id)
                return self.flow_id
            f0, band = frequency_converter(band_range, slots)
            if debug:
                print(f0, band)
            print("INFO: RSA completed for normal wavelenght connection")
            if flow_list is None:
                self.null_values(self.flow_id)
                return self.flow_id
            slots_i = []
            for i in slots:
                slots_i.append(int(i))
Loading