Commit 9f60c87c authored by Javier Velázquez's avatar Javier Velázquez
Browse files

API improved to be consistent

parent 747d37e0
Loading
Loading
Loading
Loading
+154 −123
Original line number Diff line number Diff line
@@ -42,15 +42,13 @@ class NSController:
    - 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): 
    def __init__(self, controller_type = "TFS"): 
        """
        Initialize the Network Slice Controller.

        Args:
            controller_type (str): Flag to determine if configurations 
                should be uploaded to Teraflow or IXIA system.
            need_l2vpn_support (bool, optional): Flag to determine if additional
                L2VPN configuration support is required. Defaults to False.
        
        Attributes:
            controller_type (str): Flag for Teraflow or Ixia upload
@@ -60,15 +58,13 @@ class NSController:
            need_l2vpn_support (bool): Flag for additional L2VPN configuration support
        """
        self.controller_type = controller_type
        self.tfs_ip = tfs_ip

        self.path = ""
        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 = ""
@@ -93,7 +89,19 @@ class NSController:
            ValueError: If no transport network slices are found
            Exception: For unexpected errors during slice creation process
        """
        return self.nsc(intent)
        try:
            result = self.nsc(intent)
            if not result:
                return self.__send_response(False, code=404, message="No intents found")

            return self.__send_response(
                True,
                code=201,
                data=result 
            )
        except Exception as e:
            # Handle unexpected errors
            return self.__send_response(False, code=500, message=str(e))

    def get_flows(self,slice_id=None):
        """
@@ -128,13 +136,14 @@ class NSController:
            if slice_id:
                for slice in content:
                    if slice["slice_id"] == slice_id:
                        return slice
                        return slice, 200
                raise ValueError("Transport network slices not found")
            # 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
            return [slice for slice in content if slice.get("controller") == self.controller_type]
            return [slice for slice in content if slice.get("controller") == self.controller_type], 200
        
        except ValueError as e:
            # Handle case where no slices are found
@@ -157,7 +166,20 @@ class NSController:
        API Endpoint:
            PUT /slice/{id}
        """
        return self.nsc(intent, slice_id)
        try:
            result = self.nsc(intent, slice_id)
            if not result:
                return self.__send_response(False, code=404, message="Slice not found")

            return self.__send_response(
                True,
                code=200,
                message="Slice modified successfully",
                data=result
            )
        except Exception as e:
            # Handle unexpected errors
            return self.__send_response(False, code=500, message=str(e))

    def delete_flows(self, slice_id=None):
        """
@@ -206,14 +228,14 @@ class NSController:
                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")
                return {}, 204
            
            # Delete all slices
            else:
                # Optional: Delete in Teraflow if configured
                if self.controller_type == "TFS":
                    # TODO: should send a delete request to Teraflow
                    if self.need_l2vpn_support:
                    if TFS_L2VPN_SUPPORT:
                        self.__tfs_l2vpn_delete()

                data_removed = [slice for slice in content if slice.get("controller") == self.controller_type] 
@@ -228,7 +250,7 @@ class NSController:
                    json.dump(filtered_data, file, indent=4)

                logging.info("All slices removed successfully")
                return self.__send_response(False, code=200, status="success", message="All transport network slices deleted successfully.")
                return {}, 204
        
        except ValueError as e:
            return self.__send_response(False, code=404, message=str(e))
