Skip to content
Snippets Groups Projects
network_slice_controller.py 48 KiB
Newer Older
# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
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
from datetime import datetime
from src.helpers import tfs_connector, cisco_connector
from src.Constants import DEFAULT_LOGGING_LEVEL, TFS_UPLOAD, TFS_IP, TFS_L2VPN_SUPPORT, SRC_PATH, TEMPLATES_PATH

# 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, upload_to_tfs = TFS_UPLOAD, tfs_ip=TFS_IP, need_l2vpn_support=TFS_L2VPN_SUPPORT):
        """
        Initialize the Network Slice Controller.

        Args:
            upload_to_tfs (bool, optional): Flag to determine if configurations 
                should be uploaded to Teraflow system. Defaults to False.
            need_l2vpn_support (bool, optional): Flag to determine if additional
                L2VPN configuration support is required. Defaults to False.
        
        Attributes:
            upload_to_tfs (bool): Flag for Teraflow upload
            answer (dict): Stores slice creation responses
            tfs_requests (dict): Stores requests to be sent to Teraflow
            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
        """
        self.upload_to_tfs = upload_to_tfs
        self.tfs_ip = tfs_ip
        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
            return content
        
        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:
            - If upload_to_tfs is True, attempts to delete from Teraflow
            - 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):
                    if slice["slice_id"] == slice_id:
                        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
