Loading src/config/.env.example +1 −3 Original line number Diff line number Diff line Loading @@ -19,7 +19,7 @@ # ------------------------- NSC_PORT=8086 # Options: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET LOGGING_LEVEL=DEBUG LOGGING_LEVEL=INFO DUMP_TEMPLATES=false # ------------------------- Loading Loading @@ -69,8 +69,6 @@ TFS_E2E_IP=127.0.0.1 RESTCONF_IP=127.0.0.1 # Options: TFS or IXIA SDN_CONTROLLER_TYPE=TFS # Options: FRR, CISCO DATAPLANE_SUPPORT=FRR # ------------------------- # WebUI Loading src/config/config.py +0 −1 Original line number Diff line number Diff line Loading @@ -67,6 +67,5 @@ def create_config(app: Flask): # Restconf Controller app.config["RESTCONF_IP"] = os.getenv("RESTCONF_IP", "127.0.0.1") app.config["SDN_CONTROLLER_TYPE"] = os.getenv("SDN_CONTROLLER_TYPE", "TFS") app.config["DATAPLANE_SUPPORT"] = os.getenv("DATAPLANE_SUPPORT", "FRR") return app src/realizer/restconf/connectors/frr_connector.pydeleted 100644 → 0 +0 −89 Original line number Diff line number Diff line # frr_connector.py import logging from netmiko import ConnectHandler class frr_connector(): """Class to interact with FRR devices via SSH using Netmiko.""" def __init__(self, address): self.address = address def execute_commands(self, commands): try: device = { 'device_type': 'linux', 'host': self.address, 'username': 'root', 'password': 'root', } connection = ConnectHandler(**device) output = connection.send_config_set(commands) logging.debug(output) connection.disconnect() except Exception as e: logging.error(f"Failed to execute commands on {self.address}: {str(e)}") raise def setup_slice(self, config: dict, assignments: dict[int, int]) -> list[str]: """ Build PBR commands dynamically based on active slot assignments. Args: config: Device config (interfaces, addresses, etc.) assignments: {slot_number: dscp_value} for currently active slices. e.g. {1: 52, 2: 51} Returns: List of FRR/vtysh commands. """ commands = ["vtysh", "conf te"] # Nexthop groups - one per non-default slice + DEFAULT for i, iface in enumerate(config['output_interfaces'], start=1): commands += [ f"nexthop-group SLICE-{i}", f"nexthop {iface}", "exit", ] # PBR rules - one per active slot, ordered by seq seq = 10 for slot, dscp in sorted(assignments.items()): commands += [ f"pbr-map PBR-DSCP seq {seq}", f"match mark {dscp}", f"set nexthop-group SLICE-{slot}", "exit", ] seq += 10 # Default catch-all rule always last commands += [ "pbr-map PBR-DSCP seq 100", "match dst-ip 0.0.0.0/0", "set nexthop-group DEFAULT", "exit", ] # Apply PBR policy to input interface commands += [ f"interface {config['input_interface']}", f"ip address {config['input_address']}/24", "pbr-policy PBR-DSCP", "exit", "end", ] return commands def delete_all_slices(self) -> list[str]: return [ "vtysh", "conf te", "no pbr-map PBR-DSCP", "no nexthop-group SLICE-1", "no nexthop-group SLICE-2", "no nexthop-group DEFAULT", "end", ] No newline at end of file src/realizer/restconf/restconf_connect.py +0 −28 Original line number Diff line number Diff line from src.utils.slice_manager import SliceManager from .connectors.frr_connector import frr_connector from .connectors.tfs_connector import tfs_connector from src.utils.send_response import send_response from src.utils.safe_get import safe_get Loading Loading @@ -42,34 +41,7 @@ def restconf_connect(requests, restconf_ip): elif key == "ietf-l3vpn-svc:l3vpn-svc": path = NBI_L3_PATH dscp = safe_get(intent, [ "sites", 0, "site-network-accesses", [0], "service", "qos", "qos-classification-policy", "rule", 0, "match-flow", "dscp" ]) if dscp is not None and current_app.config["DATAPLANE_SUPPORT"] == "FRR": if _slice_manager.is_full(): return send_response( False, code=429, message=f"No available slices. Current assignments: {_slice_manager.get_active_assignments()}" ) slot = _slice_manager.assign_slot(dscp) if slot is None: return send_response(False, code=429, message=f"Could not assign slot for DSCP {dscp}") logging.info(f"Assigned DSCP {dscp} to SLICE-{slot}") assignments = _slice_manager.get_active_assignments() for device_config in FRR_DEVICES: connector = frr_connector(device_config["management_address"]) commands = connector.setup_slice(device_config, assignments) try: connector.execute_commands(commands) except Exception as e: return send_response(False, code=500, message=f"FRR config failed on {device_config['management_address']}: {str(e)}") else: return send_response(False, code=400, message=f"Unsupported service type: {key}") Loading src/utils/slice_manager.pydeleted 100644 → 0 +0 −65 Original line number Diff line number Diff line # Copyright 2022-2025 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. # This file is an original contribution from Telefonica Innovación Digital S.L. class SliceManager: """ Manages available PBR slices and their DSCP assignments. Slices are 1-indexed. Slice with the highest index is always the default. """ TOTAL_SLICES = 3 DEFAULT_SLICE = TOTAL_SLICES # SLICE-3 is always default def __init__(self): # slot -> dscp (None means free) self._slots: dict[int, int | None] = { i: None for i in range(1, self.TOTAL_SLICES) # {1: None, 2: None} } def assign_slot(self, dscp: int) -> int | None: """ Try to assign a free slot to a DSCP value. Returns the assigned slot number, or None if no slots are available. """ # Check if DSCP already assigned for slot, assigned_dscp in self._slots.items(): if assigned_dscp == dscp: return slot # idempotent # Find first free slot for slot, assigned_dscp in self._slots.items(): if assigned_dscp is None: self._slots[slot] = dscp return slot return None # No free slots def release_slot(self, dscp: int) -> bool: """ Release the slot assigned to a DSCP value. Returns True if released, False if not found. """ for slot, assigned_dscp in self._slots.items(): if assigned_dscp == dscp: self._slots[slot] = None return True return False def get_active_assignments(self) -> dict[int, int]: """Returns only the occupied slots {slot: dscp}.""" return {slot: dscp for slot, dscp in self._slots.items() if dscp is not None} def is_full(self) -> bool: return all(dscp is not None for dscp in self._slots.values()) No newline at end of file Loading
src/config/.env.example +1 −3 Original line number Diff line number Diff line Loading @@ -19,7 +19,7 @@ # ------------------------- NSC_PORT=8086 # Options: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET LOGGING_LEVEL=DEBUG LOGGING_LEVEL=INFO DUMP_TEMPLATES=false # ------------------------- Loading Loading @@ -69,8 +69,6 @@ TFS_E2E_IP=127.0.0.1 RESTCONF_IP=127.0.0.1 # Options: TFS or IXIA SDN_CONTROLLER_TYPE=TFS # Options: FRR, CISCO DATAPLANE_SUPPORT=FRR # ------------------------- # WebUI Loading
src/config/config.py +0 −1 Original line number Diff line number Diff line Loading @@ -67,6 +67,5 @@ def create_config(app: Flask): # Restconf Controller app.config["RESTCONF_IP"] = os.getenv("RESTCONF_IP", "127.0.0.1") app.config["SDN_CONTROLLER_TYPE"] = os.getenv("SDN_CONTROLLER_TYPE", "TFS") app.config["DATAPLANE_SUPPORT"] = os.getenv("DATAPLANE_SUPPORT", "FRR") return app
src/realizer/restconf/connectors/frr_connector.pydeleted 100644 → 0 +0 −89 Original line number Diff line number Diff line # frr_connector.py import logging from netmiko import ConnectHandler class frr_connector(): """Class to interact with FRR devices via SSH using Netmiko.""" def __init__(self, address): self.address = address def execute_commands(self, commands): try: device = { 'device_type': 'linux', 'host': self.address, 'username': 'root', 'password': 'root', } connection = ConnectHandler(**device) output = connection.send_config_set(commands) logging.debug(output) connection.disconnect() except Exception as e: logging.error(f"Failed to execute commands on {self.address}: {str(e)}") raise def setup_slice(self, config: dict, assignments: dict[int, int]) -> list[str]: """ Build PBR commands dynamically based on active slot assignments. Args: config: Device config (interfaces, addresses, etc.) assignments: {slot_number: dscp_value} for currently active slices. e.g. {1: 52, 2: 51} Returns: List of FRR/vtysh commands. """ commands = ["vtysh", "conf te"] # Nexthop groups - one per non-default slice + DEFAULT for i, iface in enumerate(config['output_interfaces'], start=1): commands += [ f"nexthop-group SLICE-{i}", f"nexthop {iface}", "exit", ] # PBR rules - one per active slot, ordered by seq seq = 10 for slot, dscp in sorted(assignments.items()): commands += [ f"pbr-map PBR-DSCP seq {seq}", f"match mark {dscp}", f"set nexthop-group SLICE-{slot}", "exit", ] seq += 10 # Default catch-all rule always last commands += [ "pbr-map PBR-DSCP seq 100", "match dst-ip 0.0.0.0/0", "set nexthop-group DEFAULT", "exit", ] # Apply PBR policy to input interface commands += [ f"interface {config['input_interface']}", f"ip address {config['input_address']}/24", "pbr-policy PBR-DSCP", "exit", "end", ] return commands def delete_all_slices(self) -> list[str]: return [ "vtysh", "conf te", "no pbr-map PBR-DSCP", "no nexthop-group SLICE-1", "no nexthop-group SLICE-2", "no nexthop-group DEFAULT", "end", ] No newline at end of file
src/realizer/restconf/restconf_connect.py +0 −28 Original line number Diff line number Diff line from src.utils.slice_manager import SliceManager from .connectors.frr_connector import frr_connector from .connectors.tfs_connector import tfs_connector from src.utils.send_response import send_response from src.utils.safe_get import safe_get Loading Loading @@ -42,34 +41,7 @@ def restconf_connect(requests, restconf_ip): elif key == "ietf-l3vpn-svc:l3vpn-svc": path = NBI_L3_PATH dscp = safe_get(intent, [ "sites", 0, "site-network-accesses", [0], "service", "qos", "qos-classification-policy", "rule", 0, "match-flow", "dscp" ]) if dscp is not None and current_app.config["DATAPLANE_SUPPORT"] == "FRR": if _slice_manager.is_full(): return send_response( False, code=429, message=f"No available slices. Current assignments: {_slice_manager.get_active_assignments()}" ) slot = _slice_manager.assign_slot(dscp) if slot is None: return send_response(False, code=429, message=f"Could not assign slot for DSCP {dscp}") logging.info(f"Assigned DSCP {dscp} to SLICE-{slot}") assignments = _slice_manager.get_active_assignments() for device_config in FRR_DEVICES: connector = frr_connector(device_config["management_address"]) commands = connector.setup_slice(device_config, assignments) try: connector.execute_commands(commands) except Exception as e: return send_response(False, code=500, message=f"FRR config failed on {device_config['management_address']}: {str(e)}") else: return send_response(False, code=400, message=f"Unsupported service type: {key}") Loading
src/utils/slice_manager.pydeleted 100644 → 0 +0 −65 Original line number Diff line number Diff line # Copyright 2022-2025 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. # This file is an original contribution from Telefonica Innovación Digital S.L. class SliceManager: """ Manages available PBR slices and their DSCP assignments. Slices are 1-indexed. Slice with the highest index is always the default. """ TOTAL_SLICES = 3 DEFAULT_SLICE = TOTAL_SLICES # SLICE-3 is always default def __init__(self): # slot -> dscp (None means free) self._slots: dict[int, int | None] = { i: None for i in range(1, self.TOTAL_SLICES) # {1: None, 2: None} } def assign_slot(self, dscp: int) -> int | None: """ Try to assign a free slot to a DSCP value. Returns the assigned slot number, or None if no slots are available. """ # Check if DSCP already assigned for slot, assigned_dscp in self._slots.items(): if assigned_dscp == dscp: return slot # idempotent # Find first free slot for slot, assigned_dscp in self._slots.items(): if assigned_dscp is None: self._slots[slot] = dscp return slot return None # No free slots def release_slot(self, dscp: int) -> bool: """ Release the slot assigned to a DSCP value. Returns True if released, False if not found. """ for slot, assigned_dscp in self._slots.items(): if assigned_dscp == dscp: self._slots[slot] = None return True return False def get_active_assignments(self) -> dict[int, int]: """Returns only the occupied slots {slot: dscp}.""" return {slot: dscp for slot, dscp in self._slots.items() if dscp is not None} def is_full(self) -> bool: return all(dscp is not None for dscp in self._slots.values()) No newline at end of file