@@ -257,7 +279,6 @@ class NSController:
            tuple: Response status and HTTP status code
        
        """
        try:
        # Start performance tracking
        self.start_time = time.perf_counter()

@@ -266,19 +287,16 @@ class NSController:
        requests = {"services":[]}

        # 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))
        self.__dump_templates("nbi_template", intent_json)
        
        # Process intent (translate if 3GPP)
        ietf_intents = self.__nbi_processor(intent_json)
        if not ietf_intents:
            return None  # Nothing to process

        # 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))
        self.__dump_templates("ietf_template", ietf_intents)

            if ietf_intents:
        for intent in ietf_intents:
                # Extract and store slice request details
            self.__extract_data(intent)
@@ -288,47 +306,26 @@ class NSController:
            # Realizer
            tfs_request = self.__realizer(intent)
            requests["services"].append(tfs_request)
            else:
                return self.__send_response(False, code=404, message="No intents found")
        
        # 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))
        self.__dump_templates("realizer_template", requests)
        
        # Optional: Upload template to Teraflow
            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)
        response = self.__send_controller(self.controller_type, requests)

                            if not response.ok:
                                return self.__send_response(False, code=response.status_code, message=f"Teraflow upload failed. Response: {response.text}")
                    
                    # For deploying an L2VPN with path selection (not supported by Teraflow)
                    if self.need_l2vpn_support:
                        self.__tfs_l2vpn_support(requests["services"])

                    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")
        if not response:
            raise Exception("Controller upload failed")
        
        # End performance tracking
        self.end_time = time.perf_counter()
            return self.__send_response(True, code=200)
        setup_time = (self.end_time - self.start_time) * 1000

        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))
        slices = self.__build_response(self.answer)

        return {
            "slices": slices,
            "setup_time": setup_time
        }
        
    def __nbi_processor(self, intent_json):
        """
@@ -472,52 +469,60 @@ class NSController:
            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):
    def __dump_templates(self, name, file):
        if DUMP_TEMPLATES:
            with open(os.path.join(TEMPLATES_PATH, f"{name}.json"), "w") as archivo:
                archivo.write(json.dumps(file,indent=2))

    def __build_response(self, answer):
        slices = []
        if hasattr(self, "answer") and isinstance(self.answer, dict):
            for subnet, data in self.answer.items():
                slices.append({
                    "id": subnet,
                    "source": data.get("Source"),
                    "destination": data.get("Destination"),
                    "vlan": data.get("VLAN"),
                    "requirements": data.get("QoS Requirements"),
                })
        return slices
    
    def __send_response(self, result, message=None, code=None, data=None):
        """
        Generate and send a response to the 3GPP client about the slice request.
        Generate and send a standardized API response for the 3GPP client.

        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
            result (bool): Indicates whether the slice request was successful.
            message (str, optional): Additional message (success or error).
            code (int, optional): HTTP response code. If not provided, defaults
                                to 200 for success and 400 for error.

        Returns:
            tuple: A tuple containing the response dictionary and status code
            tuple: (response_dict, http_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
            for subnet in self.answer:
                slice_info = {
                    "id": subnet,
                    "source": self.answer[subnet]["Source"],
                    "destination": self.answer[subnet]["Destination"],
                    "vlan": self.answer[subnet]["VLAN"],
                    "requirements": self.answer[subnet]["QoS Requirements"],
            # Ensure code is 200 if not provided
            code = code or 200

            response = {
                "success": True,
                "data": data or {},
                "error": None,
            }
                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
            # Ensure code is 400 if not provided
            code = code or 400

            logging.warning(f"Request failed. Reason: {message}")

            response = {
                "success": False,
                "data": None,
                "error": message or "An error occurred while processing the request."
            }
        return self.cool_answer, code

        return response, code

    def __extract_data(self, intent_json):
        """
