""" Harbor Client interface. Handles the REST calls and responses. """ import copy import hashlib import logging from urlparse import urlparse from oslo_utils import importutils import requests from requests.packages.urllib3.exceptions import InsecureRequestWarning try: import json except ImportError: import simplejson as json from harborclient import api_versions from harborclient import exceptions from harborclient import utils requests.packages.urllib3.disable_warnings(InsecureRequestWarning) class HTTPClient(object): USER_AGENT = 'python-harborclient' def __init__(self, username, password, project, baseurl, timeout=None, timings=False, http_log_debug=False, cacert=None, insecure=False, api_version=None): self.username = username self.password = password self.project = project self.baseurl = baseurl self.api_version = api_version or api_versions.APIVersion() self.timings = timings self.http_log_debug = http_log_debug # Has no protocol, use http if not urlparse(baseurl).scheme: self.baseurl = 'http://' + baseurl parsed_url = urlparse(self.baseurl) self.protocol = parsed_url.scheme self.host = parsed_url.hostname self.port = parsed_url.port if timeout is not None: self.timeout = float(timeout) else: self.timeout = None # https if insecure: self.verify_cert = False else: if cacert: self.verify_cert = cacert else: self.verify_cert = True self.times = [] # [("item", starttime, endtime), ...] self._logger = logging.getLogger(__name__) self.session_id = None if self.http_log_debug and not self._logger.handlers: # Logging level is already set on the root logger ch = logging.StreamHandler() self._logger.addHandler(ch) self._logger.propagate = False if hasattr(requests, 'logging'): rql = requests.logging.getLogger(requests.__name__) rql.addHandler(ch) # Since we have already setup the root logger on debug, we # have to set it up here on WARNING (its original level) # otherwise we will get all the requests logging messages rql.setLevel(logging.WARNING) def unauthenticate(self): """Forget all of our authentication information.""" requests.get( '%s://%s/logout' % (self.protocol, self.host), cookies={'beegosessionID': self.session_id}, verify=self.verify_cert) logging.debug("Successfully logout") def get_timings(self): return self.times def reset_timings(self): self.times = [] def _redact(self, target, path, text=None): """Replace the value of a key in `target`. The key can be at the top level by specifying a list with a single key as the path. Nested dictionaries are also supported by passing a list of keys to be navigated to find the one that should be replaced. In this case the last one is the one that will be replaced. :param dict target: the dictionary that may have a key to be redacted; modified in place :param list path: a list representing the nested structure in `target` that should be redacted; modified in place :param string text: optional text to use as a replacement for the redacted key. if text is not specified, the default text will be sha1 hash of the value being redacted """ key = path.pop() # move to the most nested dict for p in path: try: target = target[p] except KeyError: return if key in target: if text: target[key] = text elif target[key] is not None: # because in python3 byte string handling is ... ug value = target[key].encode('utf-8') sha1sum = hashlib.sha1(value) target[key] = "{SHA1}%s" % sha1sum.hexdigest() def http_log_req(self, method, url, kwargs): if not self.http_log_debug: return string_parts = ['curl -g -i'] if self.verify_cert is not None: if not self.verify_cert: string_parts.append(' --insecure') string_parts.append(" '%s'" % url) string_parts.append(' -X %s' % method) headers = copy.deepcopy(kwargs['headers']) # because dict ordering changes from 2 to 3 keys = sorted(headers.keys()) for name in keys: value = headers[name] header = ' -H "%s: %s"' % (name, value) string_parts.append(header) cookies = kwargs['cookies'] for name in sorted(cookies.keys()): value = cookies[name] cookie = header = ' -b "%s: %s"' % (name, value) string_parts.append(cookie) if 'data' in kwargs: data = json.loads(kwargs['data']) string_parts.append(" -d '%s'" % json.dumps(data)) self._logger.debug("REQ: %s" % "".join(string_parts)) def http_log_resp(self, resp): if not self.http_log_debug: return if resp.text and resp.status_code != 400: try: body = json.loads(resp.text) except ValueError: body = None else: body = None self._logger.debug("RESP: [%(status)s] %(headers)s\nRESP BODY: " "%(text)s\n", { 'status': resp.status_code, 'headers': resp.headers, 'text': json.dumps(body) }) def request(self, url, method, **kwargs): url = self.baseurl + "/api" + url kwargs.setdefault('headers', kwargs.get('headers', {})) kwargs['headers']['User-Agent'] = self.USER_AGENT kwargs['headers']['Accept'] = 'application/json' if 'body' in kwargs: kwargs['headers']['Content-Type'] = 'application/json' kwargs['data'] = json.dumps(kwargs['body']) del kwargs['body'] kwargs["headers"]['Harbor-API-Version'] = "v2" if self.timeout is not None: kwargs.setdefault('timeout', self.timeout) self.http_log_req(method, url, kwargs) resp = requests.request(method, url, verify=self.verify_cert, **kwargs) self.http_log_resp(resp) if resp.status_code >= 400: raise exceptions.from_response(resp, resp.text, url, method) try: body = json.loads(resp.text) except ValueError: body = resp.text return body def _time_request(self, url, method, **kwargs): with utils.record_time(self.times, self.timings, method, url): body = self.request(url, method, **kwargs) return body def _cs_request(self, url, method, **kwargs): if not self.session_id: self.authenticate() # Perform the request once. If we get a 401 back then it # might be because the auth token expired, so try to # re-authenticate and try again. If it still fails, bail. try: body = self._time_request( url, method, cookies={'beegosessionID': self.session_id}, **kwargs) return body except exceptions.Unauthorized as e: try: # first discard auth token, to avoid the possibly expired # token being re-used in the re-authentication attempt self.unauthenticate() # overwrite bad token self.authenticate() body = self._time_request(url, method, **kwargs) return body except exceptions.Unauthorized: raise e def get(self, url, **kwargs): return self._cs_request(url, 'GET', **kwargs) def post(self, url, **kwargs): return self._cs_request(url, 'POST', **kwargs) def put(self, url, **kwargs): return self._cs_request(url, 'PUT', **kwargs) def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) def authenticate(self): if not self.baseurl: msg = ("Authentication requires 'baseurl', which should be " "specified in '%s'") % self.__class__.__name__ raise exceptions.AuthorizationFailure(msg) if not self.username: msg = ("Authentication requires 'username', which should be " "specified in '%s'") % self.__class__.__name__ raise exceptions.AuthorizationFailure(msg) if not self.password: msg = ("Authentication requires 'password', which should be " "specified in '%s'") % self.__class__.__name__ raise exceptions.AuthorizationFailure(msg) try: resp = requests.post( self.baseurl + "/login", data={'principal': self.username, 'password': self.password}, verify=self.verify_cert) except requests.exceptions.SSLError: msg = ("Certificate verify failed, please use '--os-cacert' option" " to specify a CA bundle file to use in verifying a TLS" " (https) server certificate or use '--insecure' option" " to explicitly allow client to perform insecure" " TLS (https) requests.") raise exceptions.AuthorizationFailure(msg) if resp.status_code == 200: self.session_id = resp.cookies.get('beegosessionID') logging.debug( "Successfully login, session id: %s" % self.session_id) if resp.status_code >= 400: msg = resp.text or ("The request you have made requires " "authentication. (HTTP 401)") reason = '{"reason": "%s", "message": "%s"}' % (resp.reason, msg) raise exceptions.AuthorizationFailure(reason) def _construct_http_client(username=None, password=None, project=None, baseurl=None, timeout=None, extensions=None, timings=False, http_log_debug=False, user_agent='python-harborclient', api_version=None, insecure=False, cacert=None, **kwargs): return HTTPClient( username, password, project, baseurl, timeout=timeout, timings=timings, http_log_debug=http_log_debug, insecure=insecure, cacert=cacert, api_version=api_version) def _get_client_class_and_version(version): if not isinstance(version, api_versions.APIVersion): version = api_versions.get_api_version(version) else: api_versions.check_major_version(version) if version.is_latest(): raise exceptions.UnsupportedVersion(("The version should be explicit, " "not latest.")) return version, importutils.import_class( "harborclient.v%s.client.Client" % version.ver_major) def get_client_class(version): """Returns Client class based on given version.""" _api_version, client_class = _get_client_class_and_version(version) return client_class def Client(version, username=None, password=None, project=None, baseurl=None, insecure=False, cacert=None, *args, **kwargs): """Initialize client object based on given version. HOW-TO: The simplest way to create a client instance is initialization with your credentials:: >>> from harborclient import client >>> harbor = client.Client(VERSION, USERNAME, PASSWORD, ... PROJECT, HARBOR_URL) Here ``VERSION`` can be a string or ``harborclient.api_versions.APIVersion`` obj. If you prefer string value, you can use ``1.1`` (deprecated now), ``2`` or ``2.X`` (where X is a microversion). Alternatively, you can create a client instance using the keystoneauth session API. See "The harborclient Python API" page at python-harborclient's doc. """ api_version, client_class = _get_client_class_and_version(version) kwargs.pop("direct_use", None) return client_class( username=username, password=password, project=project, baseurl=baseurl, api_version=api_version, insecure=insecure, cacert=cacert, *args, **kwargs)