Skip to content
network_slice_controller.py 56.7 KiB
Newer Older
rahhal's avatar
rahhal committed
# Copyright 2025 Telefonica Innovación Digital S.L.
# Copyright 2025 Telefonica Innovación Digital S.L.
Javier Velázquez's avatar
Javier Velázquez committed

# 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 includes original contributions from Telefonica Innovación Digital S.L.

Javier Velázquez's avatar
Javier Velázquez committed
import json, time, os, logging, uuid, traceback, sys
Javier Velázquez's avatar
Javier Velázquez committed
from datetime import datetime
from src.helpers import tfs_connector, cisco_connector
from src.Constants import DEFAULT_LOGGING_LEVEL, TFS_IP, TFS_L2VPN_SUPPORT, IXIA_IP, SRC_PATH, TEMPLATES_PATH, DUMMY_MODE, DUMP_TEMPLATES, PLANNER_ENABLED, NRP_ENABLED, UPLOAD_TYPE, NBI_L2_PATH
Javier Velázquez's avatar
Javier Velázquez committed
from src.realizers.ixia.NEII_V4 import NEII_controller
Javier Velázquez's avatar
Javier Velázquez committed
from src.planner.planner import Planner
Javier Velázquez's avatar
Javier Velázquez committed

# Configure logging to provide clear and informative log messages
logging.basicConfig(
    level=DEFAULT_LOGGING_LEVEL,
    format='%(levelname)s - %(message)s')

class NSController:
    """
    Network Slice Controller (NSC) - A class to manage network slice creation, 
    modification, and deletion across different network domains.

    This controller handles the translation, mapping, and realization of network 
    slice intents from different formats (3GPP and IETF) to network-specific 
    configurations.

    Key Functionalities:
    - Intent Processing: Translate and process network slice intents
    - Slice Management: Create, modify, and delete network slices
    - NRP (Network Resource Partition) Mapping: Match slice requirements with available resources
    - Slice Realization: Convert intents to specific network configurations (L2VPN, L3VPN)
    """

    def __init__(self, controller_type = "TFS", tfs_ip=TFS_IP, ixia_ip =IXIA_IP, need_l2vpn_support=TFS_L2VPN_SUPPORT, path=NBI_L2_PATH):
Javier Velázquez's avatar
Javier Velázquez committed
        """
        Initialize the Network Slice Controller.

        Args:
Javier Velázquez's avatar
Javier Velázquez committed
            controller_type (str): Flag to determine if configurations 
                should be uploaded to Teraflow or IXIA system.
Javier Velázquez's avatar
Javier Velázquez committed
            need_l2vpn_support (bool, optional): Flag to determine if additional
                L2VPN configuration support is required. Defaults to False.
        
        Attributes:
Javier Velázquez's avatar
Javier Velázquez committed
            controller_type (str): Flag for Teraflow or Ixia upload
Javier Velázquez's avatar
Javier Velázquez committed
            answer (dict): Stores slice creation responses
            start_time (float): Tracks slice setup start time
            end_time (float): Tracks slice setup end time
            need_l2vpn_support (bool): Flag for additional L2VPN configuration support
        """
Javier Velázquez's avatar
Javier Velázquez committed
        self.controller_type = controller_type
Javier Velázquez's avatar
Javier Velázquez committed
        self.tfs_ip = tfs_ip
rahhal's avatar
rahhal committed
        self.path_l3 = path_l3
        #self.json_file = json_file
        self.path = path
        self.path_l3 = path_l3
        #self.json_file = json_file
Javier Velázquez's avatar
Javier Velázquez committed
        self.answer = {}
        self.cool_answer = {}
        self.start_time = 0
        self.end_time = 0
        self.setup_time = 0
        self.need_l2vpn_support = need_l2vpn_support
        # Internal templates and views
        self.__gpp_template = ""
        self.__ietf_template = ""
        self.__teraflow_template = ""
        self.__nrp_view = ""
        self.subnet=""

    # API Methods
    def add_flow(self, intent):
        """
        Create a new transport network slice.

        Args:
            intent (dict): Network slice intent in 3GPP or IETF format

        Returns:
            Result of the Network Slice Controller (NSC) operation

        API Endpoint:
            POST /slice

        Raises:
            ValueError: If no transport network slices are found
            Exception: For unexpected errors during slice creation process
        """
        return self.nsc(intent)

    def get_flows(self,slice_id=None):
        """
        Retrieve transport network slice information.

        This method allows retrieving:
        - All transport network slices
        - A specific slice by its ID

        Args:
            slice_id (str, optional): Unique identifier of a specific slice. 
                                      Defaults to None.

        Returns:
            dict or list: 
            - If slice_id is provided: Returns the specific slice details
            - If slice_id is None: Returns a list of all slices
            - Returns an error response if no slices are found

        API Endpoint:
            GET /slice/{id}

        Raises:
            ValueError: If no transport network slices are found
            Exception: For unexpected errors during file processing
        """
        try:
            # Read slice database from JSON file
            with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'r') as file:
                content = json.load(file)
            # If specific slice ID is provided, find and return matching slice
            if slice_id:
                for slice in content:
                    if slice["slice_id"] == slice_id:
                        return slice
            # If no slices exist, raise an error
            if len(content) == 0:
                raise ValueError("Transport network slices not found")
            
            # Return all slices if no specific ID is given
