diff --git a/app.py b/app.py index 1e34ec5486daeb0eb38f32e84494ba5f4fcb4db0..61503b3b4600483708559bac25bee7cb4588a2a6 100644 --- a/app.py +++ b/app.py @@ -14,11 +14,14 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. +import os from flask import Flask from flask_restx import Api from flask_cors import CORS -from swagger.slice_namespace import slice_ns -from src.Constants import NSC_PORT +from swagger.tfs_namespace import tfs_ns +from swagger.ixia_namespace import ixia_ns +from src.Constants import NSC_PORT, WEBUI_DEPLOY +from src.webui.gui import gui_bp app = Flask(__name__) CORS(app) @@ -33,7 +36,14 @@ api = Api( ) # Register namespaces -api.add_namespace(slice_ns, path="/slice") +api.add_namespace(tfs_ns, path="/tfs") +api.add_namespace(ixia_ns, path="/ixia") +#gui_bp = Blueprint('gui', __name__, template_folder='templates') + +if WEBUI_DEPLOY: + app.secret_key = 'clave-secreta-dev' + app.register_blueprint(gui_bp) + if __name__ == "__main__": app.run(host="0.0.0.0", port=NSC_PORT, debug=True) diff --git a/src/Constants.py b/src/Constants.py index 337be8856e4a91907a76466498fb6cb57634bca9..c88b5b9b1b8bfca46acef49f4abf34dbc49b3d1e 100644 --- a/src/Constants.py +++ b/src/Constants.py @@ -14,7 +14,8 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. -import logging, os +import logging, os, json + # Default logging level DEFAULT_LOGGING_LEVEL = logging.INFO @@ -24,14 +25,22 @@ NSC_PORT = 8081 # Paths # Obtain the absolute path of the current file SRC_PATH = os.path.dirname(os.path.abspath(__file__)) +with open(os.path.join(SRC_PATH, 'IPs.json')) as f: + ips = json.load(f) # Create the path to the desired file relative to the current file TEMPLATES_PATH = os.path.join(SRC_PATH, "templates") -# TFS Flags -# Flag to determine if configurations should be uploaded to Teraflow -TFS_UPLOAD = False +# Controller Flags +# If True, config is not sent to controllers +DUMMY_MODE = True # Teraflow IP -TFS_IP = "192.168.165.10" +TFS_IP = ips.get('TFS_IP') # Flag to determine if additional L2VPN configuration support is required for deploying L2VPNs with path selection -TFS_L2VPN_SUPPORT = False \ No newline at end of file +TFS_L2VPN_SUPPORT = False +# IXIA NEII IP +IXIA_IP = ips.get('IXIA_IP') + +# WebUI +# Flag to deploy the WebUI +WEBUI_DEPLOY = True \ No newline at end of file diff --git a/src/IPs.json b/src/IPs.json new file mode 100644 index 0000000000000000000000000000000000000000..e56bdb0ea8dbb92225dae67bdda6176929eb211a --- /dev/null +++ b/src/IPs.json @@ -0,0 +1,4 @@ +{ + "TFS_IP": "192.168.27.165", + "IXIA_IP": "192.168.27.59" +} \ No newline at end of file diff --git a/src/network_slice_controller.py b/src/network_slice_controller.py index 1669cbf9d881593b2b2d8ba1c0281306897ed855..d280ebb62ee9ede47ed03956dd3695f1cfb86cd6 100644 --- a/src/network_slice_controller.py +++ b/src/network_slice_controller.py @@ -14,10 +14,11 @@ # This file includes original contributions from Telefonica Innovación Digital S.L. -import json, time, os, logging, uuid +import json, time, os, logging, uuid, traceback, sys 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 +from src.Constants import DEFAULT_LOGGING_LEVEL, TFS_IP, TFS_L2VPN_SUPPORT, IXIA_IP, SRC_PATH, TEMPLATES_PATH, DUMMY_MODE +from src.realizers.ixia.NEII_V4 import NEII_controller # Configure logging to provide clear and informative log messages logging.basicConfig( @@ -40,25 +41,24 @@ class NSController: - 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): + def __init__(self, controller_type = "TFS", tfs_ip=TFS_IP, ixia_ip =IXIA_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. + 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: - upload_to_tfs (bool): Flag for Teraflow upload + controller_type (str): Flag for Teraflow or Ixia 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.controller_type = controller_type self.tfs_ip = tfs_ip self.answer = {} self.cool_answer = {} @@ -132,7 +132,7 @@ class NSController: raise ValueError("Transport network slices not found") # Return all slices if no specific ID is given - return content + return [slice for slice in content if slice.get("controller") == self.controller_type] except ValueError as e: # Handle case where no slices are found @@ -181,7 +181,7 @@ class NSController: Exception: For unexpected errors during deletion process Notes: - - If upload_to_tfs is True, attempts to delete from Teraflow + - If controller_type is TFS, attempts to delete from Teraflow - If need_l2vpn_support is True, performs additional L2VPN cleanup """ try: @@ -193,7 +193,7 @@ class NSController: # Delete specific slice if slice_id is provided if slice_id: for i, slice in enumerate(content): - if slice["slice_id"] == slice_id: + if slice["slice_id"] == slice_id and slice.get("controller") == self.controller_type: del content[i] id = i break @@ -209,19 +209,21 @@ class NSController: # Delete all slices else: # Optional: Delete in Teraflow if configured - if self.upload_to_tfs == True: + if self.controller_type == "TFS": # TODO: should send a delete request to Teraflow if self.need_l2vpn_support: self.__tfs_l2vpn_delete() + data_removed = [slice for slice in content if slice.get("controller") == self.controller_type] + # 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") - + 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] # Clear slice database with open(os.path.join(SRC_PATH, "slice_ddbb.json"), 'w') as file: - json.dump([], file, indent=4) + 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.") @@ -259,7 +261,7 @@ class NSController: # Reset requests and load IETF template self.__load_template(1, os.path.join(TEMPLATES_PATH, "ietf_template_empty.json")) - tfs_requests = {"services":[]} + requests = {"services":[]} # Process intent (translate if 3GPP) ietf_intents = self.__nbi_processor(intent_json) @@ -273,25 +275,32 @@ class NSController: self.__mapper(intent) # Realizer tfs_request = self.__realizer(intent) - tfs_requests["services"].append(tfs_request) + 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)) + logging.debug(json.dumps(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 DUMMY_MODE: + if self.controller_type == "TFS": + response = tfs_connector().simple_post(self.tfs_ip, 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"]) + 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") + 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") # End performance tracking self.end_time = time.perf_counter() @@ -321,7 +330,7 @@ class NSController: # Detect the input JSON format (3GPP or IETF) format = self.__detect_format(intent_json) ietf_intents = [] - logging.info("--------NEW REQUEST--------") + logging.debug("--------NEW REQUEST--------") # TODO Needs to be generalized to support different names of slicesubnets # Process different input formats @@ -334,6 +343,8 @@ class NSController: # If already in IETF format, add directly logging.info(f"IETF intent received") ietf_intents.append(intent_json) + with open(os.path.join(TEMPLATES_PATH, "ietf_template_example.json"), "w") as archivo: + archivo.write(json.dumps(ietf_intents,indent=2)) else: # Handle unrecognized format logging.error(f"JSON request format not recognized") @@ -364,29 +375,30 @@ class NSController: # 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 + 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 def __realizer(self, ietf_intent, need_nrp=False, order=None, nrp=None): """ @@ -407,7 +419,8 @@ class NSController: self.__nrp(order, nrp) else: # Select slice service method - return self.__select_way("L2VPN", ietf_intent) + 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) ### Generic functionalities def __load_template(self, which, dir_t): @@ -463,14 +476,14 @@ class NSController: "setup_time": self.setup_time } # Add slice details to the response + logging.info(self.answer) 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] + "requirements": self.answer[subnet]["QoS Requirements"], } answer["slices"].append(slice_info) self.cool_answer = answer @@ -539,7 +552,8 @@ class NSController: content.append( { "slice_id": intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"], - "intent": intent + "intent": intent, + "controller": self.controller_type, }) # # Write updated content back to file @@ -599,8 +613,38 @@ class NSController: # 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"] + + 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 + }) + # 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()}" @@ -828,11 +872,15 @@ class NSController: logging.debug("Updating NRP") answer = "" - def __select_way(self,way, ietf_intent): + def __select_way(self, controller=None, way=None, ietf_intent=None): """ Determine the method of slice realization. Args: + controller (str): The controller to use for slice realization. + Supported values: + - "IXIA": IXIA NEII for network testing + - "TFS": TeraFlow Service for network slice management way (str): The type of technology to use. Supported values: - "L2VPN": Layer 2 Virtual Private Network @@ -844,10 +892,20 @@ class NSController: dict: A realization request for the specified network slice type. """ - if way == "L2VPN": + 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.") 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): @@ -874,7 +932,7 @@ class NSController: 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"] 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"] - destination_router_if = '0/0/3-GigabitEthernet0/0/0/3' + 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"] @@ -895,10 +953,18 @@ class NSController: self.answer[self.subnet]["QoS Requirements"] = [] # Add service constraints - for i, constraint in enumerate(tfs_request["service_constraints"]): + 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"] - self.answer[self.subnet]["QoS Requirements"].append(bound) - constraint["custom"]["constraint_value"] = str(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"]) + tfs_request["service_constraints"].append(service_constraint) # Add configuration rules for i, config_rule in enumerate(tfs_request["service_config"]["config_rules"][1:], start=1): @@ -908,14 +974,15 @@ class NSController: 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) + 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" # Log and store VLAN information - logging.info(f"Intent with VLAN {vlan_value} realized\n") + logging.info(f"L2VPN Intent realized\n") self.answer[self.subnet]["VLAN"] = vlan_value return tfs_request @@ -1012,7 +1079,7 @@ class NSController: 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"] 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"] - destination_router_if = '0/0/3-GigabitEthernet0/0/0/3' + destination_router_if = '0/0/0-GigabitEthernet0/0/0/0' # Load L3VPN service template self.__load_template(2, os.path.join(TEMPLATES_PATH, "L3-VPN_template_empty.json")) @@ -1026,10 +1093,20 @@ class NSController: 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"]): + 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"] - constraint["custom"]["constraint_value"] = str(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"]) + tfs_request["service_constraints"].append(service_constraint) # Add configuration rules for i, config_rule in enumerate(tfs_request["service_config"]["config_rules"][1:], start=1): @@ -1047,11 +1124,100 @@ class NSController: resource_value["ni_name"] = 'ELAN{:s}'.format(str(vlan_value)) config_rule["custom"]["resource_key"] = f"/device[{router_id}]/endpoint[{router_if}]/settings" + logging.info(f"L3VPN Intent realized\n") + self.answer[self.subnet]["VLAN"] = vlan_value + with open(os.path.join(TEMPLATES_PATH, "L3-VPN_template_example.json"), "w") as archivo: + archivo.write(json.dumps(tfs_request,indent=2)) return tfs_request + def __ixia(self, ietf_intent): + """ + Prepare an Ixia service request based on the IETF intent. + This method configures an Ixia 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 + Args: + ietf_intent (dict): IETF-formatted network slice intent. + Returns: + dict: An Ixia service request for configuration. + """ + self.answer[self.subnet]["QoS Requirements"] = [] + # Add service constraints + 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"]) + self.answer[self.subnet]["VLAN"] = ietf_intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["value"] + # Extraer la lista de métricas de forma segura + metric_bounds = ietf_intent.get("ietf-network-slice-service:network-slice-services", {}) \ + .get("slo-sle-templates", {}) \ + .get("slo-sle-template", [{}])[0] \ + .get("slo-policy", {}) \ + .get("metric-bound", []) + + # Inicializar valores + bandwidth = None + latency = None + tolerance = None + + # Asignar valores según el tipo de métrica + for metric in metric_bounds: + metric_type = metric.get("metric-type") + bound = metric.get("bound") + + if metric_type == "one-way-bandwidth": + bandwidth = bound + elif metric_type == "one-way-delay-maximum": + latency = bound + elif metric_type == "one-way-delay-variation-maximum": + tolerance = bound + + # Construcción del diccionario intent + intent = { + "src_node_ip": ietf_intent.get("ietf-network-slice-service:network-slice-services", {}) + .get("slice-service", [{}])[0] + .get("sdps", {}).get("sdp", [{}])[0] + .get("attachment-circuits", {}).get("attachment-circuit", [{}])[0] + .get("sdp-peering", {}).get("peer-sap-id"), + + "dst_node_ip": ietf_intent.get("ietf-network-slice-service:network-slice-services", {}) + .get("slice-service", [{}])[0] + .get("sdps", {}).get("sdp", [{}, {}])[1] + .get("attachment-circuits", {}).get("attachment-circuit", [{}])[0] + .get("sdp-peering", {}).get("peer-sap-id"), + + "vlan_id": ietf_intent.get("ietf-network-slice-service:network-slice-services", {}) + .get("slice-service", [{}])[0] + .get("sdps", {}).get("sdp", [{}])[0] + .get("service-match-criteria", {}).get("match-criterion", [{}])[0] + .get("value"), + + "bandwidth": bandwidth, + "latency": latency, + "tolerance": tolerance, + + "latency_version": ietf_intent.get("ietf-network-slice-service:network-slice-services", {}) + .get("slo-sle-templates", {}).get("slo-sle-template", [{}])[0] + .get("description"), + + "reliability": ietf_intent.get("ietf-network-slice-service:network-slice-services", {}) + .get("slo-sle-templates", {}).get("slo-sle-template", [{}])[0] + .get("sle-policy", {}).get("reliability"), + } - + logging.info(f"IXIA Intent realized\n") + return intent diff --git a/src/realizers/ixia/NEII_V4.py b/src/realizers/ixia/NEII_V4.py new file mode 100644 index 0000000000000000000000000000000000000000..64a13318f4f4e1239ff5d5d36fd5ff04293b0f49 --- /dev/null +++ b/src/realizers/ixia/NEII_V4.py @@ -0,0 +1,362 @@ +###SPDX-FileCopyrightText: © 2024 Telefónica Innovación Digital S.L. +###SPDX-License-Identifier: AGPL-3.0-or-later + +from .automatizacion_ne2v4 import automatizacion +import ipaddress, logging +from src.Constants import IXIA_IP + +class NEII_controller: + def __init__(self, ixia_ip=IXIA_IP): + self.ixia_ip = ixia_ip + + def menu_principal(self, ip=IXIA_IP): + ''' + Inputs: + Outputs: + Work: The main menu of the application. + Notes: If the file is executed from the terminal, ensure that the import + of "automatizacion_ne2v4" does not include a dot at the beggining. + ''' + ip=input("¿Cuál es la IP del network emulator?: ") + accion=input("¿Qué deseas hacer?: \n(1) Ver información de IP\n(2) Consultar información del Hardware\n(3) Configurar un perfil nuevo\n(4) Consultar perfiles existentes\nSelecciona una opción: ") + if accion=="1": + self.ver_info(ip) + if accion=="2": + self.hardware(ip) + if accion=="3": + self.nuevo_perfil(ip) + if accion=="4": + self.existentes(ip) + return + + ## FUNCIONES MENÚ PRINCIPAL ## + + def ver_info(self,ip): + ''' + Inputs: -ip: the ip where the Axia API is located. + Outputs: + Work: It gives the information of the API. + + ''' + informacion_ip=automatizacion.obtener_informacion_ip(ip) + if informacion_ip: + print(informacion_ip) + + def hardware(self,ip): + ''' + Inputs: -ip: the ip where the Axia API is located. + Outputs: + Work: It gives the information of the hardware. + + ''' + informacion_hardware=automatizacion.obtener_informacion_hardware(ip) + if informacion_hardware: + print(informacion_hardware) + + def nuevo_perfil(self,ip): + ''' + Inputs: -ip: the ip where the Axia API is located. + Outputs: + Work: Creates and configures the profiles requested in the Axia API on the specified port. + Notes: It is NOT required to fill all the information requested. + + ''' + puerto=input("¿Qué puerto quieres configurar?: ") + num_perfiles=int(input("¿Cuantos perfiles quieres añadir?: ")) + configuraciones_totales=[] + for i in range(num_perfiles): + nombre=f"perfil{i+1}" + accion=input("¿Qué deseas configurar?\n1)IPv4\t2)IPv6\n3)VLAN\t4)Delay\n5)Packet Drop\t6)Policer (Rx Bandwidth)\n7)Shaper (Rx Bandwidth)\nPor favor, separa las opciones con comas: ") + opciones=accion.split(',') + configuraciones={} + for opcion in opciones: + opcion=opcion.strip() + if opcion=="1": + source_ip=input("Introduce la IP de origen (IPv4): ") + destination_ip=input("Introduce la IP destino (IPv4): ") + prt=input("¿Qué protocolo quieres usar, IP o TCP? ") + configuraciones['ipv4']=self.ipv4(source_ip,destination_ip,prt) + elif opcion=="2": + source=input("Introduce la IP de origen (IPv6): ") + destination=input("Introduce la IP destino (IPv6): ") + configuraciones['ipv6']=self.ipv6(source, destination) + elif opcion=="3": + vlan_id=int(input("Introduce identificador de VLAN: ")) + configuraciones['vlan']=self.vlan(vlan_id) + elif opcion=="4": + delay_perfil=input("Introduce el delay que quieres introducir en el perfil test_api: ") + configuraciones['ethernetDelay']=self.delay(delay_perfil) + elif opcion=="5": + configuraciones['packetDrop']=self.packetDrop() + elif opcion=="6": + bandwidth=int(input('Introduzca el ancho de banda (Kbps): ')) + configuraciones['policer']=self.policer(bandwidth) + elif opcion=="7": + configuraciones['shaper']=self.shaper() + else: + print(f"Opción '{opcion}' no es válida.") + configuracion_perfil=self.configuracion_total(configuraciones, nombre) + configuraciones_totales.append(configuracion_perfil) + perfil_final = {'profiles': configuraciones_totales} + configuracion_puerto=automatizacion.envio_peticion(ip, puerto, perfil_final) + if configuracion_puerto: + print(configuracion_puerto) + + def existentes(self,ip): + ''' + Inputs: -ip: the ip where the Axia API is located. + Outputs: + Work: Shows the information of a given port- + + ''' + puerto=input("¿Qué puerto quieres consultar?: ") + informacion_puerto=automatizacion.obtener_informacion_puerto(ip, puerto) + if informacion_puerto: + print(informacion_puerto) + + def existentes_auto(self,ip,puerto): + ''' + Inputs: -ip: the ip where the Axia API is located. + - puerto: the port we want to get the information. + Outputs: + Work: Creates and configures the profiles requested in the Axia API on the specified port. + + ''' + informacion_puerto=automatizacion.obtener_informacion_puerto(ip, puerto) + if informacion_puerto: + print(f'info puerto\n{informacion_puerto}') + return informacion_puerto + + ## FUNCION PARA LA GUI DEL NSC ## + + import ipaddress + + def nscNEII(self, json_data): + configuraciones = {} + ip = self.ixia_ip + puerto = "5" + dataProfile = self.existentes_auto(ip, puerto) + + print(f'\n\n{json_data}\n') + + ip_version = json_data.get("ip_version", None) + src_node_ip = json_data.get("src_node_ip", None) + dst_node_ip = json_data.get("dst_node_ip", None) + src_node_ipv6 = json_data.get("src_node_ipv6", None) + dst_node_ipv6 = json_data.get("dst_node_ipv6", None) + vlan_id = json_data.get("vlan_id", None) + bandwidth = json_data.get("bandwidth", None) + latency = json_data.get("latency", None) + latency_version = json_data.get("latency_version", None) + reliability = json_data.get("reliability", None) + tolerance = json_data.get("tolerance", None) + packet_reorder = json_data.get("packet_reorder", None) + num_pack = json_data.get("num_pack", None) + pack_reorder = json_data.get("pack_reorder", None) + num_reorder = json_data.get("num_reorder", None) + max_reorder = json_data.get("max_reorder", None) + drop_version = json_data.get("drop_version", None) + desv_reorder = json_data.get("desv_reorder", None) + packets_drop = json_data.get("packets_drop", None) + drops = json_data.get("drops", None) + desv_drop = json_data.get("desv_drop", None) + + # --- Variables de configuración --- + + # Configuración de IPv4 / IPv6 + if src_node_ip and dst_node_ip: + if isinstance(ipaddress.ip_address(src_node_ip), ipaddress.IPv4Address) and isinstance(ipaddress.ip_address(dst_node_ip), ipaddress.IPv4Address): + configuraciones['ipv4'] = self.ipv4(src_node_ip, dst_node_ip, 5) + if src_node_ipv6 and dst_node_ipv6: + if isinstance(ipaddress.ip_address(src_node_ipv6), ipaddress.IPv6Address) and isinstance(ipaddress.ip_address(dst_node_ipv6), ipaddress.IPv6Address): + configuraciones['ipv6'] = self.ipv6(src_node_ipv6, dst_node_ipv6) + + # VLAN + if vlan_id: + configuraciones['vlan'] = self.vlan(int(vlan_id)) + + # Policer + if bandwidth: + configuraciones['policer'] = self.policer(bandwidth) + + # Latencia + if latency: + if float(latency) > 0: + configuraciones['ethernetDelay'] = self.delay_gui(float(latency), latency_version, float(tolerance)) + + # Packet Reorder + if packet_reorder: + configuraciones['reorder'] = self.packetReorder(num_reorder, pack_reorder, num_pack, max_reorder, packet_reorder, desv_reorder) + + # Packet Reorder when reliability + if reliability: + configuraciones['reorder'] = self.packetReorder(int(reliability)) + + #Dropper + if drop_version: + configuraciones['packetDrop'] = self.packetDrop(drops, packets_drop, drop_version, desv_drop) + + # Agregar perfil + num_profiles = len(dataProfile.get("profiles", [])) + configuracion_perfil = self.configuracion_total(configuraciones, f"profile{num_profiles + 1}") + dataProfile['profiles'].append(configuracion_perfil) + logging.info(f"Configuración del perfil: {configuracion_perfil}") + + # Enviar la configuración + automatizacion.envio_peticion(ip, puerto, dataProfile) + return automatizacion.obtener_informacion_puerto(ip, puerto) + + + ## FUNCIONES DE CONFIGURACIÓN DE PUERTO ## + + def delay(self,delay_perfil): + ''' + Inputs: -delay_perfil: the delay we want to configurate. + Outputs: the information of the delay for the controller. + Work: Creates the configuration JSON for delay of the controller. + + ''' + delay_perfil = input("Enter the delay you want to set in the test_api profile: ") + print(f"delay en main: {delay_perfil}") + delay_type = input("Select one option of stadistics:\n1)None\t2)Gaussian\n3)Internet\n(Type the number)") + configuracion_delay = automatizacion.añadir_configuracion_puerto_delay(delay_perfil,delay_type) + print(f"Config delay:\n{configuracion_delay}") + return configuracion_delay + + def delay_gui(self,delay_perfil, latency_version, max_latency): + ''' + Inputs: -delay_perfil: the delay we want to configurate. + Outputs: the information of the delay for the controller. + Work: Creates the configuration JSON for delay of the controller. + + ''' + print(f'\nPero: {max_latency}\n') + configuracion_delay = automatizacion.añadir_configuracion_puerto_delay(delay_perfil,latency_version,max_latency) + return configuracion_delay + + def ipv4(self,source_ip,destination_ip,prt): + ''' + Inputs: -source_ip: the source IPv4 we want to configurate. + -destination_ip: the destination IPv4 we want to configurate. + -prt: the protocol we want to configurate + Outputs: the information of the IPv4 for the controller. + Work: Creates the configuration JSON for IPv4 of the controller. + Notes: by default, the protocol is TCP (6). + + ''' + source_hx=None + destination_hx=None + protocolo=None + if source_ip: + source_hx=hex(int(ipaddress.IPv4Address(source_ip)))[2:] + if destination_ip: + destination_hx=hex(int(ipaddress.IPv4Address(destination_ip)))[2:] + if prt: + protocolo="4" if prt=="IP" else "6" + configuracion_puerto=automatizacion.añadir_configuracion_puerto_ipv4(source_hx, destination_hx, protocolo) + return configuracion_puerto + + def ipv6(self,source, destination): + ''' + Inputs: -source: the source IPv6 we want to configurate. + -destination: the destination IPv6 we want to configurate. + Outputs: the information of the IPv6 for the controller. + Work: Creates the configuration JSON for IPv6 of the controller. + + ''' + if source: + source=ipaddress.IPv6Address(source).exploded.replace(":", "") + elif not source: source=None + if destination: + destination=ipaddress.IPv6Address(destination).exploded.replace(":", "") + elif not destination: destination=None + configuracion_puerto=automatizacion.añadir_configuracion_puerto_ipv6(source, destination) + return configuracion_puerto + + def vlan(self,vlan_id): + ''' + Inputs: -vlan_id: the VLAN we want to configurate. + Outputs: the information of the VLAN for the controller. + Work: Creates the configuration JSON for VLAN of the controller. + + ''' + configuracion_vlan=automatizacion.añadir_configuracion_VLAN(vlan_id) + return configuracion_vlan + + def packetDrop(self, drop, total,version,dev): + ''' + Inputs: + Outputs: the information of the packet drop configuration for the controller. + Work: Creates the configuration JSON for packet drop of the controller. + + ''' + configuracionPD=automatizacion.añadir_configuración_packetDrop(drop,total,version,dev) + return configuracionPD + + def policer(self,bandwidth): + ''' + Inputs: -bandwidth: the TX bandwdth we want to configurate. + Outputs: the information of the TX bandwidth for the controller. + Work: Creates the configuration JSON for the TX bandwidth of the controller. + + ''' + configuracion_policer=automatizacion.añadir_configuracion_policer(bandwidth) + return configuracion_policer + + def shaper(self): + ''' + Inputs: + Outputs: the information of the RX bandwidth for the controller. + Work: Creates the configuration JSON for the RX bandwidth of the controller. + + ''' + bandwidth=int(input('Introduzca el ancho de banda (Kbps): ')) + configuracion_policer=automatizacion.añadir_configuracion_shaper(bandwidth) + return configuracion_policer + + def packetReorder(self,reorder,packages=None, npackagesreorder=None, maxreord=None, version=None, stev=None): + print(f"\n\n\nNEII ANTES DEL PASO\nPACKAGES:{packages}\nREORDER:{reorder}\nMAXREORD:{maxreord}\nNPACKAGESORDER:{npackagesreorder}\n\n\n") + configuracion_reorder=automatizacion.añadir_configuracion_reorder(packages,reorder,npackagesreorder, maxreord, version, stev) + return configuracion_reorder + + def configuracion_total(self,configuraciones, nombre_perfil): + ''' + Inputs: -configuraciones: the informtion of all the configurations for the controller. + -nombre_perfil: the name of the profile where the information is going to be allocated. + Outputs: the information of a configurated profile. + Work: Creates the configuration JSON for a profile for the controller. + + ''' + perfil = { + 'tag': nombre_perfil, + 'dramAllocation': {}, + 'rules': [], + 'ethernetDelay': {'enabled': False}, + 'packetDrop': {'enabled': False}, + 'enabled':True + } + if 'ipv4' in configuraciones: + ipv4_config = configuraciones['ipv4'] + perfil['dramAllocation'] = ipv4_config['profiles'][0]['dramAllocation'] + perfil['rules'].extend(ipv4_config['profiles'][0]['rules']) + if 'ipv6' in configuraciones: + ipv6_config = configuraciones['ipv6'] + perfil['rules'].extend(ipv6_config['profiles'][0]['rules']) + if 'vlan' in configuraciones: + vlan_config = configuraciones['vlan'] + perfil['rules'].extend(vlan_config['profiles'][0]['rules']) + if 'ethernetDelay' in configuraciones: + perfil['ethernetDelay'] = configuraciones['ethernetDelay']['ethernetDelay'] + if 'packetDrop' in configuraciones: + perfil['packetDrop'] = configuraciones['packetDrop']['packetDrop'] + if 'policer' in configuraciones: + perfil['policer']=configuraciones['policer']['policer'] + if 'shaper' in configuraciones: + perfil['shaper']=configuraciones['shaper']['shaper'] + if 'reorder' in configuraciones: + perfil['reorder']=configuraciones['reorder']['reorder'] + return perfil + +if __name__=="__main__": + controller=NEII_controller() + controller.menu_principal() \ No newline at end of file diff --git a/src/realizers/ixia/automatizacion_ne2v4.py b/src/realizers/ixia/automatizacion_ne2v4.py new file mode 100644 index 0000000000000000000000000000000000000000..bc822a0b24a7a2f327307496cc62d999bef053f7 --- /dev/null +++ b/src/realizers/ixia/automatizacion_ne2v4.py @@ -0,0 +1,454 @@ +###SPDX-FileCopyrightText: © 2024 Telefónica Innovación Digital S.L. +###SPDX-License-Identifier: AGPL-3.0-or-later + +import requests +class automatizacion: + def obtener_informacion_ip(ip): + ''' + Gets the information IP of the NE2 + Args: + ip: IP. + Returns: + A dictionary with the IP infotmationx. + ''' + url= "http://"+ip+"/api/actions/ipInfo" + body= {"ip": ip} + response = requests.get(url, json=body, auth=('admin', 'admin')) + if response.status_code==200: + return response.json() + else: + print(f"error al obtener la informacion de la IP; {response.status_code}") + return None + + def obtener_informacion_hardware(ip): + """ + Obtiene información de una dirección IP. + + Args: + ip: La dirección IP del NE2. + + Returns: + Un diccionario con la información de IP. + """ + url = "http://"+ip+"/api/actions/hwInfo" + body = {"ip": ip} + response = requests.get(url, json=body, auth=('admin', 'admin')) + if response.status_code == 200: + return response.json() + else: + print(f"Error al obtener la información de la IP: {response.status_code}") + return None + + def obtener_informacion_puerto(ip,puerto): + """ + Obtiene información de una dirección IP. + + Args: + ip: La dirección IP del NE2. + + Returns: + Un diccionario con la información de IP. + """ + url = "http://"+ip+"/api/hw/Port/"+puerto + body = {"ip": ip} + response = requests.get(url, json=body, auth=('admin', 'admin')) + if response.status_code == 200: + return response.json() + else: + print(f"Error al obtener la información de la IP: {response.status_code}") + return None + + def añadir_configuracion_puerto_delay(delay, latency_type, max_latency): + """ + Añade una configuración de puerto según su delay a un NE2. + + Args: + delay (int): Cantidad de delay en la simulación. + + Returns: + La respuesta de la API (texto). + """ + configuracion=None + print(f'\n\nTipo de latencia: {latency_type}\n latencia: {delay}\n') + if latency_type=='1' or latency_type==None: + configuracion={'ethernetDelay': {'delay': delay, 'delayMax': 15.0, 'isUncorrelated': False, 'maxNegDelta': 0.1, 'pdvMode': 'NONE', 'delayMin': 5.0, 'units': 'MS', 'maxPosDelta': 0.1, 'enabled': True, 'spread': 1.0}} + if latency_type=='2' or latency_type=='gauss': + delay_f=float(delay) + ancho=float(max_latency) + max_delay=delay_f+ancho + min_delay=delay_f-ancho + configuracion={'ethernetDelay': {'delay': delay, 'delayMax': max_delay, 'isUncorrelated': False,'maxNegDelta': ancho/3, 'pdvMode': 'GAUSSIAN', 'delayMin':min_delay, 'units': 'MS', 'maxPosDelta':ancho/3, 'enabled': True, 'spread': 1.58}} + if latency_type =='3' or latency_type=='internet': + ancho=float(max_latency) + max_delay = float(delay)+0.9*float(ancho) + min_delay = float(delay)-0.1*float(ancho) + configuracion={'ethernetDelay': {'delay': delay, 'delayMax': max_delay, 'isUncorrelated': False, 'maxNegDelta': 0.4, 'pdvMode': 'INTERNET', 'delayMin': min_delay, 'units': 'MS', 'maxPosDelta': 0.5, 'enabled': True, 'spread': 100.0}} + print(f"\n\nConf Delay: {configuracion}\n") + return configuracion + + def añadir_configuracion_puerto_ipv4(source_hx,destination_hx,protocolo): + """ + Añade una configuración de puerto según las IPv4s de origen y destino de la comunicación. + + Args: + source_hx (int): Dirección IPv4 de origen en hexadecimal. + destination_hx (int): Dirección IPv4 de destino en hexadecimal. + protocolo (string): Numero asociado al protocolo elegido. + + Returns: + La respuesta de la API (texto). + """ + reglas = [{"field": "Common::IPv4::Version", "value": "4", "mask": "f"}] + if source_hx: + reglas.append({"field": "Common::IPv4::Source Address", "value": source_hx, "mask": "ffffffff"}) + if destination_hx: + reglas.append({"field": "Common::IPv4::Destination Address", "value": destination_hx, "mask": "ffffffff"}) + if protocolo: + reglas.append({"field": "Common::IPv4::Protocol", "value": protocolo, "mask": "ff"}) + configuracion = {"profiles": [{"dramAllocation": {"mode": "AUTO","fixedSize": 1700352},"rules": reglas,"tag": "test_api"}],"defaultProfile": {"dramAllocation": {"mode": "AUTO","fixedSize": 1700352},"tag": "defaultProfile"}} + return configuracion + + def añadir_configuracion_puerto_ipv6(source,destination): + """ + Añade una configuración de puerto según las IPv6s de origen y destino de la comunicación. + + Args: + source (int): Dirección IPv6 de origen en hexadecimal. + destination (int): Dirección IPv6 de destino en hexadecimal. + + Returns: + La respuesta de la API (texto). + """ + reglas=[{'field': 'Common::IPv6::Version', 'bitRange': 'L3@0[7]+3', 'value': '6', 'mask': 'f'}] + if source: + reglas.append({'field': 'Common::IPv6::Source Address', 'bitRange': 'L3@8[7]+127', 'value': source, 'mask': 'ffffffffffffffffffffffffffffffff'}) + if destination: + reglas.append({'field': 'Common::IPv6::Destination Address', 'bitRange': 'L3@24[7]+127', 'value': destination, 'mask': 'ffffffffffffffffffffffffffffffff'}) + configuracion = {"profiles": [{"dramAllocation": {"mode": "AUTO","fixedSize": 1700352},"rules": reglas,"tag": "test_api"}],"defaultProfile": {"dramAllocation": {"mode": "AUTO","fixedSize": 1700352},"tag": "defaultProfile"}} + return configuracion + + def añadir_configuracion_VLAN(vlan): + """ + Añade una configuración de puerto según su VLAN. + + Args: + vlan (int): Número de identificación de la VLAN. + + Returns: + La respuesta de la API (texto). + """ + vlan=hex(vlan)[2:] + configuracion={'profiles': [{'dramAllocation': {'mode': 'AUTO', 'fixedSize': 1700352}, 'rules': [{'field': 'Common::Second Tag::VLAN ID', 'bitRange': 'L2@18[3]+11', 'value': vlan, 'mask': 'fff'}]}]} + return configuracion + + def añadir_configuración_packetDrop(drops,total): + """ + Añade una configuración de puerto según su configuracion de packet Drop un NE2. + + Args: + drops (int): cantidad de paquetes dropeados + total (int): cantidad total de paquetes + + Returns: + La respuesta de la API (texto). + """ + configuracion={'packetDrop': {'rdmSel': {'dist': 'PERIODIC', 'burstlen': drops, 'interval': total, 'stddev': 10.0}, 'enabled': True}} + return configuracion + + def añadir_configuracion_policer(bitrate): + """ + Añade una configuración de puerto según su TX bandwidth a un NE2. + + Args: + bitrate (int): bandwith en Tx + + Returns: + La respuesta de la API (texto). + """ + configuracion={'policer':{'excessBitRate': bitrate, 'excessBurstTolerance': 64000, 'commitedBurstTolerance': 64000, 'commitedBitRate': bitrate, 'enabled': True, 'enableRateCoupling': False}} + return configuracion + + def añadir_configuracion_shaper(bitrate): + """ + Añade una configuración de puerto según su RX bandwidth a un NE2. + + Args: + bitrate (int): bandwith en Rx + + Returns: + La respuesta de la API (texto). + """ + configuracion={'shaper': {'burstTolerance': 64000, 'bitRate': bitrate, 'enabled': True}} + return configuracion + + def añadir_configuracion_reorder(reorder): + """ + Adds reorder configuration. + + Args: + reorder (int): reorder + + Returns: + response of the API (texto). + """ + reorder=100-reorder + print(f"\nReorder:{reorder}\n") + configuracion={"reorder": {"rdmSel": {"dist": "PERIODIC", "burstlen": reorder, "interval": 100, "stddev": 10.0 }, "reorderByMin": 1, "reorderByMax": 5, "enabled": True}} #Los demas valores los he dejado como defecto por no poder configurarlos + return configuracion + + def envio_peticion(ip,puerto, configuracion): + """ + Envía una configuración de puerto a un NE2. + + Args: + ip (int): IP del NE2 + puerto: número del puerto a configurar + configuración: la información que se quiere configurar en el puerto + + Returns: + La respuesta de la API (texto). + """ + +import requests +class automatizacion: + def obtener_informacion_ip(ip): + ''' + obtiene informacion ip del NE2 + Args: + ip: la direccion IP del NE2. + + Returns: + un diccionario con la informacion de IP. + ''' + url= "http://"+ip+"/api/actions/ipInfo" + body= {"ip": ip} + response = requests.get(url, json=body, auth=('admin', 'admin')) + if response.status_code==200: + return response.json() + else: + print(f"error al obtener la informacion de la IP; {response.status_code}") + return None + + def obtener_informacion_hardware(ip): + """ + Obtiene información de una dirección IP. + + Args: + ip: La dirección IP del NE2. + + Returns: + Un diccionario con la información de IP. + """ + url = "http://"+ip+"/api/actions/hwInfo" + body = {"ip": ip} + response = requests.get(url, json=body, auth=('admin', 'admin')) + if response.status_code == 200: + return response.json() + else: + print(f"Error al obtener la información de la IP: {response.status_code}") + return None + + def obtener_informacion_puerto(ip,puerto): + """ + Obtiene información de una dirección IP. + + Args: + ip: La dirección IP del NE2. + + Returns: + Un diccionario con la información de IP. + """ + url = "http://"+ip+"/api/hw/Port/"+puerto + body = {"ip": ip} + response = requests.get(url, json=body, auth=('admin', 'admin')) + if response.status_code == 200: + return response.json() + else: + print(f"Error al obtener la información de la IP: {response.status_code}") + return None + + def añadir_configuracion_puerto_delay(delay, latency_type, max_latency): + """ + Añade una configuración de puerto según su delay a un NE2. + + Args: + delay (int): Cantidad de delay en la simulación. + + Returns: + La respuesta de la API (texto). + """ + configuracion=None + print(f'\n\nTipo de latencia: {latency_type}\n latencia: {delay}\n') + if latency_type=='1' or latency_type==None: + configuracion={'ethernetDelay': {'delay': delay, 'delayMax': 15.0, 'isUncorrelated': False, 'maxNegDelta': 0.1, 'pdvMode': 'NONE', 'delayMin': 5.0, 'units': 'MS', 'maxPosDelta': 0.1, 'enabled': True, 'spread': 1.0}} + if latency_type=='2' or latency_type=='gauss': + delay_f=float(delay) + ancho=float(max_latency) + max_delay=delay_f+ancho + min_delay=delay_f-ancho + configuracion={'ethernetDelay': {'delay': delay, 'delayMax': max_delay, 'isUncorrelated': False,'maxNegDelta': ancho/3, 'pdvMode': 'GAUSSIAN', 'delayMin':min_delay, 'units': 'MS', 'maxPosDelta':ancho/3, 'enabled': True, 'spread': 1.58}} + if latency_type =='3' or latency_type=='internet': + ancho=float(max_latency) + max_delay = float(delay)+0.9*float(ancho) + min_delay = float(delay)-0.1*float(ancho) + configuracion={'ethernetDelay': {'delay': delay, 'delayMax': max_delay, 'isUncorrelated': False, 'maxNegDelta': 0.4, 'pdvMode': 'INTERNET', 'delayMin': min_delay, 'units': 'MS', 'maxPosDelta': 0.5, 'enabled': True, 'spread': 100.0}} + print(f"\n\nConf Delay: {configuracion}\n") + return configuracion + + def añadir_configuracion_puerto_ipv4(source_hx,destination_hx,protocolo): + """ + Añade una configuración de puerto según las IPv4s de origen y destino de la comunicación. + + Args: + source_hx (int): Dirección IPv4 de origen en hexadecimal. + destination_hx (int): Dirección IPv4 de destino en hexadecimal. + protocolo (string): Numero asociado al protocolo elegido. + + Returns: + La respuesta de la API (texto). + """ + reglas = [{"field": "Common::IPv4::Version", "value": "4", "mask": "f"}] + if source_hx: + reglas.append({"field": "Common::IPv4::Source Address", "value": source_hx, "mask": "ffffffff"}) + if destination_hx: + reglas.append({"field": "Common::IPv4::Destination Address", "value": destination_hx, "mask": "ffffffff"}) + if protocolo: + reglas.append({"field": "Common::IPv4::Protocol", "value": protocolo, "mask": "ff"}) + configuracion = {"profiles": [{"dramAllocation": {"mode": "AUTO","fixedSize": 1700352},"rules": reglas,"tag": "test_api"}],"defaultProfile": {"dramAllocation": {"mode": "AUTO","fixedSize": 1700352},"tag": "defaultProfile"}} + return configuracion + + def añadir_configuracion_puerto_ipv6(source,destination): + """ + Añade una configuración de puerto según las IPv6s de origen y destino de la comunicación. + + Args: + source (int): Dirección IPv6 de origen en hexadecimal. + destination (int): Dirección IPv6 de destino en hexadecimal. + + Returns: + La respuesta de la API (texto). + """ + reglas=[{'field': 'Common::IPv6::Version', 'bitRange': 'L3@0[7]+3', 'value': '6', 'mask': 'f'}] + if source: + reglas.append({'field': 'Common::IPv6::Source Address', 'bitRange': 'L3@8[7]+127', 'value': source, 'mask': 'ffffffffffffffffffffffffffffffff'}) + if destination: + reglas.append({'field': 'Common::IPv6::Destination Address', 'bitRange': 'L3@24[7]+127', 'value': destination, 'mask': 'ffffffffffffffffffffffffffffffff'}) + configuracion = {"profiles": [{"dramAllocation": {"mode": "AUTO","fixedSize": 1700352},"rules": reglas,"tag": "test_api"}],"defaultProfile": {"dramAllocation": {"mode": "AUTO","fixedSize": 1700352},"tag": "defaultProfile"}} + return configuracion + + def añadir_configuracion_VLAN(vlan): + """ + Añade una configuración de puerto según su VLAN. + + Args: + vlan (int): Número de identificación de la VLAN. + + Returns: + La respuesta de la API (texto). + """ + vlan=hex(vlan)[2:] + configuracion={'profiles': [{'dramAllocation': {'mode': 'AUTO', 'fixedSize': 1700352}, 'rules': [{'field': 'Common::Second Tag::VLAN ID', 'bitRange': 'L2@18[3]+11', 'value': vlan, 'mask': 'fff'}]}]} + return configuracion + + def añadir_configuración_packetDrop(drops,total,version,desv): + """ + Añade una configuración de puerto según su configuracion de packet Drop un NE2. + + Args: + drops (int): cantidad de paquetes dropeados + total (int): cantidad total de paquetes + version (string): version of the probability + + Returns: + La respuesta de la API (texto). + """ + if version != "GAUSSIAN" and version !="POISSON": + configuracion={'packetDrop': {'rdmSel': {'dist': version, 'burstlen': drops, 'interval': total, 'stddev': 10.0}, 'enabled': True}} + else: + configuracion={'packetDrop': {'rdmSel': {'dist': version, 'burstlen': drops, 'interval': total, 'stddev': desv}, 'enabled': True}} + return configuracion + + def añadir_configuracion_policer(bitrate): + """ + Añade una configuración de puerto según su TX bandwidth a un NE2. + + Args: + bitrate (int): bandwith en Tx + + Returns: + La respuesta de la API (texto). + """ + configuracion={'policer':{'excessBitRate': bitrate, 'excessBurstTolerance': 64000, 'commitedBurstTolerance': 64000, 'commitedBitRate': bitrate, 'enabled': True, 'enableRateCoupling': False}} + return configuracion + + def añadir_configuracion_shaper(bitrate): + """ + Añade una configuración de puerto según su RX bandwidth a un NE2. + + Args: + bitrate (int): bandwith en Rx + + Returns: + La respuesta de la API (texto). + """ + configuracion={'shaper': {'burstTolerance': 64000, 'bitRate': bitrate, 'enabled': True}} + return configuracion + + def añadir_configuracion_reorder(packages,reorder,npackagesreorder, maxreord, version, stev): + """ + Adds reorder configuration. + + Args: + packages: number of packages total + reorder: number of packages to reorder + npackagesreorder: number of packages of reorder + maxreord: total number of packages of reorder + version: version of reorder + stev: desviation + + Returns: + response of the API (text). + """ + if packages is not None and npackagesreorder is not None and maxreord is not None: + reorderByMin = min(npackagesreorder, maxreord) + reorderByMax = max(npackagesreorder, maxreord) + if version != 'GAUSSIAN': + configuracion={"reorder": {"rdmSel": {"dist": version, "burstlen": reorder, "interval": packages, "stddev": 10.0 }, "reorderByMin": reorderByMin, "reorderByMax": reorderByMax, "enabled": True}} + else: + configuracion={"reorder": {"rdmSel": {"dist": "GAUSSIAN", "burstlen": reorder, "interval": packages, "stddev": stev }, "reorderByMin": reorderByMin, "reorderByMax": reorderByMax, "enabled": True}} + if int(reorder) <= 0: + configuracion["filterWarning"] = "El valor de reorder es inválido, está fuera del rango permitido." + else: + reorder=10000-reorder + configuracion={"reorder": {"rdmSel": {"dist": "PERIODIC", "burstlen": int(reorder/100), "interval": 100, "stddev": 10.0 }, "reorderByMin": 1, "reorderByMax": 5, "enabled": True}} + return configuracion + + def envio_peticion(ip,puerto, configuracion): + """ + Envía una configuración de puerto a un NE2. + + Args: + ip (int): IP del NE2 + puerto: número del puerto a configurar + configuración: la información que se quiere configurar en el puerto + + Returns: + La respuesta de la API (texto). + """ + print(f'\nCONFIGURACION\n{configuracion}') + url = f"http://{ip}/api/hw/Port/{puerto}" + response = requests.put(url, json=configuracion, auth=('admin', 'admin')) + + if response.status_code == 200: + print(f'\n{configuracion}') + return response.text + else: + try: + error_info = response.json() + except ValueError: + error_info = response.text + + print(f"\n\nError al añadir configuración de puerto: {response.status_code}") + print(f"Mensaje de error: {error_info}") + print(f"Configuración enviada: {configuracion}\n\n") + return None \ No newline at end of file diff --git a/src/templates/L2-VPN_template_empty.json b/src/templates/L2-VPN_template_empty.json index 85caed08d492bff56396c2e2e87b5098dee84209..9c2137c7c72479792e0c3608e6781e521f33550d 100644 --- a/src/templates/L2-VPN_template_empty.json +++ b/src/templates/L2-VPN_template_empty.json @@ -12,8 +12,6 @@ {"device_id": {"device_uuid": {"uuid": ""}}, "endpoint_uuid": {"uuid": "eth-1/0/21"}} ], "service_constraints": [ - {"custom": {"constraint_type": "bandwidth[kbps]", "constraint_value": "0"}}, - {"custom": {"constraint_type": "latency[ms]", "constraint_value": "0"}} ], "service_config": {"config_rules": [ {"action": 1, "custom": {"resource_key": "/settings", "resource_value": { diff --git a/src/templates/L2-VPN_template_example.json b/src/templates/L2-VPN_template_example.json index b29e3c69911c332fa5bac045f0f2b994008d629f..2d6af092213d71a5ff011000c466c182fa84be45 100644 --- a/src/templates/L2-VPN_template_example.json +++ b/src/templates/L2-VPN_template_example.json @@ -1,271 +1,95 @@ { - "services": [ - { - "service_id": { - "context_id": { - "context_uuid": { - "uuid": "admin" - } - }, - "service_uuid": { - "uuid": "l2-acl-svc-17429923732568250" - } - }, - "service_type": 2, - "service_status": { - "service_status": 1 - }, - "service_endpoint_ids": [ - { - "device_id": { - "device_uuid": { - "uuid": "4.4.4.4" + "services":[ + { + "service_id":{ + "context_id":{ + "context_uuid":{ + "uuid":"admin" + } + }, + "service_uuid":{ + "uuid":"l2-acl-svc-17429923732568250765476542" } - }, - "endpoint_uuid": { - "uuid": "0/0/0-GigabitEthernet0/0/0/0" - } - }, - { - "device_id": { - "device_uuid": { - "uuid": "5.5.5.5" + }, + "service_type":2, + "service_status":{ + "service_status":1 + }, + "service_endpoint_ids":[ + { + "device_id":{ + "device_uuid":{ + "uuid":"1.1.1.1" + } + }, + "endpoint_uuid":{ + "uuid":"eth0" + } + }, + { + "device_id":{ + "device_uuid":{ + "uuid":"3.3.3.3" + } + }, + "endpoint_uuid":{ + "uuid":"eth0" + } } - }, - "endpoint_uuid": { - "uuid": "0/0/3-GigabitEthernet0/0/0/3" - } - } - ], - "service_constraints": [ - { - "custom": { - "constraint_type": "bandwidth[kbps]", - "constraint_value": "20" - } - }, - { - "custom": { - "constraint_type": "latency[ms]", - "constraint_value": "20" - } - } - ], - "service_config": { - "config_rules": [ - { - "action": 1, - "custom": { - "resource_key": "/settings", - "resource_value": {} + ], + "service_constraints":[ + { + "custom":{ + "constraint_type":"bandwidth[kbps]", + "constraint_value":"20" + } + }, + { + "custom":{ + "constraint_type":"latency[ms]", + "constraint_value":"20" + } } - }, - { - "action": 1, - "custom": { - "resource_key": "/device[4.4.4.4]/endpoint[0/0/0-GigabitEthernet0/0/0/0]/settings", - "resource_value": { - "sub_interface_index": 0, - "vlan_id": 100, - "circuit_id": "100", - "remote_router": "5.5.5.5", - "ni_name": "ELAN100" - } - } - }, - { - "action": 1, - "custom": { - "resource_key": "/device[5.5.5.5]/endpoint[0/0/3-GigabitEthernet0/0/0/3]/settings", - "resource_value": { - "sub_interface_index": 0, - "vlan_id": 100, - "circuit_id": "100", - "remote_router": "4.4.4.4", - "ni_name": "ELAN100" - } - } - } - ] - } - }, - { - "service_id": { - "context_id": { - "context_uuid": { - "uuid": "admin" - } - }, - "service_uuid": { - "uuid": "l2-acl-svc-17429923733224160" - } - }, - "service_type": 2, - "service_status": { - "service_status": 1 - }, - "service_endpoint_ids": [ - { - "device_id": { - "device_uuid": { - "uuid": "4.4.4.4" - } - }, - "endpoint_uuid": { - "uuid": "0/0/0-GigabitEthernet0/0/0/0" - } - }, - { - "device_id": { - "device_uuid": { - "uuid": "5.5.5.5" - } - }, - "endpoint_uuid": { - "uuid": "0/0/3-GigabitEthernet0/0/0/3" - } - } - ], - "service_constraints": [ - { - "custom": { - "constraint_type": "bandwidth[kbps]", - "constraint_value": "200" - } - }, - { - "custom": { - "constraint_type": "latency[ms]", - "constraint_value": "5" - } - } - ], - "service_config": { - "config_rules": [ - { - "action": 1, - "custom": { - "resource_key": "/settings", - "resource_value": {} - } - }, - { - "action": 1, - "custom": { - "resource_key": "/device[4.4.4.4]/endpoint[0/0/0-GigabitEthernet0/0/0/0]/settings", - "resource_value": { - "sub_interface_index": 0, - "vlan_id": 101, - "circuit_id": "101", - "remote_router": "5.5.5.5", - "ni_name": "ELAN101" - } - } - }, - { - "action": 1, - "custom": { - "resource_key": "/device[5.5.5.5]/endpoint[0/0/3-GigabitEthernet0/0/0/3]/settings", - "resource_value": { - "sub_interface_index": 0, - "vlan_id": 101, - "circuit_id": "101", - "remote_router": "4.4.4.4", - "ni_name": "ELAN101" - } - } - } - ] - } - }, - { - "service_id": { - "context_id": { - "context_uuid": { - "uuid": "admin" - } - }, - "service_uuid": { - "uuid": "l2-acl-svc-17429923733753200" - } - }, - "service_type": 2, - "service_status": { - "service_status": 1 - }, - "service_endpoint_ids": [ - { - "device_id": { - "device_uuid": { - "uuid": "4.4.4.4" - } - }, - "endpoint_uuid": { - "uuid": "0/0/0-GigabitEthernet0/0/0/0" - } - }, - { - "device_id": { - "device_uuid": { - "uuid": "5.5.5.5" - } - }, - "endpoint_uuid": { - "uuid": "0/0/3-GigabitEthernet0/0/0/3" - } - } - ], - "service_constraints": [ - { - "custom": { - "constraint_type": "bandwidth[kbps]", - "constraint_value": "200" - } - }, - { - "custom": { - "constraint_type": "latency[ms]", - "constraint_value": "10" - } - } - ], - "service_config": { - "config_rules": [ - { - "action": 1, - "custom": { - "resource_key": "/settings", - "resource_value": {} - } - }, - { - "action": 1, - "custom": { - "resource_key": "/device[4.4.4.4]/endpoint[0/0/0-GigabitEthernet0/0/0/0]/settings", - "resource_value": { - "sub_interface_index": 0, - "vlan_id": 102, - "circuit_id": "102", - "remote_router": "5.5.5.5", - "ni_name": "ELAN102" - } - } - }, - { - "action": 1, - "custom": { - "resource_key": "/device[5.5.5.5]/endpoint[0/0/3-GigabitEthernet0/0/0/3]/settings", - "resource_value": { - "sub_interface_index": 0, - "vlan_id": 102, - "circuit_id": "102", - "remote_router": "4.4.4.4", - "ni_name": "ELAN102" - } - } - } - ] + ], + "service_config":{ + "config_rules":[ + { + "action":1, + "custom":{ + "resource_key":"/settings", + "resource_value":{ + + } + } + }, + { + "action":1, + "custom":{ + "resource_key":"/device[1.1.1.1]/endpoint[eth0]/settings", + "resource_value":{ + "sub_interface_index":0, + "vlan_id":100, + "circuit_id":"100", + "remote_router":"3.3.3.3", + "ni_name":"ELAN100" + } + } + }, + { + "action":1, + "custom":{ + "resource_key":"/device[3.3.3.3]/endpoint[eth0]/settings", + "resource_value":{ + "sub_interface_index":0, + "vlan_id":100, + "circuit_id":"100", + "remote_router":"1.1.1.1", + "ni_name":"ELAN100" + } + } + } + ] + } } - } - ] + ] } \ No newline at end of file diff --git a/src/templates/L3-VPN_template_empty.json b/src/templates/L3-VPN_template_empty.json index e894cfa4208734dc75c822ced467b7aa8e7d67a9..41d1f1bf6562bbf1530df77883978199cb6d9f91 100644 --- a/src/templates/L3-VPN_template_empty.json +++ b/src/templates/L3-VPN_template_empty.json @@ -12,8 +12,6 @@ {"device_id": {"device_uuid": {"uuid": ""}}, "endpoint_uuid": {"uuid": ""}} ], "service_constraints": [ - {"custom": {"constraint_type": "bandwidth[kbps]", "constraint_value": "0"}}, - {"custom": {"constraint_type": "latency[ms]", "constraint_value": "0"}} ], "service_config": {"config_rules": [ {"action": 1, "custom": {"resource_key": "/settings", "resource_value": { diff --git a/src/templates/L3-VPN_template_example.json b/src/templates/L3-VPN_template_example.json index d0a70b7d738121afb27c9ad0cad178fb797d6193..b4334e98a0b6e67e0f42822c7c6a3af41f3baedd 100644 --- a/src/templates/L3-VPN_template_example.json +++ b/src/templates/L3-VPN_template_example.json @@ -1,102 +1,98 @@ { - "services": [ + "service_id": { + "context_id": { + "context_uuid": { + "uuid": "admin" + } + }, + "service_uuid": { + "uuid": "l3-acl-svc-17536941472398060" + } + }, + "service_type": 1, + "service_status": { + "service_status": 1 + }, + "service_endpoint_ids": [ { - "service_id": { - "context_id": { - "context_uuid": { - "uuid": "admin" - } - }, - "service_uuid": { - "uuid": "l3-acl-svc-17258860855224490" + "device_id": { + "device_uuid": { + "uuid": "10.10.10.10" } }, - "service_type": 1, - "service_status": { - "service_status": 1 + "endpoint_uuid": { + "uuid": "0/0/0-GigabitEthernet0/0/0/0" + } + }, + { + "device_id": { + "device_uuid": { + "uuid": "11.11.11.11" + } }, - "service_endpoint_ids": [ - { - "device_id": { - "device_uuid": { - "uuid": "4.4.4.4" - } - }, - "endpoint_uuid": { - "uuid": "0/0/1-GigabitEthernet0/0/0/1" - } - }, - { - "device_id": { - "device_uuid": { - "uuid": "5.5.5.5" - } - }, - "endpoint_uuid": { - "uuid": "0/0/1-GigabitEthernet0/0/0/1" + "endpoint_uuid": { + "uuid": "0/0/0-GigabitEthernet0/0/0/0" + } + } + ], + "service_constraints": [ + { + "custom": { + "constraint_type": "one-way-bandwidth[Mbps]", + "constraint_value": "10" + } + }, + { + "custom": { + "constraint_type": "one-way-delay-maximum[milliseconds]", + "constraint_value": "30" + } + } + ], + "service_config": { + "config_rules": [ + { + "action": 1, + "custom": { + "resource_key": "/settings", + "resource_value": { + "bgp_as": 65000, + "route_distinguisher": "65000:533" } } - ], - "service_constraints": [ - { - "custom": { - "constraint_type": "bandwidth[kbps]", - "constraint_value": "120" - } - }, - { - "custom": { - "constraint_type": "latency[ms]", - "constraint_value": "2" + }, + { + "action": 1, + "custom": { + "resource_key": "/device[10.10.10.10]/endpoint[0/0/0-GigabitEthernet0/0/0/0]/settings", + "resource_value": { + "router_id": "11.11.11.11", + "sub_interface_index": 0, + "vlan_id": 200, + "address_ip": "11.11.11.11", + "address_prefix": 16, + "policy_AZ": "policyA", + "policy_ZA": "policyB", + "ni_name": "ELAN200" } } - ], - "service_config": { - "config_rules": [ - { - "action": 1, - "custom": { - "resource_key": "/settings", - "resource_value": { - "bgp_as": 65000, - "route_distinguisher": "65000:533" - } - } - }, - { - "action": 1, - "custom": { - "resource_key": "/device[4.4.4.4]/endpoint[0/0/1-GigabitEthernet0/0/0/1]/settings", - "resource_value": { - "router_id": "5.5.5.5", - "sub_interface_index": 0, - "vlan_id": 300, - "address_ip": "5.5.5.5", - "address_prefix": 16, - "policy_AZ": "policyA", - "policy_ZA": "policyB", - "ni_name": "ELAN300" - } - } - }, - { - "action": 1, - "custom": { - "resource_key": "/device[5.5.5.5]/endpoint[0/0/1-GigabitEthernet0/0/0/1]/settings", - "resource_value": { - "router_id": "4.4.4.4", - "sub_interface_index": 0, - "vlan_id": 300, - "address_ip": "4.4.4.4", - "address_prefix": 16, - "policy_AZ": "policyA", - "policy_ZA": "policyB", - "ni_name": "ELAN300" - } - } + }, + { + "action": 1, + "custom": { + "resource_key": "/device[11.11.11.11]/endpoint[0/0/0-GigabitEthernet0/0/0/0]/settings", + "resource_value": { + "router_id": "10.10.10.10", + "sub_interface_index": 0, + "vlan_id": 200, + "address_ip": "10.10.10.10", + "address_prefix": 16, + "policy_AZ": "policyA", + "policy_ZA": "policyB", + "ni_name": "ELAN200" } - ] + } } - } - ] + ] + } } \ No newline at end of file diff --git a/src/templates/ietf_template_empty.json b/src/templates/ietf_template_empty.json index b4fb81f6d61100bfa0dcdb17fa0165eb03bd9ed8..fc458ea4b86f06b84a91241f8ceee1578e9570a1 100644 --- a/src/templates/ietf_template_empty.json +++ b/src/templates/ietf_template_empty.json @@ -7,16 +7,6 @@ "description":"", "slo-policy":{ "metric-bound":[ - { - "metric-type":"one-way-bandwidth", - "metric-unit":"kbps", - "bound":"" - }, - { - "metric-type":"one-way-delay-maximum", - "metric-unit":"milliseconds", - "bound":"" - } ] }, "sle-policy":{ diff --git a/src/templates/ietf_template_example.json b/src/templates/ietf_template_example.json index 82782208df2489ba0aba0a27c121f90a71c34d4d..d9a660f5fb41479577561c0668e10a78844f723a 100644 --- a/src/templates/ietf_template_example.json +++ b/src/templates/ietf_template_example.json @@ -1,158 +1,149 @@ -{ - "ietf-network-slice-service:network-slice-services": { - "slo-sle-templates": { - "slo-sle-template": [ - { - "id": "A", - "description": "", - "slo-policy": { - "metric-bound": [ - { - "metric-type": "one-way-bandwidth", - "metric-unit": "kbps", - "bound": 2 - }, - { - "metric-type": "one-way-delay-maximum", - "metric-unit": "milliseconds", - "bound": 20 - } - ] - }, - "sle-policy": { - "security": "", - "isolation": "", - "path-constraints": { - "service-functions": "", - "diversity": { +[ + { + "ietf-network-slice-service:network-slice-services": { + "slo-sle-templates": { + "slo-sle-template": [ + { + "id": "qos-profile-0bbf32f4-aa3d-4ef1-b3fb-806f4ed73912", + "description": "", + "slo-policy": { + "metric-bound": [] + }, + "sle-policy": { + "security": "", + "isolation": "", + "path-constraints": { + "service-functions": "", "diversity": { - "diversity-type": "" + "diversity": { + "diversity-type": "" + } } } } } - } - ] - }, - "slice-service": [ - { - "id": "slice-service-366e05b7-34e6-4597-9e28-e580abaeda71", - "description": "Transport network slice mapped with 3GPP slice NetworkSlice1", - "service-tags": { - "tag-type": { - "tag-type": "", - "value": "" - } - }, - "slo-sle-policy": { - "slo-sle-template": "A" - }, - "status": {}, - "sdps": { - "sdp": [ - { - "id": "01", - "geo-location": "", - "node-id": "CU-N2", - "sdp-ip-address": "10.60.60.105", - "tp-ref": "", - "service-match-criteria": { - "match-criterion": [ - { - "index": 1, - "match-type": "VLAN", - "value": "100", - "target-connection-group-id": "CU-N2_AMF-N2" - } - ] - }, - "incoming-qos-policy": "", - "outgoing-qos-policy": "", - "sdp-peering": { - "peer-sap-id": "", - "protocols": "" - }, - "ac-svc-ref": [], - "attachment-circuits": { - "attachment-circuit": [ - { - "id": "100", - "ac-ipv4-address": "10.60.60.105", - "ac-ipv4-prefix-length": 0, - "sdp-peering": { - "peer-sap-id": "5.5.5.5" - }, - "status": {} - } - ] - }, - "status": {}, - "sdp-monitoring": "" - }, - { - "id": "02", - "geo-location": "", - "node-id": "AMF-N2", - "sdp-ip-address": "10.60.11.3", - "tp-ref": "", - "service-match-criteria": { - "match-criterion": [ - { - "index": 1, - "match-type": "VLAN", - "value": "100", - "target-connection-group-id": "CU-N2_AMF-N2" - } - ] - }, - "incoming-qos-policy": "", - "outgoing-qos-policy": "", - "sdp-peering": { - "peer-sap-id": "", - "protocols": "" - }, - "ac-svc-ref": [], - "attachment-circuits": { - "attachment-circuit": [ - { - "id": "200", - "ac-ipv4-address": "10.60.11.3", - "ac-ipv4-prefix-length": 0, - "sdp-peering": { - "peer-sap-id": "4.4.4.4" - }, - "status": {} - } - ] - }, - "status": {}, - "sdp-monitoring": "" + ] + }, + "slice-service": [ + { + "id": "slice-service-561d9dd7-c2e0-40c9-a222-ec6913ea5a57", + "description": "example 5G Slice mapping", + "service-tags": { + "tag-type": { + "tag-type": "", + "value": "" } - ] - }, - "connection-groups": { - "connection-group": [ - { - "id": "CU-N2_AMF-N2", - "connectivity-type": "ietf-vpn-common:any-to-any", - "connectivity-construct": [ - { - "id": 1, - "a2a-sdp": [ + }, + "slo-sle-policy": { + "slo-sle-template": "qos-profile-0bbf32f4-aa3d-4ef1-b3fb-806f4ed73912" + }, + "status": {}, + "sdps": { + "sdp": [ + { + "id": "01", + "geo-location": "", + "node-id": "source-node", + "sdp-ip-address": "1.1.1.1", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ { - "sdp-id": "01" - }, + "index": 1, + "match-type": "VLAN", + "value": null, + "target-connection-group-id": "" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ { - "sdp-id": "02" + "id": "100", + "ac-ipv4-address": "", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "1.1.1.1" + }, + "status": {} } ] - } - ], - "status": {} - } - ] + }, + "status": {}, + "sdp-monitoring": "" + }, + { + "id": "02", + "geo-location": "", + "node-id": "destination-node", + "sdp-ip-address": "3.3.3.3", + "tp-ref": "", + "service-match-criteria": { + "match-criterion": [ + { + "index": 1, + "match-type": "VLAN", + "value": null, + "target-connection-group-id": "" + } + ] + }, + "incoming-qos-policy": "", + "outgoing-qos-policy": "", + "sdp-peering": { + "peer-sap-id": "", + "protocols": "" + }, + "ac-svc-ref": [], + "attachment-circuits": { + "attachment-circuit": [ + { + "id": "200", + "ac-ipv4-address": "", + "ac-ipv4-prefix-length": 0, + "sdp-peering": { + "peer-sap-id": "3.3.3.3" + }, + "status": {} + } + ] + }, + "status": {}, + "sdp-monitoring": "" + } + ] + }, + "connection-groups": { + "connection-group": [ + { + "id": "source-node_destination-node", + "connectivity-type": "ietf-vpn-common:any-to-any", + "connectivity-construct": [ + { + "id": 1, + "a2a-sdp": [ + { + "sdp-id": "01" + }, + { + "sdp-id": "02" + } + ] + } + ], + "status": {} + } + ] + } } - } - ] + ] + } } -} \ No newline at end of file +] \ No newline at end of file diff --git a/src/templates/test_service.json b/src/templates/test_service.json deleted file mode 100644 index bd956b7281afce8f3626bfa08be5dc9471e63e8b..0000000000000000000000000000000000000000 --- a/src/templates/test_service.json +++ /dev/null @@ -1,93 +0,0 @@ -{ - "services": [ - { - "service_id": { - "context_id": { - "context_uuid": { - "uuid": "admin" - } - }, - "service_uuid": { - "uuid": "l2-acl-svc-17387469754519430" - } - }, - "service_type": 2, - "service_status": { - "service_status": 1 - }, - "service_endpoint_ids": [ - { - "device_id": { - "device_uuid": { - "uuid": "172.16.185.31" - } - }, - "endpoint_uuid": { - "uuid": "eth-1/0/22" - } - }, - { - "device_id": { - "device_uuid": { - "uuid": "172.16.185.32" - } - }, - "endpoint_uuid": { - "uuid": "eth-1/0/6" - } - } - ], - "service_constraints": [ - { - "custom": { - "constraint_type": "bandwidth[kbps]", - "constraint_value": "20" - } - }, - { - "custom": { - "constraint_type": "latency[ms]", - "constraint_value": "20" - } - } - ], - "service_config": { - "config_rules": [ - { - "action": 1, - "custom": { - "resource_key": "/settings", - "resource_value": {} - } - }, - { - "action": 1, - "custom": { - "resource_key": "/device[172.16.185.31]/endpoint[eth-1/0/22]/settings", - "resource_value": { - "sub_interface_index": 0, - "vlan_id": 100, - "circuit_id": "100", - "remote_router": "172.16.185.32", - "ni_name": "ELAN100" - } - } - }, - { - "action": 1, - "custom": { - "resource_key": "/device[172.16.185.32]/endpoint[eth-1/0/6]/settings", - "resource_value": { - "sub_interface_index": 0, - "vlan_id": 100, - "circuit_id": "100", - "remote_router": "172.16.185.31", - "ni_name": "ELAN100" - } - } - } - ] - } - } - ] - } \ No newline at end of file diff --git a/src/webui/gui.py b/src/webui/gui.py new file mode 100644 index 0000000000000000000000000000000000000000..82f414deab93e2f1197f2974536342b12de8cf67 --- /dev/null +++ b/src/webui/gui.py @@ -0,0 +1,462 @@ +###SPDX-FileCopyrightText: © 2024 Telefónica Innovación Digital S.L. +###SPDX-License-Identifier: AGPL-3.0-or-later + +import json, logging, uuid +import requests +import os +import pandas as pd +from flask import Flask, render_template, request, jsonify, redirect, url_for, session, Blueprint +from collections import OrderedDict +from src.Constants import SRC_PATH, NSC_PORT, TEMPLATES_PATH, DUMMY_MODE +from src.realizers.ixia.NEII_V4 import NEII_controller + +# app =Flask(__name__) +gui_bp = Blueprint('gui', __name__, template_folder=os.path.join(SRC_PATH, 'webui', 'templates'), static_folder=os.path.join(SRC_PATH, 'webui', 'static'), static_url_path='/webui/static') + +#Variables for dev accessing +USERNAME = 'admin' +PASSWORD = 'admin' +enter=False + +def __safe_int(value): + try: + if isinstance(value, str): + value = value.strip().replace(',', '.') + number = float(value) + return int(number) if number.is_integer() else number + except (ValueError, TypeError, AttributeError): + return None + +def __build_request_ietf(src_node_ip=None, dst_node_ip=None, vlan_id=None, bandwidth=None, latency=None, tolerance=0, latency_version=None, reliability=None): + ''' + Work: Build the IETF template for the intent + ''' + # Open and read the template file + with open(os.path.join(TEMPLATES_PATH, 'ietf_template_empty.json'), 'r') as source: + # Clean up the JSON template + template = source.read().replace('\t', '').replace('\n', '').replace("'", '"').strip() + request = json.loads(template) + + request["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["id"] = f"qos-profile-{uuid.uuid4()}" + + # Generate unique slice service ID and description + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] = f"slice-service-{uuid.uuid4()}" + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["slo-sle-policy"]["slo-sle-template"] = request["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["id"] + + # Configure Source SDP + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["node-id"] = "source-node" #Pendiente de rellenar + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["sdp-ip-address"] = src_node_ip + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["match-type"] = "VLAN" + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["value"] = vlan_id + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["attachment-circuits"]["attachment-circuit"][0]["ac-ipv4-address"] = "" # Pendiente de rellenar + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"] = src_node_ip + + # Configure Destination SDP + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["node-id"] = "destination-node" + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["sdp-ip-address"] = dst_node_ip + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["service-match-criteria"]["match-criterion"][0]["match-type"] = "VLAN" + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["service-match-criteria"]["match-criterion"][0]["value"] = vlan_id + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["attachment-circuits"]["attachment-circuit"][0]["ac-ipv4-address"] = ""# Pendiente de rellenar + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["attachment-circuits"]["attachment-circuit"][0]["sdp-peering"]["peer-sap-id"] = dst_node_ip + + # Configure Connection Group and match-criteria + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["connection-groups"]["connection-group"][0]["id"] = "source-node_destination-node" #Pendiente de rellenar + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][0]["service-match-criteria"]["match-criterion"][0]["target-connection-group-id"] = "" #Pendiente de rellenar + request["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["sdps"]["sdp"][1]["service-match-criteria"]["match-criterion"][0]["target-connection-group-id"] = "" #Pendiente de rellenar + + # Populate template with SLOs (currently supporting QoS profile, latency and bandwidth) + if bandwidth: + request["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"].append({ + "metric-type": "one-way-bandwidth", + "metric-unit": "Mbps", + "bound": __safe_int(bandwidth) + }) + if latency: + request["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"].append({ + "metric-type": "one-way-delay-maximum", + "metric-unit": "milliseconds", + "bound": __safe_int(latency) + }) + # Configure gaussian latency or internet if specified + if latency_version: + request["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["description"] = latency_version + if tolerance: + request["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"].append( { + "metric-type": "one-way-delay-variation-maximum", + "metric-unit": "milliseconds", + "bound": __safe_int(tolerance) + }) + if reliability: + request["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["sle-policy"]["reliability"] = __safe_int(reliability) + + return request + +def __build_request(ip_version=None, src_node_ip=None, dst_node_ip=None, src_node_ipv6=None, dst_node_ipv6=None, + vlan_id=None, bandwidth=None, latency=None, tolerance=0, latency_version=None, + reliability=None, packet_reorder=None, num_pack=None, pack_reorder=None, num_reorder=None, + max_reorder=None, desv_reorder=None, drop_version=None, packets_drop=None, + drops=None, desv_drop=None): + ''' + Work: Build the template for the IXIA NEII + ''' + json_data = { + "ip_version": ip_version, + "src_node_ip": src_node_ip, + "dst_node_ip": dst_node_ip, + "src_node_ipv6": src_node_ipv6, + "dst_node_ipv6": dst_node_ipv6, + "vlan_id": vlan_id, + "bandwidth": bandwidth, + "latency": latency, + "tolerance": tolerance, + "latency_version": latency_version, + "reliability": reliability, + "packet_reorder": packet_reorder, + "num_pack": num_pack, + "pack_reorder": pack_reorder, + "num_reorder": num_reorder, + "max_reorder": max_reorder, + "desv_reorder": desv_reorder, + "drop_version": drop_version, + "packets_drop": packets_drop, + "drops": drops, + "desv_drop": desv_drop + } + return json_data + +def __datos_json(): + try: + with open(os.path.join(SRC_PATH, 'slice_ddbb.json'), 'r') as fichero: + datos =json.load(fichero) + print(datos) + rows =[] + for source_ip, source_info in datos["source"].items(): + vlan = source_info["vlan"] + for dest_ip, dest_info in source_info["destination"].items(): + row ={ + "Source IP": source_ip, + "Destiny IP": dest_ip, + "VLAN": vlan, + "attributes": dest_info["attributes"] + } + rows.append(row) + dataframe =pd.DataFrame(rows) + except FileNotFoundError: + dataframe =pd.DataFrame() + except ValueError as e: + dataframe =pd.DataFrame() + return dataframe + +@gui_bp.route('/webui') +def home(): + session['enter'] = False + # Leer las IPs actuales del archivo de configuración + try: + with open(os.path.join(SRC_PATH, 'IPs.json')) as f: + ips = json.load(f) + tfs_ip = ips.get('TFS_IP', 'No configurada') + ixia_ip = ips.get('IXIA_IP', 'No configurada') + except Exception: + tfs_ip = 'No configurada' + ixia_ip = 'No configurada' + return render_template('welcome.html', tfs_ip=tfs_ip, ixia_ip=ixia_ip) + +@gui_bp.route('/webui/generate/tfs', methods=['POST','GET']) +def generate_tfs(): + session['enter']=False + if request.method =='POST': + src_node_ip =request.form['src_node_ip'] + dst_node_ip =request.form['dst_node_ip'] + vlan_id =request.form['vlan_id'] + if vlan_id == '': + vlan_id = None + latency =request.form['latency_intent'] + bandwidth =request.form['bandwidth_intent'] + + slice_request = __build_request_ietf(src_node_ip=src_node_ip, dst_node_ip=dst_node_ip, vlan_id=vlan_id, latency=latency, bandwidth=bandwidth) + + # Si 'request' es un diccionario, conviértelo a JSON + json_data = json.dumps(slice_request) + files = { + 'file': ('ietf_template_example.json', json_data, 'application/json') + } + + response = requests.post( + f'http://localhost:{NSC_PORT}/tfs/slice', + headers={ + 'accept': 'application/json' + }, + files=files + ) + response.raise_for_status() + + if response.ok: + return redirect(url_for('gui.generate_tfs', src_node_ip=src_node_ip, dst_node_ip=dst_node_ip, vlan_id=vlan_id, latency=latency, bandwidth=bandwidth)) + else: + return render_template('index.html', error="Error al generar el intent") + + src_node_ip =request.args.get('src_node_ip', '') + dst_node_ip =request.args.get('dst_node_ip', '') + vlan_id =request.args.get('vlan_id', '') + latency =request.args.get('latency', '') + bandwidth =request.args.get('bandwidth', '') + reliability=request.args.get('reliability', '') + + return render_template('index.html', src_node_ip=src_node_ip, dst_node_ip=dst_node_ip, vlan_id=vlan_id, latency=latency, bandwidth=bandwidth, reliability=reliability) + +@gui_bp.route('/webui/generate/ixia', methods=['GET','POST']) +def generate_ixia(): + session['enter']=False + if request.method =='POST': + src_node_ip =request.form['src_node_ip'] + dst_node_ip =request.form['dst_node_ip'] + vlan_id =request.form['vlan_id'] + latency =request.form['latency_intent'] + bandwidth =request.form['bandwidth_intent'] + latency_version="internet" + tolerance= request.form['tolerance_intent'] + reliability=request.form['reliability'] + + if int(reliability)==100: reliability=None + slice_request = __build_request_ietf(src_node_ip=src_node_ip, dst_node_ip=dst_node_ip, vlan_id=vlan_id, bandwidth=bandwidth, latency=latency, latency_version=latency_version, tolerance=tolerance, reliability=reliability) + # Si 'request' es un diccionario, conviértelo a JSON + json_data = json.dumps(slice_request) + files = { + 'file': ('ietf_template_example.json', json_data, 'application/json') + } + + try: + response = requests.post( + f'http://localhost:{NSC_PORT}/ixia/slice', + headers={'accept': 'application/json'}, + files=files + ) + response.raise_for_status() + if response.ok: + return redirect(url_for('gui.generate_ixia', src_node_ip=src_node_ip, dst_node_ip=dst_node_ip, vlan_id=vlan_id, latency=latency, tolerance=tolerance,bandwidth=bandwidth,reliability=reliability)) + except requests.RequestException as e: + print("HTTP error:", e) + return render_template('ixia.html', error="Intent Generation Error") + + src_node_ip =request.args.get('src_node_ip', '') + dst_node_ip =request.args.get('dst_node_ip', '') + vlan_id =request.args.get('vlan_id', '') + latency =request.args.get('latency', '') + tolerance=request.args.get('tolerance', '') + bandwidth =request.args.get('bandwidth', '') + reliability=request.args.get('reliability','') + + return render_template('ixia.html', src_node_ip=src_node_ip, dst_node_ip=dst_node_ip, vlan_id=vlan_id, latency=latency, tolerance=tolerance, bandwidth=bandwidth, reliability=reliability) + + +@gui_bp.route('/webui/dev', methods=['GET', 'POST']) +def develop(): + print('Session content:', dict(session)) + if 'enter' not in session or session['enter'] is False: + return redirect(url_for('gui.login')) + + if request.method == 'POST': + ip_version = request.form.get('ip_version', '') + src_node_ipv4 = request.form.get('src_node_ipv4', '') + dst_node_ipv4 = request.form.get('dst_node_ipv4', '') + src_node_ipv6 = request.form.get('src_node_ipv6', '') + dst_node_ipv6 = request.form.get('dst_node_ipv6', '') + vlan_id = request.form.get('vlan_id', '') + bandwidth = request.form.get('bandwidth_intent', '') + latency = request.form.get('latency_intent', '') + latency_version = request.form.get('delay_statistic', '') + tolerance = request.form.get('max_delay', '') + packet_reorder = request.form.get('packet_options', '') + num_pack = request.form.get('numPack', '') + pack_reorder = request.form.get('packReod', '') + num_reorder = request.form.get('numReod', '') + max_reorder = request.form.get('num_maxReod', '') + desv_reorder = request.form.get('desv_reord', '') + drop_version = request.form.get('object_options', '') + packets_drop = request.form.get('packets', '') + drops = request.form.get('drop', '') + desv_drop = request.form.get('desv_drop', '') + + json_data = __build_request(ip_version=ip_version, src_node_ip=src_node_ipv4, dst_node_ip=dst_node_ipv4, src_node_ipv6=src_node_ipv6, dst_node_ipv6=dst_node_ipv6, vlan_id=vlan_id, latency=latency, bandwidth=bandwidth, latency_version=latency_version, tolerance=tolerance, packet_reorder=packet_reorder, num_pack=num_pack, pack_reorder=pack_reorder, num_reorder=num_reorder, max_reorder=max_reorder, desv_reorder=desv_reorder, drop_version=drop_version, packets_drop=packets_drop, drops=drops, desv_drop=desv_drop) + logging.debug("Generated JSON data: %s", json_data) + if not DUMMY_MODE: + NEII_controller().nscNEII(json_data) + + session['enter'] = True + return render_template('dev.html', + json_data=json_data, + src_node_ip=src_node_ipv4 or src_node_ipv6, + dst_node_ip=dst_node_ipv4 or dst_node_ipv6, + vlan_id=vlan_id, + latency=latency, + bandwidth=bandwidth, + latency_version=latency_version, + tolerance=tolerance, + packet_reorder=packet_reorder, + num_pack=num_pack, + pack_reorder=pack_reorder, + num_reorder=num_reorder, + max_reorder=max_reorder, + desv_reorder=desv_reorder, + packets_drop=packets_drop, + drops=drops, + desv_drop=desv_drop + ) + + return render_template('dev.html', + src_node_ip=request.args.get('src_node_ip', ''), + dst_node_ip=request.args.get('dst_node_ip', ''), + vlan_id=request.args.get('vlan_id', ''), + latency=request.args.get('latency', ''), + bandwidth=request.args.get('bandwidth', ''), + latency_version=request.args.get('latency_version', ''), + tolerance=request.args.get('max_latency', ''), + packet_reorder=request.args.get('packet_reorder', ''), + num_pack=request.args.get('num_pack', ''), + pack_reorder=request.args.get('pack_reorder', ''), + num_reorder=request.args.get('num_reorder', ''), + max_reorder=request.args.get('max_reorder', ''), + desv_reorder=request.args.get('desv', ''), + packets_drop=request.args.get('packets_drop', ''), + drops=request.args.get('drop', ''), + desv_drop=request.args.get('desv_drop', ''), + json_data=None + ) + +@gui_bp.route('/webui/search', methods=['GET', 'POST']) +def search(): + + session['enter'] = False + + try: + response = requests.get(f"http://localhost:{NSC_PORT}/tfs/slice", headers={"accept": "application/json"}) + response.raise_for_status() + tfs_slices = response.json() + + response = requests.get(f"http://localhost:{NSC_PORT}/ixia/slice", headers={"accept": "application/json"}) + response.raise_for_status() + ixia_slices = response.json() + + # Combinar los slices de TFS e IXIA + slices = tfs_slices + ixia_slices + + except requests.RequestException as e: + logging.error("Error fetching slices: %s", e) + return render_template('search.html', error="No se pudieron obtener los slices.", dataframe_html="") + + # Extraer datos relevantes y construir un DataFrame + rows = [] + for item in slices: + try: + slice_service = item["intent"]["ietf-network-slice-service:network-slice-services"]["slice-service"][0] + sdp = slice_service["sdps"]["sdp"] + metric_bound = item["intent"]["ietf-network-slice-service:network-slice-services"]["slo-sle-templates"]["slo-sle-template"][0]["slo-policy"]["metric-bound"] + + source_ip = sdp[0]["sdp-ip-address"] + dest_ip = sdp[1]["sdp-ip-address"] + vlan = sdp[0]["service-match-criteria"]["match-criterion"][0]["value"] + controller = item["controller"] + + # Construir atributos dinámicamente + attributes = [] + for metric in metric_bound: + if metric.get("metric-type", "") == "one-way-bandwidth": + metric_type = "bandwidth" + elif metric.get("metric-type", "") == "one-way-delay-maximum": + metric_type = "latency" + elif metric.get("metric-type", "") == "one-way-delay-variation-maximum": + metric_type = "tolerance" + bound = metric.get("bound") + unit = metric.get("metric-unit", "") + if bound is not None: + attributes.append(f"{metric_type}: {bound} {unit}") + + rows.append({ + "Source IP": source_ip, + "Destiny IP": dest_ip, + "Controller": controller, + "VLAN": vlan, + "attributes": attributes + }) + except Exception as e: + print(f"Error procesando slice: {e}") + + + import pandas as pd + dataframe = pd.DataFrame(rows) + + def format_attributes(attributes): + formatted_attrs = [] + for attr in attributes: + formatted_attrs.append(attr) + if formatted_attrs: + return formatted_attrs + else: + return None + + dataframe['attributes'] = dataframe['attributes'].apply([format_attributes]) + + if request.method == 'POST': + search_option = request.form.get('search_option') + search_value = request.form.get('search_value') + if search_option == 'Source IP': + results = dataframe[dataframe['Source IP'] == search_value] + elif search_option == 'Destiny IP': + results = dataframe[dataframe['Destiny IP'] == search_value] + elif search_option == 'Controller': + results = dataframe[dataframe['Controller'] == search_value] + elif search_option == 'VLAN': + results = dataframe[dataframe['VLAN'] == search_value] + else: + results = dataframe + result_html = results.to_html(classes='table table-striped') + return jsonify({'result': result_html}) + + dataframe_html = dataframe.to_html(classes='table table-striped') + return render_template('search.html', dataframe_html=dataframe_html) + +@gui_bp.route('/webui/login', methods=['GET', 'POST']) +def login(): + global enter + if request.method == 'POST': + username = request.form['username'] + password = request.form['password'] + if username == USERNAME and password == PASSWORD: + session['enter']=True + return redirect(url_for('gui.develop')) + else: + return render_template('login.html', error="Credenciales incorrectas") + + return render_template('login.html') + +@gui_bp.route('/webui/reset', methods=['POST']) +def reset(): + dataframe=__datos_json() + dataframe_html =dataframe.to_html(classes='table table-striped') + return jsonify({'result': dataframe_html}) + +@gui_bp.route('/webui/update_ips', methods=['POST']) +def update_ips(): + data = request.get_json() + tfs_ip = data.get('tfs_ip') + ixia_ip = data.get('ixia_ip') + + # Cargar datos existentes si el archivo existe + config_path = os.path.join(SRC_PATH, 'IPs.json') + if os.path.exists(config_path): + with open(config_path) as f: + ips = json.load(f) + else: + ips = {"TFS_IP": "", "IXIA_IP": ""} + + # Actualizar solo los campos recibidos + if tfs_ip: + ips['TFS_IP'] = tfs_ip + if ixia_ip: + ips['IXIA_IP'] = ixia_ip + + # Guardar de nuevo el archivo con los valores actualizados + with open(config_path, 'w') as f: + json.dump(ips, f, indent=4) + + return '', 200 \ No newline at end of file diff --git a/src/webui/static/img/MiniLogo.png b/src/webui/static/img/MiniLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..97ebca3d17c0fe8e41cbe1159ed479b26c549359 Binary files /dev/null and b/src/webui/static/img/MiniLogo.png differ diff --git a/src/webui/static/img/TelefonicaLogo.png b/src/webui/static/img/TelefonicaLogo.png new file mode 100644 index 0000000000000000000000000000000000000000..36a8be0e95a62016a680722ced4b392a9dd24f1d Binary files /dev/null and b/src/webui/static/img/TelefonicaLogo.png differ diff --git a/src/webui/static/img/banner.png b/src/webui/static/img/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..9fee130dcd0aca7550c5101855bf2de91fc469a8 Binary files /dev/null and b/src/webui/static/img/banner.png differ diff --git a/src/webui/templates/dev.html b/src/webui/templates/dev.html new file mode 100644 index 0000000000000000000000000000000000000000..ef6d18ddc123b3c214983e0d6fe5b5de18eb9cba --- /dev/null +++ b/src/webui/templates/dev.html @@ -0,0 +1,459 @@ + + + + + + + Intent Generation + + + + + +
+ + + + +
+
+

