Commit 693819d6 authored by Antonio Gines Buendia Lopez's avatar Antonio Gines Buendia Lopez
Browse files

WIP - refactor module architecture

parent d34ee10f
Loading
Loading
Loading
Loading
+124 −91
Original line number Diff line number Diff line
@@ -14,13 +14,13 @@
import json
import logging
from flask_restful import Resource, request
from flask.json import jsonify
from pathcompextended.client.PathCompExtendedClient import PathCompExtendedClient

from common.proto.context_pb2 import Empty
from common.proto.pathcompextended_pb2 import (
    IetfNetworkSlice, LivenessProbe, NetworkContext, NetworkTopology,
    TransportNetworkSliceL3, TransportOpticalSlice, UUID, GenericMessage
    IetfNetworkSlice, LivenessProbe, NetworkContext as NetworkContextProto,
    NetworkTopology, TransportNetworkSliceL3, TransportOpticalSlice, UUID,
    GenericMessage
)
from common.tools.grpc.Tools import grpc_message_to_json_string

@@ -50,10 +50,10 @@ class Health(_Resource):
            probe = self.pathcompextended_client.HealthCheck(None)
            LOGGER.debug(grpc_message_to_json_string(probe))
            output = json.loads(grpc_message_to_json_string(probe))
            return jsonify(output)
            return output
        except Exception as e:
            LOGGER.error(f"Error checking health: {e}", exc_info=True)
            return jsonify({'error': str(e), 'status': 'unhealthy'}), 500
            LOGGER.error("Error checking health: %s", e, exc_info=True)
            return {"error": str(e), "status": "unhealthy"}, 500

class Reset(_Resource):
    def post(self):
@@ -73,142 +73,176 @@ class NetworkContext(_Resource):
        try:
            net_context = self.pathcompextended_client.GetNetworkContext(None)
            LOGGER.debug(grpc_message_to_json_string(net_context))
            output = json.loads(grpc_message_to_json_string(net_context))
            return jsonify(output)
            # Convert proto to HRAT format: data with topology-id, controllerId, etc.
            output = {
                "data": [
                    {
                        "topology-id": t.topology_id,
                        "controllerId": t.controller_id,
                        "topology-type": t.topology_type,
                        "raw-json-topology": t.raw_json_topology,
                    }
                    for t in net_context.topologies
                ],
                "total": len(net_context.topologies),
            }
            return output
        except Exception as e:
            LOGGER.error(f"Error getting network context: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error getting network context: %s", e, exc_info=True)
            return {"error": str(e)}, 500

    def post(self):
        payload = request.json
        LOGGER.info(f"Operation POST /network-context")
        LOGGER.info("Operation POST /network-context")
        try:
            # Build NetworkContext from JSON payload
            net_context_msg = NetworkContext()
            if payload and 'topologies' in payload:
                for topology_json in payload['topologies']:
            # Build NetworkContext proto from JSON payload (HRAT format: topology-id, controllerId, etc.)
            net_context_msg = NetworkContextProto()
            if payload and "topologies" in payload:
                for topology_json in payload["topologies"]:
                    topology = NetworkTopology()
                    if 'topology_id' in topology_json:
                        topology.topology_id = topology_json['topology_id']
                    if 'controller_id' in topology_json:
                        topology.controller_id = topology_json['controller_id']
                    if 'topology_type' in topology_json:
                        topology.topology_type = topology_json['topology_type']
                    if 'raw_json_topology' in topology_json:
                        topology.raw_json_topology = topology_json['raw_json_topology']
                    topology.topology_id = topology_json.get("topology-id") or topology_json.get("topology_id", "")
                    topology.controller_id = topology_json.get("controllerId") or topology_json.get("controller_id", "")
                    topology.topology_type = topology_json.get("topology-type") or topology_json.get("topology_type", "")
                    topology.raw_json_topology = topology_json.get("raw-json-topology") or topology_json.get("raw_json_topology", "")
                    net_context_msg.topologies.append(topology)

            result = self.pathcompextended_client.CreateNetworkContext(net_context_msg)
            LOGGER.debug(grpc_message_to_json_string(result))

            if result.ifTrueIsJsonStringified:
                output = json.loads(grpc_message_to_json_string(result.message))
                output = json.loads(result.message)
            else:
                output = json.loads(grpc_message_to_json_string(result))
                output = {"message": result.message}

            return jsonify(output), 201
            return output, 201
        except Exception as e:
            LOGGER.error(f"Error creating network context: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error creating network context: %s", e, exc_info=True)
            return {"error": str(e)}, 500