Javier Velázquez's avatar
Javier Velázquez committed
            return [slice for slice in content if slice.get("controller") == self.controller_type]
Javier Velázquez's avatar
Javier Velázquez committed
        
        except ValueError as e:
            # Handle case where no slices are found
            return self.__send_response(False, code=404, message=str(e))
        except Exception as e:
            # Handle unexpected errors
            return self.__send_response(False, code=500, message=str(e))

    def modify_flow(self,slice_id, intent):
        """
        Modify an existing transport network slice.

        Args:
            slice_id (str): Unique identifier of the slice to modify
            intent (dict): New intent configuration for the slice

        Returns:
            Result of the Network Slice Controller (NSC) operation

        API Endpoint:
            PUT /slice/{id}
        """
        return self.nsc(intent, slice_id)

    def delete_flows(self, slice_id=None):
        """
        Delete transport network slice(s).

        This method supports:
        - Deleting a specific slice by ID
        - Deleting all slices
        - Optional cleanup of L2VPN configurations

        Args:
            slice_id (str, optional): Unique identifier of slice to delete. 
                                      Defaults to None.

        Returns:
            dict: Response indicating successful deletion or error details

        API Endpoint:
            DELETE /slice/{id}

        Raises:
            ValueError: If no slices are found to delete
            Exception: For unexpected errors during deletion process

        Notes:
Javier Velázquez's avatar
Javier Velázquez committed
            - If controller_type is TFS, attempts to delete from Teraflow
Javier Velázquez's avatar
Javier Velázquez committed
            - If need_l2vpn_support is True, performs additional L2VPN cleanup
        """
        try:
            # Read current slice database
            with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'r') as file:
                content = json.load(file)
            id = None

            # Delete specific slice if slice_id is provided
            if slice_id:
                for i, slice in enumerate(content):
Javier Velázquez's avatar
Javier Velázquez committed
                    if slice["slice_id"] == slice_id and slice.get("controller") == self.controller_type:
Javier Velázquez's avatar
Javier Velázquez committed
                        del content[i]
                        id = i
                        break
                # Raise error if slice not found
                if id is None:
Javier Velázquez's avatar
Javier Velázquez committed
                    raise ValueError("Transport network slice not found")
                # Update slice database
                with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'w') as file:
                    json.dump(content, file, indent=4)
                logging.info(f"Slice {slice_id} removed successfully")
                return self.__send_response(False, code=200, status="success", message=f"Transpor network slice {slice_id} deleted successfully")
            
            # Delete all slices
            else:
                # Optional: Delete in Teraflow if configured
Javier Velázquez's avatar
Javier Velázquez committed
                if self.controller_type == "TFS":
Javier Velázquez's avatar
Javier Velázquez committed
                    # TODO: should send a delete request to Teraflow
                    if self.need_l2vpn_support:
                        self.__tfs_l2vpn_delete()

Javier Velázquez's avatar
Javier Velázquez committed
                data_removed = [slice for slice in content if slice.get("controller") == self.controller_type] 

Javier Velázquez's avatar
Javier Velázquez committed
                # Verify slices exist before deletion
Javier Velázquez's avatar
Javier Velázquez committed
                if len(data_removed) == 0:
                    raise ValueError("Transport network slices not found")

                filtered_data = [slice for slice in content if slice.get("controller") != self.controller_type]    
Javier Velázquez's avatar
Javier Velázquez committed
                # Clear slice database
                with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'w') as file:
Javier Velázquez's avatar
Javier Velázquez committed
                    json.dump(filtered_data, file, indent=4)
Javier Velázquez's avatar
Javier Velázquez committed

                logging.info("All slices removed successfully")
                return self.__send_response(False, code=200, status="success", message="All transport network slices deleted successfully.")
        
        except ValueError as e:
            return self.__send_response(False, code=404, message=str(e))
        except Exception as e:
            return self.__send_response(False, code=500, message=str(e))

    # Main NSC Functionalities    
    def nsc(self, intent_json, slice_id=None):
        """
        Main Network Slice Controller method to process and realize network slice intents.

        Workflow:
        1. Load IETF template
        2. Process intent (detect format, translate if needed)
        3. Extract slice data
        4. Store slice information
        5. Map slice to Network Resource Pool (NRP)
        6. Realize slice configuration
        7. Upload to Teraflow (optional)

        Args:
            intent_json (dict): Network slice intent in 3GPP or IETF format
            slice_id (str, optional): Existing slice identifier for modification

        Returns:
            tuple: Response status and HTTP status code
        
        """
        try:
            # Start performance tracking
            self.start_time = time.perf_counter()

            # Reset requests and load IETF template
            self.__load_template(1, os.path.join(TEMPLATES_PATH, "ietf_template_empty.json"))  
Javier Velázquez's avatar
Javier Velázquez committed
            requests = {"services":[]}