Intent Generation:

+ +
+
+
+ +
+
+ + +
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+ + +
+ + +
+
+ + +
+
+ +
+ + +
+ + +
+ + +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+ + +
+
+
+
+
+
+
+ + + + + + + + + + + + + + + + + + diff --git a/src/webui/templates/index.html b/src/webui/templates/index.html new file mode 100644 index 0000000000000000000000000000000000000000..9328ac3c6336c07bddcdaf162f474e8a8ebf51a4 --- /dev/null +++ b/src/webui/templates/index.html @@ -0,0 +1,219 @@ + + + + + + Slice Creation + + + + +
+ + + + + +
+

Slice Creation

+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ + {% if error %} +
{{ error }}
+ {% endif %} + + {% if src_node_ip and dst_node_ip %} +
+

Intent Generated:

+
+

Source IP: {{ src_node_ip }}

+

Destiny IP: {{ dst_node_ip }}

+

VLAN ID: {{ vlan_id }}

+

Latency [ms]: {{ latency }}

+

Bandwidth [Mbps]: {{ bandwidth }}

+
+
+ {% endif %} +
+
+ + + + + + + diff --git a/src/webui/templates/ixia.html b/src/webui/templates/ixia.html new file mode 100644 index 0000000000000000000000000000000000000000..47e51a1212a2e1f111d2f107a55d113eed62453b --- /dev/null +++ b/src/webui/templates/ixia.html @@ -0,0 +1,249 @@ + + + + + + IXIA: Slice Creation + + + + +
+ + + +
+

