Loading src/common/tools/client/RestClient.py→src/common/tools/client/RestApiClient.py +28 −7 Original line number Diff line number Diff line Loading @@ -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' Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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], Loading @@ -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], Loading @@ -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( Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading src/common/tools/client/RestConfClient.py 0 → 100644 +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 ) src/device/service/drivers/ietf_l2vpn/TfsApiClient.py +2 −2 Original line number Diff line number Diff line Loading @@ -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' Loading Loading @@ -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, Loading src/device/service/drivers/ietf_l3vpn/TfsApiClient.py +2 −2 Original line number Diff line number Diff line Loading @@ -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' Loading Loading @@ -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, Loading src/device/service/drivers/optical_tfs/TfsApiClient.py +2 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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 Loading
src/common/tools/client/RestClient.py→src/common/tools/client/RestApiClient.py +28 −7 Original line number Diff line number Diff line Loading @@ -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' Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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], Loading @@ -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], Loading @@ -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( Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading
src/common/tools/client/RestConfClient.py 0 → 100644 +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 )
src/device/service/drivers/ietf_l2vpn/TfsApiClient.py +2 −2 Original line number Diff line number Diff line Loading @@ -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' Loading Loading @@ -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, Loading
src/device/service/drivers/ietf_l3vpn/TfsApiClient.py +2 −2 Original line number Diff line number Diff line Loading @@ -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' Loading Loading @@ -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, Loading
src/device/service/drivers/optical_tfs/TfsApiClient.py +2 −2 Original line number Diff line number Diff line Loading @@ -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 Loading Loading @@ -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