class NetworkContextDetails(_Resource):
    def get(self, id: str):
        LOGGER.info(f"Operation GET /network-context/{id}")
        LOGGER.info("Operation GET /network-context/%s", id)
        try:
            uuid_msg = UUID()
            uuid_msg.value = id
            topology = self.pathcompextended_client.GetSpecificNetworkContext(uuid_msg)
            LOGGER.debug(grpc_message_to_json_string(topology))
            output = json.loads(grpc_message_to_json_string(topology))
            return jsonify(output)
            output = {
                "topology-id": topology.topology_id,
                "controllerId": topology.controller_id,
                "topology-type": topology.topology_type,
                "raw-json-topology": topology.raw_json_topology,
            }
            return output
        except Exception as e:
            LOGGER.error(f"Error getting network context {id}: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error getting network context %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500

    def delete(self, id: str):
        LOGGER.info(f"Operation DELETE /network-context/{id}")
        LOGGER.info("Operation DELETE /network-context/%s", id)
        try:
            uuid_msg = UUID()
            uuid_msg.value = id
            result = self.pathcompextended_client.DeleteNetworkContext(uuid_msg)
            LOGGER.debug(grpc_message_to_json_string(result))
            output = json.loads(grpc_message_to_json_string(result))
            return jsonify(output), 204
            if result.ifTrueIsJsonStringified:
                output = json.loads(result.message)
            else:
                output = {"message": result.message}
            return output, 204
        except Exception as e:
            LOGGER.error(f"Error deleting network context {id}: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error deleting network context %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500

# ---------------------------------------------------------------------------------- #
# -- TransportOpticalSlice --------------------------------------------------------- #
#                                                                                    #
# ---------------------------------------------------------------------------------- #

def _transport_network_slice_l3_to_hrat(slice_msg):
    """Convert TransportNetworkSliceL3 proto to HRAT format."""
    return {
        "transport-network-slice-l3-uuid": slice_msg.transport_network_slice_l3_uuid.value,
        "optical-slice-uuid": slice_msg.transport_optical_slice_uuid.value,
        "viability": slice_msg.viability,
        "actions": json.loads(slice_msg.raw_actions_json) if slice_msg.raw_actions_json else [],
    }


def _transport_optical_slice_to_hrat(slice_msg):
    """Convert TransportOpticalSlice proto to HRAT format."""
    return {
        "optical-slice-uuid": slice_msg.transport_optical_slice_uuid.value,
        "viability": slice_msg.viability,
        "actions": json.loads(slice_msg.raw_optical_slice_json) if slice_msg.raw_optical_slice_json else [],
    }


class TransportOpticalSlice(_Resource):
    def get(self):
        LOGGER.info("Operation GET /transport-optical-slice")
        try:
            slices = self.pathcompextended_client.GetTransportOpticalSlices(None)
            result = []
            for slice_msg in slices:
                slice_json = json.loads(grpc_message_to_json_string(slice_msg))
                result.append(slice_json)
            LOGGER.debug(f"Retrieved {len(result)} transport optical slices")
            return jsonify(result)
            result = [_transport_optical_slice_to_hrat(s) for s in slices]
            LOGGER.debug("Retrieved %d transport optical slices", len(result))
            return result
        except Exception as e:
            LOGGER.error(f"Error getting transport optical slices: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error getting transport optical slices: %s", e, exc_info=True)
            return {"error": str(e)}, 500

    def post(self):
        LOGGER.info("Operation POST /transport-optical-slice")
        try:
            payload = request.json
            # Build IetfNetworkSlice from JSON payload
            ietf_slice = IetfNetworkSlice()
            if payload:
                if isinstance(payload, str):
                    ietf_slice.raw_json_slice = payload
                elif 'raw_json_slice' in payload:
                    ietf_slice.raw_json_slice = payload['raw_json_slice']
                elif "raw_json_slice" in payload:
                    ietf_slice.raw_json_slice = payload["raw_json_slice"]
                else:
                    ietf_slice.raw_json_slice = json.dumps(payload)

            result = self.pathcompextended_client.CreateTransportOpticalSlice(ietf_slice)
            LOGGER.debug(grpc_message_to_json_string(result))
            output = json.loads(grpc_message_to_json_string(result))
            return jsonify(output), 201
            output = _transport_optical_slice_to_hrat(result)
            return output, 201
        except Exception as e:
            LOGGER.error(f"Error creating transport optical slice: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error creating transport optical slice: %s", e, exc_info=True)
            return {"error": str(e)}, 500



class TransportOpticalSliceDetails(_Resource):
    def get(self, id: str):
        LOGGER.info(f"Operation GET /transport-optical-slice/{id}")
        LOGGER.info("Operation GET /transport-optical-slice/%s", id)
        try:
            uuid_msg = UUID()
            uuid_msg.value = id
            slice_msg = self.pathcompextended_client.GetTransportOpticalSlice(uuid_msg)
            LOGGER.debug(grpc_message_to_json_string(slice_msg))
            output = json.loads(grpc_message_to_json_string(slice_msg))
            return jsonify(output)
            output = _transport_optical_slice_to_hrat(slice_msg)
            return output
        except Exception as e:
            LOGGER.error(f"Error getting transport optical slice {id}: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error getting transport optical slice %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500

    def delete(self, id: str):
        LOGGER.info(f"Operation DELETE /transport-optical-slice/{id}")
        LOGGER.info("Operation DELETE /transport-optical-slice/%s", id)
        try:
            uuid_msg = UUID()
            uuid_msg.value = id
            result = self.pathcompextended_client.DeleteTransportOpticalSlice(uuid_msg)
            LOGGER.debug(grpc_message_to_json_string(result))
            output = json.loads(grpc_message_to_json_string(result))
            return jsonify(output), 204
            if result.ifTrueIsJsonStringified:
                output = json.loads(result.message)
            else:
                output = {"message": result.message}
            return output, 204
        except Exception as e:
            LOGGER.error(f"Error deleting transport optical slice {id}: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error deleting transport optical slice %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500

# ---------------------------------------------------------------------------------- #
# -- TransportNetworkSliceL3 ------------------------------------------------------- #
@@ -220,64 +254,63 @@ class TransportNetworkSliceL3(_Resource):
        LOGGER.info("Operation GET /transport-network-slice-l3")
        try:
            slices = self.pathcompextended_client.GetTransportNetworkSlicesL3(None)
            result = []
            for slice_msg in slices:
                slice_json = json.loads(grpc_message_to_json_string(slice_msg))
                result.append(slice_json)
            LOGGER.debug(f"Retrieved {len(result)} transport network slices L3")
            return jsonify(result)
            result = [_transport_network_slice_l3_to_hrat(s) for s in slices]
            LOGGER.debug("Retrieved %d transport network slices L3", len(result))
            return result
        except Exception as e:
            LOGGER.error(f"Error getting transport network slices L3: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error getting transport network slices L3: %s", e, exc_info=True)
            return {"error": str(e)}, 500

    def post(self):
        LOGGER.info("Operation POST /transport-network-slice-l3")
        try:
            payload = request.json
            # Build IetfNetworkSlice from JSON payload
            ietf_slice = IetfNetworkSlice()
            if payload:
                if isinstance(payload, str):
                    ietf_slice.raw_json_slice = payload
                elif 'raw_json_slice' in payload:
                    ietf_slice.raw_json_slice = payload['raw_json_slice']
                elif "raw_json_slice" in payload:
                    ietf_slice.raw_json_slice = payload["raw_json_slice"]
                else:
                    ietf_slice.raw_json_slice = json.dumps(payload)

            result = self.pathcompextended_client.CreateTransportNetworkSliceL3(ietf_slice)
            LOGGER.debug(grpc_message_to_json_string(result))
            output = json.loads(grpc_message_to_json_string(result))
            return jsonify(output), 201
            output = _transport_network_slice_l3_to_hrat(result)
            return output, 201
        except Exception as e:
            LOGGER.error(f"Error creating transport network slice L3: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error creating transport network slice L3: %s", e, exc_info=True)
            return {"error": str(e)}, 500



class TransportNetworkSliceL3Details(_Resource):
    def get(self, id: str):
        LOGGER.info(f"Operation GET /transport-network-slice-l3/{id}")
        LOGGER.info("Operation GET /transport-network-slice-l3/%s", id)
        try:
            uuid_msg = UUID()
            uuid_msg.value = id
            slice_msg = self.pathcompextended_client.GetTransportNetworkSliceL3(uuid_msg)
            LOGGER.debug(grpc_message_to_json_string(slice_msg))
            output = json.loads(grpc_message_to_json_string(slice_msg))
            return jsonify(output)
            output = _transport_network_slice_l3_to_hrat(slice_msg)
            return output
        except Exception as e:
            LOGGER.error(f"Error getting transport network slice L3 {id}: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error getting transport network slice L3 %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500

    def delete(self, id: str):
        LOGGER.info(f"Operation DELETE /transport-network-slice-l3/{id}")
        LOGGER.info("Operation DELETE /transport-network-slice-l3/%s", id)
        try:
            uuid_msg = UUID()
            uuid_msg.value = id
            result = self.pathcompextended_client.DeleteTransportNetworkSliceL3(uuid_msg)
            LOGGER.debug(grpc_message_to_json_string(result))
            output = json.loads(grpc_message_to_json_string(result))
            return jsonify(output), 204
            if result.ifTrueIsJsonStringified:
                output = json.loads(result.message)
            else:
                output = {"message": result.message}
            return output, 204
        except Exception as e:
            LOGGER.error(f"Error deleting transport network slice L3 {id}: {e}", exc_info=True)
            return jsonify({'error': str(e)}), 500
            LOGGER.error("Error deleting transport network slice L3 %s: %s", id, e, exc_info=True)
            return {"error": str(e)}, 500
+19 −32
Original line number Diff line number Diff line
# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
from typing import Any, Dict

from pydantic import BaseModel

from pathcompextended.connector.api_client import APIClient
from pathcompextended.model.health import HealthCheckResponse
from pathcompextended.schemas.common import (
    HealthCheckGetResponse,
    ExternalKpisPostRequest,
    ExternalKpisPostResponse,
)

LOGGER = logging.getLogger(__name__)


# ============================================================================
# HEALTH CHECK MODELS
# ============================================================================


class HealthCheckGetResponse(HealthCheckResponse):
    """Response model for GET /health."""

    pass


# ============================================================================
# EXTERNAL KPIs MODELS
# ============================================================================


class ExternalKpisPostRequest(BaseModel):
    """Request model for POST /external-kpis."""

    # Accepts arbitrary JSON structure
    data: Dict[str, Any]


class ExternalKpisPostResponse(BaseModel):
    """Response model for POST /external-kpis."""

    message: str


# ============================================================================
# GENERAL HRAT ENDPOINTS CONNECTOR
# ============================================================================
+0 −34
Original line number Diff line number Diff line

from typing import Any, Dict, Optional

from pathcompextended.connector.api_client import APIClient


class GeneralMethods:
    """
    Helper for performing generic HTTP operations against the H-RAT REST API.

    This class is intentionally self-contained and does not import the HRAT
    wrapper itself to avoid circular dependencies. It can be reused by
    resource-specific connector classes.
    """

    def __init__(self, api_client: APIClient, base_api_url: str) -> None:
        self._api_client = api_client
        self._base_api_url = base_api_url

    def _build_endpoint(self, relative_path: str) -> str:
        relative_path = relative_path.lstrip("/")
        return f"{self._base_api_url}/{relative_path}"

    def get(self, relative_path: str, body: Optional[Dict[str, Any]] = None):
        endpoint = self._build_endpoint(relative_path)
        return self._api_client.get(endpoint, body=body)

    def post(self, relative_path: str, body: Optional[Dict[str, Any]] = None):
        endpoint = self._build_endpoint(relative_path)
        return self._api_client.post(endpoint, body=body or {})

    def delete(self, relative_path: str, body: Optional[Dict[str, Any]] = None):
        endpoint = self._build_endpoint(relative_path)
        return self._api_client.delete(endpoint, body=body)
 No newline at end of file
+21 −51
Original line number Diff line number Diff line
import logging
from typing import List, Optional
# Copyright 2022-2025 ETSI SDG TeraFlowSDN (TFS) (https://tfs.etsi.org/)
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from pydantic import BaseModel, Field
import logging

from pathcompextended.connector.api_client import APIClient
from pathcompextended.model.network_context import NetworkContextRequest, Topology
from pathcompextended.schemas.network_context import (
    NetworkContextPostRequest,
    NetworkContextPostResponse,
    NetworkContextGetResponse,
    NetworkContextGetByControllerResponse,
    NetworkContextDeleteResponse,
)

LOGGER = logging.getLogger(__name__)


# ============================================================================
# NETWORK CONTEXT MODELS
# ============================================================================


class NetworkContextPostRequest(NetworkContextRequest):
    """Request model for POST /network-context."""

    pass


class NetworkContextPostResponse(BaseModel):
    """Response model for POST /network-context."""

    message: str
    total_topologies: int = Field(..., alias="total-topologies")

    class Config:
        populate_by_name = True


class NetworkContextGetResponse(BaseModel):
    """Response model for GET /network-context."""

    data: List[Topology]
    total: Optional[int] = None
    message: Optional[str] = None


class NetworkContextGetByControllerResponse(BaseModel):
    """Response model for GET /network-context/{controllerId}."""

    data: List[Topology]
    total: Optional[int] = None
    message: Optional[str] = None


class NetworkContextDeleteResponse(BaseModel):
    """Response model for DELETE /network-context/{controllerId}."""

    deleted_count: int = Field(..., alias="deleted-count")
    message: str

    class Config:
        populate_by_name = True


# ============================================================================
# NETWORK CONTEXT CONNECTOR
# ============================================================================
+21 −65

File changed.

Preview size limit exceeded, changes collapsed.

Loading