diff --git a/README.rst b/README.rst index b183aef..8ab3e3e 100644 --- a/README.rst +++ b/README.rst @@ -15,16 +15,29 @@ The command line client is installed as a plugin for the OpenStack client. Python API ========== -In order to use the python api directly, you must first obtain an auth -token and identify which endpoint you wish to speak to:: +You can use the API with a keystone session: - >>> adjutant_url = 'http://adjutant.example.org:8004/v1/' - >>> auth_token = '3bcc3d3a03f44e3d8377f9247b0ad155' + >>> from keystoneauth1 import session + >>> from keystoneauth1.identity import v3 + >>> from adjutantclient.client import Client + >>> auth = v3.Password(auth_url='http://keystone.host/v3', + username='user', + password='password', + project_name='demo', + user_domain_name='default', + project_domain_name='default') -Once you have done so, you can use the API like so:: + >>> sess = session.Session(auth=auth) + >>> adjutant = Client('1', session=sess) + +If you use a clouds.yaml file os_client_config can also be used: + + >>> import os_client_config + >>> sess = os_client_config.make_rest_client('registration') + >>> adjutant = Client('1', session=sess) + +A few of the endpoints (users.password_forgot(), token.submit(), signup, token.get()) don't require authentication. +In this case you can instead just pass an endpoint override to the adjutant client constructor. >>> from adjutantclient.client import Client - >>> adjutant = Client('1', endpoint=adjutant_url, token=auth_token) - -An auth token isn't needed for accessing signup, user.password_forgot(), -token.submit() or token.get(). + >>> adjutant = Client('1', endpoint='http://adjutant.host/v1') diff --git a/adjutantclient/common/http.py b/adjutantclient/common/http.py index 18b862f..14b59a9 100644 --- a/adjutantclient/common/http.py +++ b/adjutantclient/common/http.py @@ -13,19 +13,14 @@ # License for the specific language governing permissions and limitations # under the License. -import copy -import hashlib import logging import os -import socket from keystoneauth1 import adapter -from oslo_serialization import jsonutils -from oslo_utils import encodeutils +from keystoneauth1 import session as ks_session +from keystoneauth1.identity import v3 from oslo_utils import importutils import requests -import six -from six.moves.urllib import parse from adjutantclient._i18n import _ from adjutantclient import exc @@ -67,229 +62,6 @@ def get_system_ca_file(): LOG.warning("System ca file could not be found.") -class HTTPClient(object): - - def __init__(self, endpoint, **kwargs): - self.endpoint = endpoint - self.auth_url = kwargs.get('auth_url') - self.auth_token = kwargs.get('token') - self.username = kwargs.get('username') - self.password = kwargs.get('password') - self.region_name = kwargs.get('region_name') - self.include_pass = kwargs.get('include_pass') - self.endpoint_url = endpoint - - self.cert_file = kwargs.get('cert_file') - self.key_file = kwargs.get('key_file') - self.timeout = kwargs.get('timeout') - - self.ssl_connection_params = { - 'ca_file': kwargs.get('ca_file'), - 'cert_file': kwargs.get('cert_file'), - 'key_file': kwargs.get('key_file'), - 'insecure': kwargs.get('insecure'), - } - - self.verify_cert = None - if parse.urlparse(endpoint).scheme == "https": - if kwargs.get('insecure'): - self.verify_cert = False - else: - self.verify_cert = kwargs.get('ca_file', get_system_ca_file()) - - # FIXME(shardy): We need this for compatibility with the oslo apiclient - # we should move to inheriting this class from the oslo HTTPClient - self.last_request_id = None - - def safe_header(self, name, value): - if name in SENSITIVE_HEADERS: - # because in python3 byte string handling is ... ug - v = value.encode('utf-8') - h = hashlib.sha1(v) - d = h.hexdigest() - return encodeutils.safe_decode(name), "{SHA1}%s" % d - else: - return (encodeutils.safe_decode(name), - encodeutils.safe_decode(value)) - - def log_curl_request(self, method, url, kwargs): - curl = ['curl -g -i -X %s' % method] - - for (key, value) in kwargs['headers'].items(): - header = '-H \'%s: %s\'' % self.safe_header(key, value) - curl.append(header) - - conn_params_fmt = [ - ('key_file', '--key %s'), - ('cert_file', '--cert %s'), - ('ca_file', '--cacert %s'), - ] - for (key, fmt) in conn_params_fmt: - value = self.ssl_connection_params.get(key) - if value: - curl.append(fmt % value) - - if self.ssl_connection_params.get('insecure'): - curl.append('-k') - - if 'data' in kwargs: - curl.append('-d \'%s\'' % kwargs['data']) - - curl.append('%s%s' % (self.endpoint, url)) - LOG.debug(' '.join(curl)) - - @staticmethod - def log_http_response(resp): - status = (resp.raw.version / 10.0, resp.status_code, resp.reason) - dump = ['\nHTTP/%.1f %s %s' % status] - dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()]) - dump.append('') - if resp.content: - content = resp.content - if isinstance(content, six.binary_type): - content = content.decode() - dump.extend([content, '']) - LOG.debug('\n'.join(dump)) - - def _http_request(self, url, method, **kwargs): - """Send an http request with the specified characteristics. - - Wrapper around requests.request to handle tasks such as - setting headers and error handling. - """ - # Copy the kwargs so we can reuse the original in case of redirects - kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) - kwargs['headers'].setdefault('User-Agent', USER_AGENT) - if self.auth_token: - kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) - else: - kwargs['headers'].update(self.credentials_headers()) - if self.auth_url: - kwargs['headers'].setdefault('X-Auth-Url', self.auth_url) - if self.region_name: - kwargs['headers'].setdefault('X-Region-Name', self.region_name) - if self.include_pass and 'X-Auth-Key' not in kwargs['headers']: - kwargs['headers'].update(self.credentials_headers()) - if osprofiler_web: - kwargs['headers'].update(osprofiler_web.get_trace_id_headers()) - - self.log_curl_request(method, url, kwargs) - - if self.cert_file and self.key_file: - kwargs['cert'] = (self.cert_file, self.key_file) - - if self.verify_cert is not None: - kwargs['verify'] = self.verify_cert - - if self.timeout is not None: - kwargs['timeout'] = float(self.timeout) - - # Allow caller to specify not to follow redirects, in which case we - # just return the redirect response. Useful for using stacks:lookup. - redirect = kwargs.pop('redirect', True) - - # Since requests does not follow the RFC when doing redirection to sent - # back the same method on a redirect we are simply bypassing it. For - # example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says - # that we should follow that URL with the same method as before, - # requests doesn't follow that and send a GET instead for the method. - # Hopefully this could be fixed as they say in a comment in a future - # point version i.e.: 3.x - # See issue: https://github.com/kennethreitz/requests/issues/1704 - allow_redirects = False - - # Use fully qualified URL from response header for redirects - if not parse.urlparse(url).netloc: - url = self.endpoint_url + url - - try: - resp = requests.request( - method, - url, - allow_redirects=allow_redirects, - **kwargs) - except socket.gaierror as e: - message = (_("Error finding address for %(url)s: %(e)s") % - {'url': self.endpoint_url + url, 'e': e}) - raise exc.InvalidEndpoint(message=message) - except (socket.error, socket.timeout) as e: - endpoint = self.endpoint - message = (_("Error communicating with %(endpoint)s %(e)s") % - {'endpoint': endpoint, 'e': e}) - raise exc.CommunicationError(message=message) - - self.log_http_response(resp) - - if not ('X-Auth-Key' in kwargs['headers']) and ( - resp.status_code == 401 or - (resp.status_code == 500 and "(HTTP 401)" in resp.content)): - raise exc.HTTPUnauthorized(_("Authentication failed: %s") - % resp.content) - elif 400 <= resp.status_code < 600: - raise exc.from_response(resp) - elif resp.status_code in (301, 302, 305): - # Redirected. Reissue the request to the new location, - # unless caller specified redirect=False - if redirect: - location = resp.headers.get('location') - if not location: - message = _("Location not returned with redirect") - raise exc.InvalidEndpoint(message=message) - resp = self._http_request(location, method, **kwargs) - elif resp.status_code == 300: - raise exc.from_response(resp) - - return resp - - def credentials_headers(self): - creds = {} - if self.username: - creds['X-Auth-User'] = self.username - if self.password: - creds['X-Auth-Key'] = self.password - return creds - - def json_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', 'application/json') - kwargs['headers'].setdefault('Accept', 'application/json') - - if 'data' in kwargs: - kwargs['data'] = jsonutils.dumps(kwargs['data']) - - resp = self._http_request(url, method, **kwargs) - body = get_response_body(resp) - return resp, body - - def raw_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', - 'application/octet-stream') - return self._http_request(url, method, **kwargs) - - def client_request(self, method, url, **kwargs): - resp, body = self.json_request(method, url, **kwargs) - return resp - - def head(self, url, **kwargs): - return self.client_request("HEAD", url, **kwargs) - - def get(self, url, **kwargs): - return self.client_request("GET", url, **kwargs) - - def post(self, url, **kwargs): - return self.client_request("POST", url, **kwargs) - - def put(self, url, **kwargs): - return self.client_request("PUT", url, **kwargs) - - def delete(self, url, **kwargs): - return self.raw_request("DELETE", url, **kwargs) - - def patch(self, url, **kwargs): - return self.client_request("PATCH", url, **kwargs) - - class SessionClient(adapter.LegacyJsonAdapter): """HTTP client based on Keystone client session.""" @@ -337,19 +109,32 @@ def _construct_http_client(endpoint=None, username=None, password=None, session = kwargs.pop('session', None) auth = kwargs.pop('auth', None) - if session: - if 'endpoint_override' not in kwargs and endpoint: - kwargs['endpoint_override'] = endpoint + if not session: + if not auth and password: + auth = v3.Password(auth_url=auth_url, + username=username, + password=password, + **kwargs) + kwargs.pop('project_id', None) + kwargs.pop('project_name', None) + kwargs.pop('domain_id', None) + kwargs.pop('domain_name', None) + kwargs.pop('project_domain_id', None) + kwargs.pop('project_domain_name', None) + kwargs.pop('user_domain_id', None) + kwargs.pop('user_domain_name', None) - if 'service_type' not in kwargs: - kwargs['service_type'] = 'registration' + ca_path = kwargs.get('ca_file') or get_system_ca_file() + session = ks_session.Session(auth=auth, + verify=ca_path) - if 'interface' not in kwargs and endpoint_type: - kwargs['interface'] = endpoint_type + if 'endpoint_override' not in kwargs and endpoint: + kwargs['endpoint_override'] = endpoint - return SessionClient(session, auth=auth, **kwargs) - else: - return HTTPClient(endpoint=endpoint, username=username, - password=password, include_pass=include_pass, - endpoint_type=endpoint_type, auth_url=auth_url, - **kwargs) + if 'service_type' not in kwargs: + kwargs['service_type'] = 'registration' + + if 'interface' not in kwargs and endpoint_type: + kwargs['interface'] = endpoint_type + + return SessionClient(session, auth=auth, **kwargs)