202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611
                    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
                if self.upload_to_tfs == True:
                    # TODO: should send a delete request to Teraflow
                    if self.need_l2vpn_support:
                        self.__tfs_l2vpn_delete()

                # Verify slices exist before deletion
                with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'r') as file:
                    if len(json.load(file)) == 0:
                        raise ValueError("Transport network slices not found")
                    
                # Clear slice database
                with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'w') as file:
                    json.dump([], 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.")
        
        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"))  
            tfs_requests = {"services":[]}
            
            # Process intent (translate if 3GPP)
            ietf_intents = self.__nbi_processor(intent_json)

            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)
                    tfs_requests["services"].append(tfs_request)
            else:
                return self.__send_response(False, code=404, message="No intents found")

            # Generated service
            logging.debug(json.dumps(tfs_requests, indent=2))
            
            # Optional: Upload template to Teraflow
            if self.upload_to_tfs == True:
                response = tfs_connector().simple_post(self.tfs_ip, tfs_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(tfs_requests["services"])

                logging.info("Request sent to Teraflow")

            # 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 = []
        logging.info("--------NEW REQUEST--------")

        # 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.
        """ 
        # 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"]

        # 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

    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
            return self.__select_way("L2VPN", ietf_intent)

    ### 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
            for subnet in self.answer:
                slice_info = {
                    "id": subnet,
                    "source": self.answer[subnet]["Source"],
                    "destination": self.answer[subnet]["Destination"],
                    "vlan": self.answer[subnet]["VLAN"],
                    "bandwidth(Mbps)": self.answer[subnet]["QoS Requirements"][0],
                    "latency(ms)": self.answer[subnet]["QoS Requirements"][1]
                }
                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"],
                    "intent": intent
                })
        
        # # 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"]
        ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"][0]["bound"] = gpp_intent[subnet]["SliceProfileList"][0]["RANSliceSubnetProfile"]["uLThptPerSliceSubnet"]["MaxThpt"]
        ietf_i["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"][1]["bound"] = gpp_intent[subnet]["SliceProfileList"][0]["RANSliceSubnetProfile"]["uLLatency"]

        # 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]}"

        # Log translated intent for debugging
        logging.debug(json.dumps(ietf_i,indent=2))
        with open(os.path.join(TEMPLATES_PATH, "ietf_template_example.json"), "w") as archivo:
            archivo.write(json.dumps(ietf_i,indent=2))
        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
    
    def __planner(self, intent, nrp_view):
        """
        TODO
        Request slice viability from the Planner module.

        This method prepares and sends a viability request for network slice creation, 
        detaching the implementation from the main core thread.

        Args:
            intent (dict): Network slice intent
            nrp_view (list): Current Network Resource Pool view

        Returns:
            tuple: A tuple containing:
                - Viability status (str): "Good" or other status
                - Reason (str): Explanation of viability
        
        Notes:
            - Calculates source and destination service delivery points (SDP)
            - Extracts QoS requirements
            - Performs viability check through internal methods
        """
        
        #Version 1
        matriz = {}
        matriz["payloads"] = []
        #for i in intent:
        # SI ya existe, suma, si no existe, lo crea
        origen = self.__calculate_sdp(intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"])
        destino = self.__calculate_sdp(intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"])
        #qos_req = i["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["slo-sle-policy"]["slo-sle-template"]
        qos_req = intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"][0]["bound"]
        payload = {
            "Source": origen,
            "Destination": destino,
            "QoS requirements": qos_req
        }
        matriz["payloads"].append(payload)
        m_res = []
        m_qos = []
        for p in matriz["payloads"]:
            res = p
            m_res.append(res)
            m_qos.append(p["QoS requirements"])
        m_res, m_qos = self.__viability(m_res,intent, m_qos)
        reason="Viable"
        viability = "Good"
        return viability, reason

    def __viability(self, matrix, intent, qos):
        """
        TODO
        """
        aux = {}
        aux["good"] = []
        aux["bad"] = []
        l_qos = []
        for i in range(len(intent)):
            # if matrix[i]['success'] == True:
            aux["good"].append(matrix[i])
            l_qos.append(qos[i])
            # else:
            # aux["bad"].append(intent[i])
        return aux, l_qos

    def __calculate_sdp(self, pip):
        '''
        TODO
        Imput:
        Output:
        Work: Identifies the network entpoint from the IP-sdp received. 
            Version 0 will be done directly with next hop.
            Version 1 will translate directly from the public IP of each node to their Loopback address.
        '''
        nid = 0
        with open(os.path.join(TEMPLATES_PATH, "ips.json"), 'r') as source:
            jason = source.read()
            jason = jason.replace('\t', '').replace('\n', '').replace("'", '"').strip()
            nodos = json.loads(str(jason))
            #template = json.loads(str(jason))
            # Once we have the template, we search for the one charged.
            for nodo in nodos["public-prefixes"]:
                if pip == nodo["prefix"]:
                    #nid = nodo["node-id"]
                    nid = nodo["node-name"]
            for nodo in nodos["CU"]:
                if pip == nodo["prefix"]:
                    #nid = nodo["node-id"]
                    nid = nodo["node-name"]
            for nodo in nodos["DU"]:
                if pip == nodo["prefix"]:
                    #nid = nodo["node-id"]
                    nid = nodo["node-name"]
        return nid


    ### 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 = ""
    
    def __select_way(self,way, ietf_intent):
        """
        Determine the method of slice realization.

        Args:
            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.

        """
        if way == "L2VPN":
            realizing_request = self.__tfs_l2vpn(ietf_intent)
        elif way == "L3VPN":
            realizing_request = self.__tfs_l3vpn(ietf_intent)
        return realizing_request
    
    def __tfs_l2vpn(self, ietf_intent):
        """
        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

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

        Returns:
            dict: A TeraFlow service request for L2VPN configuration.

        """
        # 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"]
Javier Velázquez's avatar
Javier Velázquez committed
        origin_router_if = '0/0/0-GigabitEthernet0/0/0/0'
        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/3-GigabitEthernet0/0/0/3'

        # 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
        
        # 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

        self.answer[self.subnet]["QoS Requirements"] = []
        # Add service constraints
        for i, constraint in enumerate(tfs_request["service_constraints"]):
            bound = ietf_intent["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"][i]["bound"]
            self.answer[self.subnet]["QoS Requirements"].append(bound)
            constraint["custom"]["constraint_value"] = str(bound)

        # 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"]
            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"

        # Log and store VLAN information
        logging.info(f"Intent with VLAN {vlan_value} realized\n")
        self.answer[self.subnet]["VLAN"] = vlan_value
        return tfs_request
    
    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()
        cisco_source.execute_commands(commands)

        #cisco_destiny = cisco_connector(destination_address, ni_name, remote_router, vlan, vlan % 10 + 1)
        cisco_destiny = cisco_connector(destinations["destination"], destinations["config"])
        commands = cisco_destiny.full_create_command_template()
        cisco_destiny.execute_commands(commands)

    def __tfs_l2vpn_delete(self):
        """
        Delete L2VPN configurations from Cisco devices.

        This method removes L2VPN configurations from Cisco routers

        Notes:
            - Uses cisco_connector to generate and execute deletion commands
            - Clears Network Interface (NI) settings
        """
        # Delete Source Endpoint Configuration
        source_address = "10.60.125.44"
        cisco_source = cisco_connector(source_address)
        cisco_source.execute_commands(cisco_source.create_command_template_delete())

        # Delete Destination Endpoint Configuration
        destination_address = "10.60.125.45"
        cisco_destiny = cisco_connector(destination_address)
        cisco_destiny.execute_commands(cisco_destiny.create_command_template_delete())
    
    def __tfs_l3vpn(self, ietf_intent):
        """
        Translate L3VPN (Layer 3 Virtual Private Network) intent into a TeraFlow service request.

        Similar to __tfs_l2vpn, but configured for Layer 3 VPN:
        1. Defines endpoint routers
        2. Loads service template
        3. Generates unique service UUID
        4. Configures service endpoints