# Copyright 2017 Cloudbase Solutions Srl # # 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. """Utilities used across the project.""" import json import sys import time from oslo_log import log as logging import requests import requests_ntlm import six from hnv_client.common import constant from hnv_client.common import exception from hnv_client import config as hnv_config LOG = logging.getLogger(__name__) CONFIG = hnv_config.CONFIG class _HNVClient(object): """Minimalistic client for the Network Controller REST API. :param url: The base URL where the agent looks for Network Controller API. :param username: The username required for connecting to the Network Controller API. :param password: The password required for connecting to the Network Controller API. :param allow_insecure: Whether to disable the validation of HTTPS certificates. :param ca_bundle: The path to a CA_BUNDLE file or directory with certificates of trusted CAs. """ def __init__(self, url, username=None, password=None, allow_insecure=False, ca_bundle=None): self._base_url = url self._credentials = (username, password) self._https_allow_insecure = allow_insecure self._https_ca_bundle = ca_bundle self._http_session = None @property def _session(self): """The current session used by the client. The Session object allows you to persist certain parameters across requests. It also persists cookies across all requests made from the Session instance, and will use urllib3's connection pooling. So if you're making several requests to the same host, the underlying TCP connection will be reused, which can result in a significant performance increase. """ if self._http_session is None: self._http_session = requests.Session() self._http_session.headers.update(self._get_headers()) self._http_session.verify = self._verify_https_request() if all(self._credentials): username, password = self._credentials self._http_session.auth = requests_ntlm.HttpNtlmAuth( username=username, password=password) return self._http_session @staticmethod def _get_headers(): """Prepare the HTTP headers for the current request.""" # TODO(alexcoman): Add the x-ms-client-ip-address header in order # to improve the Network Controller requests logging. return { "Accept": "application/json", "Connection": "keep-alive", "Content-Type": "application/json; charset=UTF-8", } def _verify_https_request(self): """Whether to disable the validation of HTTPS certificates. .. notes:: When `https_allow_insecure` option is `True` the SSL certificate validation for the connection with the Network Controller API will be disabled (please don't use it if you don't know the implications of this behaviour). """ if self._https_ca_bundle: return self._https_ca_bundle else: return not self._https_allow_insecure def _http_request(self, resource, method=constant.GET, body=None): url = requests.compat.urljoin(self._base_url, resource) headers = self._get_headers() if method in (constant.PUT, constant.PATCH): etag = (body or {}).get("etag", None) if etag is not None: headers["If-Match"] = etag attemts = 0 while True: try: response = self._session.request( method=method, url=url, headers=headers, data=json.dumps(body) if body else None, timeout=CONFIG.HNV.http_request_timeout ) break except requests.ConnectionError as exc: attemts += 1 self._http_session = None LOG.debug("Request failed: %s", exc) if attemts > CONFIG.HNV.retry_count: if isinstance(exc, requests.exceptions.SSLError): raise exception.CertificateVerifyFailed( "HTTPS certificate validation failed.") raise time.sleep(CONFIG.HNV.retry_interval) try: response.raise_for_status() except requests.HTTPError as exc: status_code = exc.response.status_code content = exc.response.text LOG.debug("HTTP Error %(status_code)r: %(details)r", {"status_code": status_code, "details": content}) if status_code == 400: raise exception.ServiceException( ("HNV Client failed to communicate with the API. " "Please open an issue with the following information: " "%(resource)r: %(details)r"), resource=resource, details=content ) if status_code == 404: raise exception.NotFound( "Resource %(resource)r was not found.", resource=resource) raise return response def get_resource(self, path): """Getting the required information from the API.""" response = self._http_request(path) try: return response.json() except ValueError: raise exception.ServiceException("Invalid service response.") def update_resource(self, path, data): """Update the required resource.""" response = self._http_request(path, method="PUT", body=data) try: return response.json() except ValueError: raise exception.ServiceException("Invalid service response.") def remove_resource(self, path): """Delete the received resource.""" return self._http_request(path, method="DELETE") # pylint: disable=dangerous-default-value def run_once(function, state={}, errors={}): """A memoization decorator, whose purpose is to cache calls.""" @six.wraps(function) def _wrapper(*args, **kwargs): if function in errors: # Deliberate use of LBYL. six.reraise(*errors[function]) try: return state[function] except KeyError: try: state[function] = result = function(*args, **kwargs) return result except Exception: errors[function] = sys.exc_info() raise return _wrapper @run_once def get_client(url, username, password, allow_insecure, ca_bundle): """Create a new client for the HNV REST API.""" return _HNVClient(url, username, password, allow_insecure, ca_bundle)