Javier Velázquez's avatar
Javier Velázquez committed

            # Store the received template for debugging
            if DUMP_TEMPLATES:
                with open(os.path.join(TEMPLATES_PATH, "nbi_template.json"), "w") as file:
                    file.write(json.dumps(intent_json,indent=2))
Javier Velázquez's avatar
Javier Velázquez committed
            
            # Process intent (translate if 3GPP)
            ietf_intents = self.__nbi_processor(intent_json)

Javier Velázquez's avatar
Javier Velázquez committed
            # Store the generated template for debugging
            if DUMP_TEMPLATES:
                with open(os.path.join(TEMPLATES_PATH, "ietf_template.json"), "w") as file:
                    file.write(json.dumps(ietf_intents,indent=2))

Javier Velázquez's avatar
Javier Velázquez committed
            if ietf_intents:
                for intent in ietf_intents:
                     # Extract and store slice request details
                    self.__extract_data(intent)
                    self.__store_data(intent, slice_id)       
                    # Mapper
                    self.__mapper(intent)
                    # Realizer
                    tfs_request = self.__realizer(intent)
Javier Velázquez's avatar
Javier Velázquez committed
                    requests["services"].append(tfs_request)
Javier Velázquez's avatar
Javier Velázquez committed
            else:
                return self.__send_response(False, code=404, message="No intents found")
Javier Velázquez's avatar
Javier Velázquez committed
            
            # Store the generated template for debugging
            if DUMP_TEMPLATES:
                with open(os.path.join(TEMPLATES_PATH, "realizer_template.json"), "w") as archivo:
                    archivo.write(json.dumps(requests,indent=2))
Javier Velázquez's avatar
Javier Velázquez committed
            
            # Optional: Upload template to Teraflow
Javier Velázquez's avatar
Javier Velázquez committed
            if not DUMMY_MODE:
                if self.controller_type == "TFS":
                    if UPLOAD_TYPE == "WEBUI":
                        response = tfs_connector().webui_post(self.tfs_ip, requests)
                    elif UPLOAD_TYPE == "NBI":
                        for intent in requests["services"]:
                            # Send each separate NBI request
                            response = tfs_connector().nbi_post(self.tfs_ip, intent, self.path)
                            if not response.ok:
                                return self.__send_response(False, code=response.status_code, message=f"Teraflow upload failed. Response: {response.text}")
Javier Velázquez's avatar
Javier Velázquez committed
                    
                    # For deploying an L2VPN with path selection (not supported by Teraflow)
                    if self.need_l2vpn_support:
                        self.__tfs_l2vpn_support(requests["services"])
Javier Velázquez's avatar
Javier Velázquez committed
                    logging.info("Request sent to Teraflow")
                elif self.controller_type == "IXIA":
                    neii_controller = NEII_controller()
                    for intent in requests["services"]:
                        # Send each separate IXIA request
                        neii_controller.nscNEII(intent)
                    logging.info("Requests sent to Ixia")
Javier Velázquez's avatar
Javier Velázquez committed

            # End performance tracking
            self.end_time = time.perf_counter()
            return self.__send_response(True, code=200)

        except ValueError as e:
            return self.__send_response(False, code=400, message=str(e))
        except Exception as e:
            return self.__send_response(False, code=500, message=str(e))
        
    def __nbi_processor(self, intent_json):
        """
        Process and translate network slice intents from different formats (3GPP or IETF).

        This method detects the input JSON format and converts 3GPP intents to IETF format.
        Supports multiple slice subnets in 3GPP format.

        Args:
            intent_json (dict): Input network slice intent in either 3GPP or IETF format.

        Returns:
            list: A list of IETF-formatted network slice intents.

        Raises:
            ValueError: If the JSON request format is not recognized.
        """
        # Detect the input JSON format (3GPP or IETF)
        format = self.__detect_format(intent_json)
        ietf_intents = []

        # TODO Needs to be generalized to support different names of slicesubnets
        # Process different input formats
        if format == "3GPP":
            # Translate each subnet in 3GPP format to IETF format
            for subnet in intent_json["RANSliceSubnet1"]["networkSliceSubnetRef"]:
                ietf_intents.append(self.__translator(intent_json, subnet))
            logging.info(f"3GPP requests translated to IETF template")
        elif format == "IETF":
            # If already in IETF format, add directly
            logging.info(f"IETF intent received")
            ietf_intents.append(intent_json)
        else:
            # Handle unrecognized format
            logging.error(f"JSON request format not recognized")
            raise ValueError("JSON request format not recognized")
        
        return ietf_intents

    def __mapper(self, ietf_intent):
        """
        Map an IETF network slice intent to the most suitable Network Resource Partition (NRP).

        This method:
        1. Retrieves the current NRP view
        2. Extracts Service Level Objectives (SLOs) from the intent
        3. Finds NRPs that can meet the SLO requirements
        4. Selects the best NRP based on viability and availability
        5. Attaches the slice to the selected NRP or creates a new one

        Args:
            ietf_intent (dict): IETF-formatted network slice intent.

        Raises:
            Exception: If no suitable NRP is found and slice creation fails.
        """ 