IXIA: Slice Creation

+
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + + (Activar) +
+ +
+ El valor debe estar entre 51 y 100. +
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ + +
+ + {% if error %} +
{{ error }}
+ {% endif %} + + {% if src_node_ip and dst_node_ip %} +
+

Intent Generated:

+
+

Source IP: {{ src_node_ip }}

+

Destiny IP: {{ dst_node_ip }}

+

VLAN ID: {{ vlan_id }}

+

Latency [ms]: {{ latency }}

+

Tolerance [ms]: {{ tolerance }}

+

Bandwidth [Mbps]: {{ bandwidth }}

+

Reliability: {{ reliability or 100 }}

+
+
+ {% endif %} +
+ +
+ + + + + + + + + diff --git a/src/webui/templates/login.html b/src/webui/templates/login.html new file mode 100644 index 0000000000000000000000000000000000000000..9ef6fa42dc960a4db549617b5d4877d16aeb8f1f --- /dev/null +++ b/src/webui/templates/login.html @@ -0,0 +1,192 @@ + + + + + + Login + + + + +
+ + + + +
+ +
+
+ + + + diff --git a/src/webui/templates/search.html b/src/webui/templates/search.html new file mode 100644 index 0000000000000000000000000000000000000000..676705bb194405f0eddb25e8cbc4ffaeb68b7481 --- /dev/null +++ b/src/webui/templates/search.html @@ -0,0 +1,189 @@ + + + + + + Search Slice Data + + + + +
+ + + +

