Commit 8ad856a8 authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Service component:

- Added optional spectrum reservation
parent e2f44851
Loading
Loading
Loading
Loading
+15 −2
Original line number Diff line number Diff line
@@ -47,6 +47,7 @@ from .tools.OpticalTools import (
    get_optical_band, refresh_opticalcontroller, DelFlexLightpath , extend_optical_band,
    reconfig_flex_lightpath, adapt_reply_ob, add_alien_flex_lightpath    
)
from .tools.OpticalSpectrumReservation import parse_optical_spectrum_reservation_constraints



@@ -54,6 +55,7 @@ LOGGER = logging.getLogger(__name__)

METRICS_POOL = MetricsPool('Service', 'RPC')


class ServiceServiceServicerImpl(ServiceServiceServicer):
    def __init__(self, service_handler_factory : ServiceHandlerFactory) -> None:
        LOGGER.debug('Creating Servicer...')
@@ -296,11 +298,16 @@ class ServiceServiceServicerImpl(ServiceServiceServicer):
            alien = 0
            alien_band = 0
            alien_optical_band_id = 0
            spectrum_reservation = parse_optical_spectrum_reservation_constraints(service.service_constraints)
            for constraint in service.service_constraints:
                if constraint.WhichOneof('constraint') != 'custom':
                    continue
                if "alien" in constraint.custom.constraint_type:
                    alien = 1
                    break
            for constraint in service.service_constraints:
                if constraint.WhichOneof('constraint') != 'custom':
                    continue
                if alien == 1:
                    if "alien_spectrum" in constraint.custom.constraint_type:
                        alien_band = int(constraint.custom.constraint_value)
@@ -338,11 +345,17 @@ class ServiceServiceServicerImpl(ServiceServiceServicer):
                reply_txt = add_alien_flex_lightpath(src, ports[0], dst, ports[1], alien_band, alien_optical_band_id, bidir)
            else:
                if oc_type == 1:
                    reply_txt = add_flex_lightpath(src, dst, bitrate, bidir, preferred, ob_band, dj_optical_band_id)
                    reply_txt = add_flex_lightpath(
                        src, dst, bitrate, bidir, preferred, ob_band, dj_optical_band_id,
                        spectrum_reservation=spectrum_reservation
                    )
                elif oc_type == 2:
                    reply_txt = add_lightpath(src, dst, bitrate, bidir)
                else:
                    reply_txt = add_flex_lightpath(src, dst, bitrate, bidir, preferred, ob_band, dj_optical_band_id)
                    reply_txt = add_flex_lightpath(
                        src, dst, bitrate, bidir, preferred, ob_band, dj_optical_band_id,
                        spectrum_reservation=spectrum_reservation
                    )
            if reply_txt is None:
                return service_with_uuids.service_id
            reply_json = json.loads(reply_txt)
+75 −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.

from typing import Dict, Optional

from common.method_wrappers.ServiceExceptions import InvalidArgumentException


def normalize_optical_spectrum_band(band: str) -> str:
    normalized_band = str(band).strip().lower().replace("-", "_")
    if normalized_band in {"c", "c_band", "c_slots"}:
        return "c_slots"
    if normalized_band in {"l", "l_band", "l_slots"}:
        return "l_slots"
    if normalized_band in {"s", "s_band", "s_slots"}:
        return "s_slots"
    raise InvalidArgumentException('optical spectrum band', str(band))


def parse_optical_spectrum_reservation_text(reservation_text: str) -> Dict:
    if ":" not in reservation_text or "-" not in reservation_text:
        raise InvalidArgumentException(
            'optical-spectrum-reservation', reservation_text,
            extra_details="Expected '<band>:<start>-<end>'"
        )
    band, slot_range = reservation_text.split(":", maxsplit=1)
    n_start, n_end = slot_range.split("-", maxsplit=1)
    reservation = {
        "band": normalize_optical_spectrum_band(band),
        "n_start": int(n_start),
        "n_end": int(n_end),
    }
    if reservation["n_start"] < 0 or reservation["n_end"] < reservation["n_start"]:
        raise InvalidArgumentException('optical-spectrum-reservation', reservation_text)
    return reservation


def parse_optical_spectrum_reservation_constraints(service_constraints) -> Optional[Dict]:
    reservation = {}
    for constraint in service_constraints:
        if constraint.WhichOneof('constraint') != 'custom':
            continue
        constraint_type = constraint.custom.constraint_type
        constraint_value = constraint.custom.constraint_value
        if constraint_type == 'optical-spectrum-reservation':
            return parse_optical_spectrum_reservation_text(constraint_value)
        if constraint_type == 'optical-spectrum-band':
            reservation['band'] = normalize_optical_spectrum_band(constraint_value)
        elif constraint_type == 'optical-spectrum-n-start':
            reservation['n_start'] = int(constraint_value)
        elif constraint_type == 'optical-spectrum-n-end':
            reservation['n_end'] = int(constraint_value)

    if len(reservation) == 0:
        return None
    missing_fields = {'band', 'n_start', 'n_end'} - set(reservation.keys())
    if len(missing_fields) > 0:
        raise InvalidArgumentException(
            'optical spectrum reservation', str(reservation),
            extra_details='Missing fields: {:s}'.format(', '.join(sorted(missing_fields)))
        )
    if reservation["n_start"] < 0 or reservation["n_end"] < reservation["n_start"]:
        raise InvalidArgumentException('optical spectrum reservation', str(reservation))
    return reservation
