Commit 781118a9 authored by Javier Velázquez's avatar Javier Velázquez
Browse files

- Update slice reconfig endpoint

- Update tests
- Fix update of services in service database
- Fix update of network slices in api
- Fix bugs
parent b0938d2c
Loading
Loading
Loading
Loading
Loading
+10 −1
Original line number Diff line number Diff line
@@ -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
+75 −17
Original line number Diff line number Diff line
@@ -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)
@@ -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))

@@ -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")

@@ -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))
+51 −45
Original line number Diff line number Diff line
@@ -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(
@@ -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)
+57 −67
Original line number Diff line number Diff line
@@ -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:
@@ -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.
@@ -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": []}
@@ -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(
@@ -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.
@@ -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,
@@ -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



+215 −200
Original line number Diff line number Diff line
@@ -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
@@ -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]] = []

@@ -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:
@@ -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,
@@ -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


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