Loading src/nbi/service/tfs_api/Resources.py +80 −0 Viewed Changes for src/nbi/service/tfs_api/Resources.py: 80 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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() Loading @@ -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): Loading src/nbi/service/tfs_api/__init__.py +3 −1 Viewed Changes for src/nbi/service/tfs_api/__init__.py: 3 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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'), Loading src/nbi/tests/test_tfs_api_optical_allocation.py 0 → 100644 +62 −0 Viewed Changes for src/nbi/tests/test_tfs_api_optical_allocation.py: 62 added lines, 0 removed lines. 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' src/opticalcontroller/OpticalController.py +14 −1 Viewed Changes for src/opticalcontroller/OpticalController.py: 14 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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 Loading src/opticalcontroller/RSA.py +7 −6 Viewed Changes for src/opticalcontroller/RSA.py: 7 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -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: Loading Loading @@ -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 Loading
src/nbi/service/tfs_api/Resources.py +80 −0 Viewed Changes for src/nbi/service/tfs_api/Resources.py: 80 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -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() Loading @@ -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): Loading
src/nbi/service/tfs_api/__init__.py +3 −1 Viewed Changes for src/nbi/service/tfs_api/__init__.py: 3 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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'), Loading
src/nbi/tests/test_tfs_api_optical_allocation.py 0 → 100644 +62 −0 Viewed Changes for src/nbi/tests/test_tfs_api_optical_allocation.py: 62 added lines, 0 removed lines. 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'
src/opticalcontroller/OpticalController.py +14 −1 Viewed Changes for src/opticalcontroller/OpticalController.py: 14 added lines, 1 removed line. Original line number Diff line number Diff line Loading @@ -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 Loading
src/opticalcontroller/RSA.py +7 −6 Viewed Changes for src/opticalcontroller/RSA.py: 7 added lines, 6 removed lines. Original line number Diff line number Diff line Loading @@ -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: Loading Loading @@ -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