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

Merge branch...

Merge branch 'feat/45-tid-fix-subscriptions-database-to-include-data-from-the-whole-series' into 'develop'

Resolve "(TID) Fix subscriptions database to include data from the whole series"

See merge request !48
parents c76c4385 f2a4297c
Loading
Loading
Loading
Loading
Loading
+35 −8
Changes for src/api/handlers/restconf_handler.py: 35 added lines, 8 removed lines.
Original line number Diff line number Diff line
@@ -829,19 +829,33 @@ class RestconfHandler:
    # RESTCONF Telemetry Operations: Subscriptions
    # -------------------------------------------------------------------------

    def get_subscriptions(self, client_id: str, slice_id: str | None = None) -> tuple[dict[str, Any], int]:
        """Retrieve telemetry subscriptions for a client."""
    def get_subscriptions(
        self,
        client_id: str,
        slice_id: str | None = None,
        from_ts: float | None = None,
        to_ts: float | None = None,
        limit: int | None = None,
    ) -> tuple[dict[str, Any], int]:
        """Retrieve telemetry subscriptions for a client, with optional history filters."""
        try:
            if slice_id is not None:
                return self._get_single_client_subscription(client_id, slice_id)
            return self._get_all_client_subscriptions(client_id)
                return self._get_single_client_subscription(client_id, slice_id, from_ts, to_ts, limit)
            return self._get_all_client_subscriptions(client_id, from_ts, to_ts, limit)
        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))

    def _get_single_client_subscription(self, client_id: str, slice_id: str) -> tuple[dict[str, Any], int]:
        """Get a single subscription with telemetry data."""
    def _get_single_client_subscription(
        self,
        client_id: str,
        slice_id: str,
        from_ts: float | None = None,
        to_ts: float | None = None,
        limit: int | None = None,
    ) -> tuple[dict[str, Any], int]:
        """Get a single subscription with the latest or historic telemetry."""
        try:
            get_sub_fn = _dep("get_subscription", _real_get_subscription)
            subscription = get_sub_fn(client_id, slice_id)
@@ -851,16 +865,29 @@ class RestconfHandler:
        if not subscription:
            raise ValueError(f"Client '{client_id}' has no subscription for slice '{slice_id}'")

        if from_ts is not None or to_ts is not None or limit is not None:
            telemetry, _ = self._get_telemetry_history(slice_id, from_ts, to_ts, limit)
        else:
            telemetry, _ = self.get_telemetry(slice_id)
        return {**subscription, "telemetry": telemetry}, 200

    def _get_all_client_subscriptions(self, client_id: str) -> tuple[dict[str, Any], int]:
        """Get all subscriptions for a client with telemetry data."""
    def _get_all_client_subscriptions(
        self,
        client_id: str,
        from_ts: float | None = None,
        to_ts: float | None = None,
        limit: int | None = None,
    ) -> tuple[dict[str, Any], int]:
        """Get all subscriptions for a client with the latest or historic telemetry."""
        get_cli_subs_fn = _dep("get_client_subscriptions", _real_get_client_subscriptions)
        subscriptions = get_cli_subs_fn(client_id)
        use_history = from_ts is not None or to_ts is not None or limit is not None
        result = []
        for sub in subscriptions:
            sub_slice_id = sub["slice_id"]
            if use_history:
                telemetry, _ = self._get_telemetry_history(sub_slice_id, from_ts, to_ts, limit)
            else:
                telemetry, _ = self.get_telemetry(sub_slice_id)
            result.append({**sub, "telemetry": telemetry})

+60 −39
Changes for swagger/restconf_namespace.py: 60 added lines, 39 removed lines.
Original line number Diff line number Diff line
@@ -325,6 +325,39 @@ class SliceNetworkTopology(Resource):
# =============================================================================


TELEMETRY_HISTORY_PARAMS = {
    "from": "Optional. Inclusive lower bound as Unix epoch seconds (e.g. 1758700000.5). Returns stored history.",
    "to": "Optional. Inclusive upper bound as Unix epoch seconds. Returns stored history.",
    "limit": f"Optional. Return only the N most recent stored samples per slice (1-{MAX_TELEMETRY_HISTORY}).",
}


def _parse_telemetry_history_args() -> dict[str, Any]:
    """Parse the optional 'from', 'to' and 'limit' query parameters.

    Raises:
        ValueError: If a parameter is not numeric or 'limit' is out of range.
    """
    args: dict[str, Any] = {"from_ts": None, "to_ts": None, "limit": None}
    for param, key in (("from", "from_ts"), ("to", "to_ts")):
        raw = request.args.get(param)
        if raw is not None:
            try:
                args[key] = float(raw)
            except ValueError:
                raise ValueError(f"Query parameter '{param}' must be a Unix epoch timestamp in seconds") from None

    raw_limit = request.args.get("limit")
    if raw_limit is not None:
        try:
            args["limit"] = int(raw_limit)
        except ValueError:
            raise ValueError("Query parameter 'limit' must be an integer") from None
        if not 1 <= args["limit"] <= MAX_TELEMETRY_HISTORY:
            raise ValueError(f"Query parameter 'limit' must be between 1 and {MAX_TELEMETRY_HISTORY}")
    return args


