Commit 8df8e714 authored by Javier Velázquez's avatar Javier Velázquez
Browse files

Move some changes to feature 26

parent 71762611
Loading
Loading
Loading
Loading
+1 −3
Original line number Diff line number Diff line
@@ -19,7 +19,7 @@
# -------------------------
NSC_PORT=8086
# Options: CRITICAL, ERROR, WARNING, INFO, DEBUG, NOTSET
LOGGING_LEVEL=DEBUG
LOGGING_LEVEL=INFO
DUMP_TEMPLATES=false

# -------------------------
@@ -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
+0 −1
Original line number Diff line number Diff line
@@ -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
+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
+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
@@ -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}")

src/utils/slice_manager.py

deleted100644 → 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