@@ -582,6 +587,32 @@ class NSController:
        with open(file_path, 'w') as file:
            json.dump(content, file, indent=4)

    def __send_controller(self, controller_type, requests):
        if not DUMMY_MODE:
            if controller_type == "TFS":
                if UPLOAD_TYPE == "WEBUI":
                    response = tfs_connector().webui_post(TFS_IP, requests)
                elif UPLOAD_TYPE == "NBI":
                    for intent in requests["services"]:
                        # Send each separate NBI request
                        response = tfs_connector().nbi_post(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}")
                
                # For deploying an L2VPN with path selection (not supported by Teraflow)
                if TFS_L2VPN_SUPPORT:
                    self.__tfs_l2vpn_support(requests["services"])

                logging.info("Request sent to Teraflow")
            elif controller_type == "IXIA":
                neii_controller = NEII_controller()
                for intent in requests["services"]:
                    # Send each separate IXIA request
                    response = neii_controller.nscNEII(intent)
                logging.info("Requests sent to Ixia")
            return response
        else: return True
    ### NBI processor functionalities
    def __detect_format(self,json_data):    
        """
@@ -726,13 +757,13 @@ class NSController:
                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"]:
                        flexibility = (slo["bound"] - nrp_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"]:
                        flexibility = (nrp_slo["bound"] - 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
+24 −24
Original line number Diff line number Diff line
from flask import request
from flask_restx import Namespace, Resource, fields, reqparse
from flask_restx import Namespace, Resource, reqparse
from src.network_slice_controller import NSController
import json
from swagger.models.create_models import create_gpp_nrm_28541_model, create_ietf_network_slice_nbi_yang_model
@@ -13,13 +13,13 @@ ixia_ns = Namespace(
gpp_network_slice_request_model = create_gpp_nrm_28541_model(ixia_ns)

# IETF draft-ietf-teas-ietf-network-slice-nbi-yang Data models

slice_ddbb_model, slice_response_model = create_ietf_network_slice_nbi_yang_model(ixia_ns)

upload_parser = reqparse.RequestParser()
upload_parser.add_argument('file', location='files', type='FileStorage', help="Archivo a subir")
upload_parser.add_argument('json_data', location='form', help="Datos JSON en formato string")


# Namespace Controllers
@ixia_ns.route("/slice")
class IxiaSliceList(Resource):
@@ -30,53 +30,50 @@ class IxiaSliceList(Resource):
    def get(self):
        """Retrieve all slices"""
        controller = NSController(controller_type="IXIA")
        return controller.get_flows()
        data, code = controller.get_flows()
        return data, code
    
    @ixia_ns.doc(summary="Submit a transport network slice request", description="This endpoint allows clients to submit transport network slice requests using a JSON payload.")
    @ixia_ns.response(200, "Slice request successfully processed", slice_response_model)
    @ixia_ns.response(201, "Slice created successfully", slice_response_model)
    @ixia_ns.response(400, "Invalid request format")
    @ixia_ns.response(500, "Internal server error")
    @ixia_ns.expect(upload_parser)
    def post(self):
        """Submit a new slice request with a file"""

        json_data = None

        # Try to get the JSON data from the uploaded file
        uploaded_file = request.files.get('file')
        if uploaded_file:
            if not uploaded_file.filename.endswith('.json'):
                return {"error": "Only JSON files allowed"}, 400
            
                return {"success": False, "data": None, "error": "Only JSON files allowed"}, 400
            try:
                json_data = json.load(uploaded_file)  # Convert file to JSON
                json_data = json.load(uploaded_file)
            except json.JSONDecodeError:
                return {"error": "JSON file not valid"}, 400
                return {"success": False, "data": None, "error": "JSON file not valid"}, 400

        # If no file was uploaded, try to get the JSON data from the form
        if json_data is None:
            raw_json = request.form.get('json_data')
            if raw_json:
                try:
                    json_data = json.loads(raw_json)  # Convert string to JSON
                    json_data = json.loads(raw_json)
                except json.JSONDecodeError:
                    return {"error": "JSON file not valid"}, 400
                    return {"success": False, "data": None, "error": "JSON file not valid"}, 400

        # If no JSON data was found, return an error
        if json_data is None:
            return {"error": "No data sent"}, 400
            return {"success": False, "data": None, "error": "No data sent"}, 400

        # Process the JSON data with the NSController
        controller = NSController(controller_type="IXIA")
        return controller.add_flow(json_data)
        data, code = controller.add_flow(json_data)
        return data, code
    
    @ixia_ns.doc(summary="Delete all transport network slices", description="Deletes all transport network slices from the slice controller.")
    @ixia_ns.response(200, "All transport network slices deleted successfully.")
    @ixia_ns.response(204, "All transport network slices deleted successfully.")
    @ixia_ns.response(500, "Internal server error")
    def delete(self):
        """Delete all slices"""
        controller = NSController(controller_type="IXIA")
        return controller.delete_flows()
        data, code = controller.delete_flows()
        return data, code


@ixia_ns.route("/slice/<string:slice_id>")
@@ -89,16 +86,18 @@ class IxiaSlice(Resource):
    def get(self, slice_id):
        """Retrieve a specific slice"""
        controller = NSController(controller_type="IXIA")
        return controller.get_flows(slice_id)
        data, code = controller.get_flows(slice_id)
        return data, code

    @ixia_ns.doc(summary="Delete a specific transport network slice", description="Deletes a specific transport network slice from the slice controller based on the provided `slice_id`.")
    @ixia_ns.response(200, "Transport network slice deleted successfully.")
    @ixia_ns.response(204, "Transport network slice deleted successfully.")
    @ixia_ns.response(404, "Transport network slice not found.")
    @ixia_ns.response(500, "Internal server error")
    def delete(self, slice_id):
        """Delete a slice"""
        controller = NSController(controller_type="IXIA")
        return controller.delete_flows(slice_id)
        data, code = controller.delete_flows(slice_id)
        return data, code

    @ixia_ns.expect(slice_ddbb_model, validate=True)
    @ixia_ns.doc(summary="Modify a specific transport network slice", description="Returns a specific slice that has been modified")
@@ -109,4 +108,5 @@ class IxiaSlice(Resource):
        """Modify a slice"""
        json_data = request.get_json()
        controller = NSController(controller_type="IXIA")
        return controller.modify_flow(slice_id, json_data)
 No newline at end of file
        data, code = controller.modify_flow(slice_id, json_data)
        return data, code
 No newline at end of file
+35 −20
Original line number Diff line number Diff line
@@ -300,27 +300,42 @@ def create_ietf_network_slice_nbi_yang_model(slice_ns):
    slice_response_model = slice_ns.model(
        "SliceResponse",
        {
            "status": fields.String(description="Status of the request", example="success"),
            "success": fields.Boolean(description="Indicates if the request was successful", example=True),
            "data": fields.Nested(
                slice_ns.model(
                    "SliceData",
                    {
                        "slices": fields.List(
                            fields.Nested(
                                slice_ns.model(
                                    "SliceDetails",
                                    {
                            "id": fields.String(description="Slice ID", example="CU-UP1_DU1"),
                            "source": fields.String(description="Source IP", example="100.2.1.2"),
                            "destination": fields.String(description="Destination IP", example="100.1.1.2"),
                                        "id": fields.String(description="Slice ID", example="slice-service-11327140-7361-41b3-aa45-e84a7fb40be9"),
                                        "source": fields.String(description="Source IP", example="10.60.11.3"),
                                        "destination": fields.String(description="Destination IP", example="10.60.60.105"),
                                        "vlan": fields.String(description="VLAN ID", example="100"),
                            "bandwidth(Mbps)": fields.Integer(
                                description="Bandwidth in Mbps", example=120
                            ),
                            "latency(ms)": fields.Integer(
                                description="Latency in milliseconds", example=4
                                        "requirements": fields.List(
                                            fields.Nested(
                                                slice_ns.model(
                                                    "SliceRequirement",
                                                    {
                                                        "constraint_type": fields.String(description="Type of constraint", example="one-way-bandwidth[kbps]"),
                                                        "constraint_value": fields.String(description="Constraint value", example="2000")
                                                    }
                                                )
                                            ),
                        },
                                            description="List of requirements for the slice"
                                        )
                                    }
                                )
                            ),
                description="List of slices",
                            description="List of slices"
                        ),
        },
                        "setup_time": fields.Float(description="Slice setup time in milliseconds", example=12.57),
                    }
                )
            ),
            "error": fields.String(description="Error message if request failed", example=None)
        }
    )
    return slice_ddbb_model, slice_response_model
 No newline at end of file
+38 −16

File changed.

Preview size limit exceeded, changes collapsed.