Javier Velázquez's avatar
Javier Velázquez committed
        if NRP_ENABLED:
            # Retrieve NRP view
            self.__realizer(None, True, "READ")

            # Extract Service Level Objectives (SLOs) from the intent
            slos = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"]

            if slos:
                # Find candidate NRPs that can meet the SLO requirements
                candidates = [
                    (nrp, self.__slo_viability(slos, nrp)[1]) 
                    for nrp in self.__nrp_view 
                    if self.__slo_viability(slos, nrp)[0] and nrp["available"]
                ]
                logging.debug(f"Candidates: {candidates}")

                # Select the best NRP based on candidates
                best_nrp = max(candidates, key=lambda x: x[1])[0] if candidates else None
                logging.debug(f"Best NRP: {best_nrp}")

                if best_nrp:
                    best_nrp["slices"].append(ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"])
                    # Update NRP view
                    self.__realizer(ietf_intent, True, "UPDATE")
                    # TODO Here we should put how the slice is attached to an already created nrp
                else: 
                    # Request the controller to create a new NRP that meets the SLOs
                    answer = self.__realizer(ietf_intent, True, "CREATE", best_nrp)
                    if not answer:
                        raise Exception("Slice rejected due to lack of NRPs") 
                    # TODO Here we should put how the slice is attached to the new nrp
        
        if PLANNER_ENABLED:
            optimal_path = Planner().planner(ietf_intent)

            logging.info(f"Optimal path: {optimal_path}")
Javier Velázquez's avatar
Javier Velázquez committed

    def __realizer(self, ietf_intent, need_nrp=False, order=None, nrp=None):
        """
        Manage the slice creation workflow.

        This method handles two primary scenarios:
        1. Interact with network controllers for NRP (Network Resource Partition) operations when need_nrp is True
        2. Slice service selection when need_nrp is False

        Args:
            ietf_intent (dict): IETF-formatted network slice intent.
            need_nrp (bool, optional): Flag to indicate if NRP operations are needed. Defaults to False.
            order (str, optional): Type of NRP operation (READ, UPDATE, CREATE). Defaults to None.
            nrp (dict, optional): Specific Network Resource Partition to operate on. Defaults to None.
        """
        if need_nrp:
            # Perform NRP-related operations
            self.__nrp(order, nrp)
        else:
            # Select slice service method
Javier Velázquez's avatar
Javier Velázquez committed
            way = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["service-tags"]["tag-type"]["value"]
            return self.__select_way(controller=self.controller_type, way=way, ietf_intent=ietf_intent)
Javier Velázquez's avatar
Javier Velázquez committed

    ### Generic functionalities
    def __load_template(self, which, dir_t):
        """
        Load and process JSON templates for different network slice formats.

        Args:
            which (int): Template selector (0: 3GPP, 1: IETF, other: Teraflow)
            dir_t (str): Directory path to the template file
        """
        try:
            # Open and read the template file
            with open(dir_t, 'r') as source:
                # Clean up the JSON template
                template = source.read().replace('\t', '').replace('\n', '').replace("'", '"').strip()
                
                # Store template based on selector
                if which == 0:
                    self.__gpp_template = template
                elif which == 1:
                    self.__ietf_template = template
                else:
                    self.__teraflow_template = template
                
        except Exception as e:
            logging.error(f"Template loading error: {e}")
            return self.__send_response(False, code=500, message=f"Template loading error: {e}")

    def __send_response(self, result, status="error", message=None, code=None):
        """
        Generate and send a response to the 3GPP client about the slice request.

        Args:
            result (bool): Indicates whether the slice request was successful
            status (str, optional): Response status. Defaults to "error"
            message (str, optional): Additional error message. Defaults to None
            code (str, optional): Response code. Defaults to None

        Returns:
            tuple: A tuple containing the response dictionary and status code
        """    
        if result:
            # Successful slice creation
            logging.info("Your slice request was fulfilled sucessfully")
            self.setup_time = (self.end_time - self.start_time)*1000
            logging.info(f"Setup time: {self.setup_time:.2f}")

            # Construct detailed successful response
            answer = {
                "status": "success",
                "code": code,
                "slices": [],
                "setup_time": self.setup_time
            }
            # Add slice details to the response
rahhal's avatar
rahhal committed
            logging.info(self.answer)
            logging.info(self.answer)
Javier Velázquez's avatar
Javier Velázquez committed
            for subnet in self.answer:
                slice_info = {
                    "id": subnet,
                    "source": self.answer[subnet]["Source"],
                    "destination": self.answer[subnet]["Destination"],
                    "vlan": self.answer[subnet]["VLAN"],
Javier Velázquez's avatar
Javier Velázquez committed
                    "requirements": self.answer[subnet]["QoS Requirements"],
Javier Velázquez's avatar
Javier Velázquez committed
                }
                answer["slices"].append(slice_info)
            self.cool_answer = answer
        else:
            # Failed slice creation
            logging.info("Your request cannot be fulfilled. Reason: "+message)
            self.cool_answer = {
                "status" :status,
                "code": code,
                "message": message
            }
        return self.cool_answer, code

    def __extract_data(self, intent_json):
        """
        Extract source and destination IP addresses from the IETF intent.

        Args:
            intent_json (dict): IETF-formatted network slice intent
        """
        # Extract source and destination IP addresses
        source = intent_json["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"]
        destination = intent_json["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"]

        logging.info(f"Intent generated between {source} and {destination}") 

        # Store slice and connection details
        self.subnet = intent_json["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"]
        self.subnet = intent_json["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"]
        self.answer[self.subnet] = {
            "Source": source,
            "Destination": destination
        }
    
    def __store_data(self, intent, slice_id):
        """
        Store network slice intent information in a JSON database file.

        This method:
        1. Creates a JSON file if it doesn't exist
        2. Reads existing content
        3. Updates or adds new slice intent information

        Args:
            intent (dict): Network slice intent to be stored
            slice_id (str, optional): Existing slice ID to update. Defaults to None.
        """
        file_path = os.path.join(SRC_PATH, "slice_ddbb.json")
        # Create initial JSON file if it doesn't exist
        if not os.path.exists(file_path):
            with open(file_path, 'w') as file:
                json.dump([], file, indent=4)

        # Read existing content
        with open(file_path, 'r') as file:
            content = json.load(file)
    
        # Update or add new slice intent
        if slice_id:
            # Update existing slice intent
            for slice in content:
                if slice["slice_id"] == slice_id:
                    slice["intent"] = intent
        else:
            # Add new slice intent
            content.append(
                {
                    "slice_id": intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"],
Javier Velázquez's avatar
Javier Velázquez committed
                    "intent": intent,
                    "controller": self.controller_type,
Javier Velázquez's avatar
Javier Velázquez committed
                })
        
        # # Write updated content back to file
        with open(file_path, 'w') as file:
            json.dump(content, file, indent=4)

    ### NBI processor functionalities
    def __detect_format(self,json_data):    
        """
        Detect the format of the input network slice intent.

        This method identifies whether the input JSON is in 3GPP or IETF format 
        by checking for specific keys in the JSON structure.

        Args:
            json_data (dict): Input network slice intent JSON

        Returns:
            str or None: 
                - "IETF" if IETF-specific keys are found
                - "3GPP" if 3GPP-specific keys are found
                - None if no recognizable format is detected
        """
        # Check for IETF-specific key
        if "ietf-network-slice-service:network-slice-services" in json_data:
            return "IETF"
        # Check for 3GPP-specific keys
        if any(key in json_data for key in ["NetworkSlice1", "TopSliceSubnet1", "CNSliceSubnet1", "RANSliceSubnet1"]):
            return "3GPP"
        
        return None
    
    def __translator(self, gpp_intent, subnet):
        """
        Translate a 3GPP network slice intent to IETF format.

        This method converts a 3GPP intent into a standardized IETF intent template, 
        mapping key parameters such as QoS profiles, service endpoints, and connection details.

        Args:
            gpp_intent (dict): Original 3GPP network slice intent
            subnet (str): Specific subnet reference within the 3GPP intent

        Returns:
            dict: Translated IETF-formatted network slice intent
        
        Notes:
            - Generates a unique slice service ID using UUID
            - Maps QoS requirements, source/destination endpoints
            - Logs the translated intent to a JSON file for reference
        """
        # Load IETF template and create a copy to modify
        ietf_i = json.loads(str(self.__ietf_template))

        # Extract endpoint transport objects
        ep_transport_objects = gpp_intent[subnet]["EpTransport"]

        # Populate template with SLOs (currently supporting QoS profile, latency and bandwidth)
        ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["id"] = gpp_intent[ep_transport_objects[0]]["qosProfile"]
Javier Velázquez's avatar
Javier Velázquez committed
        
        profile = gpp_intent.get(subnet, {}).get("SliceProfileList", [{}])[0].get("RANSliceSubnetProfile", {})

    
        metrics = {
            ("uLThptPerSliceSubnet", "MaxThpt"): ("throughput", "kbps"),
            ("uLLatency",): ("latency", "ms"),
            ("EnergyConsumption",): ("energy_consumption", "Joules"),
            ("EnergyEfficiency",): ("energy_efficiency", "W/bps"),
            ("CarbonEmissions",): ("carbon_emission", "gCO2eq"),
            ("RenewableEnergyUsage",): ("renewable_energy_usage", "rate")
        }

        # Aux
        def get_nested(d, keys):
            for k in keys:
                if isinstance(d, dict) and k in d:
                    d = d[k]
                else:
                    return None
            return d

        for key_path, (metric_type, metric_unit) in metrics.items():
            value = get_nested(profile, key_path)
            if value is not None:
                ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]\
                    ["slo-sle-template"][0]["slo-policy"]["metric-bound"].append({
                    "metric-type": metric_type,
                    "metric-unit": metric_unit,
                    "bound": value
                })

