Loading src/api/handlers/base_handler.py +10 −1 Original line number Diff line number Diff line Loading @@ -45,8 +45,17 @@ logger = logging.getLogger(__name__) def _dep(name: str, fallback: Any) -> Any: """Resolve dependency from src.api.main if patched, otherwise fallback.""" """Resolve dependency from src.api.main or src.database.db if patched, otherwise fallback.""" main_mod = sys.modules.get("src.api.main") if main_mod is not None and hasattr(main_mod, name): val = getattr(main_mod, name) if hasattr(val, "return_value") or hasattr(val, "_mock_return_value") or hasattr(val, "assert_called"): return val db_mod = sys.modules.get("src.database.db") if db_mod is not None and hasattr(db_mod, name): val = getattr(db_mod, name) if hasattr(val, "return_value") or hasattr(val, "_mock_return_value") or hasattr(val, "assert_called"): return val if main_mod is not None and hasattr(main_mod, name): return getattr(main_mod, name) return fallback Loading src/api/handlers/restconf_handler.py +75 −17 Original line number Diff line number Diff line Loading @@ -196,9 +196,77 @@ class RestconfHandler: if not existing_data: return send_response(False, code=404, message="Network slice services not found") result = self.slice_service.nsc(intent) if not result: # 1. Normalize existing data and extract existing slice IDs normalize_fn = _dep("normalize_libyang_data", _real_normalize_libyang_data) normalized_existing = normalize_fn(existing_data) existing_slices_list = safe_get( normalized_existing, ["network-slice-services", "slice-service"] ) or [] existing_slice_ids = { item["id"] for item in existing_slices_list if isinstance(item, dict) and "id" in item } # 2. Normalize incoming intent and extract new slice IDs normalized_incoming = normalize_fn(intent) incoming_slices_list = safe_get( normalized_incoming, ["ietf-network-slice-service:network-slice-services", "slice-service"] ) or [] incoming_slices_map = { item["id"]: item for item in incoming_slices_list if isinstance(item, dict) and "id" in item } new_slice_ids = set(incoming_slices_map.keys()) # 3. Calculate diffs: to_delete, to_update, to_create to_delete = existing_slice_ids - new_slice_ids to_update = existing_slice_ids & new_slice_ids to_create = new_slice_ids - existing_slice_ids logger.info("Slice Update diff - Delete: %s, Update: %s, Create: %s", to_delete, to_update, to_create) # A) Delete slices no longer present for old_id in to_delete: logger.info("Deleting slice '%s' during container update...", old_id) self._delete_single_slice_service(old_id) results = [] # B) Update existing slices for slice_id in to_update: logger.info("Updating slice '%s' during container update...", slice_id) slice_item = incoming_slices_map[slice_id] full_intent = { "ietf-network-slice-service:network-slice-services": { "slo-sle-templates": safe_get(normalized_incoming, ["ietf-network-slice-service:network-slice-services", "slo-sle-templates"]) or {}, "slice-service": [slice_item], } } res = self.slice_service.nsc(full_intent, slice_id=slice_id) if not res: return send_response(False, code=500, message=f"Failed to process slice '{slice_id}' in TFS") results.append(res) # C) Create new slices for slice_id in to_create: logger.info("Creating new slice '%s' during container update...", slice_id) slice_item = incoming_slices_map[slice_id] full_intent = { "ietf-network-slice-service:network-slice-services": { "slo-sle-templates": safe_get(normalized_incoming, ["ietf-network-slice-service:network-slice-services", "slo-sle-templates"]) or {}, "slice-service": [slice_item], } } res = self.slice_service.nsc(full_intent, slice_id=None) if not res: return send_response(False, code=500, message=f"Failed to process new slice '{slice_id}' in TFS") results.append(res) if len(to_update) == 0 and len(to_create) == 0: slice_id = safe_get(intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "id"]) res = self.slice_service.nsc(intent, slice_id=slice_id) if not res: return send_response(False, code=500, message="Failed to process slice in TFS") results.append(res) update_ds_fn = _dep("update_data_store", _real_update_data_store) update_ds_fn(intent) Loading @@ -207,10 +275,12 @@ class RestconfHandler: True, code=200, message="Network slice services updated successfully", data=result, data=results if len(results) > 1 else (results[0] if results else {}), ) except ValueError as exc: return send_response(False, code=404, message=str(exc)) except RuntimeError as exc: return send_response(False, code=422, message=str(exc)) except Exception as exc: return send_response(False, code=500, message=str(exc)) Loading Loading @@ -452,7 +522,7 @@ class RestconfHandler: if err_resp is not None: return err_resp result = self.slice_service.nsc(full_intent) result = self.slice_service.nsc(full_intent, slice_id) if not result: return send_response(False, code=500, message="Slice not updated") Loading Loading @@ -985,16 +1055,4 @@ class RestconfHandler: yield f"event: error\ndata: {json.dumps({'error': str(exc)})}\n\n" break # ------------------------------------------------------------------------- # RESTCONF Reconfiguration Operations # ------------------------------------------------------------------------- def reconfig_slice(self, slice_id: str) -> tuple[dict[str, Any], int]: """Trigger slice reconfiguration via Change Scheduler Planner.""" try: result = self.slice_service.reconfig_slice(slice_id) return send_response(True, code=200, data=result) except ValueError as exc: return send_response(False, code=404, message=str(exc)) except Exception as exc: return send_response(False, code=500, message=str(exc)) src/database/store_data.py +51 −45 Original line number Diff line number Diff line Loading @@ -14,9 +14,11 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. import logging from typing import Any from src.database.db import save_data, update_data from src.database import db from src.database.sysrepo_store import update_data_store def store_data( Loading @@ -33,13 +35,17 @@ def store_data( controller_type (str, optional): Controller type. Defaults to None. """ if controller_type == "RESTCONF": try: update_data_store(intent) return except Exception as e: logging.warning(f"Could not update sysrepo datastore: {e}") effective_controller = controller_type or "TFS" if slice_id: update_data(slice_id, intent, effective_controller) db.update_data(slice_id, intent, effective_controller) return resolved_slice_id = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] save_data(resolved_slice_id, intent, effective_controller) No newline at end of file db.save_data(resolved_slice_id, intent, effective_controller) src/main.py +57 −67 Original line number Diff line number Diff line Loading @@ -91,20 +91,7 @@ def _subscribe_to_alerts(ietf_intents: list[dict[str, Any]], slice_id: str | Non logging.error(f"Failed to subscribe to notification service: {e}") def _is_change_scheduler_viable(cs_result: Any) -> bool: """Evaluate whether Change Scheduler response confirms viability.""" if not isinstance(cs_result, dict): return False if cs_result.get("success", True) or cs_result.get("status_code") in (200, 201, 202): return True cs_response = cs_result.get("response", {}) if isinstance(cs_response, dict): if cs_response.get("status") in ("VIABLE", "SCHEDULED") or cs_response.get("viable") is True: return True return False class NSController: Loading Loading @@ -133,7 +120,7 @@ class NSController: self, intent_json: dict[str, Any], slice_id: str | None = None, old_service_id: str | None = None, old_service_id: str | None = None ) -> dict[str, Any]: """ Main Network Slice Controller workflow to process and realize network slice intents. Loading @@ -144,7 +131,7 @@ class NSController: old_service_id (str, optional): Old service identifier to delete when modifying. Defaults to None. Returns: dict[str, Any]: Contains slice creation responses and setup time in milliseconds. dict[str, Any]: Contains slice creation/update responses and setup time in milliseconds. """ self.start_time = time.perf_counter() requests: dict[str, list[Any]] = {"services": []} Loading @@ -152,17 +139,24 @@ class NSController: ietf_intents = nbi_processor(intent_json) is_update = bool(slice_id) cs_result: dict[str, Any] | None = None for intent in ietf_intents: logging.debug(intent) payload = { "intent": intent, "is_update": is_update, "slice_id": slice_id, } logging.debug(f"Is Update: {is_update}") logging.debug(f"Slice ID: {slice_id}") services, rules = mapper(payload, controller_type=self.controller_type) logging.debug(f"Services: {services}") if is_update and current_app.config.get("PLANNER_TYPE") == "CHANGE_SCHEDULER" and isinstance(rules, dict): cs_result = rules self.response = build_response(intent, self.response, controller_type=self.controller_type) logging.warning(self.response) for service in services: request = realizer( Loading Loading @@ -203,11 +197,17 @@ class NSController: self.end_time = time.perf_counter() setup_time = (self.end_time - self.start_time) * 1000 return { modification_summary = { "slices": self.response, "setup_time": setup_time, } if isinstance(cs_result, dict): cs_result["slice_modification"] = modification_summary return cs_result return modification_summary def monitoring(self, slice_id: str, slo_sle_template: dict[str, Any]) -> dict[str, Any]: """ Monitor status and SLO compliance of a specific network slice. Loading @@ -219,6 +219,9 @@ class NSController: Returns: dict[str, Any]: Aggregated monitoring metrics and compliance status. """ if current_app.config.get("DUMMY_MODE"): raise Exception("Dummy mode is enabled. Cannot monitor slice with dummy mode.") payload = { "slice_id": slice_id, "slo_sle_template": slo_sle_template, Loading @@ -232,61 +235,48 @@ class NSController: if not is_compliant: logging.warning( f"SLO/SLE compliance violation (is_compliant=False) detected for slice '{slice_id}'. Triggering automatic reconfig_slice..." f"SLO/SLE compliance violation (is_compliant=False) detected for slice '{slice_id}'." ) planner_type = current_app.config.get("PLANNER_TYPE") if planner_type == "CHANGE_SCHEDULER": logging.info(f"Triggering automatic reconfig via self.nsc with planner_type='CHANGE_SCHEDULER' for slice '{slice_id}'...") try: reconfig_res = self.reconfig_slice(slice_id) if isinstance(metrics, dict): metrics["reconfig_result"] = reconfig_res except Exception as e: logging.error(f"Automatic reconfig_slice failed for slice '{slice_id}': {e}") if isinstance(metrics, dict): metrics["reconfig_error"] = str(e) return metrics def reconfig_slice(self, slice_id: str) -> dict[str, Any]: """ Reconfigure a network slice by computing optimal path and coordinating with Change Scheduler. Args: slice_id (str): Network slice identifier. Returns: dict[str, Any]: Result from Planner and slice modification. """ reconfig_data = realizer({"slice_id": slice_id}, action="RECONFIG", controller_type=self.controller_type) planner = Planner() cs_result = planner.planner(reconfig_data, type="CHANGE_SCHEDULER") if _is_change_scheduler_viable(cs_result): logging.info( f"Change Scheduler confirmed viability for slice '{slice_id}'. Performing slice PUT modification..." ) intent = None try: xpath = "/ietf-network-slice-service:network-slice-services" intent = get_data_store(xpath) except Exception as e: logging.warning(f"Could not retrieve slice '{slice_id}' from sysrepo: {e}") if not intent: try: from src.database.db import get_data slice_db_entry = get_data(slice_id) intent = slice_db_entry.get("intent") if isinstance(slice_db_entry, dict) else None except Exception as e: logging.warning(f"Could not retrieve slice '{slice_id}' from DB: {e}") if not intent: raise ValueError("Network slice services not found") intent = { "ietf-network-slice-service:network-slice-services": { "slice-service": [{"id": slice_id}] } } if isinstance(intent, dict) and "network-slice-services" in intent: intent["ietf-network-slice-service:network-slice-services"] = intent.pop("network-slice-services") logging.debug(f"Intent found for slice '{slice_id}': {intent}") mod_result = self.nsc(intent, slice_id=slice_id) if isinstance(cs_result, dict): cs_result["slice_modification"] = mod_result logging.info( f"Slice '{slice_id}' successfully modified via PUT following Change Scheduler viability confirmation." ) update_data_store(intent) reconfig_res = self.nsc(intent, slice_id=slice_id, planner_type="CHANGE_SCHEDULER") if isinstance(metrics, dict): metrics["reconfig_result"] = reconfig_res except Exception as e: logging.error(f"Error during slice '{slice_id}' PUT modification: {e}") if isinstance(cs_result, dict): cs_result["slice_modification_error"] = str(e) logging.error(f"Automatic reconfig failed for slice '{slice_id}': {e}") if isinstance(metrics, dict): metrics["reconfig_error"] = str(e) return metrics return cs_result Loading src/mapper/main.py +215 −200 Original line number Diff line number Diff line Loading @@ -15,11 +15,12 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. import logging import uuid from typing import Any from flask import current_app from src.database.service_db import save_data from src.database.service_db import save_data, delete_by_slice_id from src.database.sysrepo_store import get_data_store, normalize_libyang_data from src.planner.planner import Planner from src.realizer.main import realizer Loading Loading @@ -94,8 +95,8 @@ def _collect_available_templates(ietf_intent: dict[str, Any]) -> list[dict[str, return available_templates def _map_restconf_services(ietf_intent: dict[str, Any]) -> list[dict[str, Any]]: """Transform IETF intent into discrete RESTCONF service constructs.""" def _map_restconf_services(ietf_intent: dict[str, Any], is_update: bool) -> list[dict[str, Any]]: """Map IETF intent into services.""" available_templates = _collect_available_templates(ietf_intent) services: list[dict[str, Any]] = [] Loading @@ -107,6 +108,7 @@ def _map_restconf_services(ietf_intent: dict[str, Any]) -> list[dict[str, Any]]: service_id = safe_get(slice_service, ["id"]) way = safe_get(slice_service, ["service-tags", "tag-type", 0, "tag-type-value", 0]) service_template = get_service_template(slice_service, available_templates) logging.debug(f"Service template: {service_template}") connection_groups = safe_get(slice_service, ["connection-groups", "connection-group"]) or [] for connection_group in connection_groups: Loading @@ -114,15 +116,17 @@ def _map_restconf_services(ietf_intent: dict[str, Any]) -> list[dict[str, Any]]: group_id = f"{service_id}-{cg_id}" group_template = get_service_template(connection_group, available_templates) or service_template logging.debug(f"Group template: {group_template}") raw_conn_type = safe_get(connection_group, ["connectivity-type"]) connectivity_type = normalize_connectivity_type(raw_conn_type) constructs = safe_get(connection_group, ["connectivity-construct"]) or [] for construct in constructs: construct_id_raw = safe_get(construct, ["id"]) full_construct_id = f"{group_id}-{construct_id_raw}" full_construct_id = f"{group_id}-{construct_id_raw}-{uuid.uuid4()}" final_template = get_service_template(construct, available_templates) or group_template logging.debug(f"Final template: {final_template}") sdps = process_connectivity( cg_id, Loading @@ -143,12 +147,15 @@ def _map_restconf_services(ietf_intent: dict[str, Any]) -> list[dict[str, Any]]: } services.append(service) if not current_app.config.get("DUMMY_MODE", False): save_data(service_id=service["id"], slice_id=service_id) if connectivity_type == "point-to-point": break if not current_app.config.get("DUMMY_MODE", False): if is_update: delete_by_slice_id(service_id) for service in services: save_data(service_id=service["id"], slice_id=service_id) return services Loading @@ -173,19 +180,27 @@ def mapper( ietf_intent = payload.get("intent") services: Any = [ietf_intent] optimal_path = None slice_id = payload.get("slice_id", None) is_update = payload.get("is_update", False) if current_app.config.get("NRP_ENABLED", False): if not _handle_nrp_mapping(ietf_intent): return None planner_type = current_app.config.get("PLANNER_TYPE", None) if current_app.config.get("PLANNER_ENABLED", False): is_update = payload.get("is_update", False) planner_type = current_app.config.get("PLANNER_TYPE", "SHORTEST_PATH") if not current_app.config.get("DUMMY_MODE", False) or planner_type == "ENERGY": if is_update and planner_type == "CHANGE_SCHEDULER": reconfig_data = realizer({"slice_id": slice_id}, action="RECONFIG", controller_type=controller_type) optimal_path = Planner().planner(reconfig_data, planner_type, is_update=is_update) else: optimal_path = Planner().planner(ietf_intent, planner_type, is_update=is_update) logging.debug(f"Optimal path: {optimal_path}") if controller_type == "RESTCONF": services = _map_restconf_services(ietf_intent) logging.debug(f"SLICE ID is: {slice_id}") services = _map_restconf_services(ietf_intent, is_update) return services, optimal_path Loading Loading
src/api/handlers/base_handler.py +10 −1 Original line number Diff line number Diff line Loading @@ -45,8 +45,17 @@ logger = logging.getLogger(__name__) def _dep(name: str, fallback: Any) -> Any: """Resolve dependency from src.api.main if patched, otherwise fallback.""" """Resolve dependency from src.api.main or src.database.db if patched, otherwise fallback.""" main_mod = sys.modules.get("src.api.main") if main_mod is not None and hasattr(main_mod, name): val = getattr(main_mod, name) if hasattr(val, "return_value") or hasattr(val, "_mock_return_value") or hasattr(val, "assert_called"): return val db_mod = sys.modules.get("src.database.db") if db_mod is not None and hasattr(db_mod, name): val = getattr(db_mod, name) if hasattr(val, "return_value") or hasattr(val, "_mock_return_value") or hasattr(val, "assert_called"): return val if main_mod is not None and hasattr(main_mod, name): return getattr(main_mod, name) return fallback Loading
src/api/handlers/restconf_handler.py +75 −17 Original line number Diff line number Diff line Loading @@ -196,9 +196,77 @@ class RestconfHandler: if not existing_data: return send_response(False, code=404, message="Network slice services not found") result = self.slice_service.nsc(intent) if not result: # 1. Normalize existing data and extract existing slice IDs normalize_fn = _dep("normalize_libyang_data", _real_normalize_libyang_data) normalized_existing = normalize_fn(existing_data) existing_slices_list = safe_get( normalized_existing, ["network-slice-services", "slice-service"] ) or [] existing_slice_ids = { item["id"] for item in existing_slices_list if isinstance(item, dict) and "id" in item } # 2. Normalize incoming intent and extract new slice IDs normalized_incoming = normalize_fn(intent) incoming_slices_list = safe_get( normalized_incoming, ["ietf-network-slice-service:network-slice-services", "slice-service"] ) or [] incoming_slices_map = { item["id"]: item for item in incoming_slices_list if isinstance(item, dict) and "id" in item } new_slice_ids = set(incoming_slices_map.keys()) # 3. Calculate diffs: to_delete, to_update, to_create to_delete = existing_slice_ids - new_slice_ids to_update = existing_slice_ids & new_slice_ids to_create = new_slice_ids - existing_slice_ids logger.info("Slice Update diff - Delete: %s, Update: %s, Create: %s", to_delete, to_update, to_create) # A) Delete slices no longer present for old_id in to_delete: logger.info("Deleting slice '%s' during container update...", old_id) self._delete_single_slice_service(old_id) results = [] # B) Update existing slices for slice_id in to_update: logger.info("Updating slice '%s' during container update...", slice_id) slice_item = incoming_slices_map[slice_id] full_intent = { "ietf-network-slice-service:network-slice-services": { "slo-sle-templates": safe_get(normalized_incoming, ["ietf-network-slice-service:network-slice-services", "slo-sle-templates"]) or {}, "slice-service": [slice_item], } } res = self.slice_service.nsc(full_intent, slice_id=slice_id) if not res: return send_response(False, code=500, message=f"Failed to process slice '{slice_id}' in TFS") results.append(res) # C) Create new slices for slice_id in to_create: logger.info("Creating new slice '%s' during container update...", slice_id) slice_item = incoming_slices_map[slice_id] full_intent = { "ietf-network-slice-service:network-slice-services": { "slo-sle-templates": safe_get(normalized_incoming, ["ietf-network-slice-service:network-slice-services", "slo-sle-templates"]) or {}, "slice-service": [slice_item], } } res = self.slice_service.nsc(full_intent, slice_id=None) if not res: return send_response(False, code=500, message=f"Failed to process new slice '{slice_id}' in TFS") results.append(res) if len(to_update) == 0 and len(to_create) == 0: slice_id = safe_get(intent, ["ietf-network-slice-service:network-slice-services", "slice-service", 0, "id"]) res = self.slice_service.nsc(intent, slice_id=slice_id) if not res: return send_response(False, code=500, message="Failed to process slice in TFS") results.append(res) update_ds_fn = _dep("update_data_store", _real_update_data_store) update_ds_fn(intent) Loading @@ -207,10 +275,12 @@ class RestconfHandler: True, code=200, message="Network slice services updated successfully", data=result, data=results if len(results) > 1 else (results[0] if results else {}), ) except ValueError as exc: return send_response(False, code=404, message=str(exc)) except RuntimeError as exc: return send_response(False, code=422, message=str(exc)) except Exception as exc: return send_response(False, code=500, message=str(exc)) Loading Loading @@ -452,7 +522,7 @@ class RestconfHandler: if err_resp is not None: return err_resp result = self.slice_service.nsc(full_intent) result = self.slice_service.nsc(full_intent, slice_id) if not result: return send_response(False, code=500, message="Slice not updated") Loading Loading @@ -985,16 +1055,4 @@ class RestconfHandler: yield f"event: error\ndata: {json.dumps({'error': str(exc)})}\n\n" break # ------------------------------------------------------------------------- # RESTCONF Reconfiguration Operations # ------------------------------------------------------------------------- def reconfig_slice(self, slice_id: str) -> tuple[dict[str, Any], int]: """Trigger slice reconfiguration via Change Scheduler Planner.""" try: result = self.slice_service.reconfig_slice(slice_id) return send_response(True, code=200, data=result) except ValueError as exc: return send_response(False, code=404, message=str(exc)) except Exception as exc: return send_response(False, code=500, message=str(exc))
src/database/store_data.py +51 −45 Original line number Diff line number Diff line Loading @@ -14,9 +14,11 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. import logging from typing import Any from src.database.db import save_data, update_data from src.database import db from src.database.sysrepo_store import update_data_store def store_data( Loading @@ -33,13 +35,17 @@ def store_data( controller_type (str, optional): Controller type. Defaults to None. """ if controller_type == "RESTCONF": try: update_data_store(intent) return except Exception as e: logging.warning(f"Could not update sysrepo datastore: {e}") effective_controller = controller_type or "TFS" if slice_id: update_data(slice_id, intent, effective_controller) db.update_data(slice_id, intent, effective_controller) return resolved_slice_id = intent["ietf-network-slice-service:network-slice-services"]["slice-service"][0]["id"] save_data(resolved_slice_id, intent, effective_controller) No newline at end of file db.save_data(resolved_slice_id, intent, effective_controller)
src/main.py +57 −67 Original line number Diff line number Diff line Loading @@ -91,20 +91,7 @@ def _subscribe_to_alerts(ietf_intents: list[dict[str, Any]], slice_id: str | Non logging.error(f"Failed to subscribe to notification service: {e}") def _is_change_scheduler_viable(cs_result: Any) -> bool: """Evaluate whether Change Scheduler response confirms viability.""" if not isinstance(cs_result, dict): return False if cs_result.get("success", True) or cs_result.get("status_code") in (200, 201, 202): return True cs_response = cs_result.get("response", {}) if isinstance(cs_response, dict): if cs_response.get("status") in ("VIABLE", "SCHEDULED") or cs_response.get("viable") is True: return True return False class NSController: Loading Loading @@ -133,7 +120,7 @@ class NSController: self, intent_json: dict[str, Any], slice_id: str | None = None, old_service_id: str | None = None, old_service_id: str | None = None ) -> dict[str, Any]: """ Main Network Slice Controller workflow to process and realize network slice intents. Loading @@ -144,7 +131,7 @@ class NSController: old_service_id (str, optional): Old service identifier to delete when modifying. Defaults to None. Returns: dict[str, Any]: Contains slice creation responses and setup time in milliseconds. dict[str, Any]: Contains slice creation/update responses and setup time in milliseconds. """ self.start_time = time.perf_counter() requests: dict[str, list[Any]] = {"services": []} Loading @@ -152,17 +139,24 @@ class NSController: ietf_intents = nbi_processor(intent_json) is_update = bool(slice_id) cs_result: dict[str, Any] | None = None for intent in ietf_intents: logging.debug(intent) payload = { "intent": intent, "is_update": is_update, "slice_id": slice_id, } logging.debug(f"Is Update: {is_update}") logging.debug(f"Slice ID: {slice_id}") services, rules = mapper(payload, controller_type=self.controller_type) logging.debug(f"Services: {services}") if is_update and current_app.config.get("PLANNER_TYPE") == "CHANGE_SCHEDULER" and isinstance(rules, dict): cs_result = rules self.response = build_response(intent, self.response, controller_type=self.controller_type) logging.warning(self.response) for service in services: request = realizer( Loading Loading @@ -203,11 +197,17 @@ class NSController: self.end_time = time.perf_counter() setup_time = (self.end_time - self.start_time) * 1000 return { modification_summary = { "slices": self.response, "setup_time": setup_time, } if isinstance(cs_result, dict): cs_result["slice_modification"] = modification_summary return cs_result return modification_summary def monitoring(self, slice_id: str, slo_sle_template: dict[str, Any]) -> dict[str, Any]: """ Monitor status and SLO compliance of a specific network slice. Loading @@ -219,6 +219,9 @@ class NSController: Returns: dict[str, Any]: Aggregated monitoring metrics and compliance status. """ if current_app.config.get("DUMMY_MODE"): raise Exception("Dummy mode is enabled. Cannot monitor slice with dummy mode.") payload = { "slice_id": slice_id, "slo_sle_template": slo_sle_template, Loading @@ -232,61 +235,48 @@ class NSController: if not is_compliant: logging.warning( f"SLO/SLE compliance violation (is_compliant=False) detected for slice '{slice_id}'. Triggering automatic reconfig_slice..." f"SLO/SLE compliance violation (is_compliant=False) detected for slice '{slice_id}'." ) planner_type = current_app.config.get("PLANNER_TYPE") if planner_type == "CHANGE_SCHEDULER": logging.info(f"Triggering automatic reconfig via self.nsc with planner_type='CHANGE_SCHEDULER' for slice '{slice_id}'...") try: reconfig_res = self.reconfig_slice(slice_id) if isinstance(metrics, dict): metrics["reconfig_result"] = reconfig_res except Exception as e: logging.error(f"Automatic reconfig_slice failed for slice '{slice_id}': {e}") if isinstance(metrics, dict): metrics["reconfig_error"] = str(e) return metrics def reconfig_slice(self, slice_id: str) -> dict[str, Any]: """ Reconfigure a network slice by computing optimal path and coordinating with Change Scheduler. Args: slice_id (str): Network slice identifier. Returns: dict[str, Any]: Result from Planner and slice modification. """ reconfig_data = realizer({"slice_id": slice_id}, action="RECONFIG", controller_type=self.controller_type) planner = Planner() cs_result = planner.planner(reconfig_data, type="CHANGE_SCHEDULER") if _is_change_scheduler_viable(cs_result): logging.info( f"Change Scheduler confirmed viability for slice '{slice_id}'. Performing slice PUT modification..." ) intent = None try: xpath = "/ietf-network-slice-service:network-slice-services" intent = get_data_store(xpath) except Exception as e: logging.warning(f"Could not retrieve slice '{slice_id}' from sysrepo: {e}") if not intent: try: from src.database.db import get_data slice_db_entry = get_data(slice_id) intent = slice_db_entry.get("intent") if isinstance(slice_db_entry, dict) else None except Exception as e: logging.warning(f"Could not retrieve slice '{slice_id}' from DB: {e}") if not intent: raise ValueError("Network slice services not found") intent = { "ietf-network-slice-service:network-slice-services": { "slice-service": [{"id": slice_id}] } } if isinstance(intent, dict) and "network-slice-services" in intent: intent["ietf-network-slice-service:network-slice-services"] = intent.pop("network-slice-services") logging.debug(f"Intent found for slice '{slice_id}': {intent}") mod_result = self.nsc(intent, slice_id=slice_id) if isinstance(cs_result, dict): cs_result["slice_modification"] = mod_result logging.info( f"Slice '{slice_id}' successfully modified via PUT following Change Scheduler viability confirmation." ) update_data_store(intent) reconfig_res = self.nsc(intent, slice_id=slice_id, planner_type="CHANGE_SCHEDULER") if isinstance(metrics, dict): metrics["reconfig_result"] = reconfig_res except Exception as e: logging.error(f"Error during slice '{slice_id}' PUT modification: {e}") if isinstance(cs_result, dict): cs_result["slice_modification_error"] = str(e) logging.error(f"Automatic reconfig failed for slice '{slice_id}': {e}") if isinstance(metrics, dict): metrics["reconfig_error"] = str(e) return metrics return cs_result Loading
src/mapper/main.py +215 −200 Original line number Diff line number Diff line Loading @@ -15,11 +15,12 @@ # This file is an original contribution from Telefonica Innovación Digital S.L. import logging import uuid from typing import Any from flask import current_app from src.database.service_db import save_data from src.database.service_db import save_data, delete_by_slice_id from src.database.sysrepo_store import get_data_store, normalize_libyang_data from src.planner.planner import Planner from src.realizer.main import realizer Loading Loading @@ -94,8 +95,8 @@ def _collect_available_templates(ietf_intent: dict[str, Any]) -> list[dict[str, return available_templates def _map_restconf_services(ietf_intent: dict[str, Any]) -> list[dict[str, Any]]: """Transform IETF intent into discrete RESTCONF service constructs.""" def _map_restconf_services(ietf_intent: dict[str, Any], is_update: bool) -> list[dict[str, Any]]: """Map IETF intent into services.""" available_templates = _collect_available_templates(ietf_intent) services: list[dict[str, Any]] = [] Loading @@ -107,6 +108,7 @@ def _map_restconf_services(ietf_intent: dict[str, Any]) -> list[dict[str, Any]]: service_id = safe_get(slice_service, ["id"]) way = safe_get(slice_service, ["service-tags", "tag-type", 0, "tag-type-value", 0]) service_template = get_service_template(slice_service, available_templates) logging.debug(f"Service template: {service_template}") connection_groups = safe_get(slice_service, ["connection-groups", "connection-group"]) or [] for connection_group in connection_groups: Loading @@ -114,15 +116,17 @@ def _map_restconf_services(ietf_intent: dict[str, Any]) -> list[dict[str, Any]]: group_id = f"{service_id}-{cg_id}" group_template = get_service_template(connection_group, available_templates) or service_template logging.debug(f"Group template: {group_template}") raw_conn_type = safe_get(connection_group, ["connectivity-type"]) connectivity_type = normalize_connectivity_type(raw_conn_type) constructs = safe_get(connection_group, ["connectivity-construct"]) or [] for construct in constructs: construct_id_raw = safe_get(construct, ["id"]) full_construct_id = f"{group_id}-{construct_id_raw}" full_construct_id = f"{group_id}-{construct_id_raw}-{uuid.uuid4()}" final_template = get_service_template(construct, available_templates) or group_template logging.debug(f"Final template: {final_template}") sdps = process_connectivity( cg_id, Loading @@ -143,12 +147,15 @@ def _map_restconf_services(ietf_intent: dict[str, Any]) -> list[dict[str, Any]]: } services.append(service) if not current_app.config.get("DUMMY_MODE", False): save_data(service_id=service["id"], slice_id=service_id) if connectivity_type == "point-to-point": break if not current_app.config.get("DUMMY_MODE", False): if is_update: delete_by_slice_id(service_id) for service in services: save_data(service_id=service["id"], slice_id=service_id) return services Loading @@ -173,19 +180,27 @@ def mapper( ietf_intent = payload.get("intent") services: Any = [ietf_intent] optimal_path = None slice_id = payload.get("slice_id", None) is_update = payload.get("is_update", False) if current_app.config.get("NRP_ENABLED", False): if not _handle_nrp_mapping(ietf_intent): return None planner_type = current_app.config.get("PLANNER_TYPE", None) if current_app.config.get("PLANNER_ENABLED", False): is_update = payload.get("is_update", False) planner_type = current_app.config.get("PLANNER_TYPE", "SHORTEST_PATH") if not current_app.config.get("DUMMY_MODE", False) or planner_type == "ENERGY": if is_update and planner_type == "CHANGE_SCHEDULER": reconfig_data = realizer({"slice_id": slice_id}, action="RECONFIG", controller_type=controller_type) optimal_path = Planner().planner(reconfig_data, planner_type, is_update=is_update) else: optimal_path = Planner().planner(ietf_intent, planner_type, is_update=is_update) logging.debug(f"Optimal path: {optimal_path}") if controller_type == "RESTCONF": services = _map_restconf_services(ietf_intent) logging.debug(f"SLICE ID is: {slice_id}") services = _map_restconf_services(ietf_intent, is_update) return services, optimal_path Loading