Add HttpClient and OSC plugin
Change-Id: I272dbaf877dbdfbfa7ebb67dbf2d9c3dd3d7246d
This commit is contained in:
		
							
								
								
									
										357
									
								
								nimbleclient/common/http.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										357
									
								
								nimbleclient/common/http.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,357 @@ | |||||||
|  | #   Copyright 2016 Huawei, Inc. All rights reserved. | ||||||
|  | # | ||||||
|  | #   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 copy | ||||||
|  | import hashlib | ||||||
|  | import logging | ||||||
|  | import os | ||||||
|  | import socket | ||||||
|  |  | ||||||
|  | from keystoneauth1 import adapter | ||||||
|  | from oslo_serialization import jsonutils | ||||||
|  | from oslo_utils import encodeutils | ||||||
|  | from oslo_utils import importutils | ||||||
|  | import requests | ||||||
|  | import six | ||||||
|  | from six.moves.urllib import parse | ||||||
|  |  | ||||||
|  | from nimbleclient.common import utils | ||||||
|  | from nimbleclient import exceptions as exc | ||||||
|  | from nimbleclient.i18n import _ | ||||||
|  | from nimbleclient.i18n import _LW | ||||||
|  |  | ||||||
|  | LOG = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  | USER_AGENT = 'python-nimbleclient' | ||||||
|  | CHUNKSIZE = 1024 * 64  # 64kB | ||||||
|  | SENSITIVE_HEADERS = ('X-Auth-Token',) | ||||||
|  | osprofiler_web = importutils.try_import('osprofiler.web') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def authenticated_fetcher(hc): | ||||||
|  |     """A wrapper around the nimble client object to fetch a template.""" | ||||||
|  |  | ||||||
|  |     def _do(*args, **kwargs): | ||||||
|  |         if isinstance(hc.http_client, SessionClient): | ||||||
|  |             method, url = args | ||||||
|  |             return hc.http_client.request(url, method, **kwargs).content | ||||||
|  |         else: | ||||||
|  |             return hc.http_client.raw_request(*args, **kwargs).content | ||||||
|  |  | ||||||
|  |     return _do | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_system_ca_file(): | ||||||
|  |     """Return path to system default CA file.""" | ||||||
|  |     # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, | ||||||
|  |     # Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca | ||||||
|  |     ca_path = ['/etc/ssl/certs/ca-certificates.crt', | ||||||
|  |                '/etc/pki/tls/certs/ca-bundle.crt', | ||||||
|  |                '/etc/ssl/ca-bundle.pem', | ||||||
|  |                '/etc/ssl/cert.pem', | ||||||
|  |                '/System/Library/OpenSSL/certs/cacert.pem', | ||||||
|  |                requests.certs.where()] | ||||||
|  |     for ca in ca_path: | ||||||
|  |         LOG.debug("Looking for ca file %s", ca) | ||||||
|  |         if os.path.exists(ca): | ||||||
|  |             LOG.debug("Using ca file %s", ca) | ||||||
|  |             return ca | ||||||
|  |     LOG.warning(_LW("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 = {} | ||||||
|  |         # NOTE(dhu): (shardy) When deferred_auth_method=password, Heat | ||||||
|  |         # encrypts and stores username/password.  For Keystone v3, the | ||||||
|  |         # intent is to use trusts since SHARDY is working towards | ||||||
|  |         # deferred_auth_method=trusts as the default. | ||||||
|  |         # TODO(dhu): Make Keystone v3 work in Heat standalone mode.  Maye | ||||||
|  |         # require X-Auth-User-Domain. | ||||||
|  |         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 = utils.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.""" | ||||||
|  |  | ||||||
|  |     def request(self, url, method, **kwargs): | ||||||
|  |         redirect = kwargs.get('redirect') | ||||||
|  |         kwargs.setdefault('user_agent', USER_AGENT) | ||||||
|  |  | ||||||
|  |         if 'data' in kwargs: | ||||||
|  |             kwargs['data'] = jsonutils.dumps(kwargs['data']) | ||||||
|  |  | ||||||
|  |         resp, body = super(SessionClient, self).request( | ||||||
|  |             url, method, | ||||||
|  |             raise_exc=False, | ||||||
|  |             **kwargs) | ||||||
|  |  | ||||||
|  |         if 400 <= resp.status_code < 600: | ||||||
|  |             raise exc.from_response(resp) | ||||||
|  |         elif resp.status_code in (301, 302, 305): | ||||||
|  |             if redirect: | ||||||
|  |                 location = resp.headers.get('location') | ||||||
|  |                 path = self.strip_endpoint(location) | ||||||
|  |                 resp = self.request(path, method, **kwargs) | ||||||
|  |         elif resp.status_code == 300: | ||||||
|  |             raise exc.from_response(resp) | ||||||
|  |  | ||||||
|  |         return resp | ||||||
|  |  | ||||||
|  |     def credentials_headers(self): | ||||||
|  |         return {} | ||||||
|  |  | ||||||
|  |     def strip_endpoint(self, location): | ||||||
|  |         if location is None: | ||||||
|  |             message = _("Location not returned with 302") | ||||||
|  |             raise exc.InvalidEndpoint(message=message) | ||||||
|  |         if (self.endpoint_override is not None and | ||||||
|  |                 location.lower().startswith(self.endpoint_override.lower())): | ||||||
|  |                 return location[len(self.endpoint_override):] | ||||||
|  |         else: | ||||||
|  |             return location | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _construct_http_client(endpoint=None, username=None, password=None, | ||||||
|  |                            include_pass=None, endpoint_type=None, | ||||||
|  |                            auth_url=None, **kwargs): | ||||||
|  |     session = kwargs.pop('session', None) | ||||||
|  |     auth = kwargs.pop('auth', None) | ||||||
|  |  | ||||||
|  |     if session: | ||||||
|  |         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) | ||||||
							
								
								
									
										32
									
								
								nimbleclient/common/utils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								nimbleclient/common/utils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,32 @@ | |||||||
|  | #   Copyright 2016 Huawei, Inc. All rights reserved. | ||||||
|  | # | ||||||
|  | #   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 | ||||||
|  |  | ||||||
|  | from nimbleclient.i18n import _LE | ||||||
|  |  | ||||||
|  | LOG = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_response_body(resp): | ||||||
|  |     body = resp.content | ||||||
|  |     if 'application/json' in resp.headers.get('content-type', ''): | ||||||
|  |         try: | ||||||
|  |             body = resp.json() | ||||||
|  |         except ValueError: | ||||||
|  |             LOG.error(_LE('Could not decode response body as JSON')) | ||||||
|  |     else: | ||||||
|  |         body = None | ||||||
|  |     return body | ||||||
							
								
								
									
										132
									
								
								nimbleclient/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								nimbleclient/exceptions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,132 @@ | |||||||
|  | #   Copyright 2016 Huawei, Inc. All rights reserved. | ||||||
|  | # | ||||||
|  | #   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 sys | ||||||
|  |  | ||||||
|  | from oslo_serialization import jsonutils | ||||||
|  | from oslo_utils import reflection | ||||||
|  |  | ||||||
|  | from nimbleclient.i18n import _ | ||||||
|  |  | ||||||
|  | verbose = 0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BaseException(Exception): | ||||||
|  |     """An error occurred.""" | ||||||
|  |     def __init__(self, message=None): | ||||||
|  |         self.message = message | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         return self.message or self.__class__.__doc__ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CommandError(BaseException): | ||||||
|  |     """Invalid usage of CLI.""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class InvalidEndpoint(BaseException): | ||||||
|  |     """The provided endpoint is invalid.""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CommunicationError(BaseException): | ||||||
|  |     """Unable to communicate with server.""" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPException(BaseException): | ||||||
|  |     """Base exception for all HTTP-derived exceptions.""" | ||||||
|  |     code = 'N/A' | ||||||
|  |  | ||||||
|  |     def __init__(self, message=None, code=None): | ||||||
|  |         super(HTTPException, self).__init__(message) | ||||||
|  |         try: | ||||||
|  |             self.error = jsonutils.loads(message) | ||||||
|  |             if 'error' not in self.error: | ||||||
|  |                 raise KeyError(_('Key "error" not exists')) | ||||||
|  |         except KeyError: | ||||||
|  |             # NOTE(RuiChen): If key 'error' happens not exist, | ||||||
|  |             # self.message becomes no sense. In this case, we | ||||||
|  |             # return doc of current exception class instead. | ||||||
|  |             self.error = {'error': | ||||||
|  |                           {'message': self.__class__.__doc__}} | ||||||
|  |         except Exception: | ||||||
|  |             self.error = {'error': | ||||||
|  |                           {'message': self.message or self.__class__.__doc__}} | ||||||
|  |         if self.code == "N/A" and code is not None: | ||||||
|  |             self.code = code | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         message = self.error['error'].get('message', 'Internal Error') | ||||||
|  |         if verbose: | ||||||
|  |             traceback = self.error['error'].get('traceback', '') | ||||||
|  |             return (_('ERROR: %(message)s\n%(traceback)s') % | ||||||
|  |                     {'message': message, 'traceback': traceback}) | ||||||
|  |         else: | ||||||
|  |             return _('ERROR: %s') % message | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPMultipleChoices(HTTPException): | ||||||
|  |     code = 300 | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         self.details = _("Requested version of Heat API is not" | ||||||
|  |                          "available.") | ||||||
|  |         return (_("%(name)s (HTTP %(code)s) %(details)s") % | ||||||
|  |                 { | ||||||
|  |                 'name': reflection.get_class_name(self, fully_qualified=False), | ||||||
|  |                 'code': self.code, | ||||||
|  |                 'details': self.details}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Unauthorized(HTTPException): | ||||||
|  |     code = 401 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPUnauthorized(Unauthorized): | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPMethodNotAllowed(HTTPException): | ||||||
|  |     code = 405 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPUnsupported(HTTPException): | ||||||
|  |     code = 415 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPInternalServerError(HTTPException): | ||||||
|  |     code = 500 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPNotImplemented(HTTPException): | ||||||
|  |     code = 501 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPBadGateway(HTTPException): | ||||||
|  |     code = 502 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # NOTE(RuiChen): Build a mapping of HTTP codes to corresponding exception | ||||||
|  | # classes | ||||||
|  | _code_map = {} | ||||||
|  | for obj_name in dir(sys.modules[__name__]): | ||||||
|  |     if obj_name.startswith('HTTP'): | ||||||
|  |         obj = getattr(sys.modules[__name__], obj_name) | ||||||
|  |         _code_map[obj.code] = obj | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def from_response(response): | ||||||
|  |     """Return an instance of an HTTPException based on requests response.""" | ||||||
|  |     cls = _code_map.get(response.status_code, HTTPException) | ||||||
|  |     return cls(response.content, response.status_code) | ||||||
							
								
								
									
										31
									
								
								nimbleclient/i18n.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								nimbleclient/i18n.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | #   Copyright 2016 Huawei, Inc. All rights reserved. | ||||||
|  | # | ||||||
|  | #   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 oslo_i18n | ||||||
|  |  | ||||||
|  | _translators = oslo_i18n.TranslatorFactory(domain='nimbleclient') | ||||||
|  |  | ||||||
|  | # The primary translation function using the well-known name "_" | ||||||
|  | _ = _translators.primary | ||||||
|  |  | ||||||
|  | # Translators for log levels. | ||||||
|  | # | ||||||
|  | # The abbreviated names are meant to reflect the usual use of a short | ||||||
|  | # name like '_'. The "L" is for "log" and the other letter comes from | ||||||
|  | # the level. | ||||||
|  | _LI = _translators.log_info | ||||||
|  | _LW = _translators.log_warning | ||||||
|  | _LE = _translators.log_error | ||||||
|  | _LC = _translators.log_critical | ||||||
							
								
								
									
										0
									
								
								nimbleclient/osc/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								nimbleclient/osc/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										73
									
								
								nimbleclient/osc/plugin.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								nimbleclient/osc/plugin.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | |||||||
|  | #   Copyright 2016 Huawei, Inc. All rights reserved. | ||||||
|  | # | ||||||
|  | #   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 | ||||||
|  |  | ||||||
|  | from osc_lib import utils | ||||||
|  |  | ||||||
|  | from nimbleclient.i18n import _ | ||||||
|  |  | ||||||
|  | LOG = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  | DEFAULT_BAREMETAL_COMPUTE_API_VERSION = '1' | ||||||
|  | API_VERSION_OPTION = 'os_baremetal_compute_api_version' | ||||||
|  | API_NAME = 'baremetal_compute' | ||||||
|  | API_VERSIONS = { | ||||||
|  |     '1': 'nimbleclient.v1.client.Client', | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def make_client(instance): | ||||||
|  |     """Returns an baremetal service client""" | ||||||
|  |     nimble_client = utils.get_client_class( | ||||||
|  |         API_NAME, | ||||||
|  |         instance._api_version[API_NAME], | ||||||
|  |         API_VERSIONS) | ||||||
|  |     LOG.debug('Instantiating baremetal-compute client: %s', nimble_client) | ||||||
|  |  | ||||||
|  |     endpoint = instance.get_endpoint_for_service_type( | ||||||
|  |         API_NAME, | ||||||
|  |         region_name=instance.region_name, | ||||||
|  |         interface=instance.interface, | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     kwargs = {'endpoint': endpoint, | ||||||
|  |               'auth_url': instance.auth.auth_url, | ||||||
|  |               'region_name': instance.region_name, | ||||||
|  |               'username': instance.auth_ref.username} | ||||||
|  |  | ||||||
|  |     if instance.session: | ||||||
|  |         kwargs.update(session=instance.session) | ||||||
|  |     else: | ||||||
|  |         kwargs.update(token=instance.auth_ref.auth_token) | ||||||
|  |  | ||||||
|  |     client = nimble_client(**kwargs) | ||||||
|  |  | ||||||
|  |     return client | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def build_option_parser(parser): | ||||||
|  |     """Hook to add global options""" | ||||||
|  |     parser.add_argument( | ||||||
|  |         '--os-baremetal-compute-api-version', | ||||||
|  |         metavar='<baremetal-compute-api-version>', | ||||||
|  |         default=utils.env( | ||||||
|  |             'OS_BAREMETAL_COMPUTE_API_VERSION', | ||||||
|  |             default=DEFAULT_BAREMETAL_COMPUTE_API_VERSION), | ||||||
|  |         help=(_('Baremetal compute API version, default=%s ' | ||||||
|  |                 '(Env: OS_BAREMETAL_COMPUTE_API_VERSION)') % | ||||||
|  |               DEFAULT_BAREMETAL_COMPUTE_API_VERSION) | ||||||
|  |     ) | ||||||
|  |     return parser | ||||||
							
								
								
									
										0
									
								
								nimbleclient/osc/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								nimbleclient/osc/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								nimbleclient/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								nimbleclient/v1/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										24
									
								
								nimbleclient/v1/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								nimbleclient/v1/client.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | |||||||
|  | #   Copyright 2016 Huawei, Inc. All rights reserved. | ||||||
|  | # | ||||||
|  | #   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. | ||||||
|  | # | ||||||
|  |  | ||||||
|  | from nimbleclient.common import http | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Client(object): | ||||||
|  |     """Client for the Nimble v1 API.""" | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         """Initialize a new client for the Nimble v1 API.""" | ||||||
|  |         self.http_client = http._construct_http_client(*args, **kwargs) | ||||||
| @@ -2,5 +2,11 @@ | |||||||
| # of appearance. Changing the order has an impact on the overall integration | # of appearance. Changing the order has an impact on the overall integration | ||||||
| # process, which may cause wedges in the gate later. | # process, which may cause wedges in the gate later. | ||||||
|  |  | ||||||
| pbr>=1.6 # Apache-2.0 | keystoneauth1>=2.10.0 # Apache-2.0 | ||||||
| osc-lib>=1.0.2  # Apache-2.0 | osc-lib>=1.0.2  # Apache-2.0 | ||||||
|  | oslo.i18n>=2.1.0 # Apache-2.0 | ||||||
|  | oslo.serialization>=1.10.0 # Apache-2.0 | ||||||
|  | oslo.utils>=3.16.0 # Apache-2.0 | ||||||
|  | pbr>=1.6 # Apache-2.0 | ||||||
|  | requests>=2.10.0 # Apache-2.0 | ||||||
|  | six>=1.9.0 # MIT | ||||||
|   | |||||||
| @@ -23,6 +23,13 @@ classifier = | |||||||
| packages = | packages = | ||||||
|     nimbleclient |     nimbleclient | ||||||
|  |  | ||||||
|  | [entry_points] | ||||||
|  | openstack.cli.extension = | ||||||
|  |     baremetal_compute = nimbleclient.osc.plugin | ||||||
|  |  | ||||||
|  | openstack.baremetal_compute.v1 = | ||||||
|  |  | ||||||
|  |  | ||||||
| [build_sphinx] | [build_sphinx] | ||||||
| source-dir = doc/source | source-dir = doc/source | ||||||
| build-dir = doc/build | build-dir = doc/build | ||||||
|   | |||||||
| @@ -2,16 +2,14 @@ | |||||||
| # of appearance. Changing the order has an impact on the overall integration | # of appearance. Changing the order has an impact on the overall integration | ||||||
| # process, which may cause wedges in the gate later. | # process, which may cause wedges in the gate later. | ||||||
|  |  | ||||||
| hacking<0.12,>=0.11.0 # Apache-2.0 |  | ||||||
|  |  | ||||||
| coverage>=3.6 # Apache-2.0 | coverage>=3.6 # Apache-2.0 | ||||||
|  | hacking<0.12,>=0.11.0 # Apache-2.0 | ||||||
|  | python-openstackclient>=2.1.0 # Apache-2.0 | ||||||
| python-subunit>=0.0.18 # Apache-2.0/BSD | python-subunit>=0.0.18 # Apache-2.0/BSD | ||||||
| sphinx!=1.3b1,<1.3,>=1.2.1 # BSD |  | ||||||
| oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 | oslosphinx!=3.4.0,>=2.5.0 # Apache-2.0 | ||||||
| oslotest>=1.10.0 # Apache-2.0 | oslotest>=1.10.0 # Apache-2.0 | ||||||
|  | reno>=1.8.0 # Apache2 | ||||||
|  | sphinx!=1.3b1,<1.3,>=1.2.1 # BSD | ||||||
| testrepository>=0.0.18 # Apache-2.0/BSD | testrepository>=0.0.18 # Apache-2.0/BSD | ||||||
| testscenarios>=0.4 # Apache-2.0/BSD | testscenarios>=0.4 # Apache-2.0/BSD | ||||||
| testtools>=1.4.0 # MIT | testtools>=1.4.0 # MIT | ||||||
|  |  | ||||||
| # releasenotes |  | ||||||
| reno>=1.8.0 # Apache2 |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Rui Chen
					Rui Chen