Javier Velázquez's avatar
Javier Velázquez committed

        # Generate unique slice service ID and description
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] = f"slice-service-{uuid.uuid4()}"
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["description"] = f"Transport network slice mapped with 3GPP slice {next(iter(gpp_intent))}"
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["slo-sle-policy"]["slo-sle-template"] = ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["id"]
        
        # Configure Source SDP
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["node-id"] = ep_transport_objects[0].split(" ", 1)[1]
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"] = gpp_intent[gpp_intent[ep_transport_objects[0]]["EpApplicationRef"][0]]["localAddress"]
Javier Velázquez's avatar
Javier Velázquez committed
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["match-type"] = gpp_intent[ep_transport_objects[0]]["logicalInterfaceInfo"]["logicalInterfaceType"]
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["value"] = gpp_intent[ep_transport_objects[0]]["logicalInterfaceInfo"]["logicalInterfaceId"]
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["attachment-circuits"]["attachment-circuit"][0]["ac-ipv4-address"] = gpp_intent[ep_transport_objects[0]]["IpAddress"] 
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"] = gpp_intent[ep_transport_objects[0]]["NextHopInfo"] 
Javier Velázquez's avatar
Javier Velázquez committed

        # Configure Destination SDP
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["node-id"] = ep_transport_objects[1].split(" ", 1)[1]
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"] = gpp_intent[gpp_intent[ep_transport_objects[1]]["EpApplicationRef"][0]]["localAddress"]
Javier Velázquez's avatar
Javier Velázquez committed
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["service-match-criteria"]["match-criterion"][0]["match-type"] = gpp_intent[ep_transport_objects[1]]["logicalInterfaceInfo"]["logicalInterfaceType"]
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["service-match-criteria"]["match-criterion"][0]["value"] = gpp_intent[ep_transport_objects[1]]["logicalInterfaceInfo"]["logicalInterfaceId"]
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["attachment-circuits"]["attachment-circuit"][0]["ac-ipv4-address"] = gpp_intent[ep_transport_objects[1]]["IpAddress"] 
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"] = gpp_intent[ep_transport_objects[1]]["NextHopInfo"] 
Javier Velázquez's avatar
Javier Velázquez committed

        # Configure Connection Group and match-criteria
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["connection-groups"]["connection-group"][0]["id"] = f"{ep_transport_objects[0].split(' ', 1)[1]}_{ep_transport_objects[1].split(' ', 1)[1]}"
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["target-connection-group-id"] = f"{ep_transport_objects[0].split(' ', 1)[1]}_{ep_transport_objects[1].split(' ', 1)[1]}"
        ietf_i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["service-match-criteria"]["match-criterion"][0]["target-connection-group-id"] = f"{ep_transport_objects[0].split(' ', 1)[1]}_{ep_transport_objects[1].split(' ', 1)[1]}"

        return ietf_i
    
    ### Mapper functionalities
    def __slo_viability(self, slice_slos, nrp_slos):
        """
        Compare Service Level Objectives (SLOs) between a slice and a Network Resource Partition (NRP).

        This method assesses whether an NRP can satisfy the SLOs of a network slice.

        Args:
            slice_slos (list): Service Level Objectives of the slice
            nrp_slos (dict): Service Level Objectives of the Network Resource Pool

        Returns:
            tuple: A boolean indicating viability and a flexibility score
                - First value: True if NRP meets SLOs, False otherwise
                - Second value: A score representing how well the NRP meets the SLOs
        """
        # Define SLO types for maximum and minimum constraints
        slo_type = {
            "max": ["one-way-delay-maximum", "two-way-delay-maximum", "one-way-delay-percentile", "two-way-delay-percentile",
                    "one-way-delay-variation-maximum", "two-way-delay-variation-maximum",
                    "one-way-delay-variation-percentile", "two-way-delay-variation-percentile",
                    "one-way-packet-loss", "two-way-packet-loss"],
            "min": ["one-way-bandwidth", "two-way-bandwidth", "shared-bandwidth"]
        }
        flexibility_scores = []
        for slo in slice_slos:
            for nrp_slo in nrp_slos['slos']:
                if slo["metric-type"] == nrp_slo["metric-type"]:
                    # Handle maximum type SLOs
                    if slo["metric-type"] in slo_type["max"]:
                        flexibility = (nrp_slo["bound"] - slo["bound"]) / slo["bound"]
                        if slo["bound"] > nrp_slo["bound"]:
                            return False, 0  # Does not meet maximum constraint
                    # Handle minimum type SLOs
                    if slo["metric-type"] in slo_type["min"]:
                        flexibility = (slo["bound"] - nrp_slo["bound"]) / slo["bound"]
                        if slo["bound"] < nrp_slo["bound"]:
                            return False, 0  # Does not meet minimum constraint
                    flexibility_scores.append(flexibility)
                    break  # Exit inner loop after finding matching metric
            
            # Calculate final viability score
            score = sum(flexibility_scores) / len(flexibility_scores) if flexibility_scores else 0
        return True, score  # Si pasó todas las verificaciones, la NRP es viable
    
    ### Realizer functionalities.
    def __nrp(self, request, nrp):
        """
        Manage Network Resource Partition (NRP) operations.

        This method handles CRUD operations for Network Resource Partitions,
        interacting with Network Controllers (currently done statically via a JSON-based database file).

        Args:
            request (str): The type of operation to perform. 
                Supported values:
                - "CREATE": Add a new NRP to the database
                - "READ": Retrieve the current NRP view
                - "UPDATE": Update an existing NRP (currently a placeholder)

            nrp (dict): The Network Resource Partition details to create or update.

        Returns:
            None or answer: 
            - For "CREATE": Returns the response from the controller (currently using a static JSON)
            - For "READ": Gets the NRP view from the controller (currently using a static JSON)
            - For "UPDATE": Placeholder for update functionality

        Notes:
            - Uses a local JSON file "nrp_ddbb.json" to store NRP information as controller operation is not yet defined
        """
        if request == "CREATE":
            # TODO: Implement actual request to Controller to create an NRP
            logging.debug("Creating NRP")

            # Load existing NRP database
            with open(os.path.join(SRC_PATH, "nrp_ddbb.json"), "r") as archivo:
                self.__nrp_view = json.load(archivo)

            # Append new NRP to the view
            self.__nrp_view.append(nrp)

            # Placeholder for controller POST request
            answer = None
            return answer
        elif request == "READ":
            # TODO: Request to Controller to get topology and current NRP view
            logging.debug("Reading Topology")

            # Load NRP database
            with open(os.path.join(SRC_PATH, "nrp_ddbb.json"), "r") as archivo:
                self.__nrp_view = json.load(archivo)
            
        elif request == "UPDATE":
            # TODO: Implement request to Controller to update NRP
            logging.debug("Updating NRP")
            answer = ""
    
