mirror of
https://github.com/goharbor/harbor.git
synced 2024-11-27 12:46:03 +01:00
19f3ebd353
This patch provides a command-line tool for managing Harbor resources like users, projects, images, etc.
375 lines
13 KiB
Python
375 lines
13 KiB
Python
"""
|
|
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)
|