Search Slice Data

+

Fill in the fields below to search for data in the table:

+
+
+ + +
+
+ + +
+ + +
+
+
+ {{ dataframe_html|safe }} +
+
+
+ + + + + + diff --git a/src/webui/templates/welcome.html b/src/webui/templates/welcome.html new file mode 100644 index 0000000000000000000000000000000000000000..d04082c9d585c2baa416803e98507ecb8d94d2d8 --- /dev/null +++ b/src/webui/templates/welcome.html @@ -0,0 +1,174 @@ + + + + + + NSC WebUI + + + + + +
+ + +
+

Welcome to the NSC WebUI

+

Please, choose the controller you would like to use:

+
+ + {{ tfs_ip }} + WebUI +
+
+ + {{ ixia_ip }} + WebUI +
+
+
+ + + + \ No newline at end of file diff --git a/swagger/ixia_namespace.py b/swagger/ixia_namespace.py new file mode 100644 index 0000000000000000000000000000000000000000..6a14ffe995ad0fb2228c2d8deaf6dd91060ea510 --- /dev/null +++ b/swagger/ixia_namespace.py @@ -0,0 +1,112 @@ +from flask import request +from flask_restx import Namespace, Resource, fields, 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 + +ixia_ns = Namespace( + "ixia", + description="Operations related to transport network slices with IXIA NEII" +) + +# 3GPP NRM TS28.541 Data models +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): + @ixia_ns.doc(summary="Return all transport network slices", description="Returns all transport network slices from the slice controller.") + @ixia_ns.response(200, "Slices returned", slice_ddbb_model) + @ixia_ns.response(404, "Transport network slices not found") + @ixia_ns.response(500, "Internal server error") + def get(self): + """Retrieve all slices""" + controller = NSController(controller_type="IXIA") + return controller.get_flows() + + @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(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 + + try: + json_data = json.load(uploaded_file) # Convert file to JSON + except json.JSONDecodeError: + return {"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 + except json.JSONDecodeError: + return {"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 + + # Process the JSON data with the NSController + controller = NSController(controller_type="IXIA") + return controller.add_flow(json_data) + + @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(500, "Internal server error") + def delete(self): + """Delete all slices""" + controller = NSController(controller_type="IXIA") + return controller.delete_flows() + + +@ixia_ns.route("/slice/") +@ixia_ns.doc(params={"slice_id": "The ID of the slice to retrieve or modify"}) +class IxiaSlice(Resource): + @ixia_ns.doc(summary="Return a specific transport network slice", description="Returns specific information related to a slice by providing its id") + @ixia_ns.response(200, "Slice returned", slice_ddbb_model) + @ixia_ns.response(404, "Transport network slice not found.") + @ixia_ns.response(500, "Internal server error") + def get(self, slice_id): + """Retrieve a specific slice""" + controller = NSController(controller_type="IXIA") + return controller.get_flows(slice_id) + + @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(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) + + @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") + @ixia_ns.response(200, "Slice modified", slice_ddbb_model) + @ixia_ns.response(404, "Transport network slice not found.") + @ixia_ns.response(500, "Internal server error") + def put(self, slice_id): + """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 diff --git a/swagger/slice_namespace.py b/swagger/tfs_namespace.py similarity index 52% rename from swagger/slice_namespace.py rename to swagger/tfs_namespace.py index 0f629f97e1a8ecbf4550f3e5a24e5ecca46c15ae..c9c3e07f591d13390df92712a746843a8d2326bd 100644 --- a/swagger/slice_namespace.py +++ b/swagger/tfs_namespace.py @@ -20,39 +20,39 @@ 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 -slice_ns = Namespace( - "slice", - description="Operations related to transport network slices" +tfs_ns = Namespace( + "tfs", + description="Operations related to transport network slices with TeraflowSDN (TFS) controller" ) # 3GPP NRM TS28.541 Data models -gpp_network_slice_request_model = create_gpp_nrm_28541_model(slice_ns) +gpp_network_slice_request_model = create_gpp_nrm_28541_model(tfs_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(slice_ns) +slice_ddbb_model, slice_response_model = create_ietf_network_slice_nbi_yang_model(tfs_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 -@slice_ns.route("/") -class SliceList(Resource): - @slice_ns.doc(summary="Return all transport network slices", description="Returns all transport network slices from the slice controller.") - @slice_ns.response(200, "Slices returned", slice_ddbb_model) - @slice_ns.response(404, "Transport network slices not found") - @slice_ns.response(500, "Internal server error") +@tfs_ns.route("/slice") +class TfsSliceList(Resource): + @tfs_ns.doc(summary="Return all transport network slices", description="Returns all transport network slices from the slice controller.") + @tfs_ns.response(200, "Slices returned", slice_ddbb_model) + @tfs_ns.response(404, "Transport network slices not found") + @tfs_ns.response(500, "Internal server error") def get(self): """Retrieve all slices""" - controller = NSController() + controller = NSController(controller_type="TFS") return controller.get_flows() - @slice_ns.doc(summary="Submit a transport network slice request", description="This endpoint allows clients to submit transport network slice requests using a JSON payload.") - @slice_ns.response(200, "Slice request successfully processed", slice_response_model) - @slice_ns.response(400, "Invalid request format") - @slice_ns.response(500, "Internal server error") - @slice_ns.expect(upload_parser) + @tfs_ns.doc(summary="Submit a transport network slice request", description="This endpoint allows clients to submit transport network slice requests using a JSON payload.") + @tfs_ns.response(200, "Slice request successfully processed", slice_response_model) + @tfs_ns.response(400, "Invalid request format") + @tfs_ns.response(500, "Internal server error") + @tfs_ns.expect(upload_parser) def post(self): """Submit a new slice request with a file""" @@ -83,46 +83,48 @@ class SliceList(Resource): return {"error": "No data sent"}, 400 # Process the JSON data with the NSController - controller = NSController() + controller = NSController(controller_type="TFS") return controller.add_flow(json_data) - @slice_ns.doc(summary="Delete all transport network slices", description="Deletes all transport network slices from the slice controller.") - @slice_ns.response(200, "All transport network slices deleted successfully.") - @slice_ns.response(500, "Internal server error") + @tfs_ns.doc(summary="Delete all transport network slices", description="Deletes all transport network slices from the slice controller.") + @tfs_ns.response(200, "All transport network slices deleted successfully.") + @tfs_ns.response(500, "Internal server error") def delete(self): """Delete all slices""" - controller = NSController() + controller = NSController(controller_type="TFS") return controller.delete_flows() -@slice_ns.route("/") -@slice_ns.doc(params={"slice_id": "The ID of the slice to retrieve or modify"}) -class Slice(Resource): - @slice_ns.doc(summary="Return a specific transport network slice", description="Returns specific information related to a slice by providing its id") - @slice_ns.response(200, "Slice returned", slice_ddbb_model) - @slice_ns.response(404, "Transport network slice not found.") - @slice_ns.response(500, "Internal server error") +@tfs_ns.route("/slice/") +@tfs_ns.doc(params={"slice_id": "The ID of the slice to retrieve or modify"}) +class TfsSlice(Resource): + @tfs_ns.doc(summary="Return a specific transport network slice", description="Returns specific information related to a slice by providing its id") + @tfs_ns.response(200, "Slice returned", slice_ddbb_model) + @tfs_ns.response(404, "Transport network slice not found.") + @tfs_ns.response(500, "Internal server error") def get(self, slice_id): """Retrieve a specific slice""" - controller = NSController() + controller = NSController(controller_type="TFS") return controller.get_flows(slice_id) - @slice_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`.") - @slice_ns.response(200, "Transport network slice deleted successfully.") - @slice_ns.response(404, "Transport network slice not found.") - @slice_ns.response(500, "Internal server error") + @tfs_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`.") + @tfs_ns.response(200, "Transport network slice deleted successfully.") + @tfs_ns.response(404, "Transport network slice not found.") + @tfs_ns.response(500, "Internal server error") def delete(self, slice_id): """Delete a slice""" - controller = NSController() + controller = NSController(controller_type="TFS") return controller.delete_flows(slice_id) - @slice_ns.expect(slice_ddbb_model, validate=True) - @slice_ns.doc(summary="Modify a specific transport network slice", description="Returns a specific slice that has been modified") - @slice_ns.response(200, "Slice modified", slice_ddbb_model) - @slice_ns.response(404, "Transport network slice not found.") - @slice_ns.response(500, "Internal server error") + @tfs_ns.expect(slice_ddbb_model, validate=True) + @tfs_ns.doc(summary="Modify a specific transport network slice", description="Returns a specific slice that has been modified") + @tfs_ns.response(200, "Slice modified", slice_ddbb_model) + @tfs_ns.response(404, "Transport network slice not found.") + @tfs_ns.response(500, "Internal server error") def put(self, slice_id): """Modify a slice""" json_data = request.get_json() - controller = NSController() + controller = NSController(controller_type="TFS") return controller.modify_flow(slice_id, json_data) + +