From a1a6f0278d787eb1f991cd0245cbb2568c95da28 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 18 Jun 2026 17:43:18 +0000 Subject: [PATCH 1/7] Optical Controller component: - Enhance RSA computation to support optical spectrum reservation for flex-grid lightpaths --- src/opticalcontroller/OpticalController.py | 13 ++++++++++++- src/opticalcontroller/RSA.py | 7 ++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/opticalcontroller/OpticalController.py b/src/opticalcontroller/OpticalController.py index fd6b7f5a1..3436104aa 100644 --- a/src/opticalcontroller/OpticalController.py +++ b/src/opticalcontroller/OpticalController.py @@ -70,7 +70,18 @@ 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: return 'No path found', 404 t1 = time.time() * 1000.0 diff --git a/src/opticalcontroller/RSA.py b/src/opticalcontroller/RSA.py index d9c73a1d3..ebedbbcf3 100644 --- a/src/opticalcontroller/RSA.py +++ b/src/opticalcontroller/RSA.py @@ -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,8 +934,9 @@ 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 + ) f0, band = frequency_converter(band_range, slots) if debug: print(f0, band) -- GitLab From af45ad8b40c4f16a22752efea3fcda7758bb2272 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 18 Jun 2026 17:43:54 +0000 Subject: [PATCH 2/7] Service component: - Implement optical spectrum reservation in add_lightpath - Add tests for the new functionality --- .../service/ServiceServiceServicerImpl.py | 21 ++++++++++++-- src/service/service/tools/OpticalTools.py | 18 +++++++++++- .../test_optical_spectrum_reservation.py | 29 +++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/service/service/ServiceServiceServicerImpl.py b/src/service/service/ServiceServiceServicerImpl.py index b7864f521..4a3217b2f 100644 --- a/src/service/service/ServiceServiceServicerImpl.py +++ b/src/service/service/ServiceServiceServicerImpl.py @@ -55,6 +55,21 @@ LOGGER = logging.getLogger(__name__) METRICS_POOL = MetricsPool('Service', 'RPC') +def _load_opticalcontroller_reply(reply_txt : str) -> Dict: + try: + reply_json = json.loads(reply_txt) + except json.JSONDecodeError as exc: + raise OperationFailedException( + 'optical-controller-reply', + extra_details='reply={:s}'.format(str(reply_txt)) + ) from exc + if not isinstance(reply_json, dict): + raise OperationFailedException( + 'optical-controller-reply', + extra_details='reply={:s}'.format(str(reply_txt)) + ) + return reply_json + class ServiceServiceServicerImpl(ServiceServiceServicer): def __init__(self, service_handler_factory : ServiceHandlerFactory) -> None: @@ -350,7 +365,9 @@ class ServiceServiceServicerImpl(ServiceServiceServicer): spectrum_reservation=spectrum_reservation ) elif oc_type == 2: - reply_txt = add_lightpath(src, dst, bitrate, bidir) + reply_txt = add_lightpath( + src, dst, bitrate, bidir, spectrum_reservation=spectrum_reservation + ) else: reply_txt = add_flex_lightpath( src, dst, bitrate, bidir, preferred, ob_band, dj_optical_band_id, @@ -358,7 +375,7 @@ class ServiceServiceServicerImpl(ServiceServiceServicer): ) if reply_txt is None: return service_with_uuids.service_id - reply_json = json.loads(reply_txt) + reply_json = _load_opticalcontroller_reply(reply_txt) LOGGER.debug('[optical] reply_json[{:s}]={:s}'.format(str(type(reply_json)), str(reply_json))) optical_band_txt = "" diff --git a/src/service/service/tools/OpticalTools.py b/src/service/service/tools/OpticalTools.py index e13615497..749a6959f 100644 --- a/src/service/service/tools/OpticalTools.py +++ b/src/service/service/tools/OpticalTools.py @@ -189,7 +189,7 @@ def add_alien_flex_lightpath(src, s_port, dst, d_port, band, ob_id, bidir=None) reply = r.text return reply -def add_lightpath(src, dst, bitrate, bidir) -> str: +def add_lightpath(src, dst, bitrate, bidir, spectrum_reservation: Optional[Dict] = None) -> str: if not TESTING: urlx = "" headers = {"Content-Type": "application/json"} @@ -197,6 +197,13 @@ def add_lightpath(src, dst, bitrate, bidir) -> str: if bidir is None: bidir = 1 urlx = "{:s}/AddLightpath/{:s}/{:s}/{:s}/{:s}".format(base_url, src, dst, str(bitrate), str(bidir)) + 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 @@ -541,6 +548,9 @@ def adapt_reply( op_mode = r["op-mode"] if "op-mode" in r else None frequency = r["freq"] if "freq" in r else None flow_id = r["flow_id"] if "flow_id" in r else None + slots = r["slots"] if "slots" in r else None + links = r["links"] if "links" in r else None + path = r["path"] if "path" in r else None r_type = r["band_type"] if "band_type" in r else None if r_type == "l_slots": band_type = "L_BAND" @@ -553,6 +563,12 @@ def adapt_reply( val = {"target-output-power": "1.0", "frequency": frequency, "operational-mode": op_mode, "band": band, "flow_id": flow_id, "ob_id": ob_id, "band_type": band_type, "bidir": bidir_f} else: val = {"target-output-power": "1.0", "frequency": frequency, "operational-mode": op_mode, "band": band, "flow_id": flow_id, "band_type": band_type, "bidir": bidir_f} + if slots is not None: + val["slots"] = slots + if links is not None: + val["links"] = links + if path is not None: + val["path"] = path custom_rule = ConfigRule_Custom(resource_key="/settings", resource_value=json.dumps(val)) rule = ConfigRule(action=ConfigActionEnum.CONFIGACTION_SET, custom=custom_rule) service.service_config.config_rules.add().CopyFrom(rule) diff --git a/src/service/tests/test_optical_spectrum_reservation.py b/src/service/tests/test_optical_spectrum_reservation.py index fa1074d3b..1698b6fb2 100644 --- a/src/service/tests/test_optical_spectrum_reservation.py +++ b/src/service/tests/test_optical_spectrum_reservation.py @@ -72,3 +72,32 @@ def test_add_flex_lightpath_forwards_reservation_query(monkeypatch): assert "reservation_band=c_slots" in captured["url"] assert "reservation_start=51" in captured["url"] assert "reservation_end=66" in captured["url"] + + +def test_add_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_lightpath( + "T1", + "T2", + 100, + 0, + spectrum_reservation={"band": "c_slots", "n_start": 51, "n_end": 66}, + ) + + assert captured["url"].startswith("http://optical/OpticalTFS/AddLightpath/T1/T2/100/0?") + assert "reservation_band=c_slots" in captured["url"] + assert "reservation_start=51" in captured["url"] + assert "reservation_end=66" in captured["url"] -- GitLab From 8bfd93ab4397538d1b71be75f4f1c005c4b23bbc Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 18 Jun 2026 17:44:24 +0000 Subject: [PATCH 3/7] NBI component - TFS API connector: - Implement Optical Service Allocation and related utility functions - Add tests for slot range and custom settings decoding --- src/nbi/service/tfs_api/Resources.py | 68 +++++++++++++++++++ src/nbi/service/tfs_api/__init__.py | 4 +- .../tests/test_tfs_api_optical_allocation.py | 40 +++++++++++ 3 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 src/nbi/tests/test_tfs_api_optical_allocation.py diff --git a/src/nbi/service/tfs_api/Resources.py b/src/nbi/service/tfs_api/Resources.py index 3e1832200..d56e17736 100644 --- a/src/nbi/service/tfs_api/Resources.py +++ b/src/nbi/service/tfs_api/Resources.py @@ -254,6 +254,74 @@ class Service(_Resource): def delete(self, context_uuid : str, service_uuid : str): return format_grpc_to_json(self.service_client.DeleteService(grpc_service_id(context_uuid, service_uuid))) +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) + service = grpc_message_to_json(self.context_client.GetService(service_id)) + connections = grpc_message_to_json(self.context_client.ListConnections(service_id)) + 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): return format_grpc_to_json(self.context_client.ListSliceIds(grpc_context_id(context_uuid))) diff --git a/src/nbi/service/tfs_api/__init__.py b/src/nbi/service/tfs_api/__init__.py index 2e666f252..9f0189817 100644 --- a/src/nbi/service/tfs_api/__init__.py +++ b/src/nbi/service/tfs_api/__init__.py @@ -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//service_ids'), ('api.services', Services, '/context//services'), ('api.service', Service, '/context//service/'), + ('api.optical_service_allocation', OpticalServiceAllocation, + '/context//service//optical_allocation'), ('api.slice_ids', SliceIds, '/context//slice_ids'), ('api.slices', Slices, '/context//slices'), diff --git a/src/nbi/tests/test_tfs_api_optical_allocation.py b/src/nbi/tests/test_tfs_api_optical_allocation.py new file mode 100644 index 000000000..7426310bd --- /dev/null +++ b/src/nbi/tests/test_tfs_api_optical_allocation.py @@ -0,0 +1,40 @@ +# 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 nbi.service.tfs_api.Resources import _service_custom_settings, _slot_range + + +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]} -- GitLab From 204384c4e63d82bbafa00af00896a75a11fbe00c Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 18 Jun 2026 17:46:23 +0000 Subject: [PATCH 4/7] Service component: - Refactor optical controller reply handling: move function to OpticalTools and update usage --- .../service/ServiceServiceServicerImpl.py | 19 ++----------------- src/service/service/tools/OpticalTools.py | 17 ++++++++++++++++- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/service/service/ServiceServiceServicerImpl.py b/src/service/service/ServiceServiceServicerImpl.py index 4a3217b2f..688dcfe9a 100644 --- a/src/service/service/ServiceServiceServicerImpl.py +++ b/src/service/service/ServiceServiceServicerImpl.py @@ -45,7 +45,7 @@ from .tools.GeodesicDistance import gps_distance from .tools.OpticalTools import ( add_flex_lightpath, add_lightpath, delete_lightpath, adapt_reply, get_device_name_from_uuid, get_optical_band, refresh_opticalcontroller, DelFlexLightpath , extend_optical_band, - reconfig_flex_lightpath, adapt_reply_ob, add_alien_flex_lightpath + reconfig_flex_lightpath, adapt_reply_ob, add_alien_flex_lightpath, load_opticalcontroller_reply ) from .tools.OpticalSpectrumReservation import parse_optical_spectrum_reservation_constraints @@ -55,21 +55,6 @@ LOGGER = logging.getLogger(__name__) METRICS_POOL = MetricsPool('Service', 'RPC') -def _load_opticalcontroller_reply(reply_txt : str) -> Dict: - try: - reply_json = json.loads(reply_txt) - except json.JSONDecodeError as exc: - raise OperationFailedException( - 'optical-controller-reply', - extra_details='reply={:s}'.format(str(reply_txt)) - ) from exc - if not isinstance(reply_json, dict): - raise OperationFailedException( - 'optical-controller-reply', - extra_details='reply={:s}'.format(str(reply_txt)) - ) - return reply_json - class ServiceServiceServicerImpl(ServiceServiceServicer): def __init__(self, service_handler_factory : ServiceHandlerFactory) -> None: @@ -375,7 +360,7 @@ class ServiceServiceServicerImpl(ServiceServiceServicer): ) if reply_txt is None: return service_with_uuids.service_id - reply_json = _load_opticalcontroller_reply(reply_txt) + reply_json = load_opticalcontroller_reply(reply_txt) LOGGER.debug('[optical] reply_json[{:s}]={:s}'.format(str(type(reply_json)), str(reply_json))) optical_band_txt = "" diff --git a/src/service/service/tools/OpticalTools.py b/src/service/service/tools/OpticalTools.py index 749a6959f..07da7c037 100644 --- a/src/service/service/tools/OpticalTools.py +++ b/src/service/service/tools/OpticalTools.py @@ -16,7 +16,7 @@ import functools, json, logging, requests, uuid from typing import Dict, List, Optional, Tuple from urllib.parse import urlencode -from common.method_wrappers.ServiceExceptions import NotFoundException +from common.method_wrappers.ServiceExceptions import NotFoundException, OperationFailedException from common.proto.context_pb2 import( ConfigActionEnum, ConfigRule, ConfigRule_Custom, Connection, ContextId, Device, DeviceId, Empty, EndPointId, OpticalBand, OpticalBandId, OpticalBandList, @@ -42,6 +42,21 @@ LOGGER = logging.getLogger(__name__) TESTING = False +def load_opticalcontroller_reply(reply_txt : str) -> Dict: + try: + reply_json = json.loads(reply_txt) + except json.JSONDecodeError as exc: + raise OperationFailedException( + 'optical-controller-reply', + extra_details='reply={:s}'.format(str(reply_txt)) + ) from exc + if not isinstance(reply_json, dict): + raise OperationFailedException( + 'optical-controller-reply', + extra_details='reply={:s}'.format(str(reply_txt)) + ) + return reply_json + get_optical_controller_setting = functools.partial(get_env_var_name, ServiceNameEnum.OPTICALCONTROLLER) VAR_NAME_OPTICAL_CTRL_BASEURL_HTTP = get_optical_controller_setting(ENVVAR_SUFIX_SERVICE_BASEURL_HTTP) VAR_NAME_OPTICAL_CTRL_SCHEMA = get_optical_controller_setting('SCHEMA') -- GitLab From b1326b642a359dd32f648cd79e7f90ebc037c4dd Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 18 Jun 2026 18:35:02 +0000 Subject: [PATCH 5/7] Optical Controller component: - Add error handling for unavailable optical spectrum in lightpath reservation --- src/opticalcontroller/OpticalController.py | 2 ++ src/opticalcontroller/RSA.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/opticalcontroller/OpticalController.py b/src/opticalcontroller/OpticalController.py index 3436104aa..9171d3703 100644 --- a/src/opticalcontroller/OpticalController.py +++ b/src/opticalcontroller/OpticalController.py @@ -83,6 +83,8 @@ class AddLightpath(Resource): 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 diff --git a/src/opticalcontroller/RSA.py b/src/opticalcontroller/RSA.py index ebedbbcf3..d5b84f5da 100644 --- a/src/opticalcontroller/RSA.py +++ b/src/opticalcontroller/RSA.py @@ -937,13 +937,13 @@ class RSA(): 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)) -- GitLab From 7a9de4ed18d46912c56130176b306129a8bded18 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 18 Jun 2026 18:36:01 +0000 Subject: [PATCH 6/7] Service component: - Add error handling for optical spectrum reservation conflicts - Updated existing functions to utilize the new error handling for lightpath additions. - Added a test case to verify that a reservation conflict raises the appropriate exception. --- src/service/service/tools/OpticalTools.py | 22 +++++++++++++++- .../test_optical_spectrum_reservation.py | 25 +++++++++++++++++++ 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/src/service/service/tools/OpticalTools.py b/src/service/service/tools/OpticalTools.py index 07da7c037..fc1df76a5 100644 --- a/src/service/service/tools/OpticalTools.py +++ b/src/service/service/tools/OpticalTools.py @@ -16,7 +16,7 @@ import functools, json, logging, requests, uuid from typing import Dict, List, Optional, Tuple from urllib.parse import urlencode -from common.method_wrappers.ServiceExceptions import NotFoundException, OperationFailedException +from common.method_wrappers.ServiceExceptions import AlreadyExistsException, NotFoundException, OperationFailedException from common.proto.context_pb2 import( ConfigActionEnum, ConfigRule, ConfigRule_Custom, Connection, ContextId, Device, DeviceId, Empty, EndPointId, OpticalBand, OpticalBandId, OpticalBandList, @@ -88,6 +88,20 @@ def get_optical_controller_base_url() -> str: return base_url +def _raise_for_opticalcontroller_error(response, operation: str, spectrum_reservation: Optional[Dict] = None) -> None: + status_code = getattr(response, 'status_code', 200) + response_text = getattr(response, 'text', '') + if status_code == 409: + raise AlreadyExistsException( + 'OpticalSpectrumReservation', str(spectrum_reservation), extra_details=response_text + ) + if status_code >= 400: + raise OperationFailedException( + operation, + extra_details='HTTP {:d}: {:s}'.format(status_code, response_text) + ) + + def get_uuids_from_names( devices : List[Device], device_name : str, port_name : str ) -> Tuple[str, str]: @@ -183,6 +197,9 @@ def add_flex_lightpath( urlx = '{:s}?{:s}'.format(urlx, urlencode(query)) r = requests.put(urlx, headers=headers) LOGGER.debug(f"addpathlight {r}") + _raise_for_opticalcontroller_error( + r, 'optical-controller-add-flex-lightpath', spectrum_reservation=spectrum_reservation + ) reply = r.text return reply else: @@ -221,6 +238,9 @@ def add_lightpath(src, dst, bitrate, bidir, spectrum_reservation: Optional[Dict] urlx = '{:s}?{:s}'.format(urlx, urlencode(query)) r = requests.put(urlx, headers=headers) LOGGER.debug(f"addpathlight {r}") + _raise_for_opticalcontroller_error( + r, 'optical-controller-add-lightpath', spectrum_reservation=spectrum_reservation + ) reply = r.text return reply else: diff --git a/src/service/tests/test_optical_spectrum_reservation.py b/src/service/tests/test_optical_spectrum_reservation.py index 1698b6fb2..83880f2bc 100644 --- a/src/service/tests/test_optical_spectrum_reservation.py +++ b/src/service/tests/test_optical_spectrum_reservation.py @@ -12,6 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. +import pytest + +from common.method_wrappers.ServiceExceptions import AlreadyExistsException 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 @@ -101,3 +104,25 @@ def test_add_lightpath_forwards_reservation_query(monkeypatch): assert "reservation_band=c_slots" in captured["url"] assert "reservation_start=51" in captured["url"] assert "reservation_end=66" in captured["url"] + + +def test_add_lightpath_maps_reservation_conflict_to_already_exists(monkeypatch): + class Response: + status_code = 409 + text = "Requested optical spectrum is unavailable" + + def put(url, headers=None): + 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) + + with pytest.raises(AlreadyExistsException): + OpticalTools.add_lightpath( + "T1", + "T2", + 100, + 0, + spectrum_reservation={"band": "c_slots", "n_start": 51, "n_end": 66}, + ) -- GitLab From a1256eeb812bd0ba1f235557de5e792a2ab263d7 Mon Sep 17 00:00:00 2001 From: gifrerenom Date: Thu, 18 Jun 2026 18:36:56 +0000 Subject: [PATCH 7/7] NBI component - TFS-API connector: - Add error handling for gRPC calls in Services and OpticalServiceAllocation classes --- src/nbi/service/tfs_api/Resources.py | 22 +++++++++++++---- .../tests/test_tfs_api_optical_allocation.py | 24 ++++++++++++++++++- 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/nbi/service/tfs_api/Resources.py b/src/nbi/service/tfs_api/Resources.py index d56e17736..52f6867bb 100644 --- a/src/nbi/service/tfs_api/Resources.py +++ b/src/nbi/service/tfs_api/Resources.py @@ -240,7 +240,10 @@ class Services(_Resource): class Service(_Resource): def get(self, context_uuid : str, service_uuid : str): - return format_grpc_to_json(self.context_client.GetService(grpc_service_id(context_uuid, service_uuid))) + 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,16 @@ class Service(_Resource): if service_uuid != service['service_id']['service_uuid']['uuid']: raise BadRequest('Mismatching service_uuid') svc = format_service_custom_config_rules(service) - return format_grpc_to_json(self.service_client.UpdateService(grpc_service(svc))) + 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): - return format_grpc_to_json(self.service_client.DeleteService(grpc_service_id(context_uuid, service_uuid))) + 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): @@ -288,8 +297,11 @@ def _slot_range(slots): class OpticalServiceAllocation(_Resource): def get(self, context_uuid : str, service_uuid : str): service_id = grpc_service_id(context_uuid, service_uuid) - service = grpc_message_to_json(self.context_client.GetService(service_id)) - connections = grpc_message_to_json(self.context_client.ListConnections(service_id)) + 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): diff --git a/src/nbi/tests/test_tfs_api_optical_allocation.py b/src/nbi/tests/test_tfs_api_optical_allocation.py index 7426310bd..151574277 100644 --- a/src/nbi/tests/test_tfs_api_optical_allocation.py +++ b/src/nbi/tests/test_tfs_api_optical_allocation.py @@ -12,7 +12,22 @@ # See the License for the specific language governing permissions and # limitations under the License. -from nbi.service.tfs_api.Resources import _service_custom_settings, _slot_range +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(): @@ -38,3 +53,10 @@ def test_service_custom_settings_decodes_json_values(): 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' -- GitLab