@restconf_ns.route("/operations/telemetry/client")
class ClientsList(Resource):
    """Resource for telemetry client collection operations."""
@@ -386,15 +419,24 @@ class Subscriptions(Resource):
    """Resource for client subscription collection operations."""

    @restconf_ns.doc(
        description="Retrieves all telemetry subscriptions of a given client.",
        params={"client_id": "Client ID."},
        description=(
            "Retrieves all telemetry subscriptions of a given client. "
            "Without filters returns the latest telemetry per subscription; "
            "with 'from', 'to' or 'limit' returns stored history instead."
        ),
        params={"client_id": "Client ID.", **TELEMETRY_HISTORY_PARAMS},
    )
    @restconf_ns.response(200, "Subscriptions successfully retrieved")
    @restconf_ns.response(400, "Invalid query parameters")
    @restconf_ns.response(500, "Internal server error")
    def get(self, client_id: str) -> Any:
        """Retrieve all subscriptions for a client."""
        """Retrieve all subscriptions for a client, with latest or historic telemetry."""
        try:
            history_args = _parse_telemetry_history_args()
        except ValueError as exc:
            return send_response(False, code=400, message=str(exc))
        handler = RestconfHandler(NSController(controller_type="RESTCONF"))
        return current_app.ensure_sync(handler.get_subscriptions)(client_id)
        return current_app.ensure_sync(handler.get_subscriptions)(client_id, **history_args)

    @restconf_ns.doc(
        description="Creates a new telemetry subscription for a given client.",
@@ -438,15 +480,25 @@ class SliceSubscriptions(Resource):
    """Resource for specific client-slice subscription operations."""

    @restconf_ns.doc(
        description="Retrieves a specific telemetry subscription of a given client.",
        params={"client_id": "Client ID.", "slice_id": "Network Slice ID."},
        description=(
            "Retrieves a specific telemetry subscription of a given client. "
            "Without filters returns the latest telemetry; "
            "with 'from', 'to' or 'limit' returns stored history instead."
        ),
        params={"client_id": "Client ID.", "slice_id": "Network Slice ID.", **TELEMETRY_HISTORY_PARAMS},
    )
    @restconf_ns.response(200, "Subscription successfully retrieved")
    @restconf_ns.response(400, "Invalid query parameters")
    @restconf_ns.response(404, "Subscription not found")
    @restconf_ns.response(500, "Internal server error")
    def get(self, client_id: str, slice_id: str) -> Any:
        """Retrieve a specific subscription."""
        """Retrieve a specific subscription, with latest or historic telemetry."""
        try:
            history_args = _parse_telemetry_history_args()
        except ValueError as exc:
            return send_response(False, code=400, message=str(exc))
        handler = RestconfHandler(NSController(controller_type="RESTCONF"))
        return current_app.ensure_sync(handler.get_subscriptions)(client_id, slice_id)
        return current_app.ensure_sync(handler.get_subscriptions)(client_id, slice_id, **history_args)

    @restconf_ns.doc(
        description="Updates a telemetry subscription for a given client.",
@@ -509,37 +561,6 @@ class SliceSubscriptionsStream(Resource):
        )


TELEMETRY_HISTORY_PARAMS = {
    "from": "Optional. Inclusive lower bound as Unix epoch seconds (e.g. 1758700000.5). Returns stored history.",
    "to": "Optional. Inclusive upper bound as Unix epoch seconds. Returns stored history.",
    "limit": f"Optional. Return only the N most recent stored samples per slice (1-{MAX_TELEMETRY_HISTORY}).",
}


def _parse_telemetry_history_args() -> dict[str, Any]:
    """Parse the optional 'from', 'to' and 'limit' query parameters.

    Raises:
        ValueError: If a parameter is not numeric or 'limit' is out of range.
    """
    args: dict[str, Any] = {"from_ts": None, "to_ts": None, "limit": None}
    for param, key in (("from", "from_ts"), ("to", "to_ts")):
        raw = request.args.get(param)
        if raw is not None:
            try:
                args[key] = float(raw)
            except ValueError:
                raise ValueError(f"Query parameter '{param}' must be a Unix epoch timestamp in seconds") from None

    raw_limit = request.args.get("limit")
    if raw_limit is not None:
        try:
            args["limit"] = int(raw_limit)
        except ValueError:
            raise ValueError("Query parameter 'limit' must be an integer") from None
        if not 1 <= args["limit"] <= MAX_TELEMETRY_HISTORY:
            raise ValueError(f"Query parameter 'limit' must be between 1 and {MAX_TELEMETRY_HISTORY}")
    return args


@restconf_ns.route("/operations/telemetry/slice")