Javier Velázquez's avatar
Javier Velázquez committed
    def __select_way(self, controller=None, way=None, ietf_intent=None):
Javier Velázquez's avatar
Javier Velázquez committed
        """
        Determine the method of slice realization.

        Args:
Javier Velázquez's avatar
Javier Velázquez committed
            controller (str): The controller to use for slice realization.
                Supported values:
                - "IXIA": IXIA NEII for network testing
                - "TFS": TeraFlow Service for network slice management
Javier Velázquez's avatar
Javier Velázquez committed
            way (str): The type of technology to use.
                Supported values:
                - "L2VPN": Layer 2 Virtual Private Network
                - "L3VPN": Layer 3 Virtual Private Network

            ietf_intent (dict): IETF-formatted network slice intent.

        Returns:
            dict: A realization request for the specified network slice type.

        """
Javier Velázquez's avatar
Javier Velázquez committed
        realizing_request = None
        if controller == "TFS":
            if way == "L2VPN":
                realizing_request = self.__tfs_l2vpn(ietf_intent)
            elif way == "L3VPN":
                realizing_request = self.__tfs_l3vpn(ietf_intent)
            else:
                logging.warning(f"Unsupported way: {way}. Defaulting to L2VPN realization.")
                realizing_request = self.__tfs_l2vpn(ietf_intent)
        elif controller == "IXIA":
            realizing_request = self.__ixia(ietf_intent)
        else:
            logging.warning(f"Unsupported controller: {controller}. Defaulting to TFS L2VPN realization.")
