Loading src/service/service/ServiceServiceServicerImpl.py +15 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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...') Loading Loading @@ -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) Loading Loading @@ -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) Loading src/service/service/tools/OpticalSpectrumReservation.py 0 → 100644 +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 src/service/service/tools/OpticalTools.py +12 −2 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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"} Loading @@ -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 Loading src/service/tests/test_optical_spectrum_reservation.py 0 → 100644 +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"] Loading
src/service/service/ServiceServiceServicerImpl.py +15 −2 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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...') Loading Loading @@ -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) Loading Loading @@ -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) Loading
src/service/service/tools/OpticalSpectrumReservation.py 0 → 100644 +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
src/service/service/tools/OpticalTools.py +12 −2 Original line number Diff line number Diff line Loading @@ -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, Loading Loading @@ -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"} Loading @@ -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 Loading
src/service/tests/test_optical_spectrum_reservation.py 0 → 100644 +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"]