Commit eef68e8b authored by Lluis Gifre Renom's avatar Lluis Gifre Renom
Browse files

Common tools:

- Renamed RestClient to RestApiClient
- Added RestConfClient
parent ab1836d5
Loading
Loading
Loading
Loading
+28 −7
Original line number Diff line number Diff line
@@ -12,10 +12,12 @@
# See the License for the specific language governing permissions and
# limitations under the License.


import enum, logging, requests
from requests.auth import HTTPBasicAuth
from typing import Any, Optional, Set


class RestRequestMethod(enum.Enum):
    GET    = 'get'
    POST   = 'post'
@@ -23,6 +25,7 @@ class RestRequestMethod(enum.Enum):
    PATCH  = 'patch'
    DELETE = 'delete'


EXPECTED_STATUS_CODES : Set[int] = {
    requests.codes['OK'        ],   # 200 - OK
    requests.codes['CREATED'   ],   # 201 - Created
@@ -30,7 +33,6 @@ EXPECTED_STATUS_CODES : Set[int] = {
    requests.codes['NO_CONTENT'],   # 204 - No Content
}

URL_TEMPLATE = '{:s}://{:s}:{:d}/{:s}'

def compose_basic_auth(
    username : Optional[str] = None, password : Optional[str] = None
@@ -38,19 +40,24 @@ def compose_basic_auth(
    if username is None or password is None: return None
    return HTTPBasicAuth(username, password)


class SchemeEnum(enum.Enum):
    HTTP  = 'http'
    HTTPS = 'https'


def check_scheme(scheme : str) -> str:
    str_scheme = str(scheme).lower()
    enm_scheme = SchemeEnum._value2member_map_[str_scheme]
    return enm_scheme.value


class RestClient:
TEMPLATE_URL  = '{:s}://{:s}:{:d}/{:s}'


class RestApiClient:
    def __init__(
        self, address : str, port : int, scheme : str = 'http',
        self, address : str, port : int = 8080, scheme : str = 'http', base_url : str = '',
        username : Optional[str] = None, password : Optional[str] = None,
        timeout : int = 30, verify_certs : bool = True, allow_redirects : bool = True,
        logger : Optional[logging.Logger] = None
@@ -58,15 +65,13 @@ class RestClient:
        self._address         = address
        self._port            = int(port)
        self._scheme          = check_scheme(scheme)
        self._base_url        = base_url
        self._auth            = compose_basic_auth(username=username, password=password)
        self._timeout         = int(timeout)
        self._verify_certs    = verify_certs
        self._allow_redirects = allow_redirects
        self._logger          = logger

    def _compose_url(self, endpoint : str) -> str:
        endpoint = endpoint.lstrip('/')
        return URL_TEMPLATE.format(self._scheme, self._address, self._port, endpoint)

    def _log_msg_request(
        self, method : RestRequestMethod, request_url : str, body : Optional[Any],
@@ -77,6 +82,7 @@ class RestClient:
        if self._logger is not None: self._logger.log(log_level, msg)
        return msg


    def _log_msg_check_reply(
        self, method : RestRequestMethod, request_url : str, body : Optional[Any],
        reply : requests.Response, expected_status_codes : Set[int],
@@ -94,12 +100,20 @@ class RestClient:
        self._logger.error(msg)
        raise Exception(msg)


    def _do_rest_request(
        self, method : RestRequestMethod, endpoint : str, body : Optional[Any] = None,
        expected_status_codes : Set[int] = EXPECTED_STATUS_CODES
    ) -> Optional[Any]:
        request_url = self._compose_url(endpoint)
        candidate_schemes = tuple(['{:s}://'.format(m).lower() for m in SchemeEnum.__members__])
        if endpoint.lower().startswith(candidate_schemes):
            request_url = endpoint.lstrip('/')
        else:
            endpoint = str(self._base_url + '/' + endpoint).replace('//', '/').lstrip('/')
            request_url = TEMPLATE_URL.format(self._scheme, self._address, self._port, endpoint)

        self._log_msg_request(method, request_url, body)

        try:
            headers = {'accept': 'application/json'}
            reply = requests.request(
@@ -112,10 +126,13 @@ class RestClient:
            msg = MSG.format(str(method.value).upper(), request_url, str(body))
            self._logger.exception(msg)
            raise Exception(msg) from e

        self._log_msg_check_reply(method, request_url, body, reply, expected_status_codes)

        if reply.content and len(reply.content) > 0: return reply.json()
        return None


    def get(
        self, endpoint : str,
        expected_status_codes : Set[int] = EXPECTED_STATUS_CODES
@@ -125,6 +142,7 @@ class RestClient:
            expected_status_codes=expected_status_codes
        )


    def post(
        self, endpoint : str, body : Optional[Any] = None,
        expected_status_codes : Set[int] = EXPECTED_STATUS_CODES
@@ -134,6 +152,7 @@ class RestClient:
            expected_status_codes=expected_status_codes
        )


    def put(
        self, endpoint : str, body : Optional[Any] = None,
        expected_status_codes : Set[int] = EXPECTED_STATUS_CODES
@@ -143,6 +162,7 @@ class RestClient:
            expected_status_codes=expected_status_codes
        )


    def patch(
        self, endpoint : str, body : Optional[Any] = None,
        expected_status_codes : Set[int] = EXPECTED_STATUS_CODES
@@ -152,6 +172,7 @@ class RestClient:
            expected_status_codes=expected_status_codes
        )


    def delete(
        self, endpoint : str, body : Optional[Any] = None,
        expected_status_codes : Set[int] = EXPECTED_STATUS_CODES
+104 −0
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, requests
from typing import Any, Dict, Optional, Set
from .RestApiClient import RestApiClient


HOST_META_URL = '{:s}://{:s}:{:d}/.well-known/host-meta'


class RestConfClient(RestApiClient):
    def __init__(
        self, address : str, port : int = 8080, scheme : str = 'http',
        username : Optional[str] = None, password : Optional[str] = None,
        timeout : int = 30, verify_certs : bool = True, allow_redirects : bool = True,
        logger : Optional[logging.Logger] = None
    ) -> None:
        super().__init__(
            address, port=port, scheme=scheme, username=username, password=password,
            timeout=timeout, verify_certs=verify_certs, allow_redirects=allow_redirects,
            logger=logger
        )

        self._discover_base_url()

    def _discover_base_url(self) -> None:
        host_meta_url = HOST_META_URL.format(self._scheme, self._address, self._port)
        host_meta : Dict = self.get(host_meta_url, expected_status_codes={requests.codes['OK']})

        links = host_meta.get('links')
        if links is None: raise AttributeError('Missing attribute "links" in host-meta reply')
        if not isinstance(links, list): raise AttributeError('Attribute "links" must be a list')
        if len(links) != 1: raise AttributeError('Attribute "links" is expected to have exactly 1 item')

        link = links[0]
        if not isinstance(link, dict): raise AttributeError('Attribute "links[0]" must be a dict')

        rel = link.get('rel')
        if rel is None: raise AttributeError('Missing attribute "links[0].rel" in host-meta reply')
        if not isinstance(rel, str): raise AttributeError('Attribute "links[0].rel" must be a str')
        if rel != 'restconf': raise AttributeError('Attribute "links[0].rel" != "restconf"')

        href = link.get('href')
        if href is None: raise AttributeError('Missing attribute "links[0]" in host-meta reply')
        if not isinstance(href, str): raise AttributeError('Attribute "links[0].href" must be a str')

        self._base_url = str(href + '/data').replace('//', '/')

    def get(
        self, endpoint : str,
        expected_status_codes : Set[int] = {requests.codes['OK']}
    ) -> Optional[Any]:
        return super().get(
            endpoint,
            expected_status_codes=expected_status_codes
        )

    def post(
        self, endpoint : str, body : Optional[Any] = None,
        expected_status_codes : Set[int] = {requests.codes['CREATED']}
    ) -> Optional[Any]:
        return super().post(
            endpoint, body=body,
            expected_status_codes=expected_status_codes
        )

    def put(
        self, endpoint : str, body : Optional[Any] = None,
        expected_status_codes : Set[int] = {requests.codes['CREATED'], requests.codes['NO_CONTENT']}
    ) -> Optional[Any]:
        return super().put(
            endpoint, body=body,
            expected_status_codes=expected_status_codes
        )

    def patch(
        self, endpoint : str, body : Optional[Any] = None,
        expected_status_codes : Set[int] = {requests.codes['NO_CONTENT']}
    ) -> Optional[Any]:
        return super().patch(
            endpoint, body=body,
            expected_status_codes=expected_status_codes
        )

    def delete(
        self, endpoint : str, body : Optional[Any] = None,
        expected_status_codes : Set[int] = {requests.codes['NO_CONTENT']}
    ) -> Optional[Any]:
        return super().delete(
            endpoint, body=body,
            expected_status_codes=expected_status_codes
        )
+2 −2
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@

import logging, requests
from typing import Dict, List, Optional
from common.tools.client.RestClient import RestClient
from common.tools.client.RestApiClient import RestApiClient
from device.service.driver_api.ImportTopologyEnum import ImportTopologyEnum

GET_CONTEXT_IDS_URL = '/tfs-api/context_ids'
@@ -51,7 +51,7 @@ MAPPING_DRIVER = {

LOGGER = logging.getLogger(__name__)

class TfsApiClient(RestClient):
class TfsApiClient(RestApiClient):
    def __init__(
        self, address : str, port : int, scheme : str = 'http',
        username : Optional[str] = None, password : Optional[str] = None,
+2 −2
Original line number Diff line number Diff line
@@ -14,7 +14,7 @@

import logging, requests
from typing import Dict, List, Optional
from common.tools.client.RestClient import RestClient
from common.tools.client.RestApiClient import RestApiClient
from device.service.driver_api.ImportTopologyEnum import ImportTopologyEnum

GET_CONTEXT_IDS_URL = '/tfs-api/context_ids'
@@ -52,7 +52,7 @@ MAPPING_DRIVER = {

LOGGER = logging.getLogger(__name__)

class TfsApiClient(RestClient):
class TfsApiClient(RestApiClient):
    def __init__(
        self, address : str, port : int, scheme : str = 'http',
        username : Optional[str] = None, password : Optional[str] = None,
+2 −2
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@ import logging
from typing import Dict, List, Optional, Tuple
from common.Constants import DEFAULT_CONTEXT_NAME, DEFAULT_TOPOLOGY_NAME
from common.proto.context_pb2 import ServiceStatusEnum, ServiceTypeEnum
from common.tools.client.RestClient import RestClient
from common.tools.client.RestApiClient import RestApiClient
from common.tools.object_factory.Constraint import json_constraint_custom
from common.tools.object_factory.Context import json_context_id
from common.tools.object_factory.Device import json_device_id
@@ -59,7 +59,7 @@ MAPPING_DRIVER = {

LOGGER = logging.getLogger(__name__)

class TfsApiClient(RestClient):
class TfsApiClient(RestApiClient):
    def __init__(
        self, address : str, port : int, scheme : str = 'http',
        username : Optional[str] = None, password : Optional[str] = None,
Loading