Javier Velázquez's avatar
Javier Velázquez committed
            realizing_request = self.__tfs_l2vpn(ietf_intent)
        return realizing_request
    def __tfs_l2vpn(self, ietf_intent):
rahhal's avatar
rahhal committed
       """
       Translate slice intent into a TeraFlow service request.
       """
       Translate slice intent into a TeraFlow service request.
       This method prepares a L2VPN service request by:
       1. Defining endpoint routers
       2. Loading a service template
       3. Generating a unique service UUID
       4. Configuring service endpoints
       5. Adding QoS constraints
       6. Preparing configuration rules for network interfaces
rahhal's avatar
rahhal committed
       This method prepares a L2VPN service request by:
       1. Defining endpoint routers
       2. Loading a service template
       3. Generating a unique service UUID
       4. Configuring service endpoints
       5. Adding QoS constraints
       6. Preparing configuration rules for network interfaces
       Args:
           ietf_intent (dict): IETF-formatted network slice intent.
rahhal's avatar
rahhal committed
       Args:
           ietf_intent (dict): IETF-formatted network slice intent.
       Returns:
           dict: A TeraFlow service request for L2VPN configuration.
rahhal's avatar
rahhal committed
       Returns:
           dict: A TeraFlow service request for L2VPN configuration.
Javier Velázquez's avatar
Javier Velázquez committed

        """
        # Hardcoded router endpoints
        # TODO (should be dynamically determined)
        origin_router_id = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"]
rahhal's avatar
rahhal committed
        origin_router_if = 'eth2'
        origin_router_if = 'eth2'
        destination_router_id = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"]
Javier Velázquez's avatar
Javier Velázquez committed
        destination_router_if = '0/0/0-GigabitEthernet0/0/0/0'
        # Extract QoS Profile from intent
        QoSProfile = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["id"]
        vlan_value = 0

        self.answer[self.subnet]["QoS Requirements"] = []
        
        # Populate response with QoS requirements and VLAN from intent
        for i, constraint in enumerate(ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"]):
            bound = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"][i]["bound"]
            metric_type = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"][i]["metric-type"]
            metric_unit = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"][i]["metric-unit"]
            service_constraint ={
                "custom": {
                    "constraint_type": f"{metric_type}[{metric_unit}]",
                    "constraint_value": f"{bound}"
                }
            }
            self.answer[self.subnet]["QoS Requirements"].append(service_constraint["custom"])
        
        vlan_value = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["value"]
        self.answer[self.subnet]["VLAN"] = vlan_value

        if UPLOAD_TYPE == "WEBUI":
            # Load L2VPN service template
            self.__load_template(2, os.path.join(TEMPLATES_PATH, "L2-VPN_template_empty.json"))
            tfs_request = json.loads(str(self.__teraflow_template))["services"][0]

            # Generate unique service UUID
            tfs_request["service_id"]["service_uuid"]["uuid"] += "-" + str(int(datetime.now().timestamp() * 1e7))

            # Configure service endpoints
            for endpoint in tfs_request["service_endpoint_ids"]:
                endpoint["device_id"]["device_uuid"]["uuid"] = origin_router_id if endpoint is tfs_request["service_endpoint_ids"][0] else destination_router_id
                endpoint["endpoint_uuid"]["uuid"] = origin_router_if if endpoint is tfs_request["service_endpoint_ids"][0] else destination_router_if

            # Add service constraints
            for constraint in self.answer[self.subnet]["QoS Requirements"]:
                tfs_request["service_constraints"].append({"custom": constraint})

            # Add configuration rules
            for i, config_rule in enumerate(tfs_request["service_config"]["config_rules"][1:], start=1):
                router_id = origin_router_id if i == 1 else destination_router_id
                router_if = origin_router_if if i == 1 else destination_router_if
                resource_value = config_rule["custom"]["resource_value"]

                sdp_index = i - 1
                vlan_value = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][sdp_index]["service-match-criteria"]["match-criterion"][0]["value"]
                if vlan_value:
                    resource_value["vlan_id"] = int(vlan_value)
                resource_value["circuit_id"] = vlan_value
                resource_value["remote_router"] = destination_router_id if i == 1 else origin_router_id
                resource_value["ni_name"] = 'ELAN{:s}'.format(str(vlan_value))
                config_rule["custom"]["resource_key"] = f"/device[{router_id}]/endpoint[{router_if}]/settings"

        elif UPLOAD_TYPE == "NBI":

            # Load IETF L2VPN service template
            self.__load_template(2, os.path.join(TEMPLATES_PATH, "ietfL2VPN_template_empty.json"))
            tfs_request = json.loads(str(self.__teraflow_template))
            full_id = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"]
            uuid_only = full_id.split("slice-service-")[-1]
            tfs_request["ietf-l2vpn-svc:vpn-service"][0]["vpn-id"] = uuid_only
            for site in tfs_request["ietf-l2vpn-svc:vpn-service"][0]["site"]:
                if site is tfs_request["ietf-l2vpn-svc:vpn-service"][0]["site"][0]:
                    site["site-id"] = origin_router_id
                    site["site-location"] = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["node-id"]
                    site["site-network-access"]["interface"]["ip-address"] = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"]
                else:
                    site["site-id"] = destination_router_id
                    site["site-location"] = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["node-id"]
                    site["site-network-access"]["interface"]["ip-address"] = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"]

        logging.info(f"L2VPN Intent realized\n")
        return tfs_request
Javier Velázquez's avatar
Javier Velázquez committed
    
    def __tfs_l2vpn_support(self, requests):
        """
        Configuration support for L2VPN with path selection based on MPLS traffic-engineering tunnels

        Args:
            requests (list): A list of configuration parameters.

        """
        sources={
            "source": "10.60.125.44",
            "config":[]
        }
        destinations={
            "destination": "10.60.125.45",
            "config":[]
        }
        for request in requests:
            # Configure Source Endpoint
            temp_source = request["service_config"]["config_rules"][1]["custom"]["resource_value"]
            endpoints = request["service_endpoint_ids"]
            config = {
                "ni_name": temp_source["ni_name"],
                "remote_router": temp_source["remote_router"],
                "interface": endpoints[0]["endpoint_uuid"]["uuid"].replace("0/0/0-", ""),
                "vlan" : temp_source["vlan_id"],
                "number" : temp_source["vlan_id"] % 10 + 1
            }
            sources["config"].append(config)

            # Configure Destination Endpoint
            temp_destiny = request["service_config"]["config_rules"][2]["custom"]["resource_value"]
            config = {
                "ni_name": temp_destiny["ni_name"],
                "remote_router": temp_destiny["remote_router"],
                "interface": endpoints[1]["endpoint_uuid"]["uuid"].replace("0/0/3-", ""),
                "vlan" : temp_destiny["vlan_id"],
                "number" : temp_destiny["vlan_id"] % 10 + 1
            }
            destinations["config"].append(config)
         
        #cisco_source = cisco_connector(source_address, ni_name, remote_router, vlan, vlan % 10 + 1)
        cisco_source = cisco_connector(sources["source"], sources["config"])
        commands = cisco_source.full_create_command_template()