Merge tag '1.0.9' into debian/unstable
python-ceilometerclient 1.0.9 Includes the following changes: * Modify ceilometer client cmd line help info * Remove unused import for print_function * Remove ununsed httplib2 requirement * Update client to display data type of traits * Using common methods from oslo cliutils * Avoid discarding alarm-threshold-create --query option * Fix typos picked up by misspellings * return sample info when creating sample with CLI * Enable hacking H233 rule * Using common method 'bool_from_string' from oslo strutils * Raise traceback on error when using CLI and -debug * Remove print debugs statements. * replace assertTrue(isinstance) to assertIsInstance * Add support for groupby in statistics for API v2 * Remove dependencies on pep8, pyflakes and flake8 * Replace inheritance hierarchy with composition * fix optional parameter of creating sample * abbreviating --meter-name to -m in alarm commands * Remove unused imports * Support the Event API * Python 3: fix test_sample_list * Use Resource() class from common Oslo code * client looking at wrong cacert argument name * Supports bash_completion for ceilometerclient * Fix the ceilometerlient log curl request incorrectly * Python 3: use six.moves.zip() rather than itertools.izip() * Display message on HTTP 400 * Fix alarm-combination-update operator argument * Improve description of some commands * Updates tox.ini to use new features * Updated from global requirements * Update .gitignore * Encode exception on ceilometer-client for UnicodeDecodeError
This commit is contained in:
		
							
								
								
									
										21
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										21
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -1,21 +1,12 @@ | |||||||
|  | *.pyc | ||||||
|  | *.egg-info | ||||||
|  | build | ||||||
| .coverage | .coverage | ||||||
| .venv | .tox | ||||||
|  | cover | ||||||
| .testrepository | .testrepository | ||||||
| subunit.log | subunit.log | ||||||
| *,cover |  | ||||||
| cover |  | ||||||
| *.pyc |  | ||||||
| .idea |  | ||||||
| *.swp |  | ||||||
| *~ |  | ||||||
| AUTHORS | AUTHORS | ||||||
| build |  | ||||||
| dist |  | ||||||
| ChangeLog | ChangeLog | ||||||
| run_tests.err.log | dist | ||||||
| .tox |  | ||||||
| doc/source/api |  | ||||||
| *.egg | *.egg | ||||||
| ceilometerclient/versioninfo |  | ||||||
| python_ceilometerclient.egg-info |  | ||||||
| *.log |  | ||||||
|   | |||||||
| @@ -90,7 +90,7 @@ def get_client(api_version, **kwargs): | |||||||
|         'token': token, |         'token': token, | ||||||
|         'insecure': kwargs.get('insecure'), |         'insecure': kwargs.get('insecure'), | ||||||
|         'timeout': kwargs.get('timeout'), |         'timeout': kwargs.get('timeout'), | ||||||
|         'cacert': kwargs.get('cacert'), |         'cacert': kwargs.get('os_cacert'), | ||||||
|         'cert_file': kwargs.get('cert_file'), |         'cert_file': kwargs.get('cert_file'), | ||||||
|         'key_file': kwargs.get('key_file'), |         'key_file': kwargs.get('key_file'), | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ Base utilities to build API operation managers and objects on top of. | |||||||
|  |  | ||||||
| import copy | import copy | ||||||
|  |  | ||||||
|  | from ceilometerclient.openstack.common.apiclient import base | ||||||
|  |  | ||||||
| # Python 2.4 compat | # Python 2.4 compat | ||||||
| try: | try: | ||||||
| @@ -80,7 +81,7 @@ class Manager(object): | |||||||
|         self.api.raw_request('DELETE', url) |         self.api.raw_request('DELETE', url) | ||||||
|  |  | ||||||
|  |  | ||||||
| class Resource(object): | class Resource(base.Resource): | ||||||
|     """A resource represents a particular instance of an object (tenant, user, |     """A resource represents a particular instance of an object (tenant, user, | ||||||
|     etc). This is pretty much just a bag for attributes. |     etc). This is pretty much just a bag for attributes. | ||||||
|  |  | ||||||
| @@ -88,55 +89,6 @@ class Resource(object): | |||||||
|     :param info: dictionary representing resource attributes |     :param info: dictionary representing resource attributes | ||||||
|     :param loaded: prevent lazy-loading if set to True |     :param loaded: prevent lazy-loading if set to True | ||||||
|     """ |     """ | ||||||
|     def __init__(self, manager, info, loaded=False): |  | ||||||
|         self.manager = manager |  | ||||||
|         self._info = info |  | ||||||
|         self._add_details(info) |  | ||||||
|         self._loaded = loaded |  | ||||||
|  |  | ||||||
|     def _add_details(self, info): |  | ||||||
|         for (k, v) in info.iteritems(): |  | ||||||
|             setattr(self, k, v) |  | ||||||
|  |  | ||||||
|     def __getattr__(self, k): |  | ||||||
|         if k not in self.__dict__: |  | ||||||
|             # NOTE(bcwaldon): disallow lazy-loading if already loaded once |  | ||||||
|             if not self.is_loaded(): |  | ||||||
|                 self.get() |  | ||||||
|                 return self.__getattr__(k) |  | ||||||
|  |  | ||||||
|             raise AttributeError(k) |  | ||||||
|         else: |  | ||||||
|             return self.__dict__[k] |  | ||||||
|  |  | ||||||
|     def __repr__(self): |  | ||||||
|         reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and |  | ||||||
|                           k != 'manager') |  | ||||||
|         info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) |  | ||||||
|         return "<%s %s>" % (self.__class__.__name__, info) |  | ||||||
|  |  | ||||||
|     def get(self): |  | ||||||
|         # set_loaded() first ... so if we have to bail, we know we tried. |  | ||||||
|         self.set_loaded(True) |  | ||||||
|         if not hasattr(self.manager, 'get'): |  | ||||||
|             return |  | ||||||
|  |  | ||||||
|         new = self.manager.get(self.id) |  | ||||||
|         if new: |  | ||||||
|             self._add_details(new._info) |  | ||||||
|  |  | ||||||
|     def __eq__(self, other): |  | ||||||
|         if not isinstance(other, self.__class__): |  | ||||||
|             return False |  | ||||||
|         if hasattr(self, 'id') and hasattr(other, 'id'): |  | ||||||
|             return self.id == other.id |  | ||||||
|         return self._info == other._info |  | ||||||
|  |  | ||||||
|     def is_loaded(self): |  | ||||||
|         return self._loaded |  | ||||||
|  |  | ||||||
|     def set_loaded(self, val): |  | ||||||
|         self._loaded = val |  | ||||||
|  |  | ||||||
|     def to_dict(self): |     def to_dict(self): | ||||||
|         return copy.deepcopy(self._info) |         return copy.deepcopy(self._info) | ||||||
|   | |||||||
| @@ -14,12 +14,9 @@ | |||||||
| #    under the License. | #    under the License. | ||||||
|  |  | ||||||
| import copy | import copy | ||||||
| import httplib |  | ||||||
| import logging | import logging | ||||||
| import os | import os | ||||||
| import socket | import socket | ||||||
| import StringIO |  | ||||||
| import urlparse |  | ||||||
|  |  | ||||||
| try: | try: | ||||||
|     import ssl |     import ssl | ||||||
| @@ -32,13 +29,11 @@ try: | |||||||
| except ImportError: | except ImportError: | ||||||
|     import simplejson as json |     import simplejson as json | ||||||
|  |  | ||||||
| # Python 2.5 compat fix | import six | ||||||
| if not hasattr(urlparse, 'parse_qsl'): | from six.moves import http_client as httplib  # noqa | ||||||
|     import cgi |  | ||||||
|     urlparse.parse_qsl = cgi.parse_qsl |  | ||||||
|  |  | ||||||
|  |  | ||||||
| from ceilometerclient import exc | from ceilometerclient import exc | ||||||
|  | from ceilometerclient.openstack.common.py3kcompat import urlutils | ||||||
|  |  | ||||||
|  |  | ||||||
| LOG = logging.getLogger(__name__) | LOG = logging.getLogger(__name__) | ||||||
| @@ -52,10 +47,11 @@ class HTTPClient(object): | |||||||
|         self.endpoint = endpoint |         self.endpoint = endpoint | ||||||
|         self.auth_token = kwargs.get('token') |         self.auth_token = kwargs.get('token') | ||||||
|         self.connection_params = self.get_connection_params(endpoint, **kwargs) |         self.connection_params = self.get_connection_params(endpoint, **kwargs) | ||||||
|  |         self.proxy_url = self.get_proxy_url() | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
|     def get_connection_params(endpoint, **kwargs): |     def get_connection_params(endpoint, **kwargs): | ||||||
|         parts = urlparse.urlparse(endpoint) |         parts = urlutils.urlparse(endpoint) | ||||||
|  |  | ||||||
|         _args = (parts.hostname, parts.port, parts.path) |         _args = (parts.hostname, parts.port, parts.path) | ||||||
|         _kwargs = {'timeout': (float(kwargs.get('timeout')) |         _kwargs = {'timeout': (float(kwargs.get('timeout')) | ||||||
| @@ -63,7 +59,7 @@ class HTTPClient(object): | |||||||
|  |  | ||||||
|         if parts.scheme == 'https': |         if parts.scheme == 'https': | ||||||
|             _class = VerifiedHTTPSConnection |             _class = VerifiedHTTPSConnection | ||||||
|             _kwargs['ca_cert'] = kwargs.get('cacert', None) |             _kwargs['cacert'] = kwargs.get('cacert', None) | ||||||
|             _kwargs['cert_file'] = kwargs.get('cert_file', None) |             _kwargs['cert_file'] = kwargs.get('cert_file', None) | ||||||
|             _kwargs['key_file'] = kwargs.get('key_file', None) |             _kwargs['key_file'] = kwargs.get('key_file', None) | ||||||
|             _kwargs['insecure'] = kwargs.get('insecure', False) |             _kwargs['insecure'] = kwargs.get('insecure', False) | ||||||
| @@ -78,8 +74,13 @@ class HTTPClient(object): | |||||||
|     def get_connection(self): |     def get_connection(self): | ||||||
|         _class = self.connection_params[0] |         _class = self.connection_params[0] | ||||||
|         try: |         try: | ||||||
|             return _class(*self.connection_params[1][0:2], |             if self.proxy_url: | ||||||
|                           **self.connection_params[2]) |                 proxy_parts = urlutils.urlparse(self.proxy_url) | ||||||
|  |                 return _class(proxy_parts.hostname, proxy_parts.port, | ||||||
|  |                               **self.connection_params[2]) | ||||||
|  |             else: | ||||||
|  |                 return _class(*self.connection_params[1][0:2], | ||||||
|  |                               **self.connection_params[2]) | ||||||
|         except httplib.InvalidURL: |         except httplib.InvalidURL: | ||||||
|             raise exc.InvalidEndpoint() |             raise exc.InvalidEndpoint() | ||||||
|  |  | ||||||
| @@ -106,7 +107,7 @@ class HTTPClient(object): | |||||||
|         if 'body' in kwargs: |         if 'body' in kwargs: | ||||||
|             curl.append('-d \'%s\'' % kwargs['body']) |             curl.append('-d \'%s\'' % kwargs['body']) | ||||||
|  |  | ||||||
|         curl.append('%s%s' % (self.endpoint, url)) |         curl.append('%s/%s' % (self.endpoint.rstrip('/'), url.lstrip('/'))) | ||||||
|         LOG.debug(' '.join(curl)) |         LOG.debug(' '.join(curl)) | ||||||
|  |  | ||||||
|     @staticmethod |     @staticmethod | ||||||
| @@ -141,7 +142,10 @@ class HTTPClient(object): | |||||||
|         conn = self.get_connection() |         conn = self.get_connection() | ||||||
|  |  | ||||||
|         try: |         try: | ||||||
|             conn_url = self._make_connection_url(url) |             if self.proxy_url: | ||||||
|  |                 conn_url = self.endpoint + self._make_connection_url(url) | ||||||
|  |             else: | ||||||
|  |                 conn_url = self._make_connection_url(url) | ||||||
|             conn.request(method, conn_url, **kwargs) |             conn.request(method, conn_url, **kwargs) | ||||||
|             resp = conn.getresponse() |             resp = conn.getresponse() | ||||||
|         except socket.gaierror as e: |         except socket.gaierror as e: | ||||||
| @@ -160,13 +164,13 @@ class HTTPClient(object): | |||||||
|         if resp.getheader('content-type', None) != 'application/octet-stream': |         if resp.getheader('content-type', None) != 'application/octet-stream': | ||||||
|             body_str = ''.join([chunk for chunk in body_iter]) |             body_str = ''.join([chunk for chunk in body_iter]) | ||||||
|             self.log_http_response(resp, body_str) |             self.log_http_response(resp, body_str) | ||||||
|             body_iter = StringIO.StringIO(body_str) |             body_iter = six.StringIO(body_str) | ||||||
|         else: |         else: | ||||||
|             self.log_http_response(resp) |             self.log_http_response(resp) | ||||||
|  |  | ||||||
|         if 400 <= resp.status < 600: |         if 400 <= resp.status < 600: | ||||||
|             LOG.warn("Request returned failure status.") |             LOG.warn("Request returned failure status.") | ||||||
|             raise exc.from_response(resp) |             raise exc.from_response(resp, ''.join(body_iter)) | ||||||
|         elif resp.status in (301, 302, 305): |         elif resp.status in (301, 302, 305): | ||||||
|             # Redirected. Reissue the request to the new location. |             # Redirected. Reissue the request to the new location. | ||||||
|             return self._http_request(resp['location'], method, **kwargs) |             return self._http_request(resp['location'], method, **kwargs) | ||||||
| @@ -206,6 +210,15 @@ class HTTPClient(object): | |||||||
|                                      'application/octet-stream') |                                      'application/octet-stream') | ||||||
|         return self._http_request(url, method, **kwargs) |         return self._http_request(url, method, **kwargs) | ||||||
|  |  | ||||||
|  |     def get_proxy_url(self): | ||||||
|  |         scheme = urlutils.urlparse(self.endpoint).scheme | ||||||
|  |         if scheme == 'https': | ||||||
|  |             return os.environ.get('https_proxy') | ||||||
|  |         elif scheme == 'http': | ||||||
|  |             return os.environ.get('http_proxy') | ||||||
|  |         msg = 'Unsupported scheme: %s' % scheme | ||||||
|  |         raise exc.InvalidEndpoint(msg) | ||||||
|  |  | ||||||
|  |  | ||||||
| class VerifiedHTTPSConnection(httplib.HTTPSConnection): | class VerifiedHTTPSConnection(httplib.HTTPSConnection): | ||||||
|     """httplib-compatibile connection using client-side SSL authentication |     """httplib-compatibile connection using client-side SSL authentication | ||||||
|   | |||||||
| @@ -13,7 +13,8 @@ | |||||||
| #    License for the specific language governing permissions and limitations | #    License for the specific language governing permissions and limitations | ||||||
| #    under the License. | #    under the License. | ||||||
|  |  | ||||||
| import os | from __future__ import print_function | ||||||
|  | import six | ||||||
| import sys | import sys | ||||||
| import textwrap | import textwrap | ||||||
| import uuid | import uuid | ||||||
| @@ -21,7 +22,9 @@ import uuid | |||||||
| import prettytable | import prettytable | ||||||
|  |  | ||||||
| from ceilometerclient import exc | from ceilometerclient import exc | ||||||
|  | from ceilometerclient.openstack.common import cliutils | ||||||
| from ceilometerclient.openstack.common import importutils | from ceilometerclient.openstack.common import importutils | ||||||
|  | from ceilometerclient.openstack.common import strutils | ||||||
|  |  | ||||||
|  |  | ||||||
| # Decorator for cli-args | # Decorator for cli-args | ||||||
| @@ -46,33 +49,52 @@ def pretty_choice_list(l): | |||||||
|  |  | ||||||
|  |  | ||||||
| def print_list(objs, fields, field_labels, formatters={}, sortby=0): | def print_list(objs, fields, field_labels, formatters={}, sortby=0): | ||||||
|     pt = prettytable.PrettyTable([f for f in field_labels], |  | ||||||
|                                  caching=False, print_empty=False) |  | ||||||
|     pt.align = 'l' |  | ||||||
|  |  | ||||||
|     for o in objs: |     def _make_default_formatter(field): | ||||||
|         row = [] |         return lambda o: getattr(o, field, '') | ||||||
|         for field in fields: |  | ||||||
|             if field in formatters: |     new_formatters = {} | ||||||
|                 row.append(formatters[field](o)) |     for field, field_label in six.moves.zip(fields, field_labels): | ||||||
|             else: |         if field in formatters: | ||||||
|                 data = getattr(o, field, '') |             new_formatters[field_label] = formatters[field] | ||||||
|                 row.append(data) |         else: | ||||||
|         pt.add_row(row) |             new_formatters[field_label] = _make_default_formatter(field) | ||||||
|     print pt.get_string(sortby=field_labels[sortby]) |  | ||||||
|  |     cliutils.print_list(objs, field_labels, | ||||||
|  |                         formatters=new_formatters, | ||||||
|  |                         sortby_index=sortby) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def nested_list_of_dict_formatter(field, column_names): | ||||||
|  |     # (TMaddox) Because the formatting scheme actually drops the whole object | ||||||
|  |     # into the formatter, rather than just the specified field, we have to | ||||||
|  |     # extract it and then pass the value. | ||||||
|  |     return lambda o: format_nested_list_of_dict(getattr(o, field), | ||||||
|  |                                                 column_names) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def format_nested_list_of_dict(l, column_names): | ||||||
|  |     pt = prettytable.PrettyTable(caching=False, print_empty=False, | ||||||
|  |                                  header=True, hrules=prettytable.FRAME, | ||||||
|  |                                  field_names=column_names) | ||||||
|  |     for d in l: | ||||||
|  |         pt.add_row(map(lambda k: d[k], column_names)) | ||||||
|  |     return pt.get_string() | ||||||
|  |  | ||||||
|  |  | ||||||
| def print_dict(d, dict_property="Property", wrap=0): | def print_dict(d, dict_property="Property", wrap=0): | ||||||
|     pt = prettytable.PrettyTable([dict_property, 'Value'], |     pt = prettytable.PrettyTable([dict_property, 'Value'], | ||||||
|                                  caching=False, print_empty=False) |                                  caching=False, print_empty=False) | ||||||
|     pt.align = 'l' |     pt.align = 'l' | ||||||
|     for k, v in d.iteritems(): |     for k, v in sorted(six.iteritems(d)): | ||||||
|         # convert dict to str to check length |         # convert dict to str to check length | ||||||
|         if isinstance(v, dict): |         if isinstance(v, dict): | ||||||
|             v = str(v) |             v = str(v) | ||||||
|  |         if isinstance(v, six.string_types): | ||||||
|  |             v = strutils.safe_encode(v) | ||||||
|         # if value has a newline, add in multiple rows |         # if value has a newline, add in multiple rows | ||||||
|         # e.g. fault with stacktrace |         # e.g. fault with stacktrace | ||||||
|         if v and isinstance(v, basestring) and r'\n' in v: |         if v and isinstance(v, six.string_types) and r'\n' in v: | ||||||
|             lines = v.strip().split(r'\n') |             lines = v.strip().split(r'\n') | ||||||
|             col1 = k |             col1 = k | ||||||
|             for line in lines: |             for line in lines: | ||||||
| @@ -84,7 +106,7 @@ def print_dict(d, dict_property="Property", wrap=0): | |||||||
|             if wrap > 0: |             if wrap > 0: | ||||||
|                 v = textwrap.fill(str(v), wrap) |                 v = textwrap.fill(str(v), wrap) | ||||||
|             pt.add_row([k, v]) |             pt.add_row([k, v]) | ||||||
|     print pt.get_string() |     print(pt.get_string()) | ||||||
|  |  | ||||||
|  |  | ||||||
| def find_resource(manager, name_or_id): | def find_resource(manager, name_or_id): | ||||||
| @@ -112,23 +134,6 @@ def find_resource(manager, name_or_id): | |||||||
|         raise exc.CommandError(msg) |         raise exc.CommandError(msg) | ||||||
|  |  | ||||||
|  |  | ||||||
| def string_to_bool(arg): |  | ||||||
|     return arg.strip().lower() in ('t', 'true', 'yes', '1') |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def env(*vars, **kwargs): |  | ||||||
|     """Search for the first defined of possibly many env vars |  | ||||||
|  |  | ||||||
|     Returns the first environment variable defined in vars, or |  | ||||||
|     returns the default defined in kwargs. |  | ||||||
|     """ |  | ||||||
|     for v in vars: |  | ||||||
|         value = os.environ.get(v, None) |  | ||||||
|         if value: |  | ||||||
|             return value |  | ||||||
|     return kwargs.get('default', '') |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def import_versioned_module(version, submodule=None): | def import_versioned_module(version, submodule=None): | ||||||
|     module = 'ceilometerclient.v%s' % version |     module = 'ceilometerclient.v%s' % version | ||||||
|     if submodule: |     if submodule: | ||||||
| @@ -151,7 +156,7 @@ def args_array_to_dict(kwargs, key_to_convert): | |||||||
|  |  | ||||||
| def key_with_slash_to_nested_dict(kwargs): | def key_with_slash_to_nested_dict(kwargs): | ||||||
|     nested_kwargs = {} |     nested_kwargs = {} | ||||||
|     for k in kwargs.keys(): |     for k in list(kwargs): | ||||||
|         keys = k.split('/', 1) |         keys = k.split('/', 1) | ||||||
|         if len(keys) == 2: |         if len(keys) == 2: | ||||||
|             nested_kwargs.setdefault(keys[0], {})[keys[1]] = kwargs[k] |             nested_kwargs.setdefault(keys[0], {})[keys[1]] = kwargs[k] | ||||||
| @@ -161,7 +166,7 @@ def key_with_slash_to_nested_dict(kwargs): | |||||||
|  |  | ||||||
|  |  | ||||||
| def merge_nested_dict(dest, source, depth=0): | def merge_nested_dict(dest, source, depth=0): | ||||||
|     for (key, value) in source.iteritems(): |     for (key, value) in six.iteritems(source): | ||||||
|         if isinstance(value, dict) and depth: |         if isinstance(value, dict) and depth: | ||||||
|             merge_nested_dict(dest[key], value, |             merge_nested_dict(dest[key], value, | ||||||
|                               depth=(depth - 1)) |                               depth=(depth - 1)) | ||||||
| @@ -171,5 +176,5 @@ def merge_nested_dict(dest, source, depth=0): | |||||||
|  |  | ||||||
| def exit(msg=''): | def exit(msg=''): | ||||||
|     if msg: |     if msg: | ||||||
|         print >> sys.stderr, msg |         print(msg, file=sys.stderr) | ||||||
|     sys.exit(1) |     sys.exit(1) | ||||||
|   | |||||||
| @@ -10,6 +10,7 @@ | |||||||
| #    License for the specific language governing permissions and limitations | #    License for the specific language governing permissions and limitations | ||||||
| #    under the License. | #    under the License. | ||||||
|  |  | ||||||
|  | import json | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -65,7 +66,17 @@ class BadRequest(HTTPException): | |||||||
|  |  | ||||||
|  |  | ||||||
| class HTTPBadRequest(BadRequest): | class HTTPBadRequest(BadRequest): | ||||||
|     pass |  | ||||||
|  |     def __str__(self): | ||||||
|  |         try: | ||||||
|  |             data = json.loads(self.details) | ||||||
|  |             message = data.get("error_message", {}).get("faultstring") | ||||||
|  |             if message: | ||||||
|  |                 return "%s (HTTP %s) ERROR %s" % ( | ||||||
|  |                     self.__class__.__name__, self.code, message) | ||||||
|  |         except (ValueError, TypeError, AttributeError): | ||||||
|  |             pass | ||||||
|  |         return super(HTTPBadRequest, self).__str__() | ||||||
|  |  | ||||||
|  |  | ||||||
| class Unauthorized(HTTPException): | class Unauthorized(HTTPException): | ||||||
| @@ -147,10 +158,10 @@ for obj_name in dir(sys.modules[__name__]): | |||||||
|         _code_map[obj.code] = obj |         _code_map[obj.code] = obj | ||||||
|  |  | ||||||
|  |  | ||||||
| def from_response(response): | def from_response(response, details=None): | ||||||
|     """Return an instance of an HTTPException based on httplib response.""" |     """Return an instance of an HTTPException based on httplib response.""" | ||||||
|     cls = _code_map.get(response.status, HTTPException) |     cls = _code_map.get(response.status, HTTPException) | ||||||
|     return cls() |     return cls(details) | ||||||
|  |  | ||||||
|  |  | ||||||
| class NoTokenLookupException(Exception): | class NoTokenLookupException(Exception): | ||||||
|   | |||||||
							
								
								
									
										14
									
								
								ceilometerclient/openstack/common/apiclient/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								ceilometerclient/openstack/common/apiclient/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,14 @@ | |||||||
|  | # Copyright 2013 OpenStack Foundation | ||||||
|  | # 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. | ||||||
							
								
								
									
										225
									
								
								ceilometerclient/openstack/common/apiclient/auth.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										225
									
								
								ceilometerclient/openstack/common/apiclient/auth.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,225 @@ | |||||||
|  | # Copyright 2013 OpenStack Foundation | ||||||
|  | # Copyright 2013 Spanish National Research Council. | ||||||
|  | # 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. | ||||||
|  |  | ||||||
|  | # E0202: An attribute inherited from %s hide this method | ||||||
|  | # pylint: disable=E0202 | ||||||
|  |  | ||||||
|  | import abc | ||||||
|  | import argparse | ||||||
|  | import logging | ||||||
|  | import os | ||||||
|  |  | ||||||
|  | import six | ||||||
|  | from stevedore import extension | ||||||
|  |  | ||||||
|  | from ceilometerclient.openstack.common.apiclient import exceptions | ||||||
|  |  | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _discovered_plugins = {} | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def discover_auth_systems(): | ||||||
|  |     """Discover the available auth-systems. | ||||||
|  |  | ||||||
|  |     This won't take into account the old style auth-systems. | ||||||
|  |     """ | ||||||
|  |     global _discovered_plugins | ||||||
|  |     _discovered_plugins = {} | ||||||
|  |  | ||||||
|  |     def add_plugin(ext): | ||||||
|  |         _discovered_plugins[ext.name] = ext.plugin | ||||||
|  |  | ||||||
|  |     ep_namespace = "ceilometerclient.openstack.common.apiclient.auth" | ||||||
|  |     mgr = extension.ExtensionManager(ep_namespace) | ||||||
|  |     mgr.map(add_plugin) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def load_auth_system_opts(parser): | ||||||
|  |     """Load options needed by the available auth-systems into a parser. | ||||||
|  |  | ||||||
|  |     This function will try to populate the parser with options from the | ||||||
|  |     available plugins. | ||||||
|  |     """ | ||||||
|  |     group = parser.add_argument_group("Common auth options") | ||||||
|  |     BaseAuthPlugin.add_common_opts(group) | ||||||
|  |     for name, auth_plugin in _discovered_plugins.iteritems(): | ||||||
|  |         group = parser.add_argument_group( | ||||||
|  |             "Auth-system '%s' options" % name, | ||||||
|  |             conflict_handler="resolve") | ||||||
|  |         auth_plugin.add_opts(group) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def load_plugin(auth_system): | ||||||
|  |     try: | ||||||
|  |         plugin_class = _discovered_plugins[auth_system] | ||||||
|  |     except KeyError: | ||||||
|  |         raise exceptions.AuthSystemNotFound(auth_system) | ||||||
|  |     return plugin_class(auth_system=auth_system) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def load_plugin_from_args(args): | ||||||
|  |     """Load required plugin and populate it with options. | ||||||
|  |  | ||||||
|  |     Try to guess auth system if it is not specified. Systems are tried in | ||||||
|  |     alphabetical order. | ||||||
|  |  | ||||||
|  |     :type args: argparse.Namespace | ||||||
|  |     :raises: AuthorizationFailure | ||||||
|  |     """ | ||||||
|  |     auth_system = args.os_auth_system | ||||||
|  |     if auth_system: | ||||||
|  |         plugin = load_plugin(auth_system) | ||||||
|  |         plugin.parse_opts(args) | ||||||
|  |         plugin.sufficient_options() | ||||||
|  |         return plugin | ||||||
|  |  | ||||||
|  |     for plugin_auth_system in sorted(six.iterkeys(_discovered_plugins)): | ||||||
|  |         plugin_class = _discovered_plugins[plugin_auth_system] | ||||||
|  |         plugin = plugin_class() | ||||||
|  |         plugin.parse_opts(args) | ||||||
|  |         try: | ||||||
|  |             plugin.sufficient_options() | ||||||
|  |         except exceptions.AuthPluginOptionsMissing: | ||||||
|  |             continue | ||||||
|  |         return plugin | ||||||
|  |     raise exceptions.AuthPluginOptionsMissing(["auth_system"]) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @six.add_metaclass(abc.ABCMeta) | ||||||
|  | class BaseAuthPlugin(object): | ||||||
|  |     """Base class for authentication plugins. | ||||||
|  |  | ||||||
|  |     An authentication plugin needs to override at least the authenticate | ||||||
|  |     method to be a valid plugin. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     auth_system = None | ||||||
|  |     opt_names = [] | ||||||
|  |     common_opt_names = [ | ||||||
|  |         "auth_system", | ||||||
|  |         "username", | ||||||
|  |         "password", | ||||||
|  |         "tenant_name", | ||||||
|  |         "token", | ||||||
|  |         "auth_url", | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     def __init__(self, auth_system=None, **kwargs): | ||||||
|  |         self.auth_system = auth_system or self.auth_system | ||||||
|  |         self.opts = dict((name, kwargs.get(name)) | ||||||
|  |                          for name in self.opt_names) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def _parser_add_opt(parser, opt): | ||||||
|  |         """Add an option to parser in two variants. | ||||||
|  |  | ||||||
|  |         :param opt: option name (with underscores) | ||||||
|  |         """ | ||||||
|  |         dashed_opt = opt.replace("_", "-") | ||||||
|  |         env_var = "OS_%s" % opt.upper() | ||||||
|  |         arg_default = os.environ.get(env_var, "") | ||||||
|  |         arg_help = "Defaults to env[%s]." % env_var | ||||||
|  |         parser.add_argument( | ||||||
|  |             "--os-%s" % dashed_opt, | ||||||
|  |             metavar="<%s>" % dashed_opt, | ||||||
|  |             default=arg_default, | ||||||
|  |             help=arg_help) | ||||||
|  |         parser.add_argument( | ||||||
|  |             "--os_%s" % opt, | ||||||
|  |             metavar="<%s>" % dashed_opt, | ||||||
|  |             help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def add_opts(cls, parser): | ||||||
|  |         """Populate the parser with the options for this plugin. | ||||||
|  |         """ | ||||||
|  |         for opt in cls.opt_names: | ||||||
|  |             # use `BaseAuthPlugin.common_opt_names` since it is never | ||||||
|  |             # changed in child classes | ||||||
|  |             if opt not in BaseAuthPlugin.common_opt_names: | ||||||
|  |                 cls._parser_add_opt(parser, opt) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def add_common_opts(cls, parser): | ||||||
|  |         """Add options that are common for several plugins. | ||||||
|  |         """ | ||||||
|  |         for opt in cls.common_opt_names: | ||||||
|  |             cls._parser_add_opt(parser, opt) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_opt(opt_name, args): | ||||||
|  |         """Return option name and value. | ||||||
|  |  | ||||||
|  |         :param opt_name: name of the option, e.g., "username" | ||||||
|  |         :param args: parsed arguments | ||||||
|  |         """ | ||||||
|  |         return (opt_name, getattr(args, "os_%s" % opt_name, None)) | ||||||
|  |  | ||||||
|  |     def parse_opts(self, args): | ||||||
|  |         """Parse the actual auth-system options if any. | ||||||
|  |  | ||||||
|  |         This method is expected to populate the attribute `self.opts` with a | ||||||
|  |         dict containing the options and values needed to make authentication. | ||||||
|  |         """ | ||||||
|  |         self.opts.update(dict(self.get_opt(opt_name, args) | ||||||
|  |                               for opt_name in self.opt_names)) | ||||||
|  |  | ||||||
|  |     def authenticate(self, http_client): | ||||||
|  |         """Authenticate using plugin defined method. | ||||||
|  |  | ||||||
|  |         The method usually analyses `self.opts` and performs | ||||||
|  |         a request to authentication server. | ||||||
|  |  | ||||||
|  |         :param http_client: client object that needs authentication | ||||||
|  |         :type http_client: HTTPClient | ||||||
|  |         :raises: AuthorizationFailure | ||||||
|  |         """ | ||||||
|  |         self.sufficient_options() | ||||||
|  |         self._do_authenticate(http_client) | ||||||
|  |  | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def _do_authenticate(self, http_client): | ||||||
|  |         """Protected method for authentication. | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |     def sufficient_options(self): | ||||||
|  |         """Check if all required options are present. | ||||||
|  |  | ||||||
|  |         :raises: AuthPluginOptionsMissing | ||||||
|  |         """ | ||||||
|  |         missing = [opt | ||||||
|  |                    for opt in self.opt_names | ||||||
|  |                    if not self.opts.get(opt)] | ||||||
|  |         if missing: | ||||||
|  |             raise exceptions.AuthPluginOptionsMissing(missing) | ||||||
|  |  | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def token_and_endpoint(self, endpoint_type, service_type): | ||||||
|  |         """Return token and endpoint. | ||||||
|  |  | ||||||
|  |         :param service_type: Service type of the endpoint | ||||||
|  |         :type service_type: string | ||||||
|  |         :param endpoint_type: Type of endpoint. | ||||||
|  |                               Possible values: public or publicURL, | ||||||
|  |                                   internal or internalURL, | ||||||
|  |                                   admin or adminURL | ||||||
|  |         :type endpoint_type: string | ||||||
|  |         :returns: tuple of token and endpoint strings | ||||||
|  |         :raises: EndpointException | ||||||
|  |         """ | ||||||
							
								
								
									
										491
									
								
								ceilometerclient/openstack/common/apiclient/base.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										491
									
								
								ceilometerclient/openstack/common/apiclient/base.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,491 @@ | |||||||
|  | # Copyright 2010 Jacob Kaplan-Moss | ||||||
|  | # Copyright 2011 OpenStack Foundation | ||||||
|  | # Copyright 2012 Grid Dynamics | ||||||
|  | # Copyright 2013 OpenStack Foundation | ||||||
|  | # 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. | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | Base utilities to build API operation managers and objects on top of. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | # E1102: %s is not callable | ||||||
|  | # pylint: disable=E1102 | ||||||
|  |  | ||||||
|  | import abc | ||||||
|  | import urllib | ||||||
|  |  | ||||||
|  | import six | ||||||
|  |  | ||||||
|  | from ceilometerclient.openstack.common.apiclient import exceptions | ||||||
|  | from ceilometerclient.openstack.common import strutils | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def getid(obj): | ||||||
|  |     """Return id if argument is a Resource. | ||||||
|  |  | ||||||
|  |     Abstracts the common pattern of allowing both an object or an object's ID | ||||||
|  |     (UUID) as a parameter when dealing with relationships. | ||||||
|  |     """ | ||||||
|  |     try: | ||||||
|  |         if obj.uuid: | ||||||
|  |             return obj.uuid | ||||||
|  |     except AttributeError: | ||||||
|  |         pass | ||||||
|  |     try: | ||||||
|  |         return obj.id | ||||||
|  |     except AttributeError: | ||||||
|  |         return obj | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # TODO(aababilov): call run_hooks() in HookableMixin's child classes | ||||||
|  | class HookableMixin(object): | ||||||
|  |     """Mixin so classes can register and run hooks.""" | ||||||
|  |     _hooks_map = {} | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def add_hook(cls, hook_type, hook_func): | ||||||
|  |         """Add a new hook of specified type. | ||||||
|  |  | ||||||
|  |         :param cls: class that registers hooks | ||||||
|  |         :param hook_type: hook type, e.g., '__pre_parse_args__' | ||||||
|  |         :param hook_func: hook function | ||||||
|  |         """ | ||||||
|  |         if hook_type not in cls._hooks_map: | ||||||
|  |             cls._hooks_map[hook_type] = [] | ||||||
|  |  | ||||||
|  |         cls._hooks_map[hook_type].append(hook_func) | ||||||
|  |  | ||||||
|  |     @classmethod | ||||||
|  |     def run_hooks(cls, hook_type, *args, **kwargs): | ||||||
|  |         """Run all hooks of specified type. | ||||||
|  |  | ||||||
|  |         :param cls: class that registers hooks | ||||||
|  |         :param hook_type: hook type, e.g., '__pre_parse_args__' | ||||||
|  |         :param **args: args to be passed to every hook function | ||||||
|  |         :param **kwargs: kwargs to be passed to every hook function | ||||||
|  |         """ | ||||||
|  |         hook_funcs = cls._hooks_map.get(hook_type) or [] | ||||||
|  |         for hook_func in hook_funcs: | ||||||
|  |             hook_func(*args, **kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BaseManager(HookableMixin): | ||||||
|  |     """Basic manager type providing common operations. | ||||||
|  |  | ||||||
|  |     Managers interact with a particular type of API (servers, flavors, images, | ||||||
|  |     etc.) and provide CRUD operations for them. | ||||||
|  |     """ | ||||||
|  |     resource_class = None | ||||||
|  |  | ||||||
|  |     def __init__(self, client): | ||||||
|  |         """Initializes BaseManager with `client`. | ||||||
|  |  | ||||||
|  |         :param client: instance of BaseClient descendant for HTTP requests | ||||||
|  |         """ | ||||||
|  |         super(BaseManager, self).__init__() | ||||||
|  |         self.client = client | ||||||
|  |  | ||||||
|  |     def _list(self, url, response_key, obj_class=None, json=None): | ||||||
|  |         """List the collection. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         :param response_key: the key to be looked up in response dictionary, | ||||||
|  |             e.g., 'servers' | ||||||
|  |         :param obj_class: class for constructing the returned objects | ||||||
|  |             (self.resource_class will be used by default) | ||||||
|  |         :param json: data that will be encoded as JSON and passed in POST | ||||||
|  |             request (GET will be sent by default) | ||||||
|  |         """ | ||||||
|  |         if json: | ||||||
|  |             body = self.client.post(url, json=json).json() | ||||||
|  |         else: | ||||||
|  |             body = self.client.get(url).json() | ||||||
|  |  | ||||||
|  |         if obj_class is None: | ||||||
|  |             obj_class = self.resource_class | ||||||
|  |  | ||||||
|  |         data = body[response_key] | ||||||
|  |         # NOTE(ja): keystone returns values as list as {'values': [ ... ]} | ||||||
|  |         #           unlike other services which just return the list... | ||||||
|  |         try: | ||||||
|  |             data = data['values'] | ||||||
|  |         except (KeyError, TypeError): | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |         return [obj_class(self, res, loaded=True) for res in data if res] | ||||||
|  |  | ||||||
|  |     def _get(self, url, response_key): | ||||||
|  |         """Get an object from collection. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         :param response_key: the key to be looked up in response dictionary, | ||||||
|  |             e.g., 'server' | ||||||
|  |         """ | ||||||
|  |         body = self.client.get(url).json() | ||||||
|  |         return self.resource_class(self, body[response_key], loaded=True) | ||||||
|  |  | ||||||
|  |     def _head(self, url): | ||||||
|  |         """Retrieve request headers for an object. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         """ | ||||||
|  |         resp = self.client.head(url) | ||||||
|  |         return resp.status_code == 204 | ||||||
|  |  | ||||||
|  |     def _post(self, url, json, response_key, return_raw=False): | ||||||
|  |         """Create an object. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         :param json: data that will be encoded as JSON and passed in POST | ||||||
|  |             request (GET will be sent by default) | ||||||
|  |         :param response_key: the key to be looked up in response dictionary, | ||||||
|  |             e.g., 'servers' | ||||||
|  |         :param return_raw: flag to force returning raw JSON instead of | ||||||
|  |             Python object of self.resource_class | ||||||
|  |         """ | ||||||
|  |         body = self.client.post(url, json=json).json() | ||||||
|  |         if return_raw: | ||||||
|  |             return body[response_key] | ||||||
|  |         return self.resource_class(self, body[response_key]) | ||||||
|  |  | ||||||
|  |     def _put(self, url, json=None, response_key=None): | ||||||
|  |         """Update an object with PUT method. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         :param json: data that will be encoded as JSON and passed in POST | ||||||
|  |             request (GET will be sent by default) | ||||||
|  |         :param response_key: the key to be looked up in response dictionary, | ||||||
|  |             e.g., 'servers' | ||||||
|  |         """ | ||||||
|  |         resp = self.client.put(url, json=json) | ||||||
|  |         # PUT requests may not return a body | ||||||
|  |         if resp.content: | ||||||
|  |             body = resp.json() | ||||||
|  |             if response_key is not None: | ||||||
|  |                 return self.resource_class(self, body[response_key]) | ||||||
|  |             else: | ||||||
|  |                 return self.resource_class(self, body) | ||||||
|  |  | ||||||
|  |     def _patch(self, url, json=None, response_key=None): | ||||||
|  |         """Update an object with PATCH method. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers' | ||||||
|  |         :param json: data that will be encoded as JSON and passed in POST | ||||||
|  |             request (GET will be sent by default) | ||||||
|  |         :param response_key: the key to be looked up in response dictionary, | ||||||
|  |             e.g., 'servers' | ||||||
|  |         """ | ||||||
|  |         body = self.client.patch(url, json=json).json() | ||||||
|  |         if response_key is not None: | ||||||
|  |             return self.resource_class(self, body[response_key]) | ||||||
|  |         else: | ||||||
|  |             return self.resource_class(self, body) | ||||||
|  |  | ||||||
|  |     def _delete(self, url): | ||||||
|  |         """Delete an object. | ||||||
|  |  | ||||||
|  |         :param url: a partial URL, e.g., '/servers/my-server' | ||||||
|  |         """ | ||||||
|  |         return self.client.delete(url) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @six.add_metaclass(abc.ABCMeta) | ||||||
|  | class ManagerWithFind(BaseManager): | ||||||
|  |     """Manager with additional `find()`/`findall()` methods.""" | ||||||
|  |  | ||||||
|  |     @abc.abstractmethod | ||||||
|  |     def list(self): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     def find(self, **kwargs): | ||||||
|  |         """Find a single item with attributes matching ``**kwargs``. | ||||||
|  |  | ||||||
|  |         This isn't very efficient: it loads the entire list then filters on | ||||||
|  |         the Python side. | ||||||
|  |         """ | ||||||
|  |         matches = self.findall(**kwargs) | ||||||
|  |         num_matches = len(matches) | ||||||
|  |         if num_matches == 0: | ||||||
|  |             msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) | ||||||
|  |             raise exceptions.NotFound(msg) | ||||||
|  |         elif num_matches > 1: | ||||||
|  |             raise exceptions.NoUniqueMatch() | ||||||
|  |         else: | ||||||
|  |             return matches[0] | ||||||
|  |  | ||||||
|  |     def findall(self, **kwargs): | ||||||
|  |         """Find all items with attributes matching ``**kwargs``. | ||||||
|  |  | ||||||
|  |         This isn't very efficient: it loads the entire list then filters on | ||||||
|  |         the Python side. | ||||||
|  |         """ | ||||||
|  |         found = [] | ||||||
|  |         searches = kwargs.items() | ||||||
|  |  | ||||||
|  |         for obj in self.list(): | ||||||
|  |             try: | ||||||
|  |                 if all(getattr(obj, attr) == value | ||||||
|  |                        for (attr, value) in searches): | ||||||
|  |                     found.append(obj) | ||||||
|  |             except AttributeError: | ||||||
|  |                 continue | ||||||
|  |  | ||||||
|  |         return found | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CrudManager(BaseManager): | ||||||
|  |     """Base manager class for manipulating entities. | ||||||
|  |  | ||||||
|  |     Children of this class are expected to define a `collection_key` and `key`. | ||||||
|  |  | ||||||
|  |     - `collection_key`: Usually a plural noun by convention (e.g. `entities`); | ||||||
|  |       used to refer collections in both URL's (e.g.  `/v3/entities`) and JSON | ||||||
|  |       objects containing a list of member resources (e.g. `{'entities': [{}, | ||||||
|  |       {}, {}]}`). | ||||||
|  |     - `key`: Usually a singular noun by convention (e.g. `entity`); used to | ||||||
|  |       refer to an individual member of the collection. | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |     collection_key = None | ||||||
|  |     key = None | ||||||
|  |  | ||||||
|  |     def build_url(self, base_url=None, **kwargs): | ||||||
|  |         """Builds a resource URL for the given kwargs. | ||||||
|  |  | ||||||
|  |         Given an example collection where `collection_key = 'entities'` and | ||||||
|  |         `key = 'entity'`, the following URL's could be generated. | ||||||
|  |  | ||||||
|  |         By default, the URL will represent a collection of entities, e.g.:: | ||||||
|  |  | ||||||
|  |             /entities | ||||||
|  |  | ||||||
|  |         If kwargs contains an `entity_id`, then the URL will represent a | ||||||
|  |         specific member, e.g.:: | ||||||
|  |  | ||||||
|  |             /entities/{entity_id} | ||||||
|  |  | ||||||
|  |         :param base_url: if provided, the generated URL will be appended to it | ||||||
|  |         """ | ||||||
|  |         url = base_url if base_url is not None else '' | ||||||
|  |  | ||||||
|  |         url += '/%s' % self.collection_key | ||||||
|  |  | ||||||
|  |         # do we have a specific entity? | ||||||
|  |         entity_id = kwargs.get('%s_id' % self.key) | ||||||
|  |         if entity_id is not None: | ||||||
|  |             url += '/%s' % entity_id | ||||||
|  |  | ||||||
|  |         return url | ||||||
|  |  | ||||||
|  |     def _filter_kwargs(self, kwargs): | ||||||
|  |         """Drop null values and handle ids.""" | ||||||
|  |         for key, ref in kwargs.copy().iteritems(): | ||||||
|  |             if ref is None: | ||||||
|  |                 kwargs.pop(key) | ||||||
|  |             else: | ||||||
|  |                 if isinstance(ref, Resource): | ||||||
|  |                     kwargs.pop(key) | ||||||
|  |                     kwargs['%s_id' % key] = getid(ref) | ||||||
|  |         return kwargs | ||||||
|  |  | ||||||
|  |     def create(self, **kwargs): | ||||||
|  |         kwargs = self._filter_kwargs(kwargs) | ||||||
|  |         return self._post( | ||||||
|  |             self.build_url(**kwargs), | ||||||
|  |             {self.key: kwargs}, | ||||||
|  |             self.key) | ||||||
|  |  | ||||||
|  |     def get(self, **kwargs): | ||||||
|  |         kwargs = self._filter_kwargs(kwargs) | ||||||
|  |         return self._get( | ||||||
|  |             self.build_url(**kwargs), | ||||||
|  |             self.key) | ||||||
|  |  | ||||||
|  |     def head(self, **kwargs): | ||||||
|  |         kwargs = self._filter_kwargs(kwargs) | ||||||
|  |         return self._head(self.build_url(**kwargs)) | ||||||
|  |  | ||||||
|  |     def list(self, base_url=None, **kwargs): | ||||||
|  |         """List the collection. | ||||||
|  |  | ||||||
|  |         :param base_url: if provided, the generated URL will be appended to it | ||||||
|  |         """ | ||||||
|  |         kwargs = self._filter_kwargs(kwargs) | ||||||
|  |  | ||||||
|  |         return self._list( | ||||||
|  |             '%(base_url)s%(query)s' % { | ||||||
|  |                 'base_url': self.build_url(base_url=base_url, **kwargs), | ||||||
|  |                 'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '', | ||||||
|  |             }, | ||||||
|  |             self.collection_key) | ||||||
|  |  | ||||||
|  |     def put(self, base_url=None, **kwargs): | ||||||
|  |         """Update an element. | ||||||
|  |  | ||||||
|  |         :param base_url: if provided, the generated URL will be appended to it | ||||||
|  |         """ | ||||||
|  |         kwargs = self._filter_kwargs(kwargs) | ||||||
|  |  | ||||||
|  |         return self._put(self.build_url(base_url=base_url, **kwargs)) | ||||||
|  |  | ||||||
|  |     def update(self, **kwargs): | ||||||
|  |         kwargs = self._filter_kwargs(kwargs) | ||||||
|  |         params = kwargs.copy() | ||||||
|  |         params.pop('%s_id' % self.key) | ||||||
|  |  | ||||||
|  |         return self._patch( | ||||||
|  |             self.build_url(**kwargs), | ||||||
|  |             {self.key: params}, | ||||||
|  |             self.key) | ||||||
|  |  | ||||||
|  |     def delete(self, **kwargs): | ||||||
|  |         kwargs = self._filter_kwargs(kwargs) | ||||||
|  |  | ||||||
|  |         return self._delete( | ||||||
|  |             self.build_url(**kwargs)) | ||||||
|  |  | ||||||
|  |     def find(self, base_url=None, **kwargs): | ||||||
|  |         """Find a single item with attributes matching ``**kwargs``. | ||||||
|  |  | ||||||
|  |         :param base_url: if provided, the generated URL will be appended to it | ||||||
|  |         """ | ||||||
|  |         kwargs = self._filter_kwargs(kwargs) | ||||||
|  |  | ||||||
|  |         rl = self._list( | ||||||
|  |             '%(base_url)s%(query)s' % { | ||||||
|  |                 'base_url': self.build_url(base_url=base_url, **kwargs), | ||||||
|  |                 'query': '?%s' % urllib.urlencode(kwargs) if kwargs else '', | ||||||
|  |             }, | ||||||
|  |             self.collection_key) | ||||||
|  |         num = len(rl) | ||||||
|  |  | ||||||
|  |         if num == 0: | ||||||
|  |             msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) | ||||||
|  |             raise exceptions.NotFound(404, msg) | ||||||
|  |         elif num > 1: | ||||||
|  |             raise exceptions.NoUniqueMatch | ||||||
|  |         else: | ||||||
|  |             return rl[0] | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Extension(HookableMixin): | ||||||
|  |     """Extension descriptor.""" | ||||||
|  |  | ||||||
|  |     SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') | ||||||
|  |     manager_class = None | ||||||
|  |  | ||||||
|  |     def __init__(self, name, module): | ||||||
|  |         super(Extension, self).__init__() | ||||||
|  |         self.name = name | ||||||
|  |         self.module = module | ||||||
|  |         self._parse_extension_module() | ||||||
|  |  | ||||||
|  |     def _parse_extension_module(self): | ||||||
|  |         self.manager_class = None | ||||||
|  |         for attr_name, attr_value in self.module.__dict__.items(): | ||||||
|  |             if attr_name in self.SUPPORTED_HOOKS: | ||||||
|  |                 self.add_hook(attr_name, attr_value) | ||||||
|  |             else: | ||||||
|  |                 try: | ||||||
|  |                     if issubclass(attr_value, BaseManager): | ||||||
|  |                         self.manager_class = attr_value | ||||||
|  |                 except TypeError: | ||||||
|  |                     pass | ||||||
|  |  | ||||||
|  |     def __repr__(self): | ||||||
|  |         return "<Extension '%s'>" % self.name | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Resource(object): | ||||||
|  |     """Base class for OpenStack resources (tenant, user, etc.). | ||||||
|  |  | ||||||
|  |     This is pretty much just a bag for attributes. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     HUMAN_ID = False | ||||||
|  |     NAME_ATTR = 'name' | ||||||
|  |  | ||||||
|  |     def __init__(self, manager, info, loaded=False): | ||||||
|  |         """Populate and bind to a manager. | ||||||
|  |  | ||||||
|  |         :param manager: BaseManager object | ||||||
|  |         :param info: dictionary representing resource attributes | ||||||
|  |         :param loaded: prevent lazy-loading if set to True | ||||||
|  |         """ | ||||||
|  |         self.manager = manager | ||||||
|  |         self._info = info | ||||||
|  |         self._add_details(info) | ||||||
|  |         self._loaded = loaded | ||||||
|  |  | ||||||
|  |     def __repr__(self): | ||||||
|  |         reprkeys = sorted(k | ||||||
|  |                           for k in self.__dict__.keys() | ||||||
|  |                           if k[0] != '_' and k != 'manager') | ||||||
|  |         info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) | ||||||
|  |         return "<%s %s>" % (self.__class__.__name__, info) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def human_id(self): | ||||||
|  |         """Human-readable ID which can be used for bash completion. | ||||||
|  |         """ | ||||||
|  |         if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: | ||||||
|  |             return strutils.to_slug(getattr(self, self.NAME_ATTR)) | ||||||
|  |         return None | ||||||
|  |  | ||||||
|  |     def _add_details(self, info): | ||||||
|  |         for (k, v) in info.iteritems(): | ||||||
|  |             try: | ||||||
|  |                 setattr(self, k, v) | ||||||
|  |                 self._info[k] = v | ||||||
|  |             except AttributeError: | ||||||
|  |                 # In this case we already defined the attribute on the class | ||||||
|  |                 pass | ||||||
|  |  | ||||||
|  |     def __getattr__(self, k): | ||||||
|  |         if k not in self.__dict__: | ||||||
|  |             #NOTE(bcwaldon): disallow lazy-loading if already loaded once | ||||||
|  |             if not self.is_loaded(): | ||||||
|  |                 self.get() | ||||||
|  |                 return self.__getattr__(k) | ||||||
|  |  | ||||||
|  |             raise AttributeError(k) | ||||||
|  |         else: | ||||||
|  |             return self.__dict__[k] | ||||||
|  |  | ||||||
|  |     def get(self): | ||||||
|  |         # set_loaded() first ... so if we have to bail, we know we tried. | ||||||
|  |         self.set_loaded(True) | ||||||
|  |         if not hasattr(self.manager, 'get'): | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         new = self.manager.get(self.id) | ||||||
|  |         if new: | ||||||
|  |             self._add_details(new._info) | ||||||
|  |  | ||||||
|  |     def __eq__(self, other): | ||||||
|  |         if not isinstance(other, Resource): | ||||||
|  |             return NotImplemented | ||||||
|  |         # two resources of different types are not equal | ||||||
|  |         if not isinstance(other, self.__class__): | ||||||
|  |             return False | ||||||
|  |         if hasattr(self, 'id') and hasattr(other, 'id'): | ||||||
|  |             return self.id == other.id | ||||||
|  |         return self._info == other._info | ||||||
|  |  | ||||||
|  |     def is_loaded(self): | ||||||
|  |         return self._loaded | ||||||
|  |  | ||||||
|  |     def set_loaded(self, val): | ||||||
|  |         self._loaded = val | ||||||
							
								
								
									
										358
									
								
								ceilometerclient/openstack/common/apiclient/client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										358
									
								
								ceilometerclient/openstack/common/apiclient/client.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,358 @@ | |||||||
|  | # Copyright 2010 Jacob Kaplan-Moss | ||||||
|  | # Copyright 2011 OpenStack Foundation | ||||||
|  | # Copyright 2011 Piston Cloud Computing, Inc. | ||||||
|  | # Copyright 2013 Alessio Ababilov | ||||||
|  | # Copyright 2013 Grid Dynamics | ||||||
|  | # Copyright 2013 OpenStack Foundation | ||||||
|  | # 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. | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | OpenStack Client interface. Handles the REST calls and responses. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | # E0202: An attribute inherited from %s hide this method | ||||||
|  | # pylint: disable=E0202 | ||||||
|  |  | ||||||
|  | import logging | ||||||
|  | import time | ||||||
|  |  | ||||||
|  | try: | ||||||
|  |     import simplejson as json | ||||||
|  | except ImportError: | ||||||
|  |     import json | ||||||
|  |  | ||||||
|  | import requests | ||||||
|  |  | ||||||
|  | from ceilometerclient.openstack.common.apiclient import exceptions | ||||||
|  | from ceilometerclient.openstack.common import importutils | ||||||
|  |  | ||||||
|  |  | ||||||
|  | _logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPClient(object): | ||||||
|  |     """This client handles sending HTTP requests to OpenStack servers. | ||||||
|  |  | ||||||
|  |     Features: | ||||||
|  |     - share authentication information between several clients to different | ||||||
|  |       services (e.g., for compute and image clients); | ||||||
|  |     - reissue authentication request for expired tokens; | ||||||
|  |     - encode/decode JSON bodies; | ||||||
|  |     - raise exceptions on HTTP errors; | ||||||
|  |     - pluggable authentication; | ||||||
|  |     - store authentication information in a keyring; | ||||||
|  |     - store time spent for requests; | ||||||
|  |     - register clients for particular services, so one can use | ||||||
|  |       `http_client.identity` or `http_client.compute`; | ||||||
|  |     - log requests and responses in a format that is easy to copy-and-paste | ||||||
|  |       into terminal and send the same request with curl. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     user_agent = "ceilometerclient.openstack.common.apiclient" | ||||||
|  |  | ||||||
|  |     def __init__(self, | ||||||
|  |                  auth_plugin, | ||||||
|  |                  region_name=None, | ||||||
|  |                  endpoint_type="publicURL", | ||||||
|  |                  original_ip=None, | ||||||
|  |                  verify=True, | ||||||
|  |                  cert=None, | ||||||
|  |                  timeout=None, | ||||||
|  |                  timings=False, | ||||||
|  |                  keyring_saver=None, | ||||||
|  |                  debug=False, | ||||||
|  |                  user_agent=None, | ||||||
|  |                  http=None): | ||||||
|  |         self.auth_plugin = auth_plugin | ||||||
|  |  | ||||||
|  |         self.endpoint_type = endpoint_type | ||||||
|  |         self.region_name = region_name | ||||||
|  |  | ||||||
|  |         self.original_ip = original_ip | ||||||
|  |         self.timeout = timeout | ||||||
|  |         self.verify = verify | ||||||
|  |         self.cert = cert | ||||||
|  |  | ||||||
|  |         self.keyring_saver = keyring_saver | ||||||
|  |         self.debug = debug | ||||||
|  |         self.user_agent = user_agent or self.user_agent | ||||||
|  |  | ||||||
|  |         self.times = []  # [("item", starttime, endtime), ...] | ||||||
|  |         self.timings = timings | ||||||
|  |  | ||||||
|  |         # requests within the same session can reuse TCP connections from pool | ||||||
|  |         self.http = http or requests.Session() | ||||||
|  |  | ||||||
|  |         self.cached_token = None | ||||||
|  |  | ||||||
|  |     def _http_log_req(self, method, url, kwargs): | ||||||
|  |         if not self.debug: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         string_parts = [ | ||||||
|  |             "curl -i", | ||||||
|  |             "-X '%s'" % method, | ||||||
|  |             "'%s'" % url, | ||||||
|  |         ] | ||||||
|  |  | ||||||
|  |         for element in kwargs['headers']: | ||||||
|  |             header = "-H '%s: %s'" % (element, kwargs['headers'][element]) | ||||||
|  |             string_parts.append(header) | ||||||
|  |  | ||||||
|  |         _logger.debug("REQ: %s" % " ".join(string_parts)) | ||||||
|  |         if 'data' in kwargs: | ||||||
|  |             _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) | ||||||
|  |  | ||||||
|  |     def _http_log_resp(self, resp): | ||||||
|  |         if not self.debug: | ||||||
|  |             return | ||||||
|  |         _logger.debug( | ||||||
|  |             "RESP: [%s] %s\n", | ||||||
|  |             resp.status_code, | ||||||
|  |             resp.headers) | ||||||
|  |         if resp._content_consumed: | ||||||
|  |             _logger.debug( | ||||||
|  |                 "RESP BODY: %s\n", | ||||||
|  |                 resp.text) | ||||||
|  |  | ||||||
|  |     def serialize(self, kwargs): | ||||||
|  |         if kwargs.get('json') is not None: | ||||||
|  |             kwargs['headers']['Content-Type'] = 'application/json' | ||||||
|  |             kwargs['data'] = json.dumps(kwargs['json']) | ||||||
|  |         try: | ||||||
|  |             del kwargs['json'] | ||||||
|  |         except KeyError: | ||||||
|  |             pass | ||||||
|  |  | ||||||
|  |     def get_timings(self): | ||||||
|  |         return self.times | ||||||
|  |  | ||||||
|  |     def reset_timings(self): | ||||||
|  |         self.times = [] | ||||||
|  |  | ||||||
|  |     def request(self, method, url, **kwargs): | ||||||
|  |         """Send an http request with the specified characteristics. | ||||||
|  |  | ||||||
|  |         Wrapper around `requests.Session.request` to handle tasks such as | ||||||
|  |         setting headers, JSON encoding/decoding, and error handling. | ||||||
|  |  | ||||||
|  |         :param method: method of HTTP request | ||||||
|  |         :param url: URL of HTTP request | ||||||
|  |         :param kwargs: any other parameter that can be passed to | ||||||
|  | '            requests.Session.request (such as `headers`) or `json` | ||||||
|  |              that will be encoded as JSON and used as `data` argument | ||||||
|  |         """ | ||||||
|  |         kwargs.setdefault("headers", kwargs.get("headers", {})) | ||||||
|  |         kwargs["headers"]["User-Agent"] = self.user_agent | ||||||
|  |         if self.original_ip: | ||||||
|  |             kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( | ||||||
|  |                 self.original_ip, self.user_agent) | ||||||
|  |         if self.timeout is not None: | ||||||
|  |             kwargs.setdefault("timeout", self.timeout) | ||||||
|  |         kwargs.setdefault("verify", self.verify) | ||||||
|  |         if self.cert is not None: | ||||||
|  |             kwargs.setdefault("cert", self.cert) | ||||||
|  |         self.serialize(kwargs) | ||||||
|  |  | ||||||
|  |         self._http_log_req(method, url, kwargs) | ||||||
|  |         if self.timings: | ||||||
|  |             start_time = time.time() | ||||||
|  |         resp = self.http.request(method, url, **kwargs) | ||||||
|  |         if self.timings: | ||||||
|  |             self.times.append(("%s %s" % (method, url), | ||||||
|  |                                start_time, time.time())) | ||||||
|  |         self._http_log_resp(resp) | ||||||
|  |  | ||||||
|  |         if resp.status_code >= 400: | ||||||
|  |             _logger.debug( | ||||||
|  |                 "Request returned failure status: %s", | ||||||
|  |                 resp.status_code) | ||||||
|  |             raise exceptions.from_response(resp, method, url) | ||||||
|  |  | ||||||
|  |         return resp | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def concat_url(endpoint, url): | ||||||
|  |         """Concatenate endpoint and final URL. | ||||||
|  |  | ||||||
|  |         E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to | ||||||
|  |         "http://keystone/v2.0/tokens". | ||||||
|  |  | ||||||
|  |         :param endpoint: the base URL | ||||||
|  |         :param url: the final URL | ||||||
|  |         """ | ||||||
|  |         return "%s/%s" % (endpoint.rstrip("/"), url.strip("/")) | ||||||
|  |  | ||||||
|  |     def client_request(self, client, method, url, **kwargs): | ||||||
|  |         """Send an http request using `client`'s endpoint and specified `url`. | ||||||
|  |  | ||||||
|  |         If request was rejected as unauthorized (possibly because the token is | ||||||
|  |         expired), issue one authorization attempt and send the request once | ||||||
|  |         again. | ||||||
|  |  | ||||||
|  |         :param client: instance of BaseClient descendant | ||||||
|  |         :param method: method of HTTP request | ||||||
|  |         :param url: URL of HTTP request | ||||||
|  |         :param kwargs: any other parameter that can be passed to | ||||||
|  | '            `HTTPClient.request` | ||||||
|  |         """ | ||||||
|  |  | ||||||
|  |         filter_args = { | ||||||
|  |             "endpoint_type": client.endpoint_type or self.endpoint_type, | ||||||
|  |             "service_type": client.service_type, | ||||||
|  |         } | ||||||
|  |         token, endpoint = (self.cached_token, client.cached_endpoint) | ||||||
|  |         just_authenticated = False | ||||||
|  |         if not (token and endpoint): | ||||||
|  |             try: | ||||||
|  |                 token, endpoint = self.auth_plugin.token_and_endpoint( | ||||||
|  |                     **filter_args) | ||||||
|  |             except exceptions.EndpointException: | ||||||
|  |                 pass | ||||||
|  |             if not (token and endpoint): | ||||||
|  |                 self.authenticate() | ||||||
|  |                 just_authenticated = True | ||||||
|  |                 token, endpoint = self.auth_plugin.token_and_endpoint( | ||||||
|  |                     **filter_args) | ||||||
|  |                 if not (token and endpoint): | ||||||
|  |                     raise exceptions.AuthorizationFailure( | ||||||
|  |                         "Cannot find endpoint or token for request") | ||||||
|  |  | ||||||
|  |         old_token_endpoint = (token, endpoint) | ||||||
|  |         kwargs.setdefault("headers", {})["X-Auth-Token"] = token | ||||||
|  |         self.cached_token = token | ||||||
|  |         client.cached_endpoint = endpoint | ||||||
|  |         # Perform the request once. If we get Unauthorized, then it | ||||||
|  |         # might be because the auth token expired, so try to | ||||||
|  |         # re-authenticate and try again. If it still fails, bail. | ||||||
|  |         try: | ||||||
|  |             return self.request( | ||||||
|  |                 method, self.concat_url(endpoint, url), **kwargs) | ||||||
|  |         except exceptions.Unauthorized as unauth_ex: | ||||||
|  |             if just_authenticated: | ||||||
|  |                 raise | ||||||
|  |             self.cached_token = None | ||||||
|  |             client.cached_endpoint = None | ||||||
|  |             self.authenticate() | ||||||
|  |             try: | ||||||
|  |                 token, endpoint = self.auth_plugin.token_and_endpoint( | ||||||
|  |                     **filter_args) | ||||||
|  |             except exceptions.EndpointException: | ||||||
|  |                 raise unauth_ex | ||||||
|  |             if (not (token and endpoint) or | ||||||
|  |                     old_token_endpoint == (token, endpoint)): | ||||||
|  |                 raise unauth_ex | ||||||
|  |             self.cached_token = token | ||||||
|  |             client.cached_endpoint = endpoint | ||||||
|  |             kwargs["headers"]["X-Auth-Token"] = token | ||||||
|  |             return self.request( | ||||||
|  |                 method, self.concat_url(endpoint, url), **kwargs) | ||||||
|  |  | ||||||
|  |     def add_client(self, base_client_instance): | ||||||
|  |         """Add a new instance of :class:`BaseClient` descendant. | ||||||
|  |  | ||||||
|  |         `self` will store a reference to `base_client_instance`. | ||||||
|  |  | ||||||
|  |         Example: | ||||||
|  |  | ||||||
|  |         >>> def test_clients(): | ||||||
|  |         ...     from keystoneclient.auth import keystone | ||||||
|  |         ...     from openstack.common.apiclient import client | ||||||
|  |         ...     auth = keystone.KeystoneAuthPlugin( | ||||||
|  |         ...         username="user", password="pass", tenant_name="tenant", | ||||||
|  |         ...         auth_url="http://auth:5000/v2.0") | ||||||
|  |         ...     openstack_client = client.HTTPClient(auth) | ||||||
|  |         ...     # create nova client | ||||||
|  |         ...     from novaclient.v1_1 import client | ||||||
|  |         ...     client.Client(openstack_client) | ||||||
|  |         ...     # create keystone client | ||||||
|  |         ...     from keystoneclient.v2_0 import client | ||||||
|  |         ...     client.Client(openstack_client) | ||||||
|  |         ...     # use them | ||||||
|  |         ...     openstack_client.identity.tenants.list() | ||||||
|  |         ...     openstack_client.compute.servers.list() | ||||||
|  |         """ | ||||||
|  |         service_type = base_client_instance.service_type | ||||||
|  |         if service_type and not hasattr(self, service_type): | ||||||
|  |             setattr(self, service_type, base_client_instance) | ||||||
|  |  | ||||||
|  |     def authenticate(self): | ||||||
|  |         self.auth_plugin.authenticate(self) | ||||||
|  |         # Store the authentication results in the keyring for later requests | ||||||
|  |         if self.keyring_saver: | ||||||
|  |             self.keyring_saver.save(self) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BaseClient(object): | ||||||
|  |     """Top-level object to access the OpenStack API. | ||||||
|  |  | ||||||
|  |     This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient` | ||||||
|  |     will handle a bunch of issues such as authentication. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     service_type = None | ||||||
|  |     endpoint_type = None  # "publicURL" will be used | ||||||
|  |     cached_endpoint = None | ||||||
|  |  | ||||||
|  |     def __init__(self, http_client, extensions=None): | ||||||
|  |         self.http_client = http_client | ||||||
|  |         http_client.add_client(self) | ||||||
|  |  | ||||||
|  |         # Add in any extensions... | ||||||
|  |         if extensions: | ||||||
|  |             for extension in extensions: | ||||||
|  |                 if extension.manager_class: | ||||||
|  |                     setattr(self, extension.name, | ||||||
|  |                             extension.manager_class(self)) | ||||||
|  |  | ||||||
|  |     def client_request(self, method, url, **kwargs): | ||||||
|  |         return self.http_client.client_request( | ||||||
|  |             self, method, url, **kwargs) | ||||||
|  |  | ||||||
|  |     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.client_request("DELETE", url, **kwargs) | ||||||
|  |  | ||||||
|  |     def patch(self, url, **kwargs): | ||||||
|  |         return self.client_request("PATCH", url, **kwargs) | ||||||
|  |  | ||||||
|  |     @staticmethod | ||||||
|  |     def get_class(api_name, version, version_map): | ||||||
|  |         """Returns the client class for the requested API version | ||||||
|  |  | ||||||
|  |         :param api_name: the name of the API, e.g. 'compute', 'image', etc | ||||||
|  |         :param version: the requested API version | ||||||
|  |         :param version_map: a dict of client classes keyed by version | ||||||
|  |         :rtype: a client class for the requested API version | ||||||
|  |         """ | ||||||
|  |         try: | ||||||
|  |             client_path = version_map[str(version)] | ||||||
|  |         except (KeyError, ValueError): | ||||||
|  |             msg = "Invalid %s client version '%s'. must be one of: %s" % ( | ||||||
|  |                   (api_name, version, ', '.join(version_map.keys()))) | ||||||
|  |             raise exceptions.UnsupportedVersion(msg) | ||||||
|  |  | ||||||
|  |         return importutils.import_class(client_path) | ||||||
							
								
								
									
										439
									
								
								ceilometerclient/openstack/common/apiclient/exceptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										439
									
								
								ceilometerclient/openstack/common/apiclient/exceptions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,439 @@ | |||||||
|  | # Copyright 2010 Jacob Kaplan-Moss | ||||||
|  | # Copyright 2011 Nebula, Inc. | ||||||
|  | # Copyright 2013 Alessio Ababilov | ||||||
|  | # Copyright 2013 OpenStack Foundation | ||||||
|  | # 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. | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | Exception definitions. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import inspect | ||||||
|  | import sys | ||||||
|  |  | ||||||
|  | import six | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ClientException(Exception): | ||||||
|  |     """The base exception class for all exceptions this library raises. | ||||||
|  |     """ | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MissingArgs(ClientException): | ||||||
|  |     """Supplied arguments are not sufficient for calling a function.""" | ||||||
|  |     def __init__(self, missing): | ||||||
|  |         self.missing = missing | ||||||
|  |         msg = "Missing argument(s): %s" % ", ".join(missing) | ||||||
|  |         super(MissingArgs, self).__init__(msg) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ValidationError(ClientException): | ||||||
|  |     """Error in validation on API client side.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UnsupportedVersion(ClientException): | ||||||
|  |     """User is trying to use an unsupported version of the API.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class CommandError(ClientException): | ||||||
|  |     """Error in CLI tool.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AuthorizationFailure(ClientException): | ||||||
|  |     """Cannot authorize API client.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AuthPluginOptionsMissing(AuthorizationFailure): | ||||||
|  |     """Auth plugin misses some options.""" | ||||||
|  |     def __init__(self, opt_names): | ||||||
|  |         super(AuthPluginOptionsMissing, self).__init__( | ||||||
|  |             "Authentication failed. Missing options: %s" % | ||||||
|  |             ", ".join(opt_names)) | ||||||
|  |         self.opt_names = opt_names | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AuthSystemNotFound(AuthorizationFailure): | ||||||
|  |     """User has specified a AuthSystem that is not installed.""" | ||||||
|  |     def __init__(self, auth_system): | ||||||
|  |         super(AuthSystemNotFound, self).__init__( | ||||||
|  |             "AuthSystemNotFound: %s" % repr(auth_system)) | ||||||
|  |         self.auth_system = auth_system | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class NoUniqueMatch(ClientException): | ||||||
|  |     """Multiple entities found instead of one.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EndpointException(ClientException): | ||||||
|  |     """Something is rotten in Service Catalog.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EndpointNotFound(EndpointException): | ||||||
|  |     """Could not find requested endpoint in Service Catalog.""" | ||||||
|  |     pass | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class AmbiguousEndpoints(EndpointException): | ||||||
|  |     """Found more than one matching endpoint in Service Catalog.""" | ||||||
|  |     def __init__(self, endpoints=None): | ||||||
|  |         super(AmbiguousEndpoints, self).__init__( | ||||||
|  |             "AmbiguousEndpoints: %s" % repr(endpoints)) | ||||||
|  |         self.endpoints = endpoints | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HttpError(ClientException): | ||||||
|  |     """The base exception class for all HTTP exceptions. | ||||||
|  |     """ | ||||||
|  |     http_status = 0 | ||||||
|  |     message = "HTTP Error" | ||||||
|  |  | ||||||
|  |     def __init__(self, message=None, details=None, | ||||||
|  |                  response=None, request_id=None, | ||||||
|  |                  url=None, method=None, http_status=None): | ||||||
|  |         self.http_status = http_status or self.http_status | ||||||
|  |         self.message = message or self.message | ||||||
|  |         self.details = details | ||||||
|  |         self.request_id = request_id | ||||||
|  |         self.response = response | ||||||
|  |         self.url = url | ||||||
|  |         self.method = method | ||||||
|  |         formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) | ||||||
|  |         if request_id: | ||||||
|  |             formatted_string += " (Request-ID: %s)" % request_id | ||||||
|  |         super(HttpError, self).__init__(formatted_string) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPClientError(HttpError): | ||||||
|  |     """Client-side HTTP error. | ||||||
|  |  | ||||||
|  |     Exception for cases in which the client seems to have erred. | ||||||
|  |     """ | ||||||
|  |     message = "HTTP Client Error" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HttpServerError(HttpError): | ||||||
|  |     """Server-side HTTP error. | ||||||
|  |  | ||||||
|  |     Exception for cases in which the server is aware that it has | ||||||
|  |     erred or is incapable of performing the request. | ||||||
|  |     """ | ||||||
|  |     message = "HTTP Server Error" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BadRequest(HTTPClientError): | ||||||
|  |     """HTTP 400 - Bad Request. | ||||||
|  |  | ||||||
|  |     The request cannot be fulfilled due to bad syntax. | ||||||
|  |     """ | ||||||
|  |     http_status = 400 | ||||||
|  |     message = "Bad Request" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Unauthorized(HTTPClientError): | ||||||
|  |     """HTTP 401 - Unauthorized. | ||||||
|  |  | ||||||
|  |     Similar to 403 Forbidden, but specifically for use when authentication | ||||||
|  |     is required and has failed or has not yet been provided. | ||||||
|  |     """ | ||||||
|  |     http_status = 401 | ||||||
|  |     message = "Unauthorized" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PaymentRequired(HTTPClientError): | ||||||
|  |     """HTTP 402 - Payment Required. | ||||||
|  |  | ||||||
|  |     Reserved for future use. | ||||||
|  |     """ | ||||||
|  |     http_status = 402 | ||||||
|  |     message = "Payment Required" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Forbidden(HTTPClientError): | ||||||
|  |     """HTTP 403 - Forbidden. | ||||||
|  |  | ||||||
|  |     The request was a valid request, but the server is refusing to respond | ||||||
|  |     to it. | ||||||
|  |     """ | ||||||
|  |     http_status = 403 | ||||||
|  |     message = "Forbidden" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class NotFound(HTTPClientError): | ||||||
|  |     """HTTP 404 - Not Found. | ||||||
|  |  | ||||||
|  |     The requested resource could not be found but may be available again | ||||||
|  |     in the future. | ||||||
|  |     """ | ||||||
|  |     http_status = 404 | ||||||
|  |     message = "Not Found" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class MethodNotAllowed(HTTPClientError): | ||||||
|  |     """HTTP 405 - Method Not Allowed. | ||||||
|  |  | ||||||
|  |     A request was made of a resource using a request method not supported | ||||||
|  |     by that resource. | ||||||
|  |     """ | ||||||
|  |     http_status = 405 | ||||||
|  |     message = "Method Not Allowed" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class NotAcceptable(HTTPClientError): | ||||||
|  |     """HTTP 406 - Not Acceptable. | ||||||
|  |  | ||||||
|  |     The requested resource is only capable of generating content not | ||||||
|  |     acceptable according to the Accept headers sent in the request. | ||||||
|  |     """ | ||||||
|  |     http_status = 406 | ||||||
|  |     message = "Not Acceptable" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ProxyAuthenticationRequired(HTTPClientError): | ||||||
|  |     """HTTP 407 - Proxy Authentication Required. | ||||||
|  |  | ||||||
|  |     The client must first authenticate itself with the proxy. | ||||||
|  |     """ | ||||||
|  |     http_status = 407 | ||||||
|  |     message = "Proxy Authentication Required" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestTimeout(HTTPClientError): | ||||||
|  |     """HTTP 408 - Request Timeout. | ||||||
|  |  | ||||||
|  |     The server timed out waiting for the request. | ||||||
|  |     """ | ||||||
|  |     http_status = 408 | ||||||
|  |     message = "Request Timeout" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Conflict(HTTPClientError): | ||||||
|  |     """HTTP 409 - Conflict. | ||||||
|  |  | ||||||
|  |     Indicates that the request could not be processed because of conflict | ||||||
|  |     in the request, such as an edit conflict. | ||||||
|  |     """ | ||||||
|  |     http_status = 409 | ||||||
|  |     message = "Conflict" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Gone(HTTPClientError): | ||||||
|  |     """HTTP 410 - Gone. | ||||||
|  |  | ||||||
|  |     Indicates that the resource requested is no longer available and will | ||||||
|  |     not be available again. | ||||||
|  |     """ | ||||||
|  |     http_status = 410 | ||||||
|  |     message = "Gone" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LengthRequired(HTTPClientError): | ||||||
|  |     """HTTP 411 - Length Required. | ||||||
|  |  | ||||||
|  |     The request did not specify the length of its content, which is | ||||||
|  |     required by the requested resource. | ||||||
|  |     """ | ||||||
|  |     http_status = 411 | ||||||
|  |     message = "Length Required" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class PreconditionFailed(HTTPClientError): | ||||||
|  |     """HTTP 412 - Precondition Failed. | ||||||
|  |  | ||||||
|  |     The server does not meet one of the preconditions that the requester | ||||||
|  |     put on the request. | ||||||
|  |     """ | ||||||
|  |     http_status = 412 | ||||||
|  |     message = "Precondition Failed" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestEntityTooLarge(HTTPClientError): | ||||||
|  |     """HTTP 413 - Request Entity Too Large. | ||||||
|  |  | ||||||
|  |     The request is larger than the server is willing or able to process. | ||||||
|  |     """ | ||||||
|  |     http_status = 413 | ||||||
|  |     message = "Request Entity Too Large" | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         try: | ||||||
|  |             self.retry_after = int(kwargs.pop('retry_after')) | ||||||
|  |         except (KeyError, ValueError): | ||||||
|  |             self.retry_after = 0 | ||||||
|  |  | ||||||
|  |         super(RequestEntityTooLarge, self).__init__(*args, **kwargs) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestUriTooLong(HTTPClientError): | ||||||
|  |     """HTTP 414 - Request-URI Too Long. | ||||||
|  |  | ||||||
|  |     The URI provided was too long for the server to process. | ||||||
|  |     """ | ||||||
|  |     http_status = 414 | ||||||
|  |     message = "Request-URI Too Long" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UnsupportedMediaType(HTTPClientError): | ||||||
|  |     """HTTP 415 - Unsupported Media Type. | ||||||
|  |  | ||||||
|  |     The request entity has a media type which the server or resource does | ||||||
|  |     not support. | ||||||
|  |     """ | ||||||
|  |     http_status = 415 | ||||||
|  |     message = "Unsupported Media Type" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class RequestedRangeNotSatisfiable(HTTPClientError): | ||||||
|  |     """HTTP 416 - Requested Range Not Satisfiable. | ||||||
|  |  | ||||||
|  |     The client has asked for a portion of the file, but the server cannot | ||||||
|  |     supply that portion. | ||||||
|  |     """ | ||||||
|  |     http_status = 416 | ||||||
|  |     message = "Requested Range Not Satisfiable" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ExpectationFailed(HTTPClientError): | ||||||
|  |     """HTTP 417 - Expectation Failed. | ||||||
|  |  | ||||||
|  |     The server cannot meet the requirements of the Expect request-header field. | ||||||
|  |     """ | ||||||
|  |     http_status = 417 | ||||||
|  |     message = "Expectation Failed" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class UnprocessableEntity(HTTPClientError): | ||||||
|  |     """HTTP 422 - Unprocessable Entity. | ||||||
|  |  | ||||||
|  |     The request was well-formed but was unable to be followed due to semantic | ||||||
|  |     errors. | ||||||
|  |     """ | ||||||
|  |     http_status = 422 | ||||||
|  |     message = "Unprocessable Entity" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class InternalServerError(HttpServerError): | ||||||
|  |     """HTTP 500 - Internal Server Error. | ||||||
|  |  | ||||||
|  |     A generic error message, given when no more specific message is suitable. | ||||||
|  |     """ | ||||||
|  |     http_status = 500 | ||||||
|  |     message = "Internal Server Error" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # NotImplemented is a python keyword. | ||||||
|  | class HttpNotImplemented(HttpServerError): | ||||||
|  |     """HTTP 501 - Not Implemented. | ||||||
|  |  | ||||||
|  |     The server either does not recognize the request method, or it lacks | ||||||
|  |     the ability to fulfill the request. | ||||||
|  |     """ | ||||||
|  |     http_status = 501 | ||||||
|  |     message = "Not Implemented" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class BadGateway(HttpServerError): | ||||||
|  |     """HTTP 502 - Bad Gateway. | ||||||
|  |  | ||||||
|  |     The server was acting as a gateway or proxy and received an invalid | ||||||
|  |     response from the upstream server. | ||||||
|  |     """ | ||||||
|  |     http_status = 502 | ||||||
|  |     message = "Bad Gateway" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ServiceUnavailable(HttpServerError): | ||||||
|  |     """HTTP 503 - Service Unavailable. | ||||||
|  |  | ||||||
|  |     The server is currently unavailable. | ||||||
|  |     """ | ||||||
|  |     http_status = 503 | ||||||
|  |     message = "Service Unavailable" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class GatewayTimeout(HttpServerError): | ||||||
|  |     """HTTP 504 - Gateway Timeout. | ||||||
|  |  | ||||||
|  |     The server was acting as a gateway or proxy and did not receive a timely | ||||||
|  |     response from the upstream server. | ||||||
|  |     """ | ||||||
|  |     http_status = 504 | ||||||
|  |     message = "Gateway Timeout" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HttpVersionNotSupported(HttpServerError): | ||||||
|  |     """HTTP 505 - HttpVersion Not Supported. | ||||||
|  |  | ||||||
|  |     The server does not support the HTTP protocol version used in the request. | ||||||
|  |     """ | ||||||
|  |     http_status = 505 | ||||||
|  |     message = "HTTP Version Not Supported" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # _code_map contains all the classes that have http_status attribute. | ||||||
|  | _code_map = dict( | ||||||
|  |     (getattr(obj, 'http_status', None), obj) | ||||||
|  |     for name, obj in six.iteritems(vars(sys.modules[__name__])) | ||||||
|  |     if inspect.isclass(obj) and getattr(obj, 'http_status', False) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def from_response(response, method, url): | ||||||
|  |     """Returns an instance of :class:`HttpError` or subclass based on response. | ||||||
|  |  | ||||||
|  |     :param response: instance of `requests.Response` class | ||||||
|  |     :param method: HTTP method used for request | ||||||
|  |     :param url: URL used for request | ||||||
|  |     """ | ||||||
|  |     kwargs = { | ||||||
|  |         "http_status": response.status_code, | ||||||
|  |         "response": response, | ||||||
|  |         "method": method, | ||||||
|  |         "url": url, | ||||||
|  |         "request_id": response.headers.get("x-compute-request-id"), | ||||||
|  |     } | ||||||
|  |     if "retry-after" in response.headers: | ||||||
|  |         kwargs["retry_after"] = response.headers["retry-after"] | ||||||
|  |  | ||||||
|  |     content_type = response.headers.get("Content-Type", "") | ||||||
|  |     if content_type.startswith("application/json"): | ||||||
|  |         try: | ||||||
|  |             body = response.json() | ||||||
|  |         except ValueError: | ||||||
|  |             pass | ||||||
|  |         else: | ||||||
|  |             if hasattr(body, "keys"): | ||||||
|  |                 error = body[body.keys()[0]] | ||||||
|  |                 kwargs["message"] = error.get("message", None) | ||||||
|  |                 kwargs["details"] = error.get("details", None) | ||||||
|  |     elif content_type.startswith("text/"): | ||||||
|  |         kwargs["details"] = response.text | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         cls = _code_map[response.status_code] | ||||||
|  |     except KeyError: | ||||||
|  |         if 500 <= response.status_code < 600: | ||||||
|  |             cls = HttpServerError | ||||||
|  |         elif 400 <= response.status_code < 500: | ||||||
|  |             cls = HTTPClientError | ||||||
|  |         else: | ||||||
|  |             cls = HttpError | ||||||
|  |     return cls(**kwargs) | ||||||
							
								
								
									
										170
									
								
								ceilometerclient/openstack/common/apiclient/fake_client.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										170
									
								
								ceilometerclient/openstack/common/apiclient/fake_client.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,170 @@ | |||||||
|  | # Copyright 2013 OpenStack Foundation | ||||||
|  | # 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. | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | A fake server that "responds" to API methods with pre-canned responses. | ||||||
|  |  | ||||||
|  | All of these responses come from the spec, so if for some reason the spec's | ||||||
|  | wrong the tests might raise AssertionError. I've indicated in comments the | ||||||
|  | places where actual behavior differs from the spec. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | # W0102: Dangerous default value %s as argument | ||||||
|  | # pylint: disable=W0102 | ||||||
|  |  | ||||||
|  | import json | ||||||
|  |  | ||||||
|  | import requests | ||||||
|  |  | ||||||
|  | from ceilometerclient.openstack.common.apiclient import client | ||||||
|  | from ceilometerclient.openstack.common.py3kcompat import urlutils | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def assert_has_keys(dct, required=[], optional=[]): | ||||||
|  |     for k in required: | ||||||
|  |         try: | ||||||
|  |             assert k in dct | ||||||
|  |         except AssertionError: | ||||||
|  |             extra_keys = set(dct.keys()).difference(set(required + optional)) | ||||||
|  |             raise AssertionError("found unexpected keys: %s" % | ||||||
|  |                                  list(extra_keys)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TestResponse(requests.Response): | ||||||
|  |     """Wrap requests.Response and provide a convenient initialization. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, data): | ||||||
|  |         super(TestResponse, self).__init__() | ||||||
|  |         self._content_consumed = True | ||||||
|  |         if isinstance(data, dict): | ||||||
|  |             self.status_code = data.get('status_code', 200) | ||||||
|  |             # Fake the text attribute to streamline Response creation | ||||||
|  |             text = data.get('text', "") | ||||||
|  |             if isinstance(text, (dict, list)): | ||||||
|  |                 self._content = json.dumps(text) | ||||||
|  |                 default_headers = { | ||||||
|  |                     "Content-Type": "application/json", | ||||||
|  |                 } | ||||||
|  |             else: | ||||||
|  |                 self._content = text | ||||||
|  |                 default_headers = {} | ||||||
|  |             self.headers = data.get('headers') or default_headers | ||||||
|  |         else: | ||||||
|  |             self.status_code = data | ||||||
|  |  | ||||||
|  |     def __eq__(self, other): | ||||||
|  |         return (self.status_code == other.status_code and | ||||||
|  |                 self.headers == other.headers and | ||||||
|  |                 self._content == other._content) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class FakeHTTPClient(client.HTTPClient): | ||||||
|  |  | ||||||
|  |     def __init__(self, *args, **kwargs): | ||||||
|  |         self.callstack = [] | ||||||
|  |         self.fixtures = kwargs.pop("fixtures", None) or {} | ||||||
|  |         if not args and not "auth_plugin" in kwargs: | ||||||
|  |             args = (None, ) | ||||||
|  |         super(FakeHTTPClient, self).__init__(*args, **kwargs) | ||||||
|  |  | ||||||
|  |     def assert_called(self, method, url, body=None, pos=-1): | ||||||
|  |         """Assert than an API method was just called. | ||||||
|  |         """ | ||||||
|  |         expected = (method, url) | ||||||
|  |         called = self.callstack[pos][0:2] | ||||||
|  |         assert self.callstack, \ | ||||||
|  |             "Expected %s %s but no calls were made." % expected | ||||||
|  |  | ||||||
|  |         assert expected == called, 'Expected %s %s; got %s %s' % \ | ||||||
|  |             (expected + called) | ||||||
|  |  | ||||||
|  |         if body is not None: | ||||||
|  |             if self.callstack[pos][3] != body: | ||||||
|  |                 raise AssertionError('%r != %r' % | ||||||
|  |                                      (self.callstack[pos][3], body)) | ||||||
|  |  | ||||||
|  |     def assert_called_anytime(self, method, url, body=None): | ||||||
|  |         """Assert than an API method was called anytime in the test. | ||||||
|  |         """ | ||||||
|  |         expected = (method, url) | ||||||
|  |  | ||||||
|  |         assert self.callstack, \ | ||||||
|  |             "Expected %s %s but no calls were made." % expected | ||||||
|  |  | ||||||
|  |         found = False | ||||||
|  |         entry = None | ||||||
|  |         for entry in self.callstack: | ||||||
|  |             if expected == entry[0:2]: | ||||||
|  |                 found = True | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert found, 'Expected %s %s; got %s' % \ | ||||||
|  |             (method, url, self.callstack) | ||||||
|  |         if body is not None: | ||||||
|  |             assert entry[3] == body, "%s != %s" % (entry[3], body) | ||||||
|  |  | ||||||
|  |         self.callstack = [] | ||||||
|  |  | ||||||
|  |     def clear_callstack(self): | ||||||
|  |         self.callstack = [] | ||||||
|  |  | ||||||
|  |     def authenticate(self): | ||||||
|  |         pass | ||||||
|  |  | ||||||
|  |     def client_request(self, client, method, url, **kwargs): | ||||||
|  |         # Check that certain things are called correctly | ||||||
|  |         if method in ["GET", "DELETE"]: | ||||||
|  |             assert "json" not in kwargs | ||||||
|  |  | ||||||
|  |         # Note the call | ||||||
|  |         self.callstack.append( | ||||||
|  |             (method, | ||||||
|  |              url, | ||||||
|  |              kwargs.get("headers") or {}, | ||||||
|  |              kwargs.get("json") or kwargs.get("data"))) | ||||||
|  |         try: | ||||||
|  |             fixture = self.fixtures[url][method] | ||||||
|  |         except KeyError: | ||||||
|  |             pass | ||||||
|  |         else: | ||||||
|  |             return TestResponse({"headers": fixture[0], | ||||||
|  |                                  "text": fixture[1]}) | ||||||
|  |  | ||||||
|  |         # Call the method | ||||||
|  |         args = urlutils.parse_qsl(urlutils.urlparse(url)[4]) | ||||||
|  |         kwargs.update(args) | ||||||
|  |         munged_url = url.rsplit('?', 1)[0] | ||||||
|  |         munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') | ||||||
|  |         munged_url = munged_url.replace('-', '_') | ||||||
|  |  | ||||||
|  |         callback = "%s_%s" % (method.lower(), munged_url) | ||||||
|  |  | ||||||
|  |         if not hasattr(self, callback): | ||||||
|  |             raise AssertionError('Called unknown API method: %s %s, ' | ||||||
|  |                                  'expected fakes method name: %s' % | ||||||
|  |                                  (method, url, callback)) | ||||||
|  |  | ||||||
|  |         resp = getattr(self, callback)(**kwargs) | ||||||
|  |         if len(resp) == 3: | ||||||
|  |             status, headers, body = resp | ||||||
|  |         else: | ||||||
|  |             status, body = resp | ||||||
|  |             headers = {} | ||||||
|  |         return TestResponse({ | ||||||
|  |             "status_code": status, | ||||||
|  |             "text": body, | ||||||
|  |             "headers": headers, | ||||||
|  |         }) | ||||||
							
								
								
									
										213
									
								
								ceilometerclient/openstack/common/cliutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										213
									
								
								ceilometerclient/openstack/common/cliutils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,213 @@ | |||||||
|  | # Copyright 2012 Red Hat, Inc. | ||||||
|  | # | ||||||
|  | #    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. | ||||||
|  |  | ||||||
|  | # W0603: Using the global statement | ||||||
|  | # W0621: Redefining name %s from outer scope | ||||||
|  | # pylint: disable=W0603,W0621 | ||||||
|  |  | ||||||
|  | import getpass | ||||||
|  | import inspect | ||||||
|  | import os | ||||||
|  | import sys | ||||||
|  | import textwrap | ||||||
|  |  | ||||||
|  | import prettytable | ||||||
|  | import six | ||||||
|  | from six import moves | ||||||
|  |  | ||||||
|  | from ceilometerclient.openstack.common.apiclient import exceptions | ||||||
|  | from ceilometerclient.openstack.common import strutils | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_args(fn, *args, **kwargs): | ||||||
|  |     """Check that the supplied args are sufficient for calling a function. | ||||||
|  |  | ||||||
|  |     >>> validate_args(lambda a: None) | ||||||
|  |     Traceback (most recent call last): | ||||||
|  |         ... | ||||||
|  |     MissingArgs: Missing argument(s): a | ||||||
|  |     >>> validate_args(lambda a, b, c, d: None, 0, c=1) | ||||||
|  |     Traceback (most recent call last): | ||||||
|  |         ... | ||||||
|  |     MissingArgs: Missing argument(s): b, d | ||||||
|  |  | ||||||
|  |     :param fn: the function to check | ||||||
|  |     :param arg: the positional arguments supplied | ||||||
|  |     :param kwargs: the keyword arguments supplied | ||||||
|  |     """ | ||||||
|  |     argspec = inspect.getargspec(fn) | ||||||
|  |  | ||||||
|  |     num_defaults = len(argspec.defaults or []) | ||||||
|  |     required_args = argspec.args[:len(argspec.args) - num_defaults] | ||||||
|  |  | ||||||
|  |     def isbound(method): | ||||||
|  |         return getattr(method, 'im_self', None) is not None | ||||||
|  |  | ||||||
|  |     if isbound(fn): | ||||||
|  |         required_args.pop(0) | ||||||
|  |  | ||||||
|  |     missing = [arg for arg in required_args if arg not in kwargs] | ||||||
|  |     missing = missing[len(args):] | ||||||
|  |     if missing: | ||||||
|  |         raise exceptions.MissingArgs(missing) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def arg(*args, **kwargs): | ||||||
|  |     """Decorator for CLI args. | ||||||
|  |  | ||||||
|  |     Example: | ||||||
|  |  | ||||||
|  |     >>> @arg("name", help="Name of the new entity") | ||||||
|  |     ... def entity_create(args): | ||||||
|  |     ...     pass | ||||||
|  |     """ | ||||||
|  |     def _decorator(func): | ||||||
|  |         add_arg(func, *args, **kwargs) | ||||||
|  |         return func | ||||||
|  |     return _decorator | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def env(*args, **kwargs): | ||||||
|  |     """Returns the first environment variable set. | ||||||
|  |  | ||||||
|  |     If all are empty, defaults to '' or keyword arg `default`. | ||||||
|  |     """ | ||||||
|  |     for arg in args: | ||||||
|  |         value = os.environ.get(arg, None) | ||||||
|  |         if value: | ||||||
|  |             return value | ||||||
|  |     return kwargs.get('default', '') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def add_arg(func, *args, **kwargs): | ||||||
|  |     """Bind CLI arguments to a shell.py `do_foo` function.""" | ||||||
|  |  | ||||||
|  |     if not hasattr(func, 'arguments'): | ||||||
|  |         func.arguments = [] | ||||||
|  |  | ||||||
|  |     # NOTE(sirp): avoid dups that can occur when the module is shared across | ||||||
|  |     # tests. | ||||||
|  |     if (args, kwargs) not in func.arguments: | ||||||
|  |         # Because of the semantics of decorator composition if we just append | ||||||
|  |         # to the options list positional options will appear to be backwards. | ||||||
|  |         func.arguments.insert(0, (args, kwargs)) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def unauthenticated(func): | ||||||
|  |     """Adds 'unauthenticated' attribute to decorated function. | ||||||
|  |  | ||||||
|  |     Usage: | ||||||
|  |  | ||||||
|  |     >>> @unauthenticated | ||||||
|  |     ... def mymethod(f): | ||||||
|  |     ...     pass | ||||||
|  |     """ | ||||||
|  |     func.unauthenticated = True | ||||||
|  |     return func | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def isunauthenticated(func): | ||||||
|  |     """Checks if the function does not require authentication. | ||||||
|  |  | ||||||
|  |     Mark such functions with the `@unauthenticated` decorator. | ||||||
|  |  | ||||||
|  |     :returns: bool | ||||||
|  |     """ | ||||||
|  |     return getattr(func, 'unauthenticated', False) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def print_list(objs, fields, formatters=None, sortby_index=0, | ||||||
|  |                mixed_case_fields=None): | ||||||
|  |     """Print a list or objects as a table, one row per object. | ||||||
|  |  | ||||||
|  |     :param objs: iterable of :class:`Resource` | ||||||
|  |     :param fields: attributes that correspond to columns, in order | ||||||
|  |     :param formatters: `dict` of callables for field formatting | ||||||
|  |     :param sortby_index: index of the field for sorting table rows | ||||||
|  |     :param mixed_case_fields: fields corresponding to object attributes that | ||||||
|  |         have mixed case names (e.g., 'serverId') | ||||||
|  |     """ | ||||||
|  |     formatters = formatters or {} | ||||||
|  |     mixed_case_fields = mixed_case_fields or [] | ||||||
|  |     if sortby_index is None: | ||||||
|  |         kwargs = {} | ||||||
|  |     else: | ||||||
|  |         kwargs = {'sortby': fields[sortby_index]} | ||||||
|  |     pt = prettytable.PrettyTable(fields, caching=False) | ||||||
|  |     pt.align = 'l' | ||||||
|  |  | ||||||
|  |     for o in objs: | ||||||
|  |         row = [] | ||||||
|  |         for field in fields: | ||||||
|  |             if field in formatters: | ||||||
|  |                 row.append(formatters[field](o)) | ||||||
|  |             else: | ||||||
|  |                 if field in mixed_case_fields: | ||||||
|  |                     field_name = field.replace(' ', '_') | ||||||
|  |                 else: | ||||||
|  |                     field_name = field.lower().replace(' ', '_') | ||||||
|  |                 data = getattr(o, field_name, '') | ||||||
|  |                 row.append(data) | ||||||
|  |         pt.add_row(row) | ||||||
|  |  | ||||||
|  |     print(strutils.safe_encode(pt.get_string(**kwargs))) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def print_dict(dct, dict_property="Property", wrap=0): | ||||||
|  |     """Print a `dict` as a table of two columns. | ||||||
|  |  | ||||||
|  |     :param dct: `dict` to print | ||||||
|  |     :param dict_property: name of the first column | ||||||
|  |     :param wrap: wrapping for the second column | ||||||
|  |     """ | ||||||
|  |     pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False) | ||||||
|  |     pt.align = 'l' | ||||||
|  |     for k, v in dct.iteritems(): | ||||||
|  |         # convert dict to str to check length | ||||||
|  |         if isinstance(v, dict): | ||||||
|  |             v = str(v) | ||||||
|  |         if wrap > 0: | ||||||
|  |             v = textwrap.fill(str(v), wrap) | ||||||
|  |         # if value has a newline, add in multiple rows | ||||||
|  |         # e.g. fault with stacktrace | ||||||
|  |         if v and isinstance(v, six.string_types) and r'\n' in v: | ||||||
|  |             lines = v.strip().split(r'\n') | ||||||
|  |             col1 = k | ||||||
|  |             for line in lines: | ||||||
|  |                 pt.add_row([col1, line]) | ||||||
|  |                 col1 = '' | ||||||
|  |         else: | ||||||
|  |             pt.add_row([k, v]) | ||||||
|  |     print(strutils.safe_encode(pt.get_string())) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_password(max_password_prompts=3): | ||||||
|  |     """Read password from TTY.""" | ||||||
|  |     verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD")) | ||||||
|  |     pw = None | ||||||
|  |     if hasattr(sys.stdin, "isatty") and sys.stdin.isatty(): | ||||||
|  |         # Check for Ctrl-D | ||||||
|  |         try: | ||||||
|  |             for _ in moves.range(max_password_prompts): | ||||||
|  |                 pw1 = getpass.getpass("OS Password: ") | ||||||
|  |                 if verify: | ||||||
|  |                     pw2 = getpass.getpass("Please verify: ") | ||||||
|  |                 else: | ||||||
|  |                     pw2 = pw1 | ||||||
|  |                 if pw1 == pw2 and pw1: | ||||||
|  |                     pw = pw1 | ||||||
|  |                     break | ||||||
|  |         except EOFError: | ||||||
|  |             pass | ||||||
|  |     return pw | ||||||
							
								
								
									
										371
									
								
								ceilometerclient/openstack/common/gettextutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										371
									
								
								ceilometerclient/openstack/common/gettextutils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,371 @@ | |||||||
|  | # Copyright 2012 Red Hat, Inc. | ||||||
|  | # Copyright 2013 IBM Corp. | ||||||
|  | # 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. | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | gettext for openstack-common modules. | ||||||
|  |  | ||||||
|  | Usual usage in an openstack.common module: | ||||||
|  |  | ||||||
|  |     from ceilometerclient.openstack.common.gettextutils import _ | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import copy | ||||||
|  | import gettext | ||||||
|  | import logging | ||||||
|  | import os | ||||||
|  | import re | ||||||
|  | try: | ||||||
|  |     import UserString as _userString | ||||||
|  | except ImportError: | ||||||
|  |     import collections as _userString | ||||||
|  |  | ||||||
|  | from babel import localedata | ||||||
|  | import six | ||||||
|  |  | ||||||
|  | _localedir = os.environ.get('ceilometerclient'.upper() + '_LOCALEDIR') | ||||||
|  | _t = gettext.translation('ceilometerclient', localedir=_localedir, fallback=True) | ||||||
|  |  | ||||||
|  | _AVAILABLE_LANGUAGES = {} | ||||||
|  | USE_LAZY = False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def enable_lazy(): | ||||||
|  |     """Convenience function for configuring _() to use lazy gettext | ||||||
|  |  | ||||||
|  |     Call this at the start of execution to enable the gettextutils._ | ||||||
|  |     function to use lazy gettext functionality. This is useful if | ||||||
|  |     your project is importing _ directly instead of using the | ||||||
|  |     gettextutils.install() way of importing the _ function. | ||||||
|  |     """ | ||||||
|  |     global USE_LAZY | ||||||
|  |     USE_LAZY = True | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _(msg): | ||||||
|  |     if USE_LAZY: | ||||||
|  |         return Message(msg, 'ceilometerclient') | ||||||
|  |     else: | ||||||
|  |         if six.PY3: | ||||||
|  |             return _t.gettext(msg) | ||||||
|  |         return _t.ugettext(msg) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def install(domain, lazy=False): | ||||||
|  |     """Install a _() function using the given translation domain. | ||||||
|  |  | ||||||
|  |     Given a translation domain, install a _() function using gettext's | ||||||
|  |     install() function. | ||||||
|  |  | ||||||
|  |     The main difference from gettext.install() is that we allow | ||||||
|  |     overriding the default localedir (e.g. /usr/share/locale) using | ||||||
|  |     a translation-domain-specific environment variable (e.g. | ||||||
|  |     NOVA_LOCALEDIR). | ||||||
|  |  | ||||||
|  |     :param domain: the translation domain | ||||||
|  |     :param lazy: indicates whether or not to install the lazy _() function. | ||||||
|  |                  The lazy _() introduces a way to do deferred translation | ||||||
|  |                  of messages by installing a _ that builds Message objects, | ||||||
|  |                  instead of strings, which can then be lazily translated into | ||||||
|  |                  any available locale. | ||||||
|  |     """ | ||||||
|  |     if lazy: | ||||||
|  |         # NOTE(mrodden): Lazy gettext functionality. | ||||||
|  |         # | ||||||
|  |         # The following introduces a deferred way to do translations on | ||||||
|  |         # messages in OpenStack. We override the standard _() function | ||||||
|  |         # and % (format string) operation to build Message objects that can | ||||||
|  |         # later be translated when we have more information. | ||||||
|  |         # | ||||||
|  |         # Also included below is an example LocaleHandler that translates | ||||||
|  |         # Messages to an associated locale, effectively allowing many logs, | ||||||
|  |         # each with their own locale. | ||||||
|  |  | ||||||
|  |         def _lazy_gettext(msg): | ||||||
|  |             """Create and return a Message object. | ||||||
|  |  | ||||||
|  |             Lazy gettext function for a given domain, it is a factory method | ||||||
|  |             for a project/module to get a lazy gettext function for its own | ||||||
|  |             translation domain (i.e. nova, glance, cinder, etc.) | ||||||
|  |  | ||||||
|  |             Message encapsulates a string so that we can translate | ||||||
|  |             it later when needed. | ||||||
|  |             """ | ||||||
|  |             return Message(msg, domain) | ||||||
|  |  | ||||||
|  |         from six import moves | ||||||
|  |         moves.builtins.__dict__['_'] = _lazy_gettext | ||||||
|  |     else: | ||||||
|  |         localedir = '%s_LOCALEDIR' % domain.upper() | ||||||
|  |         if six.PY3: | ||||||
|  |             gettext.install(domain, | ||||||
|  |                             localedir=os.environ.get(localedir)) | ||||||
|  |         else: | ||||||
|  |             gettext.install(domain, | ||||||
|  |                             localedir=os.environ.get(localedir), | ||||||
|  |                             unicode=True) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Message(_userString.UserString, object): | ||||||
|  |     """Class used to encapsulate translatable messages.""" | ||||||
|  |     def __init__(self, msg, domain): | ||||||
|  |         # _msg is the gettext msgid and should never change | ||||||
|  |         self._msg = msg | ||||||
|  |         self._left_extra_msg = '' | ||||||
|  |         self._right_extra_msg = '' | ||||||
|  |         self._locale = None | ||||||
|  |         self.params = None | ||||||
|  |         self.domain = domain | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def data(self): | ||||||
|  |         # NOTE(mrodden): this should always resolve to a unicode string | ||||||
|  |         # that best represents the state of the message currently | ||||||
|  |  | ||||||
|  |         localedir = os.environ.get(self.domain.upper() + '_LOCALEDIR') | ||||||
|  |         if self.locale: | ||||||
|  |             lang = gettext.translation(self.domain, | ||||||
|  |                                        localedir=localedir, | ||||||
|  |                                        languages=[self.locale], | ||||||
|  |                                        fallback=True) | ||||||
|  |         else: | ||||||
|  |             # use system locale for translations | ||||||
|  |             lang = gettext.translation(self.domain, | ||||||
|  |                                        localedir=localedir, | ||||||
|  |                                        fallback=True) | ||||||
|  |  | ||||||
|  |         if six.PY3: | ||||||
|  |             ugettext = lang.gettext | ||||||
|  |         else: | ||||||
|  |             ugettext = lang.ugettext | ||||||
|  |  | ||||||
|  |         full_msg = (self._left_extra_msg + | ||||||
|  |                     ugettext(self._msg) + | ||||||
|  |                     self._right_extra_msg) | ||||||
|  |  | ||||||
|  |         if self.params is not None: | ||||||
|  |             full_msg = full_msg % self.params | ||||||
|  |  | ||||||
|  |         return six.text_type(full_msg) | ||||||
|  |  | ||||||
|  |     @property | ||||||
|  |     def locale(self): | ||||||
|  |         return self._locale | ||||||
|  |  | ||||||
|  |     @locale.setter | ||||||
|  |     def locale(self, value): | ||||||
|  |         self._locale = value | ||||||
|  |         if not self.params: | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         # This Message object may have been constructed with one or more | ||||||
|  |         # Message objects as substitution parameters, given as a single | ||||||
|  |         # Message, or a tuple or Map containing some, so when setting the | ||||||
|  |         # locale for this Message we need to set it for those Messages too. | ||||||
|  |         if isinstance(self.params, Message): | ||||||
|  |             self.params.locale = value | ||||||
|  |             return | ||||||
|  |         if isinstance(self.params, tuple): | ||||||
|  |             for param in self.params: | ||||||
|  |                 if isinstance(param, Message): | ||||||
|  |                     param.locale = value | ||||||
|  |             return | ||||||
|  |         if isinstance(self.params, dict): | ||||||
|  |             for param in self.params.values(): | ||||||
|  |                 if isinstance(param, Message): | ||||||
|  |                     param.locale = value | ||||||
|  |  | ||||||
|  |     def _save_dictionary_parameter(self, dict_param): | ||||||
|  |         full_msg = self.data | ||||||
|  |         # look for %(blah) fields in string; | ||||||
|  |         # ignore %% and deal with the | ||||||
|  |         # case where % is first character on the line | ||||||
|  |         keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', full_msg) | ||||||
|  |  | ||||||
|  |         # if we don't find any %(blah) blocks but have a %s | ||||||
|  |         if not keys and re.findall('(?:[^%]|^)%[a-z]', full_msg): | ||||||
|  |             # apparently the full dictionary is the parameter | ||||||
|  |             params = copy.deepcopy(dict_param) | ||||||
|  |         else: | ||||||
|  |             params = {} | ||||||
|  |             for key in keys: | ||||||
|  |                 try: | ||||||
|  |                     params[key] = copy.deepcopy(dict_param[key]) | ||||||
|  |                 except TypeError: | ||||||
|  |                     # cast uncopyable thing to unicode string | ||||||
|  |                     params[key] = six.text_type(dict_param[key]) | ||||||
|  |  | ||||||
|  |         return params | ||||||
|  |  | ||||||
|  |     def _save_parameters(self, other): | ||||||
|  |         # we check for None later to see if | ||||||
|  |         # we actually have parameters to inject, | ||||||
|  |         # so encapsulate if our parameter is actually None | ||||||
|  |         if other is None: | ||||||
|  |             self.params = (other, ) | ||||||
|  |         elif isinstance(other, dict): | ||||||
|  |             self.params = self._save_dictionary_parameter(other) | ||||||
|  |         else: | ||||||
|  |             # fallback to casting to unicode, | ||||||
|  |             # this will handle the problematic python code-like | ||||||
|  |             # objects that cannot be deep-copied | ||||||
|  |             try: | ||||||
|  |                 self.params = copy.deepcopy(other) | ||||||
|  |             except TypeError: | ||||||
|  |                 self.params = six.text_type(other) | ||||||
|  |  | ||||||
|  |         return self | ||||||
|  |  | ||||||
|  |     # overrides to be more string-like | ||||||
|  |     def __unicode__(self): | ||||||
|  |         return self.data | ||||||
|  |  | ||||||
|  |     def __str__(self): | ||||||
|  |         if six.PY3: | ||||||
|  |             return self.__unicode__() | ||||||
|  |         return self.data.encode('utf-8') | ||||||
|  |  | ||||||
|  |     def __getstate__(self): | ||||||
|  |         to_copy = ['_msg', '_right_extra_msg', '_left_extra_msg', | ||||||
|  |                    'domain', 'params', '_locale'] | ||||||
|  |         new_dict = self.__dict__.fromkeys(to_copy) | ||||||
|  |         for attr in to_copy: | ||||||
|  |             new_dict[attr] = copy.deepcopy(self.__dict__[attr]) | ||||||
|  |  | ||||||
|  |         return new_dict | ||||||
|  |  | ||||||
|  |     def __setstate__(self, state): | ||||||
|  |         for (k, v) in state.items(): | ||||||
|  |             setattr(self, k, v) | ||||||
|  |  | ||||||
|  |     # operator overloads | ||||||
|  |     def __add__(self, other): | ||||||
|  |         copied = copy.deepcopy(self) | ||||||
|  |         copied._right_extra_msg += other.__str__() | ||||||
|  |         return copied | ||||||
|  |  | ||||||
|  |     def __radd__(self, other): | ||||||
|  |         copied = copy.deepcopy(self) | ||||||
|  |         copied._left_extra_msg += other.__str__() | ||||||
|  |         return copied | ||||||
|  |  | ||||||
|  |     def __mod__(self, other): | ||||||
|  |         # do a format string to catch and raise | ||||||
|  |         # any possible KeyErrors from missing parameters | ||||||
|  |         self.data % other | ||||||
|  |         copied = copy.deepcopy(self) | ||||||
|  |         return copied._save_parameters(other) | ||||||
|  |  | ||||||
|  |     def __mul__(self, other): | ||||||
|  |         return self.data * other | ||||||
|  |  | ||||||
|  |     def __rmul__(self, other): | ||||||
|  |         return other * self.data | ||||||
|  |  | ||||||
|  |     def __getitem__(self, key): | ||||||
|  |         return self.data[key] | ||||||
|  |  | ||||||
|  |     def __getslice__(self, start, end): | ||||||
|  |         return self.data.__getslice__(start, end) | ||||||
|  |  | ||||||
|  |     def __getattribute__(self, name): | ||||||
|  |         # NOTE(mrodden): handle lossy operations that we can't deal with yet | ||||||
|  |         # These override the UserString implementation, since UserString | ||||||
|  |         # uses our __class__ attribute to try and build a new message | ||||||
|  |         # after running the inner data string through the operation. | ||||||
|  |         # At that point, we have lost the gettext message id and can just | ||||||
|  |         # safely resolve to a string instead. | ||||||
|  |         ops = ['capitalize', 'center', 'decode', 'encode', | ||||||
|  |                'expandtabs', 'ljust', 'lstrip', 'replace', 'rjust', 'rstrip', | ||||||
|  |                'strip', 'swapcase', 'title', 'translate', 'upper', 'zfill'] | ||||||
|  |         if name in ops: | ||||||
|  |             return getattr(self.data, name) | ||||||
|  |         else: | ||||||
|  |             return _userString.UserString.__getattribute__(self, name) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_available_languages(domain): | ||||||
|  |     """Lists the available languages for the given translation domain. | ||||||
|  |  | ||||||
|  |     :param domain: the domain to get languages for | ||||||
|  |     """ | ||||||
|  |     if domain in _AVAILABLE_LANGUAGES: | ||||||
|  |         return copy.copy(_AVAILABLE_LANGUAGES[domain]) | ||||||
|  |  | ||||||
|  |     localedir = '%s_LOCALEDIR' % domain.upper() | ||||||
|  |     find = lambda x: gettext.find(domain, | ||||||
|  |                                   localedir=os.environ.get(localedir), | ||||||
|  |                                   languages=[x]) | ||||||
|  |  | ||||||
|  |     # NOTE(mrodden): en_US should always be available (and first in case | ||||||
|  |     # order matters) since our in-line message strings are en_US | ||||||
|  |     language_list = ['en_US'] | ||||||
|  |     # NOTE(luisg): Babel <1.0 used a function called list(), which was | ||||||
|  |     # renamed to locale_identifiers() in >=1.0, the requirements master list | ||||||
|  |     # requires >=0.9.6, uncapped, so defensively work with both. We can remove | ||||||
|  |     # this check when the master list updates to >=1.0, and update all projects | ||||||
|  |     list_identifiers = (getattr(localedata, 'list', None) or | ||||||
|  |                         getattr(localedata, 'locale_identifiers')) | ||||||
|  |     locale_identifiers = list_identifiers() | ||||||
|  |     for i in locale_identifiers: | ||||||
|  |         if find(i) is not None: | ||||||
|  |             language_list.append(i) | ||||||
|  |     _AVAILABLE_LANGUAGES[domain] = language_list | ||||||
|  |     return copy.copy(language_list) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def get_localized_message(message, user_locale): | ||||||
|  |     """Gets a localized version of the given message in the given locale. | ||||||
|  |  | ||||||
|  |     If the message is not a Message object the message is returned as-is. | ||||||
|  |     If the locale is None the message is translated to the default locale. | ||||||
|  |  | ||||||
|  |     :returns: the translated message in unicode, or the original message if | ||||||
|  |               it could not be translated | ||||||
|  |     """ | ||||||
|  |     translated = message | ||||||
|  |     if isinstance(message, Message): | ||||||
|  |         original_locale = message.locale | ||||||
|  |         message.locale = user_locale | ||||||
|  |         translated = six.text_type(message) | ||||||
|  |         message.locale = original_locale | ||||||
|  |     return translated | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class LocaleHandler(logging.Handler): | ||||||
|  |     """Handler that can have a locale associated to translate Messages. | ||||||
|  |  | ||||||
|  |     A quick example of how to utilize the Message class above. | ||||||
|  |     LocaleHandler takes a locale and a target logging.Handler object | ||||||
|  |     to forward LogRecord objects to after translating the internal Message. | ||||||
|  |     """ | ||||||
|  |  | ||||||
|  |     def __init__(self, locale, target): | ||||||
|  |         """Initialize a LocaleHandler | ||||||
|  |  | ||||||
|  |         :param locale: locale to use for translating messages | ||||||
|  |         :param target: logging.Handler object to forward | ||||||
|  |                        LogRecord objects to after translation | ||||||
|  |         """ | ||||||
|  |         logging.Handler.__init__(self) | ||||||
|  |         self.locale = locale | ||||||
|  |         self.target = target | ||||||
|  |  | ||||||
|  |     def emit(self, record): | ||||||
|  |         if isinstance(record.msg, Message): | ||||||
|  |             # set the locale and resolve to a string | ||||||
|  |             record.msg.locale = self.locale | ||||||
|  |  | ||||||
|  |         self.target.emit(record) | ||||||
| @@ -1,5 +1,3 @@ | |||||||
| # vim: tabstop=4 shiftwidth=4 softtabstop=4 |  | ||||||
|  |  | ||||||
| # Copyright 2011 OpenStack Foundation. | # Copyright 2011 OpenStack Foundation. | ||||||
| # All Rights Reserved. | # All Rights Reserved. | ||||||
| # | # | ||||||
| @@ -24,7 +22,7 @@ import traceback | |||||||
|  |  | ||||||
|  |  | ||||||
| def import_class(import_str): | def import_class(import_str): | ||||||
|     """Returns a class from a string including module and class""" |     """Returns a class from a string including module and class.""" | ||||||
|     mod_str, _sep, class_str = import_str.rpartition('.') |     mod_str, _sep, class_str = import_str.rpartition('.') | ||||||
|     try: |     try: | ||||||
|         __import__(mod_str) |         __import__(mod_str) | ||||||
| @@ -41,8 +39,9 @@ def import_object(import_str, *args, **kwargs): | |||||||
|  |  | ||||||
|  |  | ||||||
| def import_object_ns(name_space, import_str, *args, **kwargs): | def import_object_ns(name_space, import_str, *args, **kwargs): | ||||||
|     """ |     """Tries to import object from default namespace. | ||||||
|     Import a class and return an instance of it, first by trying |  | ||||||
|  |     Imports a class and return an instance of it, first by trying | ||||||
|     to find the class in a default namespace, then failing back to |     to find the class in a default namespace, then failing back to | ||||||
|     a full path if not found in the default namespace. |     a full path if not found in the default namespace. | ||||||
|     """ |     """ | ||||||
|   | |||||||
							
								
								
									
										16
									
								
								ceilometerclient/openstack/common/py3kcompat/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								ceilometerclient/openstack/common/py3kcompat/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | # | ||||||
|  | # Copyright 2013 Canonical Ltd. | ||||||
|  | # 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. | ||||||
|  | # | ||||||
							
								
								
									
										63
									
								
								ceilometerclient/openstack/common/py3kcompat/urlutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										63
									
								
								ceilometerclient/openstack/common/py3kcompat/urlutils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,63 @@ | |||||||
|  | # | ||||||
|  | # Copyright 2013 Canonical Ltd. | ||||||
|  | # 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. | ||||||
|  | # | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | Python2/Python3 compatibility layer for OpenStack | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import six | ||||||
|  |  | ||||||
|  | if six.PY3: | ||||||
|  |     # python3 | ||||||
|  |     import urllib.error | ||||||
|  |     import urllib.parse | ||||||
|  |     import urllib.request | ||||||
|  |  | ||||||
|  |     urlencode = urllib.parse.urlencode | ||||||
|  |     urljoin = urllib.parse.urljoin | ||||||
|  |     quote = urllib.parse.quote | ||||||
|  |     parse_qsl = urllib.parse.parse_qsl | ||||||
|  |     unquote = urllib.parse.unquote | ||||||
|  |     urlparse = urllib.parse.urlparse | ||||||
|  |     urlsplit = urllib.parse.urlsplit | ||||||
|  |     urlunsplit = urllib.parse.urlunsplit | ||||||
|  |     SplitResult = urllib.parse.SplitResult | ||||||
|  |  | ||||||
|  |     urlopen = urllib.request.urlopen | ||||||
|  |     URLError = urllib.error.URLError | ||||||
|  |     pathname2url = urllib.request.pathname2url | ||||||
|  | else: | ||||||
|  |     # python2 | ||||||
|  |     import urllib | ||||||
|  |     import urllib2 | ||||||
|  |     import urlparse | ||||||
|  |  | ||||||
|  |     urlencode = urllib.urlencode | ||||||
|  |     quote = urllib.quote | ||||||
|  |     unquote = urllib.unquote | ||||||
|  |  | ||||||
|  |     parse = urlparse | ||||||
|  |     parse_qsl = parse.parse_qsl | ||||||
|  |     urljoin = parse.urljoin | ||||||
|  |     urlparse = parse.urlparse | ||||||
|  |     urlsplit = parse.urlsplit | ||||||
|  |     urlunsplit = parse.urlunsplit | ||||||
|  |     SplitResult = parse.SplitResult | ||||||
|  |  | ||||||
|  |     urlopen = urllib2.urlopen | ||||||
|  |     URLError = urllib2.URLError | ||||||
|  |     pathname2url = urllib.pathname2url | ||||||
							
								
								
									
										216
									
								
								ceilometerclient/openstack/common/strutils.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										216
									
								
								ceilometerclient/openstack/common/strutils.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,216 @@ | |||||||
|  | # Copyright 2011 OpenStack Foundation. | ||||||
|  | # 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. | ||||||
|  |  | ||||||
|  | """ | ||||||
|  | System-level utilities and helper functions. | ||||||
|  | """ | ||||||
|  |  | ||||||
|  | import re | ||||||
|  | import sys | ||||||
|  | import unicodedata | ||||||
|  |  | ||||||
|  | import six | ||||||
|  |  | ||||||
|  | from ceilometerclient.openstack.common.gettextutils import _  # noqa | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Used for looking up extensions of text | ||||||
|  | # to their 'multiplied' byte amount | ||||||
|  | BYTE_MULTIPLIERS = { | ||||||
|  |     '': 1, | ||||||
|  |     't': 1024 ** 4, | ||||||
|  |     'g': 1024 ** 3, | ||||||
|  |     'm': 1024 ** 2, | ||||||
|  |     'k': 1024, | ||||||
|  | } | ||||||
|  | BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)') | ||||||
|  |  | ||||||
|  | TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes') | ||||||
|  | FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no') | ||||||
|  |  | ||||||
|  | SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]") | ||||||
|  | SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def int_from_bool_as_string(subject): | ||||||
|  |     """Interpret a string as a boolean and return either 1 or 0. | ||||||
|  |  | ||||||
|  |     Any string value in: | ||||||
|  |  | ||||||
|  |         ('True', 'true', 'On', 'on', '1') | ||||||
|  |  | ||||||
|  |     is interpreted as a boolean True. | ||||||
|  |  | ||||||
|  |     Useful for JSON-decoded stuff and config file parsing | ||||||
|  |     """ | ||||||
|  |     return bool_from_string(subject) and 1 or 0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def bool_from_string(subject, strict=False): | ||||||
|  |     """Interpret a string as a boolean. | ||||||
|  |  | ||||||
|  |     A case-insensitive match is performed such that strings matching 't', | ||||||
|  |     'true', 'on', 'y', 'yes', or '1' are considered True and, when | ||||||
|  |     `strict=False`, anything else is considered False. | ||||||
|  |  | ||||||
|  |     Useful for JSON-decoded stuff and config file parsing. | ||||||
|  |  | ||||||
|  |     If `strict=True`, unrecognized values, including None, will raise a | ||||||
|  |     ValueError which is useful when parsing values passed in from an API call. | ||||||
|  |     Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'. | ||||||
|  |     """ | ||||||
|  |     if not isinstance(subject, six.string_types): | ||||||
|  |         subject = str(subject) | ||||||
|  |  | ||||||
|  |     lowered = subject.strip().lower() | ||||||
|  |  | ||||||
|  |     if lowered in TRUE_STRINGS: | ||||||
|  |         return True | ||||||
|  |     elif lowered in FALSE_STRINGS: | ||||||
|  |         return False | ||||||
|  |     elif strict: | ||||||
|  |         acceptable = ', '.join( | ||||||
|  |             "'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS)) | ||||||
|  |         msg = _("Unrecognized value '%(val)s', acceptable values are:" | ||||||
|  |                 " %(acceptable)s") % {'val': subject, | ||||||
|  |                                       'acceptable': acceptable} | ||||||
|  |         raise ValueError(msg) | ||||||
|  |     else: | ||||||
|  |         return False | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def safe_decode(text, incoming=None, errors='strict'): | ||||||
|  |     """Decodes incoming str using `incoming` if they're not already unicode. | ||||||
|  |  | ||||||
|  |     :param incoming: Text's current encoding | ||||||
|  |     :param errors: Errors handling policy. See here for valid | ||||||
|  |         values http://docs.python.org/2/library/codecs.html | ||||||
|  |     :returns: text or a unicode `incoming` encoded | ||||||
|  |                 representation of it. | ||||||
|  |     :raises TypeError: If text is not an instance of str | ||||||
|  |     """ | ||||||
|  |     if not isinstance(text, six.string_types): | ||||||
|  |         raise TypeError("%s can't be decoded" % type(text)) | ||||||
|  |  | ||||||
|  |     if isinstance(text, six.text_type): | ||||||
|  |         return text | ||||||
|  |  | ||||||
|  |     if not incoming: | ||||||
|  |         incoming = (sys.stdin.encoding or | ||||||
|  |                     sys.getdefaultencoding()) | ||||||
|  |  | ||||||
|  |     try: | ||||||
|  |         return text.decode(incoming, errors) | ||||||
|  |     except UnicodeDecodeError: | ||||||
|  |         # Note(flaper87) If we get here, it means that | ||||||
|  |         # sys.stdin.encoding / sys.getdefaultencoding | ||||||
|  |         # didn't return a suitable encoding to decode | ||||||
|  |         # text. This happens mostly when global LANG | ||||||
|  |         # var is not set correctly and there's no | ||||||
|  |         # default encoding. In this case, most likely | ||||||
|  |         # python will use ASCII or ANSI encoders as | ||||||
|  |         # default encodings but they won't be capable | ||||||
|  |         # of decoding non-ASCII characters. | ||||||
|  |         # | ||||||
|  |         # Also, UTF-8 is being used since it's an ASCII | ||||||
|  |         # extension. | ||||||
|  |         return text.decode('utf-8', errors) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def safe_encode(text, incoming=None, | ||||||
|  |                 encoding='utf-8', errors='strict'): | ||||||
|  |     """Encodes incoming str/unicode using `encoding`. | ||||||
|  |  | ||||||
|  |     If incoming is not specified, text is expected to be encoded with | ||||||
|  |     current python's default encoding. (`sys.getdefaultencoding`) | ||||||
|  |  | ||||||
|  |     :param incoming: Text's current encoding | ||||||
|  |     :param encoding: Expected encoding for text (Default UTF-8) | ||||||
|  |     :param errors: Errors handling policy. See here for valid | ||||||
|  |         values http://docs.python.org/2/library/codecs.html | ||||||
|  |     :returns: text or a bytestring `encoding` encoded | ||||||
|  |                 representation of it. | ||||||
|  |     :raises TypeError: If text is not an instance of str | ||||||
|  |     """ | ||||||
|  |     if not isinstance(text, six.string_types): | ||||||
|  |         raise TypeError("%s can't be encoded" % type(text)) | ||||||
|  |  | ||||||
|  |     if not incoming: | ||||||
|  |         incoming = (sys.stdin.encoding or | ||||||
|  |                     sys.getdefaultencoding()) | ||||||
|  |  | ||||||
|  |     if isinstance(text, six.text_type): | ||||||
|  |         return text.encode(encoding, errors) | ||||||
|  |     elif text and encoding != incoming: | ||||||
|  |         # Decode text before encoding it with `encoding` | ||||||
|  |         text = safe_decode(text, incoming, errors) | ||||||
|  |         return text.encode(encoding, errors) | ||||||
|  |  | ||||||
|  |     return text | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def to_bytes(text, default=0): | ||||||
|  |     """Converts a string into an integer of bytes. | ||||||
|  |  | ||||||
|  |     Looks at the last characters of the text to determine | ||||||
|  |     what conversion is needed to turn the input text into a byte number. | ||||||
|  |     Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive) | ||||||
|  |  | ||||||
|  |     :param text: String input for bytes size conversion. | ||||||
|  |     :param default: Default return value when text is blank. | ||||||
|  |  | ||||||
|  |     """ | ||||||
|  |     match = BYTE_REGEX.search(text) | ||||||
|  |     if match: | ||||||
|  |         magnitude = int(match.group(1)) | ||||||
|  |         mult_key_org = match.group(2) | ||||||
|  |         if not mult_key_org: | ||||||
|  |             return magnitude | ||||||
|  |     elif text: | ||||||
|  |         msg = _('Invalid string format: %s') % text | ||||||
|  |         raise TypeError(msg) | ||||||
|  |     else: | ||||||
|  |         return default | ||||||
|  |     mult_key = mult_key_org.lower().replace('b', '', 1) | ||||||
|  |     multiplier = BYTE_MULTIPLIERS.get(mult_key) | ||||||
|  |     if multiplier is None: | ||||||
|  |         msg = _('Unknown byte multiplier: %s') % mult_key_org | ||||||
|  |         raise TypeError(msg) | ||||||
|  |     return magnitude * multiplier | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def to_slug(value, incoming=None, errors="strict"): | ||||||
|  |     """Normalize string. | ||||||
|  |  | ||||||
|  |     Convert to lowercase, remove non-word characters, and convert spaces | ||||||
|  |     to hyphens. | ||||||
|  |  | ||||||
|  |     Inspired by Django's `slugify` filter. | ||||||
|  |  | ||||||
|  |     :param value: Text to slugify | ||||||
|  |     :param incoming: Text's current encoding | ||||||
|  |     :param errors: Errors handling policy. See here for valid | ||||||
|  |         values http://docs.python.org/2/library/codecs.html | ||||||
|  |     :returns: slugified unicode representation of `value` | ||||||
|  |     :raises TypeError: If text is not an instance of str | ||||||
|  |     """ | ||||||
|  |     value = safe_decode(value, incoming, errors) | ||||||
|  |     # NOTE(aababilov): no need to use safe_(encode|decode) here: | ||||||
|  |     # encodings are always "ascii", error handling is always "ignore" | ||||||
|  |     # and types are always known (first: unicode; second: str) | ||||||
|  |     value = unicodedata.normalize("NFKD", value).encode( | ||||||
|  |         "ascii", "ignore").decode("ascii") | ||||||
|  |     value = SLUGIFY_STRIP_RE.sub("", value).strip().lower() | ||||||
|  |     return SLUGIFY_HYPHENATE_RE.sub("-", value) | ||||||
| @@ -11,18 +11,23 @@ | |||||||
| #    under the License. | #    under the License. | ||||||
|  |  | ||||||
| """ | """ | ||||||
| Command-line interface to the OpenStack Metering API. | Command-line interface to the OpenStack Telemetry API. | ||||||
| """ | """ | ||||||
|  |  | ||||||
|  | from __future__ import print_function | ||||||
|  |  | ||||||
| import argparse | import argparse | ||||||
| import httplib2 |  | ||||||
| import logging | import logging | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
|  | import six | ||||||
|  |  | ||||||
| import ceilometerclient | import ceilometerclient | ||||||
| from ceilometerclient import client as ceiloclient | from ceilometerclient import client as ceiloclient | ||||||
| from ceilometerclient.common import utils | from ceilometerclient.common import utils | ||||||
| from ceilometerclient import exc | from ceilometerclient import exc | ||||||
|  | from ceilometerclient.openstack.common import cliutils | ||||||
|  | from ceilometerclient.openstack.common import strutils | ||||||
|  |  | ||||||
|  |  | ||||||
| class CeilometerShell(object): | class CeilometerShell(object): | ||||||
| @@ -48,7 +53,8 @@ class CeilometerShell(object): | |||||||
|                             version=ceilometerclient.__version__) |                             version=ceilometerclient.__version__) | ||||||
|  |  | ||||||
|         parser.add_argument('-d', '--debug', |         parser.add_argument('-d', '--debug', | ||||||
|                             default=bool(utils.env('CEILOMETERCLIENT_DEBUG')), |                             default=bool(cliutils.env('CEILOMETERCLIENT_DEBUG') | ||||||
|  |                                          ), | ||||||
|                             action='store_true', |                             action='store_true', | ||||||
|                             help='Defaults to env[CEILOMETERCLIENT_DEBUG]') |                             help='Defaults to env[CEILOMETERCLIENT_DEBUG]') | ||||||
|  |  | ||||||
| @@ -79,7 +85,7 @@ class CeilometerShell(object): | |||||||
|         parser.add_argument('--os-cacert', |         parser.add_argument('--os-cacert', | ||||||
|                             metavar='<ca-certificate-file>', |                             metavar='<ca-certificate-file>', | ||||||
|                             dest='os_cacert', |                             dest='os_cacert', | ||||||
|                             default=utils.env('OS_CACERT'), |                             default=cliutils.env('OS_CACERT'), | ||||||
|                             help='Path of CA TLS certificate(s) used to verify' |                             help='Path of CA TLS certificate(s) used to verify' | ||||||
|                             'the remote server\'s certificate. Without this ' |                             'the remote server\'s certificate. Without this ' | ||||||
|                             'option ceilometer looks for the default system ' |                             'option ceilometer looks for the default system ' | ||||||
| @@ -93,63 +99,63 @@ class CeilometerShell(object): | |||||||
|                             help='Number of seconds to wait for a response') |                             help='Number of seconds to wait for a response') | ||||||
|  |  | ||||||
|         parser.add_argument('--os-username', |         parser.add_argument('--os-username', | ||||||
|                             default=utils.env('OS_USERNAME'), |                             default=cliutils.env('OS_USERNAME'), | ||||||
|                             help='Defaults to env[OS_USERNAME]') |                             help='Defaults to env[OS_USERNAME]') | ||||||
|  |  | ||||||
|         parser.add_argument('--os_username', |         parser.add_argument('--os_username', | ||||||
|                             help=argparse.SUPPRESS) |                             help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|         parser.add_argument('--os-password', |         parser.add_argument('--os-password', | ||||||
|                             default=utils.env('OS_PASSWORD'), |                             default=cliutils.env('OS_PASSWORD'), | ||||||
|                             help='Defaults to env[OS_PASSWORD]') |                             help='Defaults to env[OS_PASSWORD]') | ||||||
|  |  | ||||||
|         parser.add_argument('--os_password', |         parser.add_argument('--os_password', | ||||||
|                             help=argparse.SUPPRESS) |                             help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|         parser.add_argument('--os-tenant-id', |         parser.add_argument('--os-tenant-id', | ||||||
|                             default=utils.env('OS_TENANT_ID'), |                             default=cliutils.env('OS_TENANT_ID'), | ||||||
|                             help='Defaults to env[OS_TENANT_ID]') |                             help='Defaults to env[OS_TENANT_ID]') | ||||||
|  |  | ||||||
|         parser.add_argument('--os_tenant_id', |         parser.add_argument('--os_tenant_id', | ||||||
|                             help=argparse.SUPPRESS) |                             help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|         parser.add_argument('--os-tenant-name', |         parser.add_argument('--os-tenant-name', | ||||||
|                             default=utils.env('OS_TENANT_NAME'), |                             default=cliutils.env('OS_TENANT_NAME'), | ||||||
|                             help='Defaults to env[OS_TENANT_NAME]') |                             help='Defaults to env[OS_TENANT_NAME]') | ||||||
|  |  | ||||||
|         parser.add_argument('--os_tenant_name', |         parser.add_argument('--os_tenant_name', | ||||||
|                             help=argparse.SUPPRESS) |                             help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|         parser.add_argument('--os-auth-url', |         parser.add_argument('--os-auth-url', | ||||||
|                             default=utils.env('OS_AUTH_URL'), |                             default=cliutils.env('OS_AUTH_URL'), | ||||||
|                             help='Defaults to env[OS_AUTH_URL]') |                             help='Defaults to env[OS_AUTH_URL]') | ||||||
|  |  | ||||||
|         parser.add_argument('--os_auth_url', |         parser.add_argument('--os_auth_url', | ||||||
|                             help=argparse.SUPPRESS) |                             help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|         parser.add_argument('--os-region-name', |         parser.add_argument('--os-region-name', | ||||||
|                             default=utils.env('OS_REGION_NAME'), |                             default=cliutils.env('OS_REGION_NAME'), | ||||||
|                             help='Defaults to env[OS_REGION_NAME]') |                             help='Defaults to env[OS_REGION_NAME]') | ||||||
|  |  | ||||||
|         parser.add_argument('--os_region_name', |         parser.add_argument('--os_region_name', | ||||||
|                             help=argparse.SUPPRESS) |                             help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|         parser.add_argument('--os-auth-token', |         parser.add_argument('--os-auth-token', | ||||||
|                             default=utils.env('OS_AUTH_TOKEN'), |                             default=cliutils.env('OS_AUTH_TOKEN'), | ||||||
|                             help='Defaults to env[OS_AUTH_TOKEN]') |                             help='Defaults to env[OS_AUTH_TOKEN]') | ||||||
|  |  | ||||||
|         parser.add_argument('--os_auth_token', |         parser.add_argument('--os_auth_token', | ||||||
|                             help=argparse.SUPPRESS) |                             help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|         parser.add_argument('--ceilometer-url', |         parser.add_argument('--ceilometer-url', | ||||||
|                             default=utils.env('CEILOMETER_URL'), |                             default=cliutils.env('CEILOMETER_URL'), | ||||||
|                             help='Defaults to env[CEILOMETER_URL]') |                             help='Defaults to env[CEILOMETER_URL]') | ||||||
|  |  | ||||||
|         parser.add_argument('--ceilometer_url', |         parser.add_argument('--ceilometer_url', | ||||||
|                             help=argparse.SUPPRESS) |                             help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|         parser.add_argument('--ceilometer-api-version', |         parser.add_argument('--ceilometer-api-version', | ||||||
|                             default=utils.env( |                             default=cliutils.env( | ||||||
|                             'CEILOMETER_API_VERSION', default='2'), |                             'CEILOMETER_API_VERSION', default='2'), | ||||||
|                             help='Defaults to env[CEILOMETER_API_VERSION] ' |                             help='Defaults to env[CEILOMETER_API_VERSION] ' | ||||||
|                             'or 2') |                             'or 2') | ||||||
| @@ -158,14 +164,14 @@ class CeilometerShell(object): | |||||||
|                             help=argparse.SUPPRESS) |                             help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|         parser.add_argument('--os-service-type', |         parser.add_argument('--os-service-type', | ||||||
|                             default=utils.env('OS_SERVICE_TYPE'), |                             default=cliutils.env('OS_SERVICE_TYPE'), | ||||||
|                             help='Defaults to env[OS_SERVICE_TYPE]') |                             help='Defaults to env[OS_SERVICE_TYPE]') | ||||||
|  |  | ||||||
|         parser.add_argument('--os_service_type', |         parser.add_argument('--os_service_type', | ||||||
|                             help=argparse.SUPPRESS) |                             help=argparse.SUPPRESS) | ||||||
|  |  | ||||||
|         parser.add_argument('--os-endpoint-type', |         parser.add_argument('--os-endpoint-type', | ||||||
|                             default=utils.env('OS_ENDPOINT_TYPE'), |                             default=cliutils.env('OS_ENDPOINT_TYPE'), | ||||||
|                             help='Defaults to env[OS_ENDPOINT_TYPE]') |                             help='Defaults to env[OS_ENDPOINT_TYPE]') | ||||||
|  |  | ||||||
|         parser.add_argument('--os_endpoint_type', |         parser.add_argument('--os_endpoint_type', | ||||||
| @@ -181,9 +187,19 @@ class CeilometerShell(object): | |||||||
|         submodule = utils.import_versioned_module(version, 'shell') |         submodule = utils.import_versioned_module(version, 'shell') | ||||||
|         self._find_actions(subparsers, submodule) |         self._find_actions(subparsers, submodule) | ||||||
|         self._find_actions(subparsers, self) |         self._find_actions(subparsers, self) | ||||||
|  |         self._add_bash_completion_subparser(subparsers) | ||||||
|  |  | ||||||
|         return parser |         return parser | ||||||
|  |  | ||||||
|  |     def _add_bash_completion_subparser(self, subparsers): | ||||||
|  |         subparser = subparsers.add_parser( | ||||||
|  |             'bash_completion', | ||||||
|  |             add_help=False, | ||||||
|  |             formatter_class=HelpFormatter | ||||||
|  |         ) | ||||||
|  |         self.subcommands['bash_completion'] = subparser | ||||||
|  |         subparser.set_defaults(func=self.do_bash_completion) | ||||||
|  |  | ||||||
|     def _find_actions(self, subparsers, actions_module): |     def _find_actions(self, subparsers, actions_module): | ||||||
|         for attr in (a for a in dir(actions_module) if a.startswith('do_')): |         for attr in (a for a in dir(actions_module) if a.startswith('do_')): | ||||||
|             # I prefer to be hypen-separated instead of underscores. |             # I prefer to be hypen-separated instead of underscores. | ||||||
| @@ -204,19 +220,18 @@ class CeilometerShell(object): | |||||||
|                 subparser.add_argument(*args, **kwargs) |                 subparser.add_argument(*args, **kwargs) | ||||||
|             subparser.set_defaults(func=callback) |             subparser.set_defaults(func=callback) | ||||||
|  |  | ||||||
|     def _setup_debugging(self, debug): |     def _setup_logging(self, debug): | ||||||
|  |         format = '%(levelname)s (%(module)s:%(lineno)d) %(message)s' | ||||||
|         if debug: |         if debug: | ||||||
|             logging.basicConfig( |             logging.basicConfig(format=format, level=logging.DEBUG) | ||||||
|                 format="%(levelname)s (%(module)s:%(lineno)d) %(message)s", |         else: | ||||||
|                 level=logging.DEBUG) |             logging.basicConfig(format=format, level=logging.WARN) | ||||||
|  |  | ||||||
|             httplib2.debuglevel = 1 |     def parse_args(self, argv): | ||||||
|  |  | ||||||
|     def main(self, argv): |  | ||||||
|         # Parse args once to find version |         # Parse args once to find version | ||||||
|         parser = self.get_base_parser() |         parser = self.get_base_parser() | ||||||
|         (options, args) = parser.parse_known_args(argv) |         (options, args) = parser.parse_known_args(argv) | ||||||
|         self._setup_debugging(options.debug) |         self._setup_logging(options.debug) | ||||||
|  |  | ||||||
|         # build available subcommands based on version |         # build available subcommands based on version | ||||||
|         api_version = options.ceilometer_api_version |         api_version = options.ceilometer_api_version | ||||||
| @@ -229,13 +244,22 @@ class CeilometerShell(object): | |||||||
|             self.do_help(options) |             self.do_help(options) | ||||||
|             return 0 |             return 0 | ||||||
|  |  | ||||||
|         # Parse args again and call whatever callback was selected |         # Return parsed args | ||||||
|         args = subcommand_parser.parse_args(argv) |         return api_version, subcommand_parser.parse_args(argv) | ||||||
|  |  | ||||||
|  |     def main(self, argv): | ||||||
|  |         parsed = self.parse_args(argv) | ||||||
|  |         if parsed == 0: | ||||||
|  |             return 0 | ||||||
|  |         api_version, args = parsed | ||||||
|  |  | ||||||
|         # Short-circuit and deal with help command right away. |         # Short-circuit and deal with help command right away. | ||||||
|         if args.func == self.do_help: |         if args.func == self.do_help: | ||||||
|             self.do_help(args) |             self.do_help(args) | ||||||
|             return 0 |             return 0 | ||||||
|  |         elif args.func == self.do_bash_completion: | ||||||
|  |             self.do_bash_completion(args) | ||||||
|  |             return 0 | ||||||
|  |  | ||||||
|         if not (args.os_auth_token and args.ceilometer_url): |         if not (args.os_auth_token and args.ceilometer_url): | ||||||
|             if not args.os_username: |             if not args.os_username: | ||||||
| @@ -260,11 +284,28 @@ class CeilometerShell(object): | |||||||
|  |  | ||||||
|         client = ceiloclient.get_client(api_version, **(args.__dict__)) |         client = ceiloclient.get_client(api_version, **(args.__dict__)) | ||||||
|  |  | ||||||
|  |         # call whatever callback was selected | ||||||
|         try: |         try: | ||||||
|             args.func(client, args) |             args.func(client, args) | ||||||
|         except exc.Unauthorized: |         except exc.Unauthorized: | ||||||
|             raise exc.CommandError("Invalid OpenStack Identity credentials.") |             raise exc.CommandError("Invalid OpenStack Identity credentials.") | ||||||
|  |  | ||||||
|  |     def do_bash_completion(self, args): | ||||||
|  |         """Prints all of the commands and options to stdout. | ||||||
|  |  | ||||||
|  |         The ceilometer.bash_completion script doesn't have to hard code them. | ||||||
|  |         """ | ||||||
|  |         commands = set() | ||||||
|  |         options = set() | ||||||
|  |         for sc_str, sc in self.subcommands.items(): | ||||||
|  |             commands.add(sc_str) | ||||||
|  |             for option in list(sc._optionals._option_string_actions): | ||||||
|  |                 options.add(option) | ||||||
|  |  | ||||||
|  |         commands.remove('bash-completion') | ||||||
|  |         commands.remove('bash_completion') | ||||||
|  |         print(' '.join(commands | options)) | ||||||
|  |  | ||||||
|     @utils.arg('command', metavar='<subcommand>', nargs='?', |     @utils.arg('command', metavar='<subcommand>', nargs='?', | ||||||
|                help='Display help for <subcommand>') |                help='Display help for <subcommand>') | ||||||
|     def do_help(self, args): |     def do_help(self, args): | ||||||
| @@ -286,12 +327,18 @@ class HelpFormatter(argparse.HelpFormatter): | |||||||
|         super(HelpFormatter, self).start_section(heading) |         super(HelpFormatter, self).start_section(heading) | ||||||
|  |  | ||||||
|  |  | ||||||
| def main(): | def main(args=None): | ||||||
|     try: |     try: | ||||||
|         CeilometerShell().main(sys.argv[1:]) |         if args is None: | ||||||
|  |             args = sys.argv[1:] | ||||||
|  |  | ||||||
|  |         CeilometerShell().main(args) | ||||||
|  |  | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         print >> sys.stderr, e |         if '--debug' in args or '-d' in args: | ||||||
|  |             raise | ||||||
|  |         else: | ||||||
|  |             print(strutils.safe_encode(six.text_type(e)), file=sys.stderr) | ||||||
|         sys.exit(1) |         sys.exit(1) | ||||||
|  |  | ||||||
| if __name__ == "__main__": | if __name__ == "__main__": | ||||||
|   | |||||||
| @@ -15,12 +15,11 @@ from keystoneclient.v2_0 import client as ksclient | |||||||
|  |  | ||||||
| def script_keystone_client(): | def script_keystone_client(): | ||||||
|     ksclient.Client(auth_url='http://no.where', |     ksclient.Client(auth_url='http://no.where', | ||||||
|                 insecure=False, |                     insecure=False, | ||||||
|                 password='password', |                     password='password', | ||||||
|                 tenant_id='', |                     tenant_id='', | ||||||
|                 tenant_name='tenant_name', |                     tenant_name='tenant_name', | ||||||
|                 username='username').AndReturn( |                     username='username').AndReturn(FakeKeystone('abcd1234')) | ||||||
|                 FakeKeystone('abcd1234')) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def fake_headers(): | def fake_headers(): | ||||||
|   | |||||||
							
								
								
									
										51
									
								
								ceilometerclient/tests/test_exc.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								ceilometerclient/tests/test_exc.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,51 @@ | |||||||
|  | # Copyright 2013 eNovance | ||||||
|  | # 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 json | ||||||
|  |  | ||||||
|  | from ceilometerclient import exc | ||||||
|  |  | ||||||
|  | from ceilometerclient.tests import utils | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HTTPBadRequestTest(utils.BaseTestCase): | ||||||
|  |  | ||||||
|  |     def test_str_no_details(self): | ||||||
|  |         exception = exc.HTTPBadRequest() | ||||||
|  |         self.assertEqual("HTTPBadRequest (HTTP 400)", str(exception)) | ||||||
|  |  | ||||||
|  |     def test_str_no_json(self): | ||||||
|  |         exception = exc.HTTPBadRequest(details="foo") | ||||||
|  |         self.assertEqual("HTTPBadRequest (HTTP 400)", str(exception)) | ||||||
|  |  | ||||||
|  |     def test_str_no_error_message(self): | ||||||
|  |         exception = exc.HTTPBadRequest(details=json.dumps({})) | ||||||
|  |         self.assertEqual("HTTPBadRequest (HTTP 400)", str(exception)) | ||||||
|  |  | ||||||
|  |     def test_str_no_faultstring(self): | ||||||
|  |         exception = exc.HTTPBadRequest( | ||||||
|  |             details=json.dumps({"error_message": {"foo": "bar"}})) | ||||||
|  |         self.assertEqual("HTTPBadRequest (HTTP 400)", str(exception)) | ||||||
|  |  | ||||||
|  |     def test_str_error_message_unknown_format(self): | ||||||
|  |         exception = exc.HTTPBadRequest( | ||||||
|  |             details=json.dumps({"error_message": "oops"})) | ||||||
|  |         self.assertEqual("HTTPBadRequest (HTTP 400)", str(exception)) | ||||||
|  |  | ||||||
|  |     def test_str_faultstring(self): | ||||||
|  |         exception = exc.HTTPBadRequest( | ||||||
|  |             details=json.dumps({"error_message": {"faultstring": "oops"}})) | ||||||
|  |         self.assertEqual("HTTPBadRequest (HTTP 400) ERROR oops", | ||||||
|  |                          str(exception)) | ||||||
| @@ -13,36 +13,37 @@ | |||||||
| #    License for the specific language governing permissions and limitations | #    License for the specific language governing permissions and limitations | ||||||
| #    under the License. | #    under the License. | ||||||
|  |  | ||||||
| from ceilometerclient.tests import utils |  | ||||||
|  |  | ||||||
| from ceilometerclient.common import http | from ceilometerclient.common import http | ||||||
|  | from ceilometerclient.tests import utils | ||||||
|  |  | ||||||
| fixtures = {} |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class HttpClientTest(utils.BaseTestCase): | class HttpClientTest(utils.BaseTestCase): | ||||||
|  |     url = 'http://localhost' | ||||||
|  |  | ||||||
|     def test_url_generation_trailing_slash_in_base(self): |     def test_url_generation_trailing_slash_in_base(self): | ||||||
|         client = http.HTTPClient('http://localhost/') |         client = http.HTTPClient("%s/" % self.url) | ||||||
|         url = client._make_connection_url('/v1/resources') |         url = client._make_connection_url('/v1/resources') | ||||||
|         print client.connection_params |  | ||||||
|         self.assertEqual(url, '/v1/resources') |         self.assertEqual(url, '/v1/resources') | ||||||
|  |  | ||||||
|     def test_url_generation_without_trailing_slash_in_base(self): |     def test_url_generation_without_trailing_slash_in_base(self): | ||||||
|         client = http.HTTPClient('http://localhost') |         client = http.HTTPClient(self.url) | ||||||
|         url = client._make_connection_url('/v1/resources') |         url = client._make_connection_url('/v1/resources') | ||||||
|         print client.connection_params |  | ||||||
|         self.assertEqual(url, '/v1/resources') |         self.assertEqual(url, '/v1/resources') | ||||||
|  |  | ||||||
|     def test_url_generation_prefix_slash_in_path(self): |     def test_url_generation_prefix_slash_in_path(self): | ||||||
|         client = http.HTTPClient('http://localhost/') |         client = http.HTTPClient("%s/" % self.url) | ||||||
|         url = client._make_connection_url('/v1/resources') |         url = client._make_connection_url('/v1/resources') | ||||||
|         print client.connection_params |  | ||||||
|         self.assertEqual(url, '/v1/resources') |         self.assertEqual(url, '/v1/resources') | ||||||
|  |  | ||||||
|     def test_url_generation_without_prefix_slash_in_path(self): |     def test_url_generation_without_prefix_slash_in_path(self): | ||||||
|         client = http.HTTPClient('http://localhost') |         client = http.HTTPClient(self.url) | ||||||
|         url = client._make_connection_url('v1/resources') |         url = client._make_connection_url('v1/resources') | ||||||
|         print client.connection_params |  | ||||||
|         self.assertEqual(url, '/v1/resources') |         self.assertEqual(url, '/v1/resources') | ||||||
|  |  | ||||||
|  |     def test_get_connection(self): | ||||||
|  |         client = http.HTTPClient(self.url) | ||||||
|  |         self.assertIsNotNone(client.get_connection()) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class HttpsClientTest(HttpClientTest): | ||||||
|  |     url = 'https://localhost' | ||||||
|   | |||||||
| @@ -10,12 +10,12 @@ | |||||||
| #   License for the specific language governing permissions and limitations | #   License for the specific language governing permissions and limitations | ||||||
| #   under the License. | #   under the License. | ||||||
|  |  | ||||||
| import cStringIO |  | ||||||
| import httplib2 |  | ||||||
| import re | import re | ||||||
|  | import six | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| import fixtures | import fixtures | ||||||
|  | import mock | ||||||
| from testtools import matchers | from testtools import matchers | ||||||
|  |  | ||||||
| from keystoneclient.v2_0 import client as ksclient | from keystoneclient.v2_0 import client as ksclient | ||||||
| @@ -41,14 +41,14 @@ class ShellTest(utils.BaseTestCase): | |||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super(ShellTest, self).setUp() |         super(ShellTest, self).setUp() | ||||||
|         self.m.StubOutWithMock(ksclient, 'Client') |  | ||||||
|         self.m.StubOutWithMock(v1client.Client, 'json_request') |  | ||||||
|         self.m.StubOutWithMock(v1client.Client, 'raw_request') |  | ||||||
|  |  | ||||||
|     def shell(self, argstr): |     @mock.patch.object(ksclient, 'Client') | ||||||
|  |     @mock.patch.object(v1client.http.HTTPClient, 'json_request') | ||||||
|  |     @mock.patch.object(v1client.http.HTTPClient, 'raw_request') | ||||||
|  |     def shell(self, argstr, mock_ksclient, mock_json, mock_raw): | ||||||
|         orig = sys.stdout |         orig = sys.stdout | ||||||
|         try: |         try: | ||||||
|             sys.stdout = cStringIO.StringIO() |             sys.stdout = six.StringIO() | ||||||
|             _shell = ceilometer_shell.CeilometerShell() |             _shell = ceilometer_shell.CeilometerShell() | ||||||
|             _shell.main(argstr.split()) |             _shell.main(argstr.split()) | ||||||
|         except SystemExit: |         except SystemExit: | ||||||
| @@ -64,11 +64,6 @@ class ShellTest(utils.BaseTestCase): | |||||||
|     def test_help_unknown_command(self): |     def test_help_unknown_command(self): | ||||||
|         self.assertRaises(exc.CommandError, self.shell, 'help foofoo') |         self.assertRaises(exc.CommandError, self.shell, 'help foofoo') | ||||||
|  |  | ||||||
|     def test_debug(self): |  | ||||||
|         httplib2.debuglevel = 0 |  | ||||||
|         self.shell('--debug help') |  | ||||||
|         self.assertEqual(httplib2.debuglevel, 1) |  | ||||||
|  |  | ||||||
|     def test_help(self): |     def test_help(self): | ||||||
|         required = [ |         required = [ | ||||||
|             '.*?^usage: ceilometer', |             '.*?^usage: ceilometer', | ||||||
| @@ -99,3 +94,24 @@ class ShellTest(utils.BaseTestCase): | |||||||
|     def test_auth_param(self): |     def test_auth_param(self): | ||||||
|         self.make_env(exclude='OS_USERNAME') |         self.make_env(exclude='OS_USERNAME') | ||||||
|         self.test_help() |         self.test_help() | ||||||
|  |  | ||||||
|  |     @mock.patch.object(ksclient, 'Client') | ||||||
|  |     def test_debug_switch_raises_error(self, mock_ksclient): | ||||||
|  |         mock_ksclient.side_effect = exc.Unauthorized | ||||||
|  |         self.make_env() | ||||||
|  |         args = ['--debug', 'event-list'] | ||||||
|  |         self.assertRaises(exc.Unauthorized, ceilometer_shell.main, args) | ||||||
|  |  | ||||||
|  |     @mock.patch.object(ksclient, 'Client') | ||||||
|  |     def test_dash_d_switch_raises_error(self, mock_ksclient): | ||||||
|  |         mock_ksclient.side_effect = exc.CommandError("FAIL") | ||||||
|  |         self.make_env() | ||||||
|  |         args = ['-d', 'event-list'] | ||||||
|  |         self.assertRaises(exc.CommandError, ceilometer_shell.main, args) | ||||||
|  |  | ||||||
|  |     @mock.patch.object(ksclient, 'Client') | ||||||
|  |     def test_no_debug_switch_no_raises_errors(self, mock_ksclient): | ||||||
|  |         mock_ksclient.side_effect = exc.Unauthorized("FAIL") | ||||||
|  |         self.make_env() | ||||||
|  |         args = ['event-list'] | ||||||
|  |         self.assertRaises(SystemExit, ceilometer_shell.main, args) | ||||||
|   | |||||||
| @@ -14,7 +14,8 @@ | |||||||
| #    under the License. | #    under the License. | ||||||
|  |  | ||||||
|  |  | ||||||
| import cStringIO | import mock | ||||||
|  | import six | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from ceilometerclient.common import utils | from ceilometerclient.common import utils | ||||||
| @@ -31,7 +32,7 @@ class UtilsTest(test_utils.BaseTestCase): | |||||||
|         # test that the prettytable output is wellformatted (left-aligned) |         # test that the prettytable output is wellformatted (left-aligned) | ||||||
|         saved_stdout = sys.stdout |         saved_stdout = sys.stdout | ||||||
|         try: |         try: | ||||||
|             sys.stdout = output_dict = cStringIO.StringIO() |             sys.stdout = output_dict = six.StringIO() | ||||||
|             utils.print_dict({'K': 'k', 'Key': 'Value'}) |             utils.print_dict({'K': 'k', 'Key': 'Value'}) | ||||||
|  |  | ||||||
|         finally: |         finally: | ||||||
| @@ -44,6 +45,64 @@ class UtilsTest(test_utils.BaseTestCase): | |||||||
| | K        | k     | | | K        | k     | | ||||||
| | Key      | Value | | | Key      | Value | | ||||||
| +----------+-------+ | +----------+-------+ | ||||||
|  | ''') | ||||||
|  |  | ||||||
|  |     def test_print_list(self): | ||||||
|  |         class Foo: | ||||||
|  |             def __init__(self, one, two, three): | ||||||
|  |                 self.one = one | ||||||
|  |                 self.two = two | ||||||
|  |                 self.three = three | ||||||
|  |  | ||||||
|  |         foo_list = [ | ||||||
|  |             Foo(10, 'a', 'B'), | ||||||
|  |             Foo(8, 'c', 'c'), | ||||||
|  |             Foo(12, '0', 'Z')] | ||||||
|  |  | ||||||
|  |         def do_print_list(sortby): | ||||||
|  |             saved_stdout = sys.stdout | ||||||
|  |             try: | ||||||
|  |                 sys.stdout = output = six.StringIO() | ||||||
|  |                 utils.print_list(foo_list, | ||||||
|  |                                  ['one', 'two', 'three'], | ||||||
|  |                                  ['1st', '2nd', '3rd'], | ||||||
|  |                                  {'one': lambda o: o.one * 10}, | ||||||
|  |                                  sortby) | ||||||
|  |             finally: | ||||||
|  |                 sys.stdout = saved_stdout | ||||||
|  |             return output.getvalue() | ||||||
|  |  | ||||||
|  |         printed = do_print_list(None) | ||||||
|  |         self.assertEqual(printed, '''\ | ||||||
|  | +-----+-----+-----+ | ||||||
|  | | 1st | 2nd | 3rd | | ||||||
|  | +-----+-----+-----+ | ||||||
|  | | 100 | a   | B   | | ||||||
|  | | 80  | c   | c   | | ||||||
|  | | 120 | 0   | Z   | | ||||||
|  | +-----+-----+-----+ | ||||||
|  | ''') | ||||||
|  |  | ||||||
|  |         printed = do_print_list(0) | ||||||
|  |         self.assertEqual(printed, '''\ | ||||||
|  | +-----+-----+-----+ | ||||||
|  | | 1st | 2nd | 3rd | | ||||||
|  | +-----+-----+-----+ | ||||||
|  | | 80  | c   | c   | | ||||||
|  | | 100 | a   | B   | | ||||||
|  | | 120 | 0   | Z   | | ||||||
|  | +-----+-----+-----+ | ||||||
|  | ''') | ||||||
|  |  | ||||||
|  |         printed = do_print_list(1) | ||||||
|  |         self.assertEqual(printed, '''\ | ||||||
|  | +-----+-----+-----+ | ||||||
|  | | 1st | 2nd | 3rd | | ||||||
|  | +-----+-----+-----+ | ||||||
|  | | 120 | 0   | Z   | | ||||||
|  | | 100 | a   | B   | | ||||||
|  | | 80  | c   | c   | | ||||||
|  | +-----+-----+-----+ | ||||||
| ''') | ''') | ||||||
|  |  | ||||||
|     def test_args_array_to_dict(self): |     def test_args_array_to_dict(self): | ||||||
| @@ -124,3 +183,27 @@ class UtilsTest(test_utils.BaseTestCase): | |||||||
|         self.assertEqual(dest, {'key': 'modified', |         self.assertEqual(dest, {'key': 'modified', | ||||||
|                                 'nested': {'key3': 'modified3', |                                 'nested': {'key3': 'modified3', | ||||||
|                                            'nested2': {'key5': 'value5'}}}) |                                            'nested2': {'key5': 'value5'}}}) | ||||||
|  |  | ||||||
|  |     @mock.patch('prettytable.PrettyTable') | ||||||
|  |     def test_format_nested_list_of_dict(self, pt_mock): | ||||||
|  |         actual_rows = [] | ||||||
|  |  | ||||||
|  |         def mock_add_row(row): | ||||||
|  |             actual_rows.append(row) | ||||||
|  |  | ||||||
|  |         table = mock.Mock() | ||||||
|  |         table.add_row = mock_add_row | ||||||
|  |         table.get_string.return_value = "the table" | ||||||
|  |  | ||||||
|  |         test_data = [ | ||||||
|  |             {'column_1': 'value_11', 'column_2': 'value_21'}, | ||||||
|  |             {'column_1': 'value_12', 'column_2': 'value_22'} | ||||||
|  |  | ||||||
|  |         ] | ||||||
|  |         columns = ['column_1', 'column_2'] | ||||||
|  |         pt_mock.return_value = table | ||||||
|  |  | ||||||
|  |         rval = utils.format_nested_list_of_dict(test_data, columns) | ||||||
|  |         self.assertEqual("the table", rval) | ||||||
|  |         self.assertEqual([['value_11', 'value_21'], ['value_12', 'value_22']], | ||||||
|  |                          actual_rows) | ||||||
|   | |||||||
| @@ -15,8 +15,7 @@ | |||||||
|  |  | ||||||
| import copy | import copy | ||||||
| import fixtures | import fixtures | ||||||
| import mox | import six | ||||||
| import StringIO |  | ||||||
| import testtools | import testtools | ||||||
|  |  | ||||||
| from ceilometerclient.common import http | from ceilometerclient.common import http | ||||||
| @@ -26,8 +25,6 @@ class BaseTestCase(testtools.TestCase): | |||||||
|  |  | ||||||
|     def setUp(self): |     def setUp(self): | ||||||
|         super(BaseTestCase, self).setUp() |         super(BaseTestCase, self).setUp() | ||||||
|         self.m = mox.Mox() |  | ||||||
|         self.addCleanup(self.m.UnsetStubs) |  | ||||||
|         self.useFixture(fixtures.FakeLogger()) |         self.useFixture(fixtures.FakeLogger()) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -43,7 +40,7 @@ class FakeAPI(object): | |||||||
|  |  | ||||||
|     def raw_request(self, *args, **kwargs): |     def raw_request(self, *args, **kwargs): | ||||||
|         fixture = self._request(*args, **kwargs) |         fixture = self._request(*args, **kwargs) | ||||||
|         body_iter = http.ResponseBodyIterator(StringIO.StringIO(fixture[1])) |         body_iter = http.ResponseBodyIterator(six.StringIO(fixture[1])) | ||||||
|         return FakeResponse(fixture[0]), body_iter |         return FakeResponse(fixture[0]), body_iter | ||||||
|  |  | ||||||
|     def json_request(self, *args, **kwargs): |     def json_request(self, *args, **kwargs): | ||||||
|   | |||||||
| @@ -17,6 +17,9 @@ | |||||||
| #    under the License. | #    under the License. | ||||||
|  |  | ||||||
| import copy | import copy | ||||||
|  |  | ||||||
|  | import six | ||||||
|  | from six.moves import xrange  # noqa | ||||||
| import testtools | import testtools | ||||||
|  |  | ||||||
| from ceilometerclient.tests import utils | from ceilometerclient.tests import utils | ||||||
| @@ -185,8 +188,8 @@ fixtures = { | |||||||
|         ), |         ), | ||||||
|  |  | ||||||
|     }, |     }, | ||||||
|     '/v2/alarms?q.op=&q.op=&q.value=project-id&q.value=SwiftObjectAlarm' |     '/v2/alarms?q.field=project_id&q.field=name&q.op=&q.op=' | ||||||
|     '&q.field=project_id&q.field=name': |     '&q.type=&q.type=&q.value=project-id&q.value=SwiftObjectAlarm': | ||||||
|     { |     { | ||||||
|         'GET': ( |         'GET': ( | ||||||
|             {}, |             {}, | ||||||
| @@ -207,7 +210,7 @@ fixtures = { | |||||||
|             ALARM_HISTORY, |             ALARM_HISTORY, | ||||||
|         ), |         ), | ||||||
|     }, |     }, | ||||||
|     '/v2/alarms/alarm-id/history?q.op=&q.value=NOW&q.field=timestamp': |     '/v2/alarms/alarm-id/history?q.field=timestamp&q.op=&q.type=&q.value=NOW': | ||||||
|     { |     { | ||||||
|         'GET': ( |         'GET': ( | ||||||
|             {}, |             {}, | ||||||
| @@ -234,16 +237,14 @@ class AlarmManagerTest(testtools.TestCase): | |||||||
|         self.assertEqual(alarms[0].alarm_id, 'alarm-id') |         self.assertEqual(alarms[0].alarm_id, 'alarm-id') | ||||||
|  |  | ||||||
|     def test_list_with_query(self): |     def test_list_with_query(self): | ||||||
|         alarms = list(self.mgr.list(q=[ |         alarms = list(self.mgr.list(q=[{"field": "project_id", | ||||||
|                                       {"field": "project_id", |                                         "value": "project-id"}, | ||||||
|                                        "value": "project-id"}, |                                        {"field": "name", | ||||||
|                                       {"field": "name", |                                         "value": "SwiftObjectAlarm"}])) | ||||||
|                                        "value": "SwiftObjectAlarm"}, |  | ||||||
|                                      ])) |  | ||||||
|         expect = [ |         expect = [ | ||||||
|             ('GET', |             ('GET', | ||||||
|              '/v2/alarms?q.op=&q.op=&q.value=project-id&q.value=' |              '/v2/alarms?q.field=project_id&q.field=name&q.op=&q.op=' | ||||||
|              'SwiftObjectAlarm&q.field=project_id&q.field=name', |              '&q.type=&q.type=&q.value=project-id&q.value=SwiftObjectAlarm', | ||||||
|              {}, None), |              {}, None), | ||||||
|         ] |         ] | ||||||
|         self.assertEqual(self.api.calls, expect) |         self.assertEqual(self.api.calls, expect) | ||||||
| @@ -277,7 +278,7 @@ class AlarmManagerTest(testtools.TestCase): | |||||||
|         self.assertEqual(self.api.calls, expect) |         self.assertEqual(self.api.calls, expect) | ||||||
|         self.assertTrue(alarm) |         self.assertTrue(alarm) | ||||||
|         self.assertEqual(alarm.alarm_id, 'alarm-id') |         self.assertEqual(alarm.alarm_id, 'alarm-id') | ||||||
|         for (key, value) in UPDATED_ALARM.iteritems(): |         for (key, value) in six.iteritems(UPDATED_ALARM): | ||||||
|             self.assertEqual(getattr(alarm, key), value) |             self.assertEqual(getattr(alarm, key), value) | ||||||
|  |  | ||||||
|     def test_update_delta(self): |     def test_update_delta(self): | ||||||
| @@ -289,7 +290,7 @@ class AlarmManagerTest(testtools.TestCase): | |||||||
|         self.assertEqual(self.api.calls, expect) |         self.assertEqual(self.api.calls, expect) | ||||||
|         self.assertTrue(alarm) |         self.assertTrue(alarm) | ||||||
|         self.assertEqual(alarm.alarm_id, 'alarm-id') |         self.assertEqual(alarm.alarm_id, 'alarm-id') | ||||||
|         for (key, value) in UPDATED_ALARM.iteritems(): |         for (key, value) in six.iteritems(UPDATED_ALARM): | ||||||
|             self.assertEqual(getattr(alarm, key), value) |             self.assertEqual(getattr(alarm, key), value) | ||||||
|  |  | ||||||
|     def test_set_state(self): |     def test_set_state(self): | ||||||
| @@ -322,8 +323,8 @@ class AlarmManagerTest(testtools.TestCase): | |||||||
|         self.assertEqual(self.api.calls, expect) |         self.assertEqual(self.api.calls, expect) | ||||||
|         for i in xrange(len(history)): |         for i in xrange(len(history)): | ||||||
|             change = history[i] |             change = history[i] | ||||||
|             self.assertTrue(isinstance(change, alarms.AlarmChange)) |             self.assertIsInstance(change, alarms.AlarmChange) | ||||||
|             for k, v in ALARM_HISTORY[i].iteritems(): |             for k, v in six.iteritems(ALARM_HISTORY[i]): | ||||||
|                 self.assertEqual(getattr(change, k), v) |                 self.assertEqual(getattr(change, k), v) | ||||||
|  |  | ||||||
|     def test_get_all_history(self): |     def test_get_all_history(self): | ||||||
| @@ -332,8 +333,8 @@ class AlarmManagerTest(testtools.TestCase): | |||||||
|  |  | ||||||
|     def test_get_constrained_history(self): |     def test_get_constrained_history(self): | ||||||
|         q = [dict(field='timestamp', value='NOW')] |         q = [dict(field='timestamp', value='NOW')] | ||||||
|         url = ('/v2/alarms/alarm-id/history' |         url = ('/v2/alarms/alarm-id/history?q.field=timestamp' | ||||||
|                '?q.op=&q.value=NOW&q.field=timestamp') |                '&q.op=&q.type=&q.value=NOW') | ||||||
|         self._do_test_get_history(q, url) |         self._do_test_get_history(q, url) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -373,7 +374,7 @@ class AlarmLegacyManagerTest(testtools.TestCase): | |||||||
|         self.assertEqual(self.api.calls, expect) |         self.assertEqual(self.api.calls, expect) | ||||||
|         self.assertTrue(alarm) |         self.assertTrue(alarm) | ||||||
|         self.assertEqual(alarm.alarm_id, 'alarm-id') |         self.assertEqual(alarm.alarm_id, 'alarm-id') | ||||||
|         for (key, value) in UPDATED_ALARM.iteritems(): |         for (key, value) in six.iteritems(UPDATED_ALARM): | ||||||
|             self.assertEqual(getattr(alarm, key), value) |             self.assertEqual(getattr(alarm, key), value) | ||||||
|  |  | ||||||
|     def test_update_counter_name(self): |     def test_update_counter_name(self): | ||||||
| @@ -389,5 +390,5 @@ class AlarmLegacyManagerTest(testtools.TestCase): | |||||||
|         self.assertEqual(self.api.calls, expect) |         self.assertEqual(self.api.calls, expect) | ||||||
|         self.assertTrue(alarm) |         self.assertTrue(alarm) | ||||||
|         self.assertEqual(alarm.alarm_id, 'alarm-id') |         self.assertEqual(alarm.alarm_id, 'alarm-id') | ||||||
|         for (key, value) in UPDATED_ALARM.iteritems(): |         for (key, value) in six.iteritems(UPDATED_ALARM): | ||||||
|             self.assertEqual(getattr(alarm, key), value) |             self.assertEqual(getattr(alarm, key), value) | ||||||
|   | |||||||
							
								
								
									
										47
									
								
								ceilometerclient/tests/v2/test_event_types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								ceilometerclient/tests/v2/test_event_types.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,47 @@ | |||||||
|  | # -*- encoding: utf-8 -*- | ||||||
|  | # Copyright 2014 Hewlett-Packard Development Company, L.P. | ||||||
|  | # | ||||||
|  | #    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 ceilometerclient.tests import utils | ||||||
|  | import ceilometerclient.v2.event_types | ||||||
|  |  | ||||||
|  |  | ||||||
|  | fixtures = { | ||||||
|  |     '/v2/event_types/': { | ||||||
|  |         'GET': ( | ||||||
|  |             {}, | ||||||
|  |             ['Foo', 'Bar', 'Sna', 'Fu'] | ||||||
|  |         ), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EventTypesManagerTest(utils.BaseTestCase): | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super(EventTypesManagerTest, self).setUp() | ||||||
|  |         self.api = utils.FakeAPI(fixtures) | ||||||
|  |         self.mgr = ceilometerclient.v2.event_types.EventTypeManager(self.api) | ||||||
|  |  | ||||||
|  |     def test_list(self): | ||||||
|  |         event_types = list(self.mgr.list()) | ||||||
|  |         expect = [ | ||||||
|  |             ('GET', '/v2/event_types/', {}, None), | ||||||
|  |         ] | ||||||
|  |         self.assertEqual(self.api.calls, expect) | ||||||
|  |         self.assertEqual(len(event_types), 4) | ||||||
|  |         self.assertEqual(event_types[0].event_type, "Foo") | ||||||
|  |         self.assertEqual(event_types[1].event_type, "Bar") | ||||||
|  |         self.assertEqual(event_types[2].event_type, "Sna") | ||||||
|  |         self.assertEqual(event_types[3].event_type, "Fu") | ||||||
							
								
								
									
										189
									
								
								ceilometerclient/tests/v2/test_events.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								ceilometerclient/tests/v2/test_events.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,189 @@ | |||||||
|  | # -*- encoding: utf-8 -*- | ||||||
|  | # Copyright 2014 Hewlett-Packard Development Company, L.P. | ||||||
|  | # | ||||||
|  | #    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 ceilometerclient.tests import utils | ||||||
|  | import ceilometerclient.v2.events | ||||||
|  |  | ||||||
|  |  | ||||||
|  | fixtures = { | ||||||
|  |     '/v2/events': { | ||||||
|  |         'GET': ( | ||||||
|  |             {}, | ||||||
|  |             [ | ||||||
|  |                 { | ||||||
|  |                     'event_type': 'Foo', | ||||||
|  |                     'generated': '1970-01-01T00:00:00', | ||||||
|  |                     'traits': {'trait_A': 'abc', | ||||||
|  |                                'message_id': '1'}, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     'event_type': 'Foo', | ||||||
|  |                     'generated': '1970-01-01T00:00:00', | ||||||
|  |                     'traits': {'trait_A': 'def', | ||||||
|  |                                'message_id': '2'}, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     'event_type': 'Bar', | ||||||
|  |                     'generated': '1970-01-01T00:00:00', | ||||||
|  |                     'traits': {'trait_B': 'bartrait', | ||||||
|  |                                'message_id': '3'}, | ||||||
|  |                 }, | ||||||
|  |             ] | ||||||
|  |         ), | ||||||
|  |     }, | ||||||
|  |     '/v2/events?q.field=hostname&q.op=&q.type=string&q.value=localhost': | ||||||
|  |     { | ||||||
|  |         'GET': ( | ||||||
|  |             {}, | ||||||
|  |             [ | ||||||
|  |                 { | ||||||
|  |                     'event_type': 'Foo', | ||||||
|  |                     'generated': '1970-01-01T00:00:00', | ||||||
|  |                     'traits': {'trait_A': 'abc', | ||||||
|  |                                'hostname': 'localhost', | ||||||
|  |                                'message_id': '1'}, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     'event_type': 'Foo', | ||||||
|  |                     'generated': '1970-01-01T00:00:00', | ||||||
|  |                     'traits': {'trait_A': 'def', | ||||||
|  |                                'hostname': 'localhost', | ||||||
|  |                                'message_id': '2'}, | ||||||
|  |                 } | ||||||
|  |             ] | ||||||
|  |         ), | ||||||
|  |     }, | ||||||
|  |     '/v2/events?q.field=hostname&q.op=&q.type=&q.value=foreignhost': | ||||||
|  |     { | ||||||
|  |         'GET': ( | ||||||
|  |             {}, | ||||||
|  |             [ | ||||||
|  |                 { | ||||||
|  |                     'event_type': 'Foo', | ||||||
|  |                     'generated': '1970-01-01T00:00:00', | ||||||
|  |                     'traits': {'trait_A': 'abc', | ||||||
|  |                                'hostname': 'foreignhost', | ||||||
|  |                                'message_id': '1'}, | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     'event_type': 'Foo', | ||||||
|  |                     'generated': '1970-01-01T00:00:00', | ||||||
|  |                     'traits': {'trait_A': 'def', | ||||||
|  |                                'hostname': 'foreignhost', | ||||||
|  |                                'message_id': '2'}, | ||||||
|  |                 } | ||||||
|  |             ] | ||||||
|  |         ), | ||||||
|  |     }, | ||||||
|  |     '/v2/events?q.field=hostname&q.field=num_cpus&q.op=&q.op=' | ||||||
|  |     '&q.type=&q.type=integer&q.value=localhost&q.value=5': | ||||||
|  |     { | ||||||
|  |         'GET': ( | ||||||
|  |             {}, | ||||||
|  |             [ | ||||||
|  |                 { | ||||||
|  |                     'event_type': 'Bar', | ||||||
|  |                     'generated': '1970-01-01T00:00:00', | ||||||
|  |                     'traits': {'trait_A': 'abc', | ||||||
|  |                                'hostname': 'localhost', | ||||||
|  |                                'num_cpus': '5', | ||||||
|  |                                'message_id': '1'}, | ||||||
|  |                 }, | ||||||
|  |             ] | ||||||
|  |         ), | ||||||
|  |     }, | ||||||
|  |  | ||||||
|  |     '/v2/events/2': | ||||||
|  |     { | ||||||
|  |         'GET': ( | ||||||
|  |             {}, | ||||||
|  |             { | ||||||
|  |                 'event_type': 'Foo', | ||||||
|  |                 'generated': '1970-01-01T00:00:00', | ||||||
|  |                 'traits': {'trait_A': 'def', | ||||||
|  |                            'message_id': '2', | ||||||
|  |                            'intTrait': '42'}, | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |     }, | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EventManagerTest(utils.BaseTestCase): | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super(EventManagerTest, self).setUp() | ||||||
|  |         self.api = utils.FakeAPI(fixtures) | ||||||
|  |         self.mgr = ceilometerclient.v2.events.EventManager(self.api) | ||||||
|  |  | ||||||
|  |     def test_list_all(self): | ||||||
|  |         events = list(self.mgr.list()) | ||||||
|  |         expect = [ | ||||||
|  |             ('GET', '/v2/events', {}, None), | ||||||
|  |         ] | ||||||
|  |         self.assertEqual(self.api.calls, expect) | ||||||
|  |         self.assertEqual(len(events), 3) | ||||||
|  |         self.assertEqual(events[0].event_type, 'Foo') | ||||||
|  |         self.assertEqual(events[1].event_type, 'Foo') | ||||||
|  |         self.assertEqual(events[2].event_type, 'Bar') | ||||||
|  |  | ||||||
|  |     def test_list_one(self): | ||||||
|  |         event = self.mgr.get(2) | ||||||
|  |         expect = [ | ||||||
|  |             ('GET', '/v2/events/2', {}, None), | ||||||
|  |         ] | ||||||
|  |         self.assertEqual(self.api.calls, expect) | ||||||
|  |         self.assertTrue(event) | ||||||
|  |         self.assertEqual(event.event_type, 'Foo') | ||||||
|  |  | ||||||
|  |     def test_list_with_query(self): | ||||||
|  |         events = list(self.mgr.list(q=[{"field": "hostname", | ||||||
|  |                                         "value": "localhost", | ||||||
|  |                                         "type": "string"}])) | ||||||
|  |         expect = [ | ||||||
|  |             ('GET', '/v2/events?q.field=hostname&q.op=&q.type=string' | ||||||
|  |                     '&q.value=localhost', | ||||||
|  |              {}, None), | ||||||
|  |         ] | ||||||
|  |         self.assertEqual(self.api.calls, expect) | ||||||
|  |         self.assertEqual(len(events), 2) | ||||||
|  |         self.assertEqual(events[0].event_type, 'Foo') | ||||||
|  |  | ||||||
|  |     def test_list_with_query_no_type(self): | ||||||
|  |         events = list(self.mgr.list(q=[{"field": "hostname", | ||||||
|  |                                         "value": "foreignhost"}])) | ||||||
|  |         expect = [ | ||||||
|  |             ('GET', '/v2/events?q.field=hostname&q.op=' | ||||||
|  |                     '&q.type=&q.value=foreignhost', | ||||||
|  |              {}, None), | ||||||
|  |         ] | ||||||
|  |         self.assertEqual(self.api.calls, expect) | ||||||
|  |         self.assertEqual(len(events), 2) | ||||||
|  |         self.assertEqual(events[0].event_type, 'Foo') | ||||||
|  |  | ||||||
|  |     def test_list_with_multiple_filters(self): | ||||||
|  |         events = list(self.mgr.list(q=[{"field": "hostname", | ||||||
|  |                                         "value": "localhost"}, | ||||||
|  |                                        {"field": "num_cpus", | ||||||
|  |                                         "value": "5", | ||||||
|  |                                         "type": "integer"}])) | ||||||
|  |  | ||||||
|  |         expect = [ | ||||||
|  |             ('GET', '/v2/events?q.field=hostname&q.field=num_cpus&q.op=&q.op=' | ||||||
|  |                     '&q.type=&q.type=integer&q.value=localhost&q.value=5', | ||||||
|  |              {}, None), | ||||||
|  |         ] | ||||||
|  |         self.assertEqual(self.api.calls, expect) | ||||||
|  |         self.assertEqual(len(events), 1) | ||||||
| @@ -21,7 +21,7 @@ class BuildUrlTest(utils.BaseTestCase): | |||||||
|         url = options.build_url('/', [{'field': 'this', |         url = options.build_url('/', [{'field': 'this', | ||||||
|                                        'op': 'gt', |                                        'op': 'gt', | ||||||
|                                        'value': 43}]) |                                        'value': 43}]) | ||||||
|         self.assertEqual(url, '/?q.op=gt&q.value=43&q.field=this') |         self.assertEqual(url, '/?q.field=this&q.op=gt&q.type=&q.value=43') | ||||||
|  |  | ||||||
|     def test_two(self): |     def test_two(self): | ||||||
|         url = options.build_url('/', [{'field': 'this', |         url = options.build_url('/', [{'field': 'this', | ||||||
| @@ -32,13 +32,14 @@ class BuildUrlTest(utils.BaseTestCase): | |||||||
|                                        'value': 88}]) |                                        'value': 88}]) | ||||||
|         ops = 'q.op=gt&q.op=lt' |         ops = 'q.op=gt&q.op=lt' | ||||||
|         vals = 'q.value=43&q.value=88' |         vals = 'q.value=43&q.value=88' | ||||||
|  |         types = 'q.type=&q.type=' | ||||||
|         fields = 'q.field=this&q.field=that' |         fields = 'q.field=this&q.field=that' | ||||||
|         self.assertEqual(url, '/?%s&%s&%s' % (ops, vals, fields)) |         self.assertEqual(url, '/?%s&%s&%s&%s' % (fields, ops, types, vals)) | ||||||
|  |  | ||||||
|     def test_default_op(self): |     def test_default_op(self): | ||||||
|         url = options.build_url('/', [{'field': 'this', |         url = options.build_url('/', [{'field': 'this', | ||||||
|                                        'value': 43}]) |                                        'value': 43}]) | ||||||
|         self.assertEqual(url, '/?q.op=&q.value=43&q.field=this') |         self.assertEqual(url, '/?q.field=this&q.op=&q.type=&q.value=43') | ||||||
|  |  | ||||||
|     def test_one_param(self): |     def test_one_param(self): | ||||||
|         url = options.build_url('/', None, ['period=60']) |         url = options.build_url('/', None, ['period=60']) | ||||||
| @@ -49,26 +50,38 @@ class BuildUrlTest(utils.BaseTestCase): | |||||||
|                                             'others=value']) |                                             'others=value']) | ||||||
|         self.assertEqual(url, '/?period=60&others=value') |         self.assertEqual(url, '/?period=60&others=value') | ||||||
|  |  | ||||||
|  |     def test_with_data_type(self): | ||||||
|  |         url = options.build_url('/', [{'field': 'f1', | ||||||
|  |                                        'value': '10', | ||||||
|  |                                        'type': 'integer'}]) | ||||||
|  |  | ||||||
|  |         self.assertEqual('/?q.field=f1&q.op=&q.type=integer&q.value=10', url) | ||||||
|  |  | ||||||
|  |  | ||||||
| class CliTest(utils.BaseTestCase): | class CliTest(utils.BaseTestCase): | ||||||
|  |  | ||||||
|     def test_one(self): |     def test_one(self): | ||||||
|         ar = options.cli_to_array('this<=34') |         ar = options.cli_to_array('this<=34') | ||||||
|         self.assertEqual(ar, [{'field': 'this', 'op': 'le', 'value': '34'}]) |         self.assertEqual(ar, [{'field': 'this', 'op': 'le', | ||||||
|  |                                'value': '34', 'type': ''}]) | ||||||
|  |  | ||||||
|     def test_two(self): |     def test_two(self): | ||||||
|         ar = options.cli_to_array('this<=34;that!=foo') |         ar = options.cli_to_array('this<=34;that!=foo') | ||||||
|         self.assertEqual(ar, [{'field': 'this', 'op': 'le', 'value': '34'}, |         self.assertEqual(ar, [{'field': 'this', 'op': 'le', | ||||||
|                               {'field': 'that', 'op': 'ne', 'value': 'foo'}]) |                                'value': '34', 'type': ''}, | ||||||
|  |                               {'field': 'that', 'op': 'ne', | ||||||
|  |                                'value': 'foo', 'type': ''}]) | ||||||
|  |  | ||||||
|     def test_negative(self): |     def test_negative(self): | ||||||
|         ar = options.cli_to_array('this>=-783') |         ar = options.cli_to_array('this>=-783') | ||||||
|         self.assertEqual(ar, [{'field': 'this', 'op': 'ge', 'value': '-783'}]) |         self.assertEqual(ar, [{'field': 'this', 'op': 'ge', | ||||||
|  |                                'value': '-783', 'type': ''}]) | ||||||
|  |  | ||||||
|     def test_float(self): |     def test_float(self): | ||||||
|         ar = options.cli_to_array('this<=283.347') |         ar = options.cli_to_array('this<=283.347') | ||||||
|         self.assertEqual(ar, [{'field': 'this', |         self.assertEqual(ar, [{'field': 'this', | ||||||
|                                'op': 'le', 'value': '283.347'}]) |                                'op': 'le', 'value': '283.347', | ||||||
|  |                                'type': ''}]) | ||||||
|  |  | ||||||
|     def test_invalid_seperator(self): |     def test_invalid_seperator(self): | ||||||
|         self.assertRaises(ValueError, options.cli_to_array, |         self.assertRaises(ValueError, options.cli_to_array, | ||||||
| @@ -81,4 +94,61 @@ class CliTest(utils.BaseTestCase): | |||||||
|     def test_with_dot(self): |     def test_with_dot(self): | ||||||
|         ar = options.cli_to_array('metadata.this<=34') |         ar = options.cli_to_array('metadata.this<=34') | ||||||
|         self.assertEqual(ar, [{'field': 'metadata.this', |         self.assertEqual(ar, [{'field': 'metadata.this', | ||||||
|                                'op': 'le', 'value': '34'}]) |                                'op': 'le', 'value': '34', | ||||||
|  |                                'type': ''}]) | ||||||
|  |  | ||||||
|  |     def test_without_data_type(self): | ||||||
|  |         ar = options.cli_to_array('hostname=localhost') | ||||||
|  |         self.assertEqual(ar, [{'field': 'hostname', | ||||||
|  |                                'op': 'eq', | ||||||
|  |                                'value': 'localhost', | ||||||
|  |                                'type': ''}]) | ||||||
|  |  | ||||||
|  |     def test_with_string_data_type(self): | ||||||
|  |         ar = options.cli_to_array('hostname=string::localhost') | ||||||
|  |         self.assertEqual(ar, [{'field': 'hostname', | ||||||
|  |                                'op': 'eq', | ||||||
|  |                                'type': 'string', | ||||||
|  |                                'value': 'localhost'}]) | ||||||
|  |  | ||||||
|  |     def test_with_int_data_type(self): | ||||||
|  |         ar = options.cli_to_array('port=integer::1234') | ||||||
|  |         self.assertEqual(ar, [{'field': 'port', | ||||||
|  |                                'op': 'eq', | ||||||
|  |                                'type': 'integer', | ||||||
|  |                                'value': '1234'}]) | ||||||
|  |  | ||||||
|  |     def test_with_bool_data_type(self): | ||||||
|  |         ar = options.cli_to_array('port=boolean::true') | ||||||
|  |         self.assertEqual(ar, [{'field': 'port', | ||||||
|  |                                'op': 'eq', | ||||||
|  |                                'type': 'boolean', | ||||||
|  |                                'value': 'true'}]) | ||||||
|  |  | ||||||
|  |     def test_with_float_data_type(self): | ||||||
|  |         ar = options.cli_to_array('average=float::1234.5678') | ||||||
|  |         self.assertEqual(ar, [{'field': 'average', | ||||||
|  |                                'op': 'eq', | ||||||
|  |                                'type': 'float', | ||||||
|  |                                'value': '1234.5678'}]) | ||||||
|  |  | ||||||
|  |     def test_with_datetime_data_type(self): | ||||||
|  |         ar = options.cli_to_array('timestamp=datetime::sometimestamp') | ||||||
|  |         self.assertEqual(ar, [{'field': 'timestamp', | ||||||
|  |                                'op': 'eq', | ||||||
|  |                                'type': 'datetime', | ||||||
|  |                                'value': 'sometimestamp'}]) | ||||||
|  |  | ||||||
|  |     def test_with_incorrect_type(self): | ||||||
|  |         ar = options.cli_to_array('timestamp=invalid::sometimestamp') | ||||||
|  |         self.assertEqual(ar, [{'field': 'timestamp', | ||||||
|  |                                'op': 'eq', | ||||||
|  |                                'type': '', | ||||||
|  |                                'value': 'invalid::sometimestamp'}]) | ||||||
|  |  | ||||||
|  |     def test_with_single_colon(self): | ||||||
|  |         ar = options.cli_to_array('timestamp=datetime:sometimestamp') | ||||||
|  |         self.assertEqual(ar, [{'field': 'timestamp', | ||||||
|  |                                'op': 'eq', | ||||||
|  |                                'type': '', | ||||||
|  |                                'value': 'datetime:sometimestamp'}]) | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ fixtures = { | |||||||
|             ] |             ] | ||||||
|         ), |         ), | ||||||
|     }, |     }, | ||||||
|     '/v2/resources?q.op=&q.value=a&q.field=resource_id': |     '/v2/resources?q.field=resource_id&q.op=&q.type=&q.value=a': | ||||||
|     { |     { | ||||||
|         'GET': ( |         'GET': ( | ||||||
|             {}, |             {}, | ||||||
| @@ -97,7 +97,8 @@ class ResourceManagerTest(utils.BaseTestCase): | |||||||
|                                            "value": "a"}, |                                            "value": "a"}, | ||||||
|                                           ])) |                                           ])) | ||||||
|         expect = [ |         expect = [ | ||||||
|             ('GET', '/v2/resources?q.op=&q.value=a&q.field=resource_id', |             ('GET', '/v2/resources?q.field=resource_id&q.op=' | ||||||
|  |                     '&q.type=&q.value=a', | ||||||
|              {}, None), |              {}, None), | ||||||
|         ] |         ] | ||||||
|         self.assertEqual(self.api.calls, expect) |         self.assertEqual(self.api.calls, expect) | ||||||
|   | |||||||
| @@ -35,7 +35,9 @@ del CREATE_SAMPLE['message_id'] | |||||||
| del CREATE_SAMPLE['source'] | del CREATE_SAMPLE['source'] | ||||||
|  |  | ||||||
| base_url = '/v2/meters/instance' | base_url = '/v2/meters/instance' | ||||||
| args = 'q.op=&q.op=&q.value=foo&q.value=bar&q.field=resource_id&q.field=source' | args = ('q.field=resource_id&q.field=source&q.op=&q.op=' | ||||||
|  |         '&q.type=&q.type=&q.value=foo&q.value=bar') | ||||||
|  | args_limit = 'limit=1' | ||||||
| fixtures = { | fixtures = { | ||||||
|     base_url: |     base_url: | ||||||
|     { |     { | ||||||
| @@ -54,6 +56,13 @@ fixtures = { | |||||||
|             {}, |             {}, | ||||||
|             [], |             [], | ||||||
|         ), |         ), | ||||||
|  |     }, | ||||||
|  |     '%s?%s' % (base_url, args_limit): | ||||||
|  |     { | ||||||
|  |         'GET': ( | ||||||
|  |             {}, | ||||||
|  |             [GET_SAMPLE] | ||||||
|  |         ), | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -93,3 +102,9 @@ class SampleManagerTest(utils.BaseTestCase): | |||||||
|         ] |         ] | ||||||
|         self.assertEqual(self.api.calls, expect) |         self.assertEqual(self.api.calls, expect) | ||||||
|         self.assertTrue(sample) |         self.assertTrue(sample) | ||||||
|  |  | ||||||
|  |     def test_limit(self): | ||||||
|  |         samples = list(self.mgr.list(meter_name='instance', limit=1)) | ||||||
|  |         expect = [('GET', '/v2/meters/instance?limit=1', {}, None)] | ||||||
|  |         self.assertEqual(self.api.calls, expect) | ||||||
|  |         self.assertEqual(len(samples), 1) | ||||||
|   | |||||||
| @@ -10,15 +10,17 @@ | |||||||
| #   License for the specific language governing permissions and limitations | #   License for the specific language governing permissions and limitations | ||||||
| #   under the License. | #   under the License. | ||||||
|  |  | ||||||
| import cStringIO |  | ||||||
| import mock | import mock | ||||||
| import re | import re | ||||||
|  | import six | ||||||
| import sys | import sys | ||||||
|  |  | ||||||
| from testtools import matchers | from testtools import matchers | ||||||
|  |  | ||||||
|  | from ceilometerclient import shell as base_shell | ||||||
| from ceilometerclient.tests import utils | from ceilometerclient.tests import utils | ||||||
| from ceilometerclient.v2 import alarms | from ceilometerclient.v2 import alarms | ||||||
|  | from ceilometerclient.v2 import samples | ||||||
| from ceilometerclient.v2 import shell as ceilometer_shell | from ceilometerclient.v2 import shell as ceilometer_shell | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -102,7 +104,7 @@ class ShellAlarmHistoryCommandTest(utils.BaseTestCase): | |||||||
|     def _do_test_alarm_history(self, raw_query=None, parsed_query=None): |     def _do_test_alarm_history(self, raw_query=None, parsed_query=None): | ||||||
|         self.args.query = raw_query |         self.args.query = raw_query | ||||||
|         orig = sys.stdout |         orig = sys.stdout | ||||||
|         sys.stdout = cStringIO.StringIO() |         sys.stdout = six.StringIO() | ||||||
|         history = [alarms.AlarmChange(mock.Mock(), change) |         history = [alarms.AlarmChange(mock.Mock(), change) | ||||||
|                    for change in self.ALARM_HISTORY] |                    for change in self.ALARM_HISTORY] | ||||||
|         self.cc.alarms.get_history.return_value = history |         self.cc.alarms.get_history.return_value = history | ||||||
| @@ -132,6 +134,280 @@ class ShellAlarmHistoryCommandTest(utils.BaseTestCase): | |||||||
|     def test_alarm_constrained_history(self): |     def test_alarm_constrained_history(self): | ||||||
|         parsed_query = [dict(field='timestamp', |         parsed_query = [dict(field='timestamp', | ||||||
|                              value='2013-10-03T08:59:28', |                              value='2013-10-03T08:59:28', | ||||||
|                              op='gt')] |                              op='gt', | ||||||
|  |                              type='')] | ||||||
|         self._do_test_alarm_history(raw_query='timestamp>2013-10-03T08:59:28', |         self._do_test_alarm_history(raw_query='timestamp>2013-10-03T08:59:28', | ||||||
|                                     parsed_query=parsed_query) |                                     parsed_query=parsed_query) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ShellAlarmCommandTest(utils.BaseTestCase): | ||||||
|  |  | ||||||
|  |     ALARM_ID = '768ff714-8cfb-4db9-9753-d484cb33a1cc' | ||||||
|  |     ALARM = {"alarm_actions": ["log://"], | ||||||
|  |              "ok_actions": [], | ||||||
|  |              "description": "instance running hot", | ||||||
|  |              "timestamp": "2013-11-20T10:38:42.206952", | ||||||
|  |              "enabled": True, | ||||||
|  |              "state_timestamp": "2013-11-19T17:20:44", | ||||||
|  |              "threshold_rule": {"meter_name": "cpu_util", | ||||||
|  |                                 "evaluation_periods": 3, | ||||||
|  |                                 "period": 600, | ||||||
|  |                                 "statistic": "avg", | ||||||
|  |                                 "threshold": 99.0, | ||||||
|  |                                 "query": [{"field": "resource_id", | ||||||
|  |                                            "value": "INSTANCE_ID", | ||||||
|  |                                            "op": "eq"}], | ||||||
|  |                                 "comparison_operator": "gt"}, | ||||||
|  |              "alarm_id": ALARM_ID, | ||||||
|  |              "state": "insufficient data", | ||||||
|  |              "insufficient_data_actions": [], | ||||||
|  |              "repeat_actions": True, | ||||||
|  |              "user_id": "528d9b68fa774689834b5c04b4564f8a", | ||||||
|  |              "project_id": "ed9d4e2be2a748bc80108053cf4598f5", | ||||||
|  |              "type": "threshold", | ||||||
|  |              "name": "cpu_high"} | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super(ShellAlarmCommandTest, self).setUp() | ||||||
|  |         self.cc = mock.Mock() | ||||||
|  |         self.cc.alarms = mock.Mock() | ||||||
|  |         self.args = mock.Mock() | ||||||
|  |         self.args.alarm_id = self.ALARM_ID | ||||||
|  |  | ||||||
|  |     def _do_test_alarm_update_repeat_actions(self, method, repeat_actions): | ||||||
|  |         self.args.threshold = 42.0 | ||||||
|  |         if repeat_actions is not None: | ||||||
|  |             self.args.repeat_actions = repeat_actions | ||||||
|  |         orig = sys.stdout | ||||||
|  |         sys.stdout = six.StringIO() | ||||||
|  |         alarm = [alarms.Alarm(mock.Mock(), self.ALARM)] | ||||||
|  |         self.cc.alarms.get.return_value = alarm | ||||||
|  |         self.cc.alarms.update.return_value = alarm[0] | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             method(self.cc, self.args) | ||||||
|  |             args, kwargs = self.cc.alarms.update.call_args | ||||||
|  |             self.assertEqual(self.ALARM_ID, args[0]) | ||||||
|  |             self.assertEqual(42.0, kwargs.get('threshold')) | ||||||
|  |             if repeat_actions is not None: | ||||||
|  |                 self.assertEqual(repeat_actions, kwargs.get('repeat_actions')) | ||||||
|  |             else: | ||||||
|  |                 self.assertFalse('repeat_actions' in kwargs) | ||||||
|  |         finally: | ||||||
|  |             sys.stdout.close() | ||||||
|  |             sys.stdout = orig | ||||||
|  |  | ||||||
|  |     def test_alarm_update_repeat_actions_untouched(self): | ||||||
|  |         method = ceilometer_shell.do_alarm_update | ||||||
|  |         self._do_test_alarm_update_repeat_actions(method, None) | ||||||
|  |  | ||||||
|  |     def test_alarm_update_repeat_actions_set(self): | ||||||
|  |         method = ceilometer_shell.do_alarm_update | ||||||
|  |         self._do_test_alarm_update_repeat_actions(method, True) | ||||||
|  |  | ||||||
|  |     def test_alarm_update_repeat_actions_clear(self): | ||||||
|  |         method = ceilometer_shell.do_alarm_update | ||||||
|  |         self._do_test_alarm_update_repeat_actions(method, False) | ||||||
|  |  | ||||||
|  |     def test_alarm_combination_update_repeat_actions_untouched(self): | ||||||
|  |         method = ceilometer_shell.do_alarm_combination_update | ||||||
|  |         self._do_test_alarm_update_repeat_actions(method, None) | ||||||
|  |  | ||||||
|  |     def test_alarm_combination_update_repeat_actions_set(self): | ||||||
|  |         method = ceilometer_shell.do_alarm_combination_update | ||||||
|  |         self._do_test_alarm_update_repeat_actions(method, True) | ||||||
|  |  | ||||||
|  |     def test_alarm_combination_update_repeat_actions_clear(self): | ||||||
|  |         method = ceilometer_shell.do_alarm_combination_update | ||||||
|  |         self._do_test_alarm_update_repeat_actions(method, False) | ||||||
|  |  | ||||||
|  |     def test_alarm_threshold_update_repeat_actions_untouched(self): | ||||||
|  |         method = ceilometer_shell.do_alarm_threshold_update | ||||||
|  |         self._do_test_alarm_update_repeat_actions(method, None) | ||||||
|  |  | ||||||
|  |     def test_alarm_threshold_update_repeat_actions_set(self): | ||||||
|  |         method = ceilometer_shell.do_alarm_threshold_update | ||||||
|  |         self._do_test_alarm_update_repeat_actions(method, True) | ||||||
|  |  | ||||||
|  |     def test_alarm_threshold_update_repeat_actions_clear(self): | ||||||
|  |         method = ceilometer_shell.do_alarm_threshold_update | ||||||
|  |         self._do_test_alarm_update_repeat_actions(method, False) | ||||||
|  |  | ||||||
|  |     def test_alarm_threshold_create_args(self): | ||||||
|  |         shell = base_shell.CeilometerShell() | ||||||
|  |         argv = ['alarm-threshold-create', | ||||||
|  |                 '--name', 'cpu_high', | ||||||
|  |                 '--description', 'instance running hot', | ||||||
|  |                 '--meter-name', 'cpu_util', | ||||||
|  |                 '--threshold', '70.0', | ||||||
|  |                 '--comparison-operator', 'gt', | ||||||
|  |                 '--statistic', 'avg', | ||||||
|  |                 '--period', '600', | ||||||
|  |                 '--evaluation-periods', '3', | ||||||
|  |                 '--alarm-action', 'log://', | ||||||
|  |                 '--alarm-action', 'http://example.com/alarm/state', | ||||||
|  |                 '--query', 'resource_id=INSTANCE_ID'] | ||||||
|  |         _, args = shell.parse_args(argv) | ||||||
|  |  | ||||||
|  |         orig = sys.stdout | ||||||
|  |         sys.stdout = six.StringIO() | ||||||
|  |         alarm = alarms.Alarm(mock.Mock(), self.ALARM) | ||||||
|  |         self.cc.alarms.create.return_value = alarm | ||||||
|  |  | ||||||
|  |         try: | ||||||
|  |             ceilometer_shell.do_alarm_threshold_create(self.cc, args) | ||||||
|  |             _, kwargs = self.cc.alarms.create.call_args | ||||||
|  |             self.assertEqual('cpu_high', kwargs.get('name')) | ||||||
|  |             self.assertEqual('instance running hot', kwargs.get('description')) | ||||||
|  |             actions = ['log://', 'http://example.com/alarm/state'] | ||||||
|  |             self.assertEqual(actions, kwargs.get('alarm_actions')) | ||||||
|  |             self.assertTrue('threshold_rule' in kwargs) | ||||||
|  |             rule = kwargs['threshold_rule'] | ||||||
|  |             self.assertEqual('cpu_util', rule.get('meter_name')) | ||||||
|  |             self.assertEqual(70.0, rule.get('threshold')) | ||||||
|  |             self.assertEqual('gt', rule.get('comparison_operator')) | ||||||
|  |             self.assertEqual('avg', rule.get('statistic')) | ||||||
|  |             self.assertEqual(600, rule.get('period')) | ||||||
|  |             self.assertEqual(3, rule.get('evaluation_periods')) | ||||||
|  |             query = dict(field='resource_id', type='', | ||||||
|  |                          value='INSTANCE_ID', op='eq') | ||||||
|  |             self.assertEqual([query], rule['query']) | ||||||
|  |         finally: | ||||||
|  |             sys.stdout.close() | ||||||
|  |             sys.stdout = orig | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ShellSampleListCommandTest(utils.BaseTestCase): | ||||||
|  |  | ||||||
|  |     METER = 'cpu_util' | ||||||
|  |     SAMPLES = [{"counter_name": "cpu_util", | ||||||
|  |                 "resource_id": "5dcf5537-3161-4e25-9235-407e1385bd35", | ||||||
|  |                 "timestamp": "2013-10-15T05:50:30", | ||||||
|  |                 "counter_unit": "%", | ||||||
|  |                 "counter_volume": 0.261666666667, | ||||||
|  |                 "counter_type": "gauge"}, | ||||||
|  |                {"counter_name": "cpu_util", | ||||||
|  |                 "resource_id": "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f", | ||||||
|  |                 "timestamp": "2013-10-15T05:50:29", | ||||||
|  |                 "counter_unit": "%", | ||||||
|  |                 "counter_volume": 0.261666666667, | ||||||
|  |                 "counter_type": "gauge"}, | ||||||
|  |                {"counter_name": "cpu_util", | ||||||
|  |                 "resource_id": "5dcf5537-3161-4e25-9235-407e1385bd35", | ||||||
|  |                 "timestamp": "2013-10-15T05:40:30", | ||||||
|  |                 "counter_unit": "%", | ||||||
|  |                 "counter_volume": 0.251247920133, | ||||||
|  |                 "counter_type": "gauge"}, | ||||||
|  |                {"counter_name": "cpu_util", | ||||||
|  |                 "resource_id": "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f", | ||||||
|  |                 "timestamp": "2013-10-15T05:40:29", | ||||||
|  |                 "counter_unit": "%", | ||||||
|  |                 "counter_volume": 0.26, | ||||||
|  |                 "counter_type": "gauge"}] | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super(ShellSampleListCommandTest, self).setUp() | ||||||
|  |         self.cc = mock.Mock() | ||||||
|  |         self.cc.alarms = mock.Mock() | ||||||
|  |         self.args = mock.Mock() | ||||||
|  |         self.args.meter = self.METER | ||||||
|  |         self.args.query = None | ||||||
|  |         self.args.limit = None | ||||||
|  |  | ||||||
|  |     def test_sample_list(self): | ||||||
|  |  | ||||||
|  |         sample_list = [samples.Sample(mock.Mock(), sample) | ||||||
|  |                        for sample in self.SAMPLES] | ||||||
|  |         self.cc.samples.list.return_value = sample_list | ||||||
|  |  | ||||||
|  |         org_stdout = sys.stdout | ||||||
|  |         try: | ||||||
|  |             sys.stdout = output = six.StringIO() | ||||||
|  |             ceilometer_shell.do_sample_list(self.cc, self.args) | ||||||
|  |             self.cc.samples.list.assert_called_once_with( | ||||||
|  |                 meter_name=self.METER, | ||||||
|  |                 q=None, | ||||||
|  |                 limit=None) | ||||||
|  |         finally: | ||||||
|  |             sys.stdout = org_stdout | ||||||
|  |  | ||||||
|  |         self.assertEqual(output.getvalue(), '''\ | ||||||
|  | +--------------------------------------+----------+-------+----------------\ | ||||||
|  | +------+---------------------+ | ||||||
|  | | Resource ID                          | Name     | Type  | Volume         \ | ||||||
|  | | Unit | Timestamp           | | ||||||
|  | +--------------------------------------+----------+-------+----------------\ | ||||||
|  | +------+---------------------+ | ||||||
|  | | 5dcf5537-3161-4e25-9235-407e1385bd35 | cpu_util | gauge | 0.261666666667 \ | ||||||
|  | | %    | 2013-10-15T05:50:30 | | ||||||
|  | | 87d197e9-9cf6-4c25-bc66-1b1f4cedb52f | cpu_util | gauge | 0.261666666667 \ | ||||||
|  | | %    | 2013-10-15T05:50:29 | | ||||||
|  | | 5dcf5537-3161-4e25-9235-407e1385bd35 | cpu_util | gauge | 0.251247920133 \ | ||||||
|  | | %    | 2013-10-15T05:40:30 | | ||||||
|  | | 87d197e9-9cf6-4c25-bc66-1b1f4cedb52f | cpu_util | gauge | 0.26           \ | ||||||
|  | | %    | 2013-10-15T05:40:29 | | ||||||
|  | +--------------------------------------+----------+-------+----------------\ | ||||||
|  | +------+---------------------+ | ||||||
|  | ''') | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class ShellSampleCreateCommandTest(utils.BaseTestCase): | ||||||
|  |  | ||||||
|  |     METER = 'instance' | ||||||
|  |     METER_TYPE = 'gauge' | ||||||
|  |     RESOURCE_ID = '0564c64c-3545-4e34-abfb-9d18e5f2f2f9' | ||||||
|  |     SAMPLE_VOLUME = '1' | ||||||
|  |     METER_UNIT = 'instance' | ||||||
|  |     SAMPLE = [{ | ||||||
|  |         u'counter_name': u'instance', | ||||||
|  |         u'user_id': u'21b442b8101d407d8242b6610e0ed0eb', | ||||||
|  |         u'resource_id': u'0564c64c-3545-4e34-abfb-9d18e5f2f2f9', | ||||||
|  |         u'timestamp': u'2014-01-10T03: 05: 33.951170', | ||||||
|  |         u'message_id': u'1247cbe6-79a4-11e3-a296-000c294c58e2', | ||||||
|  |         u'source': u'384260c6987b451d8290e66e1f108082: openstack', | ||||||
|  |         u'counter_unit': u'instance', | ||||||
|  |         u'counter_volume': 1.0, | ||||||
|  |         u'project_id': u'384260c6987b451d8290e66e1f108082', | ||||||
|  |         u'resource_metadata': {}, | ||||||
|  |         u'counter_type': u'gauge' | ||||||
|  |     }] | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super(ShellSampleCreateCommandTest, self).setUp() | ||||||
|  |         self.cc = mock.Mock() | ||||||
|  |         self.args = mock.Mock() | ||||||
|  |         self.args.meter_name = self.METER | ||||||
|  |         self.args.meter_type = self.METER_TYPE | ||||||
|  |         self.args.meter_unit = self.METER_UNIT | ||||||
|  |         self.args.resource_id = self.RESOURCE_ID | ||||||
|  |         self.args.sample_volume = self.SAMPLE_VOLUME | ||||||
|  |  | ||||||
|  |     def test_sample_create(self): | ||||||
|  |  | ||||||
|  |         ret_sample = [samples.Sample(mock.Mock(), sample) | ||||||
|  |                       for sample in self.SAMPLE] | ||||||
|  |         self.cc.samples.create.return_value = ret_sample | ||||||
|  |         org_stdout = sys.stdout | ||||||
|  |         try: | ||||||
|  |             sys.stdout = output = six.StringIO() | ||||||
|  |             ceilometer_shell.do_sample_create(self.cc, self.args) | ||||||
|  |         finally: | ||||||
|  |             sys.stdout = org_stdout | ||||||
|  |  | ||||||
|  |         self.assertEqual(output.getvalue(), '''\ | ||||||
|  | +-------------------+---------------------------------------------+ | ||||||
|  | | Property          | Value                                       | | ||||||
|  | +-------------------+---------------------------------------------+ | ||||||
|  | | message_id        | 1247cbe6-79a4-11e3-a296-000c294c58e2        | | ||||||
|  | | name              | instance                                    | | ||||||
|  | | project_id        | 384260c6987b451d8290e66e1f108082            | | ||||||
|  | | resource_id       | 0564c64c-3545-4e34-abfb-9d18e5f2f2f9        | | ||||||
|  | | resource_metadata | {}                                          | | ||||||
|  | | source            | 384260c6987b451d8290e66e1f108082: openstack | | ||||||
|  | | timestamp         | 2014-01-10T03: 05: 33.951170                | | ||||||
|  | | type              | gauge                                       | | ||||||
|  | | unit              | instance                                    | | ||||||
|  | | user_id           | 21b442b8101d407d8242b6610e0ed0eb            | | ||||||
|  | | volume            | 1.0                                         | | ||||||
|  | +-------------------+---------------------------------------------+ | ||||||
|  | ''') | ||||||
|   | |||||||
| @@ -17,19 +17,45 @@ from ceilometerclient.tests import utils | |||||||
| import ceilometerclient.v2.statistics | import ceilometerclient.v2.statistics | ||||||
|  |  | ||||||
| base_url = '/v2/meters/instance/statistics' | base_url = '/v2/meters/instance/statistics' | ||||||
| qry = 'q.op=&q.op=&q.value=foo&q.value=bar&q.field=resource_id&q.field=source' | qry = ('q.field=resource_id&q.field=source&q.op=&q.op=' | ||||||
|  |        '&q.type=&q.type=&q.value=foo&q.value=bar') | ||||||
| period = '&period=60' | period = '&period=60' | ||||||
| samples = [{ | groupby = '&groupby=resource_id' | ||||||
|               u'count': 135, | samples = [ | ||||||
|               u'duration_start': u'2013-02-04T10:51:42', |     {u'count': 135, | ||||||
|               u'min': 1.0, |      u'duration_start': u'2013-02-04T10:51:42', | ||||||
|               u'max': 1.0, |      u'min': 1.0, | ||||||
|               u'duration_end': |      u'max': 1.0, | ||||||
|               u'2013-02-05T15:46:09', |      u'duration_end': | ||||||
|               u'duration': 1734.0, |      u'2013-02-05T15:46:09', | ||||||
|               u'avg': 1.0, |      u'duration': 1734.0, | ||||||
|               u'sum': 135.0, |      u'avg': 1.0, | ||||||
|           }] |      u'sum': 135.0}, | ||||||
|  | ] | ||||||
|  | groupby_samples = [ | ||||||
|  |     {u'count': 135, | ||||||
|  |      u'duration_start': u'2013-02-04T10:51:42', | ||||||
|  |      u'min': 1.0, | ||||||
|  |      u'max': 1.0, | ||||||
|  |      u'duration_end': | ||||||
|  |      u'2013-02-05T15:46:09', | ||||||
|  |      u'duration': 1734.0, | ||||||
|  |      u'avg': 1.0, | ||||||
|  |      u'sum': 135.0, | ||||||
|  |      u'groupby': {u'resource_id': u'foo'} | ||||||
|  |      }, | ||||||
|  |     {u'count': 12, | ||||||
|  |      u'duration_start': u'2013-02-04T10:51:42', | ||||||
|  |      u'min': 1.0, | ||||||
|  |      u'max': 1.0, | ||||||
|  |      u'duration_end': | ||||||
|  |      u'2013-02-05T15:46:09', | ||||||
|  |      u'duration': 1734.0, | ||||||
|  |      u'avg': 1.0, | ||||||
|  |      u'sum': 12.0, | ||||||
|  |      u'groupby': {u'resource_id': u'bar'} | ||||||
|  |      }, | ||||||
|  | ] | ||||||
| fixtures = { | fixtures = { | ||||||
|     base_url: |     base_url: | ||||||
|     { |     { | ||||||
| @@ -52,6 +78,13 @@ fixtures = { | |||||||
|             samples |             samples | ||||||
|         ), |         ), | ||||||
|     }, |     }, | ||||||
|  |     '%s?%s%s' % (base_url, qry, groupby): | ||||||
|  |     { | ||||||
|  |         'GET': ( | ||||||
|  |             {}, | ||||||
|  |             groupby_samples | ||||||
|  |         ), | ||||||
|  |     }, | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -103,3 +136,23 @@ class StatisticsManagerTest(utils.BaseTestCase): | |||||||
|         self.assertEqual(self.api.calls, expect) |         self.assertEqual(self.api.calls, expect) | ||||||
|         self.assertEqual(len(stats), 1) |         self.assertEqual(len(stats), 1) | ||||||
|         self.assertEqual(stats[0].count, 135) |         self.assertEqual(stats[0].count, 135) | ||||||
|  |  | ||||||
|  |     def test_list_by_meter_name_with_groupby(self): | ||||||
|  |         stats = list(self.mgr.list(meter_name='instance', | ||||||
|  |                                    q=[ | ||||||
|  |                                        {"field": "resource_id", | ||||||
|  |                                         "value": "foo"}, | ||||||
|  |                                        {"field": "source", | ||||||
|  |                                         "value": "bar"}, | ||||||
|  |                                    ], | ||||||
|  |                                    groupby=['resource_id'])) | ||||||
|  |         expect = [ | ||||||
|  |             ('GET', | ||||||
|  |              '%s?%s%s' % (base_url, qry, groupby), {}, None), | ||||||
|  |         ] | ||||||
|  |         self.assertEqual(self.api.calls, expect) | ||||||
|  |         self.assertEqual(len(stats), 2) | ||||||
|  |         self.assertEqual(stats[0].count, 135) | ||||||
|  |         self.assertEqual(stats[1].count, 12) | ||||||
|  |         self.assertEqual(stats[0].groupby.get('resource_id'), 'foo') | ||||||
|  |         self.assertEqual(stats[1].groupby.get('resource_id'), 'bar') | ||||||
|   | |||||||
							
								
								
									
										55
									
								
								ceilometerclient/tests/v2/test_trait_descriptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								ceilometerclient/tests/v2/test_trait_descriptions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,55 @@ | |||||||
|  | # -*- encoding: utf-8 -*- | ||||||
|  | # Copyright 2014 Hewlett-Packard Development Company, L.P. | ||||||
|  | # | ||||||
|  | #    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 ceilometerclient.tests import utils | ||||||
|  | import ceilometerclient.v2.trait_descriptions | ||||||
|  |  | ||||||
|  |  | ||||||
|  | fixtures = { | ||||||
|  |     '/v2/event_types/Foo/traits': { | ||||||
|  |         'GET': ( | ||||||
|  |             {}, | ||||||
|  |             [ | ||||||
|  |                 {'name': 'trait_1', 'type': 'string'}, | ||||||
|  |                 {'name': 'trait_2', 'type': 'integer'}, | ||||||
|  |                 {'name': 'trait_3', 'type': 'datetime'} | ||||||
|  |             ] | ||||||
|  |         ), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TraitDescriptionManagerTest(utils.BaseTestCase): | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super(TraitDescriptionManagerTest, self).setUp() | ||||||
|  |         self.api = utils.FakeAPI(fixtures) | ||||||
|  |         self.mgr = (ceilometerclient.v2.trait_descriptions. | ||||||
|  |                     TraitDescriptionManager(self.api)) | ||||||
|  |  | ||||||
|  |     def test_list(self): | ||||||
|  |         trait_descriptions = list(self.mgr.list('Foo')) | ||||||
|  |         expect = [ | ||||||
|  |             ('GET', '/v2/event_types/Foo/traits', {}, None), | ||||||
|  |         ] | ||||||
|  |         self.assertEqual(self.api.calls, expect) | ||||||
|  |         self.assertEqual(len(trait_descriptions), 3) | ||||||
|  |         for i, vals in enumerate([('trait_1', 'string'), | ||||||
|  |                                   ('trait_2', 'integer'), | ||||||
|  |                                   ('trait_3', 'datetime')]): | ||||||
|  |  | ||||||
|  |             name, type = vals | ||||||
|  |             self.assertEqual(trait_descriptions[i].name, name) | ||||||
|  |             self.assertEqual(trait_descriptions[i].type, type) | ||||||
							
								
								
									
										61
									
								
								ceilometerclient/tests/v2/test_traits.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								ceilometerclient/tests/v2/test_traits.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | |||||||
|  | # -*- encoding: utf-8 -*- | ||||||
|  | # Copyright 2014 Hewlett-Packard Development Company, L.P. | ||||||
|  | # | ||||||
|  | #    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 ceilometerclient.tests import utils | ||||||
|  | import ceilometerclient.v2.traits | ||||||
|  |  | ||||||
|  |  | ||||||
|  | fixtures = { | ||||||
|  |     '/v2/event_types/Foo/traits/trait_1': { | ||||||
|  |         'GET': ( | ||||||
|  |             {}, | ||||||
|  |             [ | ||||||
|  |                 {'name': 'trait_1', | ||||||
|  |                  'type': 'datetime', | ||||||
|  |                  'value': '2014-01-07T17:22:10.925553'}, | ||||||
|  |                 {'name': 'trait_1', | ||||||
|  |                  'type': 'datetime', | ||||||
|  |                  'value': '2014-01-07T17:23:10.925553'} | ||||||
|  |             ] | ||||||
|  |         ), | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TraitManagerTest(utils.BaseTestCase): | ||||||
|  |  | ||||||
|  |     def setUp(self): | ||||||
|  |         super(TraitManagerTest, self).setUp() | ||||||
|  |         self.api = utils.FakeAPI(fixtures) | ||||||
|  |         self.mgr = ceilometerclient.v2.traits.TraitManager(self.api) | ||||||
|  |  | ||||||
|  |     def test_list(self): | ||||||
|  |         traits = list(self.mgr.list('Foo', 'trait_1')) | ||||||
|  |         expect = [ | ||||||
|  |             ('GET', '/v2/event_types/Foo/traits/trait_1', {}, None), | ||||||
|  |         ] | ||||||
|  |         self.assertEqual(self.api.calls, expect) | ||||||
|  |         self.assertEqual(len(traits), 2) | ||||||
|  |         for i, vals in enumerate([('trait_1', | ||||||
|  |                                    'datetime', | ||||||
|  |                                    '2014-01-07T17:22:10.925553'), | ||||||
|  |                                   ('trait_1', | ||||||
|  |                                    'datetime', | ||||||
|  |                                    '2014-01-07T17:23:10.925553')]): | ||||||
|  |  | ||||||
|  |             name, type, value = vals | ||||||
|  |             self.assertEqual(traits[i].name, name) | ||||||
|  |             self.assertEqual(traits[i].type, type) | ||||||
|  |             self.assertEqual(traits[i].value, value) | ||||||
| @@ -17,7 +17,7 @@ from ceilometerclient.common import http | |||||||
| from ceilometerclient.v1 import meters | from ceilometerclient.v1 import meters | ||||||
|  |  | ||||||
|  |  | ||||||
| class Client(http.HTTPClient): | class Client(object): | ||||||
|     """Client for the Ceilometer v1 API. |     """Client for the Ceilometer v1 API. | ||||||
|  |  | ||||||
|     :param string endpoint: A user-supplied endpoint URL for the ceilometer |     :param string endpoint: A user-supplied endpoint URL for the ceilometer | ||||||
| @@ -29,9 +29,9 @@ class Client(http.HTTPClient): | |||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         """Initialize a new client for the Ceilometer v1 API.""" |         """Initialize a new client for the Ceilometer v1 API.""" | ||||||
|         super(Client, self).__init__(*args, **kwargs) |         self.http_client = http.HTTPClient(*args, **kwargs) | ||||||
|         self.meters = meters.MeterManager(self) |         self.meters = meters.MeterManager(self.http_client) | ||||||
|         self.samples = meters.SampleManager(self) |         self.samples = meters.SampleManager(self.http_client) | ||||||
|         self.users = meters.UserManager(self) |         self.users = meters.UserManager(self.http_client) | ||||||
|         self.resources = meters.ResourceManager(self) |         self.resources = meters.ResourceManager(self.http_client) | ||||||
|         self.projects = meters.ProjectManager(self) |         self.projects = meters.ProjectManager(self.http_client) | ||||||
|   | |||||||
| @@ -13,6 +13,8 @@ | |||||||
| #    License for the specific language governing permissions and limitations | #    License for the specific language governing permissions and limitations | ||||||
| #    under the License. | #    under the License. | ||||||
|  |  | ||||||
|  | import six | ||||||
|  |  | ||||||
| from ceilometerclient.common import base | from ceilometerclient.common import base | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -33,7 +35,7 @@ def _get_opt_path(simple_params=[], **kwargs): | |||||||
|  |  | ||||||
| class User(base.Resource): | class User(base.Resource): | ||||||
|     def __init__(self, manager, info, loaded=False): |     def __init__(self, manager, info, loaded=False): | ||||||
|         _d = {unicode('user_id'): info} |         _d = {six.u('user_id'): info} | ||||||
|         super(User, self).__init__(manager, _d, loaded) |         super(User, self).__init__(manager, _d, loaded) | ||||||
|  |  | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
| @@ -57,7 +59,7 @@ class UserManager(base.Manager): | |||||||
|  |  | ||||||
| class Project(base.Resource): | class Project(base.Resource): | ||||||
|     def __init__(self, manager, info, loaded=False): |     def __init__(self, manager, info, loaded=False): | ||||||
|         _d = {unicode('project_id'): info} |         _d = {six.u('project_id'): info} | ||||||
|         super(Project, self).__init__(manager, _d, loaded) |         super(Project, self).__init__(manager, _d, loaded) | ||||||
|  |  | ||||||
|     def __repr__(self): |     def __repr__(self): | ||||||
| @@ -114,7 +116,7 @@ class ResourceManager(base.Manager): | |||||||
|  |  | ||||||
| class Sample(base.Resource): | class Sample(base.Resource): | ||||||
|     def __init__(self, manager, info, loaded=False): |     def __init__(self, manager, info, loaded=False): | ||||||
|         smaller = dict((k, v) for (k, v) in info.iteritems() |         smaller = dict((k, v) for (k, v) in six.iteritems(info) | ||||||
|                        if k not in ('metadata', 'message_signature')) |                        if k not in ('metadata', 'message_signature')) | ||||||
|         super(Sample, self).__init__(manager, smaller, loaded) |         super(Sample, self).__init__(manager, smaller, loaded) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,13 +15,17 @@ | |||||||
|  |  | ||||||
| from ceilometerclient.common import http | from ceilometerclient.common import http | ||||||
| from ceilometerclient.v2 import alarms | from ceilometerclient.v2 import alarms | ||||||
|  | from ceilometerclient.v2 import event_types | ||||||
|  | from ceilometerclient.v2 import events | ||||||
| from ceilometerclient.v2 import meters | from ceilometerclient.v2 import meters | ||||||
| from ceilometerclient.v2 import resources | from ceilometerclient.v2 import resources | ||||||
| from ceilometerclient.v2 import samples | from ceilometerclient.v2 import samples | ||||||
| from ceilometerclient.v2 import statistics | from ceilometerclient.v2 import statistics | ||||||
|  | from ceilometerclient.v2 import trait_descriptions | ||||||
|  | from ceilometerclient.v2 import traits | ||||||
|  |  | ||||||
|  |  | ||||||
| class Client(http.HTTPClient): | class Client(object): | ||||||
|     """Client for the Ceilometer v2 API. |     """Client for the Ceilometer v2 API. | ||||||
|  |  | ||||||
|     :param string endpoint: A user-supplied endpoint URL for the ceilometer |     :param string endpoint: A user-supplied endpoint URL for the ceilometer | ||||||
| @@ -33,9 +37,14 @@ class Client(http.HTTPClient): | |||||||
|  |  | ||||||
|     def __init__(self, *args, **kwargs): |     def __init__(self, *args, **kwargs): | ||||||
|         """Initialize a new client for the Ceilometer v1 API.""" |         """Initialize a new client for the Ceilometer v1 API.""" | ||||||
|         super(Client, self).__init__(*args, **kwargs) |         self.http_client = http.HTTPClient(*args, **kwargs) | ||||||
|         self.meters = meters.MeterManager(self) |         self.meters = meters.MeterManager(self.http_client) | ||||||
|         self.samples = samples.SampleManager(self) |         self.samples = samples.SampleManager(self.http_client) | ||||||
|         self.statistics = statistics.StatisticsManager(self) |         self.statistics = statistics.StatisticsManager(self.http_client) | ||||||
|         self.resources = resources.ResourceManager(self) |         self.resources = resources.ResourceManager(self.http_client) | ||||||
|         self.alarms = alarms.AlarmManager(self) |         self.alarms = alarms.AlarmManager(self.http_client) | ||||||
|  |         self.events = events.EventManager(self.http_client) | ||||||
|  |         self.event_types = event_types.EventTypeManager(self.http_client) | ||||||
|  |         self.traits = traits.TraitManager(self.http_client) | ||||||
|  |         self.trait_info = trait_descriptions.\ | ||||||
|  |             TraitDescriptionManager(self.http_client) | ||||||
|   | |||||||
							
								
								
									
										31
									
								
								ceilometerclient/v2/event_types.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								ceilometerclient/v2/event_types.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | |||||||
|  | # -*- encoding: utf-8 -*- | ||||||
|  | # Copyright 2014 Hewlett-Packard Development Company, L.P. | ||||||
|  | # | ||||||
|  | #    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 ceilometerclient.common import base | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EventType(base.Resource): | ||||||
|  |     def __repr__(self): | ||||||
|  |         return "<Event %s>" % self._info | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def object_class_str(mgr, value, loaded): | ||||||
|  |     return EventType(mgr, {"event_type": value}, loaded) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EventTypeManager(base.Manager): | ||||||
|  |  | ||||||
|  |     def list(self): | ||||||
|  |         return self._list('/v2/event_types/', obj_class=object_class_str) | ||||||
							
								
								
									
										37
									
								
								ceilometerclient/v2/events.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								ceilometerclient/v2/events.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | # -*- encoding: utf-8 -*- | ||||||
|  | # Copyright 2014 Hewlett-Packard Development Company, L.P. | ||||||
|  | # | ||||||
|  | #    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 ceilometerclient.common import base | ||||||
|  | from ceilometerclient.v2 import options | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Event(base.Resource): | ||||||
|  |     def __repr__(self): | ||||||
|  |         return "<Event %s>" % self._info | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class EventManager(base.Manager): | ||||||
|  |     resource_class = Event | ||||||
|  |  | ||||||
|  |     def list(self, q=None): | ||||||
|  |         path = '/v2/events' | ||||||
|  |         return self._list(options.build_url(path, q)) | ||||||
|  |  | ||||||
|  |     def get(self, message_id): | ||||||
|  |         path = '/v2/events/%s' | ||||||
|  |         try: | ||||||
|  |             return self._list(path % message_id, expect_single=True)[0] | ||||||
|  |         except IndexError: | ||||||
|  |             return None | ||||||
| @@ -11,8 +11,8 @@ | |||||||
| #    License for the specific language governing permissions and limitations | #    License for the specific language governing permissions and limitations | ||||||
| #    under the License. | #    under the License. | ||||||
|  |  | ||||||
|  | from ceilometerclient.openstack.common.py3kcompat import urlutils | ||||||
| import re | import re | ||||||
| import urllib |  | ||||||
|  |  | ||||||
|  |  | ||||||
| def build_url(path, q, params=None): | def build_url(path, q, params=None): | ||||||
| @@ -28,13 +28,17 @@ def build_url(path, q, params=None): | |||||||
|     if q: |     if q: | ||||||
|         query_params = {'q.field': [], |         query_params = {'q.field': [], | ||||||
|                         'q.value': [], |                         'q.value': [], | ||||||
|                         'q.op': []} |                         'q.op': [], | ||||||
|  |                         'q.type': []} | ||||||
|  |  | ||||||
|         for query in q: |         for query in q: | ||||||
|             for name in ['field', 'op', 'value']: |             for name in ['field', 'op', 'value', 'type']: | ||||||
|                 query_params['q.%s' % name].append(query.get(name, '')) |                 query_params['q.%s' % name].append(query.get(name, '')) | ||||||
|  |  | ||||||
|         path += "?" + urllib.urlencode(query_params, doseq=True) |         # Transform the dict to a sequence of two-element tuples in fixed | ||||||
|  |         # order, then the encoded string will be consistent in Python 2&3. | ||||||
|  |         new_qparams = sorted(query_params.items(), key=lambda x: x[0]) | ||||||
|  |         path += "?" + urlutils.urlencode(new_qparams, doseq=True) | ||||||
|  |  | ||||||
|         if params: |         if params: | ||||||
|             for p in params: |             for p in params: | ||||||
| @@ -47,13 +51,15 @@ def build_url(path, q, params=None): | |||||||
|  |  | ||||||
|  |  | ||||||
| def cli_to_array(cli_query): | def cli_to_array(cli_query): | ||||||
|     '''This converts from the cli list of queries to what is required |     """This converts from the cli list of queries to what is required | ||||||
|     by the python api. |     by the python api. | ||||||
|     so from: |     so from: | ||||||
|     "this<=34;that=foo" |     "this<=34;that=foo" | ||||||
|     to |     to | ||||||
|     "[{field=this,op=le,value=34},{field=that,op=eq,value=foo}]" |     "[{field=this,op=le,value=34},{field=that,op=eq,value=foo}]" | ||||||
|     ''' |  | ||||||
|  |     """ | ||||||
|  |  | ||||||
|     if cli_query is None: |     if cli_query is None: | ||||||
|         return None |         return None | ||||||
|  |  | ||||||
| @@ -74,12 +80,20 @@ def cli_to_array(cli_query): | |||||||
|                                string) |                                string) | ||||||
|         return frags |         return frags | ||||||
|  |  | ||||||
|  |     def split_by_data_type(string): | ||||||
|  |         frags = re.findall(r'^(string|integer|float|datetime|boolean)(::)' | ||||||
|  |                            r'([^ -,\t\n\r\f\v]+)$', string) | ||||||
|  |  | ||||||
|  |         # frags[1] is the separator. Return a list without it if the type | ||||||
|  |         # identifier was found. | ||||||
|  |         return [frags[0][0], frags[0][2]] if frags else None | ||||||
|  |  | ||||||
|     opts = [] |     opts = [] | ||||||
|     queries = cli_query.split(';') |     queries = cli_query.split(';') | ||||||
|     for q in queries: |     for q in queries: | ||||||
|         frag = split_by_op(q) |         frag = split_by_op(q) | ||||||
|         if len(frag) > 1: |         if len(frag) > 1: | ||||||
|             raise ValueError('incorrect seperator %s in query "%s"' % |             raise ValueError('incorrect separator %s in query "%s"' % | ||||||
|                              ('(should be ";")', q)) |                              ('(should be ";")', q)) | ||||||
|         if len(frag) == 0: |         if len(frag) == 0: | ||||||
|             raise ValueError('invalid query %s' % q) |             raise ValueError('invalid query %s' % q) | ||||||
| @@ -87,6 +101,15 @@ def cli_to_array(cli_query): | |||||||
|         opt = {} |         opt = {} | ||||||
|         opt['field'] = query[0] |         opt['field'] = query[0] | ||||||
|         opt['op'] = op_lookup[query[1]] |         opt['op'] = op_lookup[query[1]] | ||||||
|         opt['value'] = query[2] |  | ||||||
|  |         # Allow the data type of the value to be specified via <type>::<value>, | ||||||
|  |         # where type can be one of integer, string, float, datetime, boolean | ||||||
|  |         value_frags = split_by_data_type(query[2]) | ||||||
|  |         if not value_frags: | ||||||
|  |             opt['value'] = query[2] | ||||||
|  |             opt['type'] = '' | ||||||
|  |         else: | ||||||
|  |             opt['type'] = value_frags[0] | ||||||
|  |             opt['value'] = value_frags[1] | ||||||
|         opts.append(opt) |         opts.append(opt) | ||||||
|     return opts |     return opts | ||||||
|   | |||||||
| @@ -38,9 +38,10 @@ class SampleManager(base.Manager): | |||||||
|     def _path(counter_name=None): |     def _path(counter_name=None): | ||||||
|         return '/v2/meters/%s' % counter_name if counter_name else '/v2/meters' |         return '/v2/meters/%s' % counter_name if counter_name else '/v2/meters' | ||||||
|  |  | ||||||
|     def list(self, meter_name=None, q=None): |     def list(self, meter_name=None, q=None, limit=None): | ||||||
|         path = self._path(counter_name=meter_name) |         path = self._path(counter_name=meter_name) | ||||||
|         return self._list(options.build_url(path, q)) |         params = ['limit=%s' % str(limit)] if limit else None | ||||||
|  |         return self._list(options.build_url(path, q, params)) | ||||||
|  |  | ||||||
|     def create(self, **kwargs): |     def create(self, **kwargs): | ||||||
|         new = dict((key, value) for (key, value) in kwargs.items() |         new = dict((key, value) for (key, value) in kwargs.items() | ||||||
|   | |||||||
| @@ -18,9 +18,11 @@ | |||||||
|  |  | ||||||
| import functools | import functools | ||||||
| import json | import json | ||||||
|  | import six | ||||||
|  |  | ||||||
| from ceilometerclient.common import utils | from ceilometerclient.common import utils | ||||||
| from ceilometerclient import exc | from ceilometerclient import exc | ||||||
|  | from ceilometerclient.openstack.common import strutils | ||||||
| from ceilometerclient.v2 import options | from ceilometerclient.v2 import options | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -34,16 +36,20 @@ OPERATORS_STRING = dict(gt='>', ge='>=', | |||||||
|  |  | ||||||
|  |  | ||||||
| @utils.arg('-q', '--query', metavar='<QUERY>', | @utils.arg('-q', '--query', metavar='<QUERY>', | ||||||
|            help='key[op]value; list.') |            help='key[op]data_type::value; list. data_type is optional, ' | ||||||
|  |                 'but if supplied must be string, integer, float, or boolean') | ||||||
| @utils.arg('-m', '--meter', metavar='<NAME>', required=True, | @utils.arg('-m', '--meter', metavar='<NAME>', required=True, | ||||||
|            help='Name of meter to show samples for.') |            help='Name of meter to show samples for.') | ||||||
| @utils.arg('-p', '--period', metavar='<PERIOD>', | @utils.arg('-p', '--period', metavar='<PERIOD>', | ||||||
|            help='Period in seconds over which to group samples.') |            help='Period in seconds over which to group samples.') | ||||||
|  | @utils.arg('-g', '--groupby', metavar='<FIELD>', action='append', | ||||||
|  |            help='Field for group aggregation.') | ||||||
| def do_statistics(cc, args): | def do_statistics(cc, args): | ||||||
|     '''List the statistics for this meter.''' |     '''List the statistics for a meter.''' | ||||||
|     fields = {'meter_name': args.meter, |     fields = {'meter_name': args.meter, | ||||||
|               'q': options.cli_to_array(args.query), |               'q': options.cli_to_array(args.query), | ||||||
|               'period': args.period} |               'period': args.period, | ||||||
|  |               'groupby': args.groupby} | ||||||
|     try: |     try: | ||||||
|         statistics = cc.statistics.list(**fields) |         statistics = cc.statistics.list(**fields) | ||||||
|     except exc.HTTPNotFound: |     except exc.HTTPNotFound: | ||||||
| @@ -55,17 +61,24 @@ def do_statistics(cc, args): | |||||||
|         fields = ['period', 'period_start', 'period_end', |         fields = ['period', 'period_start', 'period_end', | ||||||
|                   'count', 'min', 'max', 'sum', 'avg', |                   'count', 'min', 'max', 'sum', 'avg', | ||||||
|                   'duration', 'duration_start', 'duration_end'] |                   'duration', 'duration_start', 'duration_end'] | ||||||
|  |         if args.groupby: | ||||||
|  |             field_labels.append('Group By') | ||||||
|  |             fields.append('groupby') | ||||||
|         utils.print_list(statistics, fields, field_labels) |         utils.print_list(statistics, fields, field_labels) | ||||||
|  |  | ||||||
|  |  | ||||||
| @utils.arg('-q', '--query', metavar='<QUERY>', | @utils.arg('-q', '--query', metavar='<QUERY>', | ||||||
|            help='key[op]value; list.') |            help='key[op]data_type::value; list. data_type is optional, ' | ||||||
|  |                 'but if supplied must be string, integer, float, or boolean') | ||||||
| @utils.arg('-m', '--meter', metavar='<NAME>', required=True, | @utils.arg('-m', '--meter', metavar='<NAME>', required=True, | ||||||
|            help='Name of meter to show samples for.') |            help='Name of meter to show samples for.') | ||||||
|  | @utils.arg('-l', '--limit', metavar='<NUMBER>', | ||||||
|  |            help='Maximum number of samples to return.') | ||||||
| def do_sample_list(cc, args): | def do_sample_list(cc, args): | ||||||
|     '''List the samples for this meters.''' |     '''List the samples for a meter.''' | ||||||
|     fields = {'meter_name': args.meter, |     fields = {'meter_name': args.meter, | ||||||
|               'q': options.cli_to_array(args.query)} |               'q': options.cli_to_array(args.query), | ||||||
|  |               'limit': args.limit} | ||||||
|     try: |     try: | ||||||
|         samples = cc.samples.list(**fields) |         samples = cc.samples.list(**fields) | ||||||
|     except exc.HTTPNotFound: |     except exc.HTTPNotFound: | ||||||
| @@ -76,7 +89,7 @@ def do_sample_list(cc, args): | |||||||
|         fields = ['resource_id', 'counter_name', 'counter_type', |         fields = ['resource_id', 'counter_name', 'counter_type', | ||||||
|                   'counter_volume', 'counter_unit', 'timestamp'] |                   'counter_volume', 'counter_unit', 'timestamp'] | ||||||
|         utils.print_list(samples, fields, field_labels, |         utils.print_list(samples, fields, field_labels, | ||||||
|                          sortby=0) |                          sortby=None) | ||||||
|  |  | ||||||
|  |  | ||||||
| @utils.arg('--project-id', metavar='<PROJECT_ID>', | @utils.arg('--project-id', metavar='<PROJECT_ID>', | ||||||
| @@ -87,7 +100,7 @@ def do_sample_list(cc, args): | |||||||
|                 '(only settable by admin users)') |                 '(only settable by admin users)') | ||||||
| @utils.arg('-r', '--resource-id', metavar='<RESOURCE_ID>', required=True, | @utils.arg('-r', '--resource-id', metavar='<RESOURCE_ID>', required=True, | ||||||
|            help='ID of the resource.') |            help='ID of the resource.') | ||||||
| @utils.arg('-m', '--meter-name', metavar='<METER_NAME>', | @utils.arg('-m', '--meter-name', metavar='<METER_NAME>', required=True, | ||||||
|            help='the meter name') |            help='the meter name') | ||||||
| @utils.arg('--meter-type', metavar='<METER_TYPE>', required=True, | @utils.arg('--meter-type', metavar='<METER_TYPE>', required=True, | ||||||
|            help='the meter type') |            help='the meter type') | ||||||
| @@ -97,6 +110,8 @@ def do_sample_list(cc, args): | |||||||
|            help='The sample volume') |            help='The sample volume') | ||||||
| @utils.arg('--resource-metadata', metavar='<RESOURCE_METADATA>', | @utils.arg('--resource-metadata', metavar='<RESOURCE_METADATA>', | ||||||
|            help='resource metadata') |            help='resource metadata') | ||||||
|  | @utils.arg('--timestamp', metavar='<TIMESTAMP>', | ||||||
|  |            help='the sample timestamp') | ||||||
| def do_sample_create(cc, args={}): | def do_sample_create(cc, args={}): | ||||||
|     '''Create a sample.''' |     '''Create a sample.''' | ||||||
|     arg_to_field_mapping = {'meter_name': 'counter_name', |     arg_to_field_mapping = {'meter_name': 'counter_name', | ||||||
| @@ -111,11 +126,19 @@ def do_sample_create(cc, args={}): | |||||||
|                 fields[k] = json.loads(v) |                 fields[k] = json.loads(v) | ||||||
|             else: |             else: | ||||||
|                 fields[arg_to_field_mapping.get(k, k)] = v |                 fields[arg_to_field_mapping.get(k, k)] = v | ||||||
|     cc.samples.create(**fields) |     sample = cc.samples.create(**fields) | ||||||
|  |     fields = ['counter_name', 'user_id', 'resource_id', | ||||||
|  |               'timestamp', 'message_id', 'source', 'counter_unit', | ||||||
|  |               'counter_volume', 'project_id', 'resource_metadata', | ||||||
|  |               'counter_type'] | ||||||
|  |     data = dict([(f.replace('counter_', ''), getattr(sample[0], f, '')) | ||||||
|  |                  for f in fields]) | ||||||
|  |     utils.print_dict(data, wrap=72) | ||||||
|  |  | ||||||
|  |  | ||||||
| @utils.arg('-q', '--query', metavar='<QUERY>', | @utils.arg('-q', '--query', metavar='<QUERY>', | ||||||
|            help='key[op]value; list.') |            help='key[op]data_type::value; list. data_type is optional, ' | ||||||
|  |                 'but if supplied must be string, integer, float, or boolean') | ||||||
| def do_meter_list(cc, args={}): | def do_meter_list(cc, args={}): | ||||||
|     '''List the user's meters.''' |     '''List the user's meters.''' | ||||||
|     meters = cc.meters.list(q=options.cli_to_array(args.query)) |     meters = cc.meters.list(q=options.cli_to_array(args.query)) | ||||||
| @@ -177,7 +200,7 @@ def alarm_change_detail_formatter(change): | |||||||
|             else: |             else: | ||||||
|                 fields.append('%s: %s' % (k, detail[k])) |                 fields.append('%s: %s' % (k, detail[k])) | ||||||
|     elif change.type == 'rule change': |     elif change.type == 'rule change': | ||||||
|         for k, v in detail.iteritems(): |         for k, v in six.iteritems(detail): | ||||||
|             if k == 'rule': |             if k == 'rule': | ||||||
|                 fields.append('rule: %s' % _display_rule(_infer_type(detail), |                 fields.append('rule: %s' % _display_rule(_infer_type(detail), | ||||||
|                                                          v)) |                                                          v)) | ||||||
| @@ -187,7 +210,8 @@ def alarm_change_detail_formatter(change): | |||||||
|  |  | ||||||
|  |  | ||||||
| @utils.arg('-q', '--query', metavar='<QUERY>', | @utils.arg('-q', '--query', metavar='<QUERY>', | ||||||
|            help='key[op]value; list.') |            help='key[op]data_type::value; list. data_type is optional, ' | ||||||
|  |                 'but if supplied must be string, integer, float, or boolean') | ||||||
| def do_alarm_list(cc, args={}): | def do_alarm_list(cc, args={}): | ||||||
|     '''List the user's alarms.''' |     '''List the user's alarms.''' | ||||||
|     alarms = cc.alarms.list(q=options.cli_to_array(args.query)) |     alarms = cc.alarms.list(q=options.cli_to_array(args.query)) | ||||||
| @@ -247,7 +271,7 @@ def common_alarm_arguments(create=False): | |||||||
|                    help='Free text description of the alarm') |                    help='Free text description of the alarm') | ||||||
|         @utils.arg('--state', metavar='<STATE>', |         @utils.arg('--state', metavar='<STATE>', | ||||||
|                    help='State of the alarm, one of: ' + str(ALARM_STATES)) |                    help='State of the alarm, one of: ' + str(ALARM_STATES)) | ||||||
|         @utils.arg('--enabled', type=utils.string_to_bool, |         @utils.arg('--enabled', type=strutils.bool_from_string, | ||||||
|                    metavar='{True|False}', |                    metavar='{True|False}', | ||||||
|                    help='True if alarm evaluation/actioning is enabled') |                    help='True if alarm evaluation/actioning is enabled') | ||||||
|         @utils.arg('--alarm-action', dest='alarm_actions', |         @utils.arg('--alarm-action', dest='alarm_actions', | ||||||
| @@ -261,13 +285,8 @@ def common_alarm_arguments(create=False): | |||||||
|         @utils.arg('--insufficient-data-action', |         @utils.arg('--insufficient-data-action', | ||||||
|                    dest='insufficient_data_actions', |                    dest='insufficient_data_actions', | ||||||
|                    metavar='<Webhook URL>', action='append', default=None, |                    metavar='<Webhook URL>', action='append', default=None, | ||||||
|                    help=('URL to invoke when state transitions to unkown. ' |                    help=('URL to invoke when state transitions to ' | ||||||
|                          'May be used multiple times.')) |                          'insufficient_data. May be used multiple times.')) | ||||||
|         @utils.arg('--repeat-actions', dest='repeat_actions', |  | ||||||
|                    metavar='{True|False}', type=utils.string_to_bool, |  | ||||||
|                    default=False, |  | ||||||
|                    help=('True if actions should be repeatedly notified ' |  | ||||||
|                          'while alarm remains in target state')) |  | ||||||
|         @functools.wraps(func) |         @functools.wraps(func) | ||||||
|         def _wrapped(*args, **kwargs): |         def _wrapped(*args, **kwargs): | ||||||
|             return func(*args, **kwargs) |             return func(*args, **kwargs) | ||||||
| @@ -280,7 +299,7 @@ def common_alarm_arguments(create=False): | |||||||
|            help='Length of each period (seconds) to evaluate over') |            help='Length of each period (seconds) to evaluate over') | ||||||
| @utils.arg('--evaluation-periods', type=int, metavar='<COUNT>', | @utils.arg('--evaluation-periods', type=int, metavar='<COUNT>', | ||||||
|            help='Number of periods to evaluate over') |            help='Number of periods to evaluate over') | ||||||
| @utils.arg('--meter-name', metavar='<METRIC>', required=True, | @utils.arg('-m', '--meter-name', metavar='<METRIC>', required=True, | ||||||
|            help='Metric to evaluate against') |            help='Metric to evaluate against') | ||||||
| @utils.arg('--statistic', metavar='<STATISTIC>', | @utils.arg('--statistic', metavar='<STATISTIC>', | ||||||
|            help='Statistic to evaluate, one of: ' + str(STATISTICS)) |            help='Statistic to evaluate, one of: ' + str(STATISTICS)) | ||||||
| @@ -292,6 +311,11 @@ def common_alarm_arguments(create=False): | |||||||
|            metavar='<Matching Metadata>', action='append', default=None, |            metavar='<Matching Metadata>', action='append', default=None, | ||||||
|            help=('A meter should match this resource metadata (key=value) ' |            help=('A meter should match this resource metadata (key=value) ' | ||||||
|                  'additionally to the meter_name')) |                  'additionally to the meter_name')) | ||||||
|  | @utils.arg('--repeat-actions', dest='repeat_actions', | ||||||
|  |            metavar='{True|False}', type=strutils.bool_from_string, | ||||||
|  |            default=False, | ||||||
|  |            help=('True if actions should be repeatedly notified ' | ||||||
|  |                  'while alarm remains in target state')) | ||||||
| def do_alarm_create(cc, args={}): | def do_alarm_create(cc, args={}): | ||||||
|     '''Create a new alarm (Deprecated).''' |     '''Create a new alarm (Deprecated).''' | ||||||
|     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) |     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) | ||||||
| @@ -301,7 +325,7 @@ def do_alarm_create(cc, args={}): | |||||||
|  |  | ||||||
|  |  | ||||||
| @common_alarm_arguments(create=True) | @common_alarm_arguments(create=True) | ||||||
| @utils.arg('--meter-name', metavar='<METRIC>', required=True, | @utils.arg('-m', '--meter-name', metavar='<METRIC>', required=True, | ||||||
|            dest='threshold_rule/meter_name', |            dest='threshold_rule/meter_name', | ||||||
|            help='Metric to evaluate against') |            help='Metric to evaluate against') | ||||||
| @utils.arg('--period', type=int, metavar='<PERIOD>', | @utils.arg('--period', type=int, metavar='<PERIOD>', | ||||||
| @@ -321,8 +345,13 @@ def do_alarm_create(cc, args={}): | |||||||
|            help='Threshold to evaluate against') |            help='Threshold to evaluate against') | ||||||
| @utils.arg('-q', '--query', metavar='<QUERY>', | @utils.arg('-q', '--query', metavar='<QUERY>', | ||||||
|            dest='threshold_rule/query', |            dest='threshold_rule/query', | ||||||
|            help='The query to find the data for computing statistics ' |            help='key[op]data_type::value; list. data_type is optional, ' | ||||||
|            '(key[op]value; list.)') |                 'but if supplied must be string, integer, float, or boolean') | ||||||
|  | @utils.arg('--repeat-actions', dest='repeat_actions', | ||||||
|  |            metavar='{True|False}', type=strutils.bool_from_string, | ||||||
|  |            default=False, | ||||||
|  |            help=('True if actions should be repeatedly notified ' | ||||||
|  |                  'while alarm remains in target state')) | ||||||
| def do_alarm_threshold_create(cc, args={}): | def do_alarm_threshold_create(cc, args={}): | ||||||
|     '''Create a new alarm based on computed statistics.''' |     '''Create a new alarm based on computed statistics.''' | ||||||
|     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) |     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) | ||||||
| @@ -343,6 +372,11 @@ def do_alarm_threshold_create(cc, args={}): | |||||||
|            dest='combination_rule/operator', |            dest='combination_rule/operator', | ||||||
|            help='Operator to compare with, one of: ' + str( |            help='Operator to compare with, one of: ' + str( | ||||||
|                ALARM_COMBINATION_OPERATORS)) |                ALARM_COMBINATION_OPERATORS)) | ||||||
|  | @utils.arg('--repeat-actions', dest='repeat_actions', | ||||||
|  |            metavar='{True|False}', type=strutils.bool_from_string, | ||||||
|  |            default=False, | ||||||
|  |            help=('True if actions should be repeatedly notified ' | ||||||
|  |                  'while alarm remains in target state')) | ||||||
| def do_alarm_combination_create(cc, args={}): | def do_alarm_combination_create(cc, args={}): | ||||||
|     '''Create a new alarm based on state of other alarms.''' |     '''Create a new alarm based on state of other alarms.''' | ||||||
|     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) |     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) | ||||||
| @@ -359,7 +393,7 @@ def do_alarm_combination_create(cc, args={}): | |||||||
|            help='Length of each period (seconds) to evaluate over') |            help='Length of each period (seconds) to evaluate over') | ||||||
| @utils.arg('--evaluation-periods', type=int, metavar='<COUNT>', | @utils.arg('--evaluation-periods', type=int, metavar='<COUNT>', | ||||||
|            help='Number of periods to evaluate over') |            help='Number of periods to evaluate over') | ||||||
| @utils.arg('--meter-name', metavar='<METRIC>', | @utils.arg('-m', '--meter-name', metavar='<METRIC>', | ||||||
|            help='Metric to evaluate against') |            help='Metric to evaluate against') | ||||||
| @utils.arg('--statistic', metavar='<STATISTIC>', | @utils.arg('--statistic', metavar='<STATISTIC>', | ||||||
|            help='Statistic to evaluate, one of: ' + str(STATISTICS)) |            help='Statistic to evaluate, one of: ' + str(STATISTICS)) | ||||||
| @@ -371,6 +405,10 @@ def do_alarm_combination_create(cc, args={}): | |||||||
|            metavar='<Matching Metadata>', action='append', default=None, |            metavar='<Matching Metadata>', action='append', default=None, | ||||||
|            help=('A meter should match this resource metadata (key=value) ' |            help=('A meter should match this resource metadata (key=value) ' | ||||||
|                  'additionally to the meter_name')) |                  'additionally to the meter_name')) | ||||||
|  | @utils.arg('--repeat-actions', dest='repeat_actions', | ||||||
|  |            metavar='{True|False}', type=strutils.bool_from_string, | ||||||
|  |            help=('True if actions should be repeatedly notified ' | ||||||
|  |                  'while alarm remains in target state')) | ||||||
| def do_alarm_update(cc, args={}): | def do_alarm_update(cc, args={}): | ||||||
|     '''Update an existing alarm.''' |     '''Update an existing alarm.''' | ||||||
|     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) |     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) | ||||||
| @@ -386,7 +424,7 @@ def do_alarm_update(cc, args={}): | |||||||
| @utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True, | @utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True, | ||||||
|            help='ID of the alarm to update.') |            help='ID of the alarm to update.') | ||||||
| @common_alarm_arguments() | @common_alarm_arguments() | ||||||
| @utils.arg('--meter-name', metavar='<METRIC>', | @utils.arg('-m', '--meter-name', metavar='<METRIC>', | ||||||
|            dest='threshold_rule/meter_name', |            dest='threshold_rule/meter_name', | ||||||
|            help='Metric to evaluate against') |            help='Metric to evaluate against') | ||||||
| @utils.arg('--period', type=int, metavar='<PERIOD>', | @utils.arg('--period', type=int, metavar='<PERIOD>', | ||||||
| @@ -405,16 +443,19 @@ def do_alarm_update(cc, args={}): | |||||||
|            dest='threshold_rule/threshold', |            dest='threshold_rule/threshold', | ||||||
|            help='Threshold to evaluate against') |            help='Threshold to evaluate against') | ||||||
| @utils.arg('-q', '--query', metavar='<QUERY>', | @utils.arg('-q', '--query', metavar='<QUERY>', | ||||||
|            dest='threshold_rule/query', |            help='key[op]data_type::value; list. data_type is optional, ' | ||||||
|            help='The query to find the data for computing statistics ' |                 'but if supplied must be string, integer, float, or boolean') | ||||||
|            '(key[op]value; list.)') | @utils.arg('--repeat-actions', dest='repeat_actions', | ||||||
|  |            metavar='{True|False}', type=strutils.bool_from_string, | ||||||
|  |            help=('True if actions should be repeatedly notified ' | ||||||
|  |                  'while alarm remains in target state')) | ||||||
| def do_alarm_threshold_update(cc, args={}): | def do_alarm_threshold_update(cc, args={}): | ||||||
|     '''Update an existing alarm based on computed statistics.''' |     '''Update an existing alarm based on computed statistics.''' | ||||||
|     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) |     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) | ||||||
|     fields = utils.key_with_slash_to_nested_dict(fields) |     fields = utils.key_with_slash_to_nested_dict(fields) | ||||||
|     fields.pop('alarm_id') |     fields.pop('alarm_id') | ||||||
|     fields['type'] = 'threshold' |     fields['type'] = 'threshold' | ||||||
|     if 'query' in fields['threshold_rule']: |     if 'threshold_rule' in fields and 'query' in fields['threshold_rule']: | ||||||
|         fields['threshold_rule']['query'] = options.cli_to_array( |         fields['threshold_rule']['query'] = options.cli_to_array( | ||||||
|             fields['threshold_rule']['query']) |             fields['threshold_rule']['query']) | ||||||
|     try: |     try: | ||||||
| @@ -430,10 +471,14 @@ def do_alarm_threshold_update(cc, args={}): | |||||||
| @utils.arg('--alarm_ids', action='append', metavar='<ALARM IDS>', | @utils.arg('--alarm_ids', action='append', metavar='<ALARM IDS>', | ||||||
|            dest='combination_rule/alarm_ids', |            dest='combination_rule/alarm_ids', | ||||||
|            help='List of alarm id') |            help='List of alarm id') | ||||||
| @utils.arg('---operator', metavar='<OPERATOR>', | @utils.arg('--operator', metavar='<OPERATOR>', | ||||||
|            dest='combination_rule/operator', |            dest='combination_rule/operator', | ||||||
|            help='Operator to compare with, one of: ' + str( |            help='Operator to compare with, one of: ' + str( | ||||||
|                ALARM_COMBINATION_OPERATORS)) |                ALARM_COMBINATION_OPERATORS)) | ||||||
|  | @utils.arg('--repeat-actions', dest='repeat_actions', | ||||||
|  |            metavar='{True|False}', type=strutils.bool_from_string, | ||||||
|  |            help=('True if actions should be repeatedly notified ' | ||||||
|  |                  'while alarm remains in target state')) | ||||||
| def do_alarm_combination_update(cc, args={}): | def do_alarm_combination_update(cc, args={}): | ||||||
|     '''Update an existing alarm based on state of other alarms.''' |     '''Update an existing alarm based on state of other alarms.''' | ||||||
|     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) |     fields = dict(filter(lambda x: not (x[1] is None), vars(args).items())) | ||||||
| @@ -484,7 +529,8 @@ def do_alarm_state_get(cc, args={}): | |||||||
| @utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True, | @utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True, | ||||||
|            help='ID of the alarm for which history is shown.') |            help='ID of the alarm for which history is shown.') | ||||||
| @utils.arg('-q', '--query', metavar='<QUERY>', | @utils.arg('-q', '--query', metavar='<QUERY>', | ||||||
|            help='key[op]value; list.') |            help='key[op]data_type::value; list. data_type is optional, ' | ||||||
|  |                 'but if supplied must be string, integer, float, or boolean') | ||||||
| def do_alarm_history(cc, args={}): | def do_alarm_history(cc, args={}): | ||||||
|     '''Display the change history of an alarm.''' |     '''Display the change history of an alarm.''' | ||||||
|     kwargs = dict(alarm_id=args.alarm_id, |     kwargs = dict(alarm_id=args.alarm_id, | ||||||
| @@ -501,7 +547,8 @@ def do_alarm_history(cc, args={}): | |||||||
|  |  | ||||||
|  |  | ||||||
| @utils.arg('-q', '--query', metavar='<QUERY>', | @utils.arg('-q', '--query', metavar='<QUERY>', | ||||||
|            help='key[op]value; list.') |            help='key[op]data_type::value; list. data_type is optional, ' | ||||||
|  |                 'but if supplied must be string, integer, float, or boolean.') | ||||||
| def do_resource_list(cc, args={}): | def do_resource_list(cc, args={}): | ||||||
|     '''List the resources.''' |     '''List the resources.''' | ||||||
|     resources = cc.resources.list(q=options.cli_to_array(args.query)) |     resources = cc.resources.list(q=options.cli_to_array(args.query)) | ||||||
| @@ -525,3 +572,64 @@ def do_resource_show(cc, args={}): | |||||||
|                   'project_id', 'metadata'] |                   'project_id', 'metadata'] | ||||||
|         data = dict([(f, getattr(resource, f, '')) for f in fields]) |         data = dict([(f, getattr(resource, f, '')) for f in fields]) | ||||||
|         utils.print_dict(data, wrap=72) |         utils.print_dict(data, wrap=72) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @utils.arg('-q', '--query', metavar='<QUERY>', | ||||||
|  |            help='key[op]data_type::value; list. data_type is optional, ' | ||||||
|  |                 'but if supplied must be string, integer, float' | ||||||
|  |                 'or datetime.') | ||||||
|  | def do_event_list(cc, args={}): | ||||||
|  |     '''List events.''' | ||||||
|  |     events = cc.events.list(q=options.cli_to_array(args.query)) | ||||||
|  |     field_labels = ['Message ID', 'Event Type', 'Generated', 'Traits'] | ||||||
|  |     fields = ['message_id', 'event_type', 'generated', 'traits'] | ||||||
|  |     utils.print_list(events, fields, field_labels, | ||||||
|  |                      formatters={ | ||||||
|  |                      'traits': utils.nested_list_of_dict_formatter('traits', | ||||||
|  |                                                                    ['name', | ||||||
|  |                                                                     'type', | ||||||
|  |                                                                     'value'])}) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @utils.arg('-m', '--message_id', metavar='<message_id>', | ||||||
|  |            help='The id of the event. Should be a UUID', | ||||||
|  |            required=True) | ||||||
|  | def do_event_show(cc, args={}): | ||||||
|  |     '''Show a particular event.''' | ||||||
|  |     event = cc.events.get(args.message_id) | ||||||
|  |     fields = ['event_type', 'generated', 'traits'] | ||||||
|  |     data = dict([(f, getattr(event, f, '')) for f in fields]) | ||||||
|  |     utils.print_dict(data, wrap=72) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def do_event_type_list(cc, args={}): | ||||||
|  |     '''List event types.''' | ||||||
|  |     event_types = cc.event_types.list() | ||||||
|  |     utils.print_list(event_types, ['event_type'], ['Event Type']) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @utils.arg('-e', '--event_type', metavar='<EVENT_TYPE>', | ||||||
|  |            help='Type of the event for which traits will be shown', | ||||||
|  |            required=True) | ||||||
|  | def do_trait_description_list(cc, args={}): | ||||||
|  |     '''List trait info for an event type.''' | ||||||
|  |     trait_descriptions = cc.trait_descriptions.list(args.event_type) | ||||||
|  |     field_labels = ['Trait Name', 'Data Type'] | ||||||
|  |     fields = ['name', 'type'] | ||||||
|  |     utils.print_list(trait_descriptions, fields, field_labels) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @utils.arg('-e', '--event_type', metavar='<EVENT_TYPE>', | ||||||
|  |            help='Type of the event for which traits will listed', | ||||||
|  |            required=True) | ||||||
|  | @utils.arg('-t', '--trait_name', metavar='<TRAIT_NAME>', | ||||||
|  |            help='The name of the trait to list', | ||||||
|  |            required=True) | ||||||
|  | def do_trait_list(cc, args={}): | ||||||
|  |     '''List trait all traits with name <trait_name> for Event Type | ||||||
|  |     <event_type>. | ||||||
|  |     ''' | ||||||
|  |     traits = cc.traits.list(args.event_type, args.trait_name) | ||||||
|  |     field_labels = ['Trait Name', 'Value', 'Data Type'] | ||||||
|  |     fields = ['name', 'value', 'type'] | ||||||
|  |     utils.print_list(traits, fields, field_labels) | ||||||
|   | |||||||
| @@ -23,8 +23,9 @@ class Statistics(base.Resource): | |||||||
| class StatisticsManager(base.Manager): | class StatisticsManager(base.Manager): | ||||||
|     resource_class = Statistics |     resource_class = Statistics | ||||||
|  |  | ||||||
|     def list(self, meter_name, q=None, period=None): |     def list(self, meter_name, q=None, period=None, groupby=[]): | ||||||
|         p = ['period=%s' % period] if period else None |         p = ['period=%s' % period] if period else [] | ||||||
|  |         p.extend(['groupby=%s' % g for g in groupby] if groupby else []) | ||||||
|         return self._list(options.build_url( |         return self._list(options.build_url( | ||||||
|             '/v2/meters/' + meter_name + '/statistics', |             '/v2/meters/' + meter_name + '/statistics', | ||||||
|             q, p)) |             q, p)) | ||||||
|   | |||||||
							
								
								
									
										29
									
								
								ceilometerclient/v2/trait_descriptions.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ceilometerclient/v2/trait_descriptions.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | # -*- encoding: utf-8 -*- | ||||||
|  | # Copyright 2013 Hewlett-Packard Development Company, L.P. | ||||||
|  | # | ||||||
|  | #    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 ceilometerclient.common import base | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TraitDescription(base.Resource): | ||||||
|  |     def __repr__(self): | ||||||
|  |         return "<Trait %s>" % self._info | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TraitDescriptionManager(base.Manager): | ||||||
|  |     resource_class = TraitDescription | ||||||
|  |  | ||||||
|  |     def list(self, event_type): | ||||||
|  |         path = '/v2/event_types/%s/traits' % event_type | ||||||
|  |         return self._list(path) | ||||||
							
								
								
									
										29
									
								
								ceilometerclient/v2/traits.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								ceilometerclient/v2/traits.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,29 @@ | |||||||
|  | # -*- encoding: utf-8 -*- | ||||||
|  | # Copyright 2013 Hewlett-Packard Development Company, L.P. | ||||||
|  | # | ||||||
|  | #    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 ceilometerclient.common import base | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class Trait(base.Resource): | ||||||
|  |     def __repr__(self): | ||||||
|  |         return "<Trait %s>" % self._info | ||||||
|  |  | ||||||
|  |  | ||||||
|  | class TraitManager(base.Manager): | ||||||
|  |     resource_class = Trait | ||||||
|  |  | ||||||
|  |     def list(self, event_type, trait_name): | ||||||
|  |         path = '/v2/event_types/%s/traits/%s' % (event_type, trait_name) | ||||||
|  |         return self._list(path) | ||||||
| @@ -1,9 +1,6 @@ | |||||||
| # -*- coding: utf-8 -*- | # -*- coding: utf-8 -*- | ||||||
| # | # | ||||||
|  |  | ||||||
| import os |  | ||||||
| import sys |  | ||||||
|  |  | ||||||
| project = 'python-ceilometerclient' | project = 'python-ceilometerclient' | ||||||
|  |  | ||||||
| # -- General configuration ---------------------------------------------------- | # -- General configuration ---------------------------------------------------- | ||||||
|   | |||||||
| @@ -1,8 +1,10 @@ | |||||||
| [DEFAULT] | [DEFAULT] | ||||||
|  |  | ||||||
| # The list of modules to copy from openstack-common | # The list of modules to copy from openstack-common | ||||||
|  | module=cliutils | ||||||
| module=importutils | module=importutils | ||||||
| module=install_venv_common | module=install_venv_common | ||||||
|  | module=py3kcompat | ||||||
|  |  | ||||||
| # The base module to hold the copy of openstack.common | # The base module to hold the copy of openstack.common | ||||||
| base=ceilometerclient | base=ceilometerclient | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| pbr>=0.5.21,<1.0 | pbr>=0.5.21,<1.0 | ||||||
| argparse | argparse | ||||||
| httplib2 | iso8601>=0.1.8 | ||||||
| iso8601>=0.1.4 | PrettyTable>=0.7,<0.8 | ||||||
| PrettyTable>=0.6,<0.8 | python-keystoneclient>=0.4.1 | ||||||
| python-keystoneclient>=0.3.2 | six>=1.4.1 | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| [metadata] | [metadata] | ||||||
| name = python-ceilometerclient | name = python-ceilometerclient | ||||||
| summary = OpenStack Metering API Client Library | summary = OpenStack Telemetry API Client Library | ||||||
| description-file = | description-file = | ||||||
|     README.rst |     README.rst | ||||||
| author = OpenStack | author = OpenStack | ||||||
| @@ -36,3 +36,6 @@ all_files = 1 | |||||||
|  |  | ||||||
| [upload_sphinx] | [upload_sphinx] | ||||||
| upload-dir = doc/build/html | upload-dir = doc/build/html | ||||||
|  |  | ||||||
|  | [wheel] | ||||||
|  | universal = 1 | ||||||
|   | |||||||
| @@ -1,14 +1,10 @@ | |||||||
| # Install bounded pep8/pyflakes first, then let flake8 install | # Hacking already pins down pep8, pyflakes and flake8 | ||||||
| pep8==1.4.5 | hacking>=0.8.0,<0.9 | ||||||
| pyflakes>=0.7.2,<0.7.4 |  | ||||||
| flake8==2.0 |  | ||||||
| hacking>=0.5.6,<0.8 |  | ||||||
| coverage>=3.6 | coverage>=3.6 | ||||||
| discover | discover | ||||||
| fixtures>=0.3.14 | fixtures>=0.3.14 | ||||||
| mock>=1.0 | mock>=1.0 | ||||||
| mox>=0.5.3 |  | ||||||
| python-subunit | python-subunit | ||||||
| sphinx>=1.1.2 | sphinx>=1.1.2,<1.2 | ||||||
| testrepository>=0.0.17 | testrepository>=0.0.17 | ||||||
| testtools>=0.9.32 | testtools>=0.9.32 | ||||||
|   | |||||||
							
								
								
									
										27
									
								
								tools/ceilometer.bash_completion
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								tools/ceilometer.bash_completion
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | |||||||
|  | # bash completion for openstack ceilometer | ||||||
|  |  | ||||||
|  | _ceilometer_opts="" # lazy init | ||||||
|  | _ceilometer_flags="" # lazy init | ||||||
|  | _ceilometer_opts_exp="" # lazy init | ||||||
|  | _ceilometer() | ||||||
|  | { | ||||||
|  |     local cur prev kbc | ||||||
|  |     COMPREPLY=() | ||||||
|  |     cur="${COMP_WORDS[COMP_CWORD]}" | ||||||
|  |     prev="${COMP_WORDS[COMP_CWORD-1]}" | ||||||
|  |  | ||||||
|  |     if [ "x$_ceilometer_opts" == "x" ] ; then | ||||||
|  |         kbc="`ceilometer bash-completion | sed -e "s/ -h / /"`" | ||||||
|  |         _ceilometer_opts="`echo "$kbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/[ ][ ]*/ /g"`" | ||||||
|  |         _ceilometer_flags="`echo " $kbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/[ ][ ]*/ /g"`" | ||||||
|  |         _ceilometer_opts_exp="`echo $_ceilometer_opts | sed -e "s/[ ]/|/g"`" | ||||||
|  |     fi | ||||||
|  |  | ||||||
|  |     if [[ " ${COMP_WORDS[@]} " =~ " "($_ceilometer_opts_exp)" " && "$prev" != "help" ]] ; then | ||||||
|  |         COMPREPLY=($(compgen -W "${_ceilometer_flags}" -- ${cur})) | ||||||
|  |     else | ||||||
|  |         COMPREPLY=($(compgen -W "${_ceilometer_opts}" -- ${cur})) | ||||||
|  |     fi | ||||||
|  |     return 0 | ||||||
|  | } | ||||||
|  | complete -F _ceilometer ceilometer | ||||||
| @@ -1,5 +1,3 @@ | |||||||
| # vim: tabstop=4 shiftwidth=4 softtabstop=4 |  | ||||||
|  |  | ||||||
| # Copyright 2013 OpenStack Foundation | # Copyright 2013 OpenStack Foundation | ||||||
| # Copyright 2013 IBM Corp. | # Copyright 2013 IBM Corp. | ||||||
| # | # | ||||||
| @@ -114,15 +112,12 @@ class InstallVenv(object): | |||||||
|         print('Installing dependencies with pip (this can take a while)...') |         print('Installing dependencies with pip (this can take a while)...') | ||||||
|  |  | ||||||
|         # First things first, make sure our venv has the latest pip and |         # First things first, make sure our venv has the latest pip and | ||||||
|         # setuptools. |         # setuptools and pbr | ||||||
|         self.pip_install('pip>=1.3') |         self.pip_install('pip>=1.4') | ||||||
|         self.pip_install('setuptools') |         self.pip_install('setuptools') | ||||||
|  |         self.pip_install('pbr') | ||||||
|  |  | ||||||
|         self.pip_install('-r', self.requirements) |         self.pip_install('-r', self.requirements, '-r', self.test_requirements) | ||||||
|         self.pip_install('-r', self.test_requirements) |  | ||||||
|  |  | ||||||
|     def post_process(self): |  | ||||||
|         self.get_distro().post_process() |  | ||||||
|  |  | ||||||
|     def parse_args(self, argv): |     def parse_args(self, argv): | ||||||
|         """Parses command-line arguments.""" |         """Parses command-line arguments.""" | ||||||
| @@ -156,14 +151,6 @@ class Distro(InstallVenv): | |||||||
|                  ' requires virtualenv, please install it using your' |                  ' requires virtualenv, please install it using your' | ||||||
|                  ' favorite package management tool' % self.project) |                  ' favorite package management tool' % self.project) | ||||||
|  |  | ||||||
|     def post_process(self): |  | ||||||
|         """Any distribution-specific post-processing gets done here. |  | ||||||
|  |  | ||||||
|         In particular, this is useful for applying patches to code inside |  | ||||||
|         the venv. |  | ||||||
|         """ |  | ||||||
|         pass |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class Fedora(Distro): | class Fedora(Distro): | ||||||
|     """This covers all Fedora-based distributions. |     """This covers all Fedora-based distributions. | ||||||
| @@ -175,10 +162,6 @@ class Fedora(Distro): | |||||||
|         return self.run_command_with_code(['rpm', '-q', pkg], |         return self.run_command_with_code(['rpm', '-q', pkg], | ||||||
|                                           check_exit_code=False)[1] == 0 |                                           check_exit_code=False)[1] == 0 | ||||||
|  |  | ||||||
|     def apply_patch(self, originalfile, patchfile): |  | ||||||
|         self.run_command(['patch', '-N', originalfile, patchfile], |  | ||||||
|                          check_exit_code=False) |  | ||||||
|  |  | ||||||
|     def install_virtualenv(self): |     def install_virtualenv(self): | ||||||
|         if self.check_cmd('virtualenv'): |         if self.check_cmd('virtualenv'): | ||||||
|             return |             return | ||||||
| @@ -187,26 +170,3 @@ class Fedora(Distro): | |||||||
|             self.die("Please install 'python-virtualenv'.") |             self.die("Please install 'python-virtualenv'.") | ||||||
|  |  | ||||||
|         super(Fedora, self).install_virtualenv() |         super(Fedora, self).install_virtualenv() | ||||||
|  |  | ||||||
|     def post_process(self): |  | ||||||
|         """Workaround for a bug in eventlet. |  | ||||||
|  |  | ||||||
|         This currently affects RHEL6.1, but the fix can safely be |  | ||||||
|         applied to all RHEL and Fedora distributions. |  | ||||||
|  |  | ||||||
|         This can be removed when the fix is applied upstream. |  | ||||||
|  |  | ||||||
|         Nova: https://bugs.launchpad.net/nova/+bug/884915 |  | ||||||
|         Upstream: https://bitbucket.org/eventlet/eventlet/issue/89 |  | ||||||
|         RHEL: https://bugzilla.redhat.com/958868 |  | ||||||
|         """ |  | ||||||
|  |  | ||||||
|         # Install "patch" program if it's not there |  | ||||||
|         if not self.check_pkg('patch'): |  | ||||||
|             self.die("Please install 'patch'.") |  | ||||||
|  |  | ||||||
|         # Apply the eventlet patch |  | ||||||
|         self.apply_patch(os.path.join(self.venv, 'lib', self.py_version, |  | ||||||
|                                       'site-packages', |  | ||||||
|                                       'eventlet/green/subprocess.py'), |  | ||||||
|                          'contrib/redhat-eventlet.patch') |  | ||||||
|   | |||||||
							
								
								
									
										6
									
								
								tox.ini
									
									
									
									
									
								
							
							
						
						
									
										6
									
								
								tox.ini
									
									
									
									
									
								
							| @@ -1,7 +1,11 @@ | |||||||
| [tox] | [tox] | ||||||
| envlist = py26,py27,pypy,pep8 | envlist = py26,py27,pypy,pep8 | ||||||
|  | minversion = 1.6 | ||||||
|  | skipsdist = True | ||||||
|  |  | ||||||
| [testenv] | [testenv] | ||||||
|  | usedevelop = True | ||||||
|  | install_command = pip install -U {opts} {packages} | ||||||
| setenv = VIRTUAL_ENV={envdir} | setenv = VIRTUAL_ENV={envdir} | ||||||
|          LANG=en_US.UTF-8 |          LANG=en_US.UTF-8 | ||||||
|          LANGUAGE=en_US:en |          LANGUAGE=en_US:en | ||||||
| @@ -24,6 +28,6 @@ commands = python setup.py testr --coverage --testr-args='{posargs}' | |||||||
| commands = {posargs} | commands = {posargs} | ||||||
|  |  | ||||||
| [flake8] | [flake8] | ||||||
| ignore = E121,E122,E123,E128,E711,E721,E712,H233,H302 | ignore = None | ||||||
| show-source = True | show-source = True | ||||||
| exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools | exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Thomas Goirand
					Thomas Goirand