+12 −2
Original line number Diff line number Diff line
@@ -14,7 +14,8 @@
# 

import functools, json, logging, requests, uuid
from typing import Dict, List, Tuple
from typing import Dict, List, Optional, Tuple
from urllib.parse import urlencode
from common.method_wrappers.ServiceExceptions import NotFoundException
from common.proto.context_pb2 import(
    ConfigActionEnum, ConfigRule, ConfigRule_Custom, Connection, ContextId,
@@ -136,7 +137,9 @@ def reconfig_flex_lightpath(flow_id) -> str:
        return reply_bid_txt


def add_flex_lightpath(src, dst, bitrate, bidir, pref, ob_band, dj_optical_band_id) -> str:
def add_flex_lightpath(
    src, dst, bitrate, bidir, pref, ob_band, dj_optical_band_id, spectrum_reservation: Optional[Dict] = None
) -> str:
    if not TESTING:
        urlx = ""
        headers = {"Content-Type": "application/json"}
@@ -156,6 +159,13 @@ def add_flex_lightpath(src, dst, bitrate, bidir, pref, ob_band, dj_optical_band_
                urlx = "{:s}/AddFlexLightpath/{:s}/{:s}/{:s}/{:s}/{:s}/{:s}".format(base_url, src, dst, str(bitrate), str(prefs), str(bidir), str(ob_band))
            else:
                urlx = "{:s}/AddFlexLightpath/{:s}/{:s}/{:s}/{:s}/{:s}/{:s}/{:s}".format(base_url, src, dst, str(bitrate), str(prefs), str(bidir), str(ob_band), str(dj_optical_band_id))                
        if spectrum_reservation is not None:
            query = {
                'reservation_band': spectrum_reservation['band'],
                'reservation_start': str(spectrum_reservation['n_start']),
                'reservation_end': str(spectrum_reservation['n_end']),
            }
            urlx = '{:s}?{:s}'.format(urlx, urlencode(query))
        r = requests.put(urlx, headers=headers)
        LOGGER.debug(f"addpathlight {r}")
        reply = r.text 
+74 −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.

from common.proto.context_pb2 import Constraint, Constraint_Custom
from service.service.tools.OpticalSpectrumReservation import parse_optical_spectrum_reservation_constraints
from service.service.tools import OpticalTools


def test_parse_compact_service_reservation_constraint():
    constraints = [
        Constraint(custom=Constraint_Custom(
            constraint_type="optical-spectrum-reservation",
            constraint_value="c_slots:51-66",
        ))
    ]

    reservation = parse_optical_spectrum_reservation_constraints(constraints)

    assert reservation == {"band": "c_slots", "n_start": 51, "n_end": 66}


def test_parse_split_service_reservation_constraints():
    constraints = [
        Constraint(custom=Constraint_Custom(constraint_type="optical-spectrum-band", constraint_value="L_BAND")),
        Constraint(custom=Constraint_Custom(constraint_type="optical-spectrum-n-start", constraint_value="8")),
        Constraint(custom=Constraint_Custom(constraint_type="optical-spectrum-n-end", constraint_value="23")),
    ]

    reservation = parse_optical_spectrum_reservation_constraints(constraints)

    assert reservation == {"band": "l_slots", "n_start": 8, "n_end": 23}


def test_add_flex_lightpath_forwards_reservation_query(monkeypatch):
    captured = {}

    class Response:
        text = "{}"

    def put(url, headers=None):
        captured["url"] = url
        captured["headers"] = headers
        return Response()

    monkeypatch.setattr(OpticalTools, "get_optical_controller_base_url", lambda: "http://optical/OpticalTFS")
    monkeypatch.setattr(OpticalTools.requests, "put", put)
    monkeypatch.setattr(OpticalTools, "TESTING", False)

    OpticalTools.add_flex_lightpath(
        "T1",
        "T2",
        100,
        0,
        "ANY",
        None,
        None,
        spectrum_reservation={"band": "c_slots", "n_start": 51, "n_end": 66},
    )

    assert captured["url"].startswith("http://optical/OpticalTFS/AddFlexLightpath/T1/T2/100/ANY/0?")
    assert "reservation_band=c_slots" in captured["url"]
    assert "reservation_start=51" in captured["url"]
    assert "reservation_end=66" in captured["url"]