diff --git a/.gitignore b/.gitignore index 85ed18c..f196e15 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,8 @@ build cover .testrepository .venv +doc/build +doc/source/ref subunit.log AUTHORS ChangeLog diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst new file mode 100644 index 0000000..6967cbd --- /dev/null +++ b/CONTRIBUTING.rst @@ -0,0 +1,16 @@ +If you would like to contribute to the development of OpenStack, +you must follow the steps documented at: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Once those steps have been completed, changes to OpenStack +should be submitted for review via the Gerrit tool, following +the workflow documented at: + + http://docs.openstack.org/infra/manual/developers.html#development-workflow + +Pull requests submitted through GitHub will be ignored. + +Bugs should be filed on Launchpad, not GitHub: + + https://bugs.launchpad.net/python-ceilometerclient diff --git a/README.rst b/README.rst index 541ad0e..184bba4 100644 --- a/README.rst +++ b/README.rst @@ -6,7 +6,7 @@ provides a Python API (the ``ceilometerclient`` module) and a command-line tool (``ceilometer``). Development takes place via the usual OpenStack processes as outlined in the -`OpenStack wiki `_. The master -repository is on `GitHub `_. +`developer guide `_. The master +repository is in `Git `_. See release notes and more at ``_. diff --git a/ceilometerclient/client.py b/ceilometerclient/client.py index f8c8ac0..c0a8be5 100644 --- a/ceilometerclient/client.py +++ b/ceilometerclient/client.py @@ -10,98 +10,311 @@ # License for the specific language governing permissions and limitations # under the License. -from keystoneclient.v2_0 import client as ksclient -import six +from keystoneclient.auth.identity import v2 as v2_auth +from keystoneclient.auth.identity import v3 as v3_auth +from keystoneclient import discover +from keystoneclient import exceptions as ks_exc +from keystoneclient import session +from oslo.utils import strutils +import six.moves.urllib.parse as urlparse from ceilometerclient.common import utils +from ceilometerclient import exc +from ceilometerclient.openstack.common.apiclient import auth +from ceilometerclient.openstack.common.apiclient import exceptions -def _get_ksclient(**kwargs): - """Get an endpoint and auth token from Keystone. - - :param kwargs: keyword args containing credentials: - * username: name of user - * password: user's password - * auth_url: endpoint to authenticate against - * cacert: path of CA TLS certificate - * insecure: allow insecure SSL (no cert verification) - * tenant_{name|id}: name or ID of tenant - """ - return ksclient.Client(username=kwargs.get('username'), - password=kwargs.get('password'), - tenant_id=kwargs.get('tenant_id'), - tenant_name=kwargs.get('tenant_name'), - auth_url=kwargs.get('auth_url'), - region_name=kwargs.get('region_name'), - cacert=kwargs.get('cacert'), - insecure=kwargs.get('insecure')) +def _discover_auth_versions(session, auth_url): + # discover the API versions the server is supporting based on the + # given URL + v2_auth_url = None + v3_auth_url = None + try: + ks_discover = discover.Discover(session=session, auth_url=auth_url) + v2_auth_url = ks_discover.url_for('2.0') + v3_auth_url = ks_discover.url_for('3.0') + except ks_exc.DiscoveryFailure: + raise + except exceptions.ClientException: + # Identity service may not support discovery. In that case, + # try to determine version from auth_url + url_parts = urlparse.urlparse(auth_url) + (scheme, netloc, path, params, query, fragment) = url_parts + path = path.lower() + if path.startswith('/v3'): + v3_auth_url = auth_url + elif path.startswith('/v2'): + v2_auth_url = auth_url + else: + raise exc.CommandError('Unable to determine the Keystone ' + 'version to authenticate with ' + 'using the given auth_url.') + return v2_auth_url, v3_auth_url -def _get_endpoint(client, **kwargs): - """Get an endpoint using the provided keystone client.""" - return client.service_catalog.url_for( - service_type=kwargs.get('service_type') or 'metering', - endpoint_type=kwargs.get('endpoint_type') or 'publicURL') +def _get_keystone_session(**kwargs): + # TODO(fabgia): the heavy lifting here should be really done by Keystone. + # Unfortunately Keystone does not support a richer method to perform + # discovery and return a single viable URL. A bug against Keystone has + # been filed: https://bugs.launchpad.net/python-keystoneclient/+bug/1330677 + + # first create a Keystone session + cacert = kwargs.pop('cacert', None) + cert = kwargs.pop('cert', None) + key = kwargs.pop('key', None) + insecure = kwargs.pop('insecure', False) + auth_url = kwargs.pop('auth_url', None) + project_id = kwargs.pop('project_id', None) + project_name = kwargs.pop('project_name', None) + + if insecure: + verify = False + else: + verify = cacert or True + + if cert and key: + # passing cert and key together is deprecated in favour of the + # requests lib form of having the cert and key as a tuple + cert = (cert, key) + + # create the keystone client session + ks_session = session.Session(verify=verify, cert=cert) + v2_auth_url, v3_auth_url = _discover_auth_versions(ks_session, auth_url) + + username = kwargs.pop('username', None) + user_id = kwargs.pop('user_id', None) + user_domain_name = kwargs.pop('user_domain_name', None) + user_domain_id = kwargs.pop('user_domain_id', None) + project_domain_name = kwargs.pop('project_domain_name', None) + project_domain_id = kwargs.pop('project_domain_id', None) + auth = None + + use_domain = (user_domain_id or user_domain_name or + project_domain_id or project_domain_name) + use_v3 = v3_auth_url and (use_domain or (not v2_auth_url)) + use_v2 = v2_auth_url and not use_domain + + if use_v3: + # the auth_url as v3 specified + # e.g. http://no.where:5000/v3 + # Keystone will return only v3 as viable option + auth = v3_auth.Password( + v3_auth_url, + username=username, + password=kwargs.pop('password', None), + user_id=user_id, + user_domain_name=user_domain_name, + user_domain_id=user_domain_id, + project_domain_name=project_domain_name, + project_domain_id=project_domain_id) + elif use_v2: + # the auth_url as v2 specified + # e.g. http://no.where:5000/v2.0 + # Keystone will return only v2 as viable option + auth = v2_auth.Password( + v2_auth_url, + username, + kwargs.pop('password', None), + tenant_id=project_id, + tenant_name=project_name) + else: + raise exc.CommandError('Unable to determine the Keystone version ' + 'to authenticate with using the given ' + 'auth_url.') + + ks_session.auth = auth + return ks_session -def get_client(api_version, **kwargs): - """Get an authtenticated client, based on the credentials - in the keyword args. +def _get_endpoint(ks_session, **kwargs): + """Get an endpoint using the provided keystone session.""" - :param api_version: the API version to use ('1' or '2') - :param kwargs: keyword args containing credentials, either: - * os_auth_token: pre-existing token to re-use - * ceilometer_url: ceilometer API endpoint - or: - * os_username: name of user - * os_password: user's password - * os_auth_url: endpoint to authenticate against - * os_cacert: path of CA TLS certificate - * insecure: allow insecure SSL (no cert verification) - * os_tenant_{name|id}: name or ID of tenant - """ - token = kwargs.get('os_auth_token') - if token: - token = (token if six.callable(token) else lambda: token) + # set service specific endpoint types + endpoint_type = kwargs.get('endpoint_type') or 'publicURL' + service_type = kwargs.get('service_type') or 'metering' - if token and kwargs.get('ceilometer_url'): - endpoint = kwargs.get('ceilometer_url') - elif (kwargs.get('os_username') and - kwargs.get('os_password') and - kwargs.get('os_auth_url') and - (kwargs.get('os_tenant_id') or kwargs.get('os_tenant_name'))): + endpoint = ks_session.get_endpoint(service_type=service_type, + interface=endpoint_type, + region_name=kwargs.get('region_name')) - ks_kwargs = { - 'username': kwargs.get('os_username'), - 'password': kwargs.get('os_password'), - 'tenant_id': kwargs.get('os_tenant_id'), - 'tenant_name': kwargs.get('os_tenant_name'), - 'auth_url': kwargs.get('os_auth_url'), - 'region_name': kwargs.get('os_region_name'), - 'service_type': kwargs.get('os_service_type'), - 'endpoint_type': kwargs.get('os_endpoint_type'), - 'cacert': kwargs.get('os_cacert'), - 'insecure': kwargs.get('insecure'), - } - _ksclient = _get_ksclient(**ks_kwargs) - token = token or (lambda: _ksclient.auth_token) + return endpoint - endpoint = kwargs.get('ceilometer_url') or \ - _get_endpoint(_ksclient, **ks_kwargs) - cli_kwargs = { - 'token': token, - 'insecure': kwargs.get('insecure'), - 'timeout': kwargs.get('timeout'), - 'cacert': kwargs.get('os_cacert'), - 'cert_file': kwargs.get('cert_file'), - 'key_file': kwargs.get('key_file'), - } +class AuthPlugin(auth.BaseAuthPlugin): + opt_names = ['tenant_id', 'region_name', 'auth_token', + 'service_type', 'endpoint_type', 'cacert', + 'auth_url', 'insecure', 'cert_file', 'key_file', + 'cert', 'key', 'tenant_name', 'project_name', + 'project_id', 'user_domain_id', 'user_domain_name', + 'password', 'username', 'endpoint'] - return Client(api_version, endpoint, **cli_kwargs) + def __init__(self, auth_system=None, **kwargs): + self.opt_names.extend(self.common_opt_names) + super(AuthPlugin, self).__init__(auth_system, **kwargs) + + def _do_authenticate(self, http_client): + token = self.opts.get('token') or self.opts.get('auth_token') + endpoint = self.opts.get('endpoint') + if not (token and endpoint): + project_id = (self.opts.get('project_id') or + self.opts.get('tenant_id')) + project_name = (self.opts.get('project_name') or + self.opts.get('tenant_name')) + ks_kwargs = { + 'username': self.opts.get('username'), + 'password': self.opts.get('password'), + 'user_id': self.opts.get('user_id'), + 'user_domain_id': self.opts.get('user_domain_id'), + 'user_domain_name': self.opts.get('user_domain_name'), + 'project_id': project_id, + 'project_name': project_name, + 'project_domain_name': self.opts.get('project_domain_name'), + 'project_domain_id': self.opts.get('project_domain_id'), + 'auth_url': self.opts.get('auth_url'), + 'cacert': self.opts.get('cacert'), + 'cert': self.opts.get('cert'), + 'key': self.opts.get('key'), + 'insecure': strutils.bool_from_string( + self.opts.get('insecure')), + 'endpoint_type': self.opts.get('endpoint_type'), + } + + # retrieve session + ks_session = _get_keystone_session(**ks_kwargs) + token = lambda: ks_session.get_token() + endpoint = (self.opts.get('endpoint') or + _get_endpoint(ks_session, **ks_kwargs)) + self.opts['token'] = token + self.opts['endpoint'] = endpoint + + def token_and_endpoint(self, endpoint_type, service_type): + token = self.opts.get('token') + if callable(token): + token = token() + return token, self.opts.get('endpoint') + + def sufficient_options(self): + """Check if all required options are present. + + :raises: AuthPluginOptionsMissing + """ + has_token = self.opts.get('token') or self.opts.get('auth_token') + no_auth = has_token and self.opts.get('endpoint') + has_tenant = self.opts.get('tenant_id') or self.opts.get('tenant_name') + has_credential = (self.opts.get('username') and has_tenant + and self.opts.get('password') + and self.opts.get('auth_url')) + missing = not (no_auth or has_credential) + if missing: + missing_opts = [] + opts = ['token', 'endpoint', 'username', 'password', 'auth_url', + 'tenant_id', 'tenant_name'] + for opt in opts: + if not self.opts.get(opt): + missing_opts.append(opt) + raise exceptions.AuthPluginOptionsMissing(missing_opts) def Client(version, *args, **kwargs): module = utils.import_versioned_module(version, 'client') client_class = getattr(module, 'Client') + kwargs['token'] = kwargs.get('token') or kwargs.get('auth_token') return client_class(*args, **kwargs) + + +def _adjust_params(kwargs): + timeout = kwargs.get('timeout') + if timeout is not None: + timeout = int(timeout) + if timeout <= 0: + timeout = None + + insecure = strutils.bool_from_string(kwargs.get('insecure')) + verify = kwargs.get('verify') + if verify is None: + if insecure: + verify = False + else: + verify = kwargs.get('cacert') or True + + cert = kwargs.get('cert_file') + key = kwargs.get('key_file') + if cert and key: + cert = cert, key + return {'verify': verify, 'cert': cert, 'timeout': timeout} + + +def get_client(version, **kwargs): + """Get an authenticated client, based on the credentials in the kwargs. + + :param api_version: the API version to use ('1' or '2') + :param kwargs: keyword args containing credentials, either: + + * os_auth_token: (DEPRECATED) pre-existing token to re-use, + use os_token instead + * os_token: pre-existing token to re-use + * ceilometer_url: (DEPRECATED) Ceilometer API endpoint, + use os_endpoint instead + * os_endpoint: Ceilometer API endpoint + or: + * os_username: name of user + * os_password: user's password + * os_user_id: user's id + * os_user_domain_id: the domain id of the user + * os_user_domain_name: the domain name of the user + * os_project_id: the user project id + * os_tenant_id: V2 alternative to os_project_id + * os_project_name: the user project name + * os_tenant_name: V2 alternative to os_project_name + * os_project_domain_name: domain name for the user project + * os_project_domain_id: domain id for the user project + * os_auth_url: endpoint to authenticate against + * os_cert|os_cacert: path of CA TLS certificate + * os_key: SSL private key + * insecure: allow insecure SSL (no cert verification) + """ + endpoint = kwargs.get('os_endpoint') or kwargs.get('ceilometer_url') + + cli_kwargs = { + 'username': kwargs.get('os_username'), + 'password': kwargs.get('os_password'), + 'tenant_id': kwargs.get('os_tenant_id'), + 'tenant_name': kwargs.get('os_tenant_name'), + 'auth_url': kwargs.get('os_auth_url'), + 'region_name': kwargs.get('os_region_name'), + 'service_type': kwargs.get('os_service_type'), + 'endpoint_type': kwargs.get('os_endpoint_type'), + 'cacert': kwargs.get('os_cacert'), + 'cert_file': kwargs.get('os_cert'), + 'key_file': kwargs.get('os_key'), + 'token': kwargs.get('os_token') or kwargs.get('os_auth_token'), + 'user_domain_name': kwargs.get('os_user_domain_name'), + 'user_domain_id': kwargs.get('os_user_domain_id'), + 'project_domain_name': kwargs.get('os_project_domain_name'), + 'project_domain_id': kwargs.get('os_project_domain_id'), + } + + cli_kwargs.update(kwargs) + cli_kwargs.update(_adjust_params(cli_kwargs)) + + return Client(version, endpoint, **cli_kwargs) + + +def get_auth_plugin(endpoint, **kwargs): + auth_plugin = AuthPlugin( + auth_url=kwargs.get('auth_url'), + service_type=kwargs.get('service_type'), + token=kwargs.get('token'), + endpoint_type=kwargs.get('endpoint_type'), + cacert=kwargs.get('cacert'), + tenant_id=kwargs.get('project_id') or kwargs.get('tenant_id'), + endpoint=endpoint, + username=kwargs.get('username'), + password=kwargs.get('password'), + tenant_name=kwargs.get('tenant_name'), + user_domain_name=kwargs.get('user_domain_name'), + user_domain_id=kwargs.get('user_domain_id'), + project_domain_name=kwargs.get('project_domain_name'), + project_domain_id=kwargs.get('project_domain_id') + ) + return auth_plugin diff --git a/ceilometerclient/common/base.py b/ceilometerclient/common/base.py index a01adbf..7e8f80f 100644 --- a/ceilometerclient/common/base.py +++ b/ceilometerclient/common/base.py @@ -19,6 +19,7 @@ Base utilities to build API operation managers and objects on top of. import copy +from ceilometerclient import exc from ceilometerclient.openstack.common.apiclient import base # Python 2.4 compat @@ -30,7 +31,9 @@ except NameError: def getid(obj): - """Abstracts the common pattern of allowing both an object or an + """Extracts object ID. + + Abstracts the common pattern of allowing both an object or an object's ID (UUID) as a parameter when dealing with relationships. """ try: @@ -40,22 +43,32 @@ def getid(obj): class Manager(object): - """Managers interact with a particular type of API - (samples, meters, alarms, etc.) and provide CRUD operations for them. + """Managers interact with a particular type of API. + + It works with samples, meters, alarms, etc. and provide CRUD operations for + them. """ resource_class = None def __init__(self, api): self.api = api + @property + def client(self): + """Compatible with latest oslo-incubator.apiclient code.""" + return self.api + def _create(self, url, body): - resp, body = self.api.json_request('POST', url, body=body) + body = self.api.post(url, json=body).json() if body: return self.resource_class(self, body) def _list(self, url, response_key=None, obj_class=None, body=None, expect_single=False): - resp, body = self.api.json_request('GET', url) + resp = self.api.get(url) + if not resp.content: + raise exc.HTTPNotFound + body = resp.json() if obj_class is None: obj_class = self.resource_class @@ -72,18 +85,20 @@ class Manager(object): return [obj_class(self, res, loaded=True) for res in data if res] def _update(self, url, body, response_key=None): - resp, body = self.api.json_request('PUT', url, body=body) + body = self.api.put(url, json=body).json() # PUT requests may not return a body if body: return self.resource_class(self, body) def _delete(self, url): - self.api.raw_request('DELETE', url) + self.api.delete(url) class Resource(base.Resource): - """A resource represents a particular instance of an object (tenant, user, - etc). This is pretty much just a bag for attributes. + """A resource represents a particular instance of an object. + + Resource might be tenant, user, etc. + This is pretty much just a bag for attributes. :param manager: Manager object :param info: dictionary representing resource attributes diff --git a/ceilometerclient/common/http.py b/ceilometerclient/common/http.py deleted file mode 100644 index cd940a1..0000000 --- a/ceilometerclient/common/http.py +++ /dev/null @@ -1,301 +0,0 @@ -# Copyright 2012 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. - -import copy -import logging -import os -import socket - -try: - import ssl -except ImportError: - #TODO(bcwaldon): Handle this failure more gracefully - pass - -try: - import json -except ImportError: - import simplejson as json - -import six -from six.moves import http_client as httplib # noqa -from six.moves.urllib import parse - -from ceilometerclient import exc - - -LOG = logging.getLogger(__name__) -USER_AGENT = 'python-ceilometerclient' -CHUNKSIZE = 1024 * 64 # 64kB - - -class HTTPClient(object): - - def __init__(self, endpoint, **kwargs): - self.endpoint = endpoint - self.auth_token = kwargs.get('token') - self.connection_params = self.get_connection_params(endpoint, **kwargs) - self.proxy_url = self.get_proxy_url() - - @staticmethod - def get_connection_params(endpoint, **kwargs): - parts = parse.urlparse(endpoint) - - _args = (parts.hostname, parts.port, parts.path) - _kwargs = {'timeout': (float(kwargs.get('timeout')) - if kwargs.get('timeout') else 600)} - - if parts.scheme == 'https': - _class = VerifiedHTTPSConnection - _kwargs['cacert'] = kwargs.get('cacert', None) - _kwargs['cert_file'] = kwargs.get('cert_file', None) - _kwargs['key_file'] = kwargs.get('key_file', None) - _kwargs['insecure'] = kwargs.get('insecure', False) - elif parts.scheme == 'http': - _class = httplib.HTTPConnection - else: - msg = 'Unsupported scheme: %s' % parts.scheme - raise exc.InvalidEndpoint(msg) - - return (_class, _args, _kwargs) - - def get_connection(self): - _class = self.connection_params[0] - try: - if self.proxy_url: - proxy_parts = parse.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: - raise exc.InvalidEndpoint() - - def log_curl_request(self, method, url, kwargs): - curl = ['curl -i -X %s' % method] - - for (key, value) in kwargs['headers'].items(): - header = '-H \'%s: %s\'' % (key, value) - curl.append(header) - - conn_params_fmt = [ - ('key_file', '--key %s'), - ('cert_file', '--cert %s'), - ('cacert', '--cacert %s'), - ] - for (key, fmt) in conn_params_fmt: - value = self.connection_params[2].get(key) - if value: - curl.append(fmt % value) - - if self.connection_params[2].get('insecure'): - curl.append('-k') - - if 'body' in kwargs: - curl.append('-d \'%s\'' % kwargs['body']) - - curl.append('%s/%s' % (self.endpoint.rstrip('/'), url.lstrip('/'))) - LOG.debug(' '.join(curl)) - - @staticmethod - def log_http_response(resp, body=None): - status = (resp.version / 10.0, resp.status, resp.reason) - dump = ['\nHTTP/%.1f %s %s' % status] - dump.extend(['%s: %s' % (k, v) for k, v in resp.getheaders()]) - dump.append('') - if body: - dump.extend([body, '']) - LOG.debug('\n'.join(dump)) - - def _make_connection_url(self, url): - (_class, _args, _kwargs) = self.connection_params - base_url = _args[2] - return '%s/%s' % (base_url.rstrip('/'), url.lstrip('/')) - - def _http_request(self, url, method, **kwargs): - """Send an http request with the specified characteristics. - - Wrapper around httplib.HTTP(S)Connection.request to handle tasks such - as setting headers and error handling. - """ - # Copy the kwargs so we can reuse the original in case of redirects - kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) - kwargs['headers'].setdefault('User-Agent', USER_AGENT) - auth_token = self.auth_token() - if auth_token: - kwargs['headers'].setdefault('X-Auth-Token', auth_token) - - self.log_curl_request(method, url, kwargs) - conn = self.get_connection() - - try: - if self.proxy_url: - conn_url = (self.endpoint.rstrip('/') + - self._make_connection_url(url)) - else: - conn_url = self._make_connection_url(url) - conn.request(method, conn_url, **kwargs) - resp = conn.getresponse() - except socket.gaierror as e: - message = ("Error finding address for %(url)s: %(e)s" - % dict(url=url, e=e)) - raise exc.InvalidEndpoint(message=message) - except (socket.error, socket.timeout) as e: - endpoint = self.endpoint - message = ("Error communicating with %(endpoint)s %(e)s" - % dict(endpoint=endpoint, e=e)) - raise exc.CommunicationError(message=message) - - body_iter = ResponseBodyIterator(resp) - - # Read body into string if it isn't obviously image data - if resp.getheader('content-type', None) != 'application/octet-stream': - body_str = ''.join([chunk for chunk in body_iter]) - self.log_http_response(resp, body_str) - body_iter = six.StringIO(body_str) - else: - self.log_http_response(resp) - - if 400 <= resp.status < 600: - LOG.warn("Request returned failure status.") - raise exc.from_response(resp, ''.join(body_iter)) - elif resp.status in (301, 302, 305): - # Redirected. Reissue the request to the new location. - return self._http_request(resp['location'], method, **kwargs) - elif resp.status == 300: - raise exc.from_response(resp) - - return resp, body_iter - - def json_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', 'application/json') - kwargs['headers'].setdefault('Accept', 'application/json') - - if 'body' in kwargs: - kwargs['body'] = json.dumps(kwargs['body']) - - resp, body_iter = self._http_request(url, method, **kwargs) - content_type = resp.getheader('content-type', None) - - if resp.status == 204 or resp.status == 205 or content_type is None: - return resp, list() - - if 'application/json' in content_type: - body = ''.join([chunk for chunk in body_iter]) - try: - body = json.loads(body) - except ValueError: - LOG.error('Could not decode response body as JSON') - else: - body = None - - return resp, body - - def raw_request(self, method, url, **kwargs): - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', - 'application/octet-stream') - return self._http_request(url, method, **kwargs) - - def get_proxy_url(self): - scheme = parse.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): - """httplib-compatibile connection using client-side SSL authentication - - :see http://code.activestate.com/recipes/ - 577548-https-httplib-client-connection-with-certificate-v/ - """ - - def __init__(self, host, port, key_file=None, cert_file=None, - cacert=None, timeout=None, insecure=False): - httplib.HTTPSConnection.__init__(self, host, port, key_file=key_file, - cert_file=cert_file) - self.key_file = key_file - self.cert_file = cert_file - if cacert is not None: - self.cacert = cacert - else: - self.cacert = self.get_system_ca_file() - self.timeout = timeout - self.insecure = insecure - - def connect(self): - """Connect to a host on a given (SSL) port. - If cacert is pointing somewhere, use it to check Server Certificate. - - Redefined/copied and extended from httplib.py:1105 (Python 2.6.x). - This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to - ssl.wrap_socket(), which forces SSL to check server certificate against - our client certificate. - """ - sock = socket.create_connection((self.host, self.port), self.timeout) - - if self._tunnel_host: - self.sock = sock - self._tunnel() - - if self.insecure is True: - kwargs = {'cert_reqs': ssl.CERT_NONE} - else: - kwargs = {'cert_reqs': ssl.CERT_REQUIRED, 'ca_certs': self.cacert} - - if self.cert_file: - kwargs['certfile'] = self.cert_file - if self.key_file: - kwargs['keyfile'] = self.key_file - - self.sock = ssl.wrap_socket(sock, **kwargs) - - @staticmethod - def get_system_ca_file(): - """Return path to system default CA file.""" - # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, - # Suse, FreeBSD/OpenBSD - ca_path = ['/etc/ssl/certs/ca-certificates.crt', - '/etc/pki/tls/certs/ca-bundle.crt', - '/etc/ssl/ca-bundle.pem', - '/etc/ssl/cert.pem'] - for ca in ca_path: - if os.path.exists(ca): - return ca - return None - - -class ResponseBodyIterator(object): - """A class that acts as an iterator over an HTTP response.""" - - def __init__(self, resp): - self.resp = resp - - def __iter__(self): - while True: - yield self.next() - - def next(self): - chunk = self.resp.read(CHUNKSIZE) - if chunk: - return chunk - else: - raise StopIteration() diff --git a/ceilometerclient/common/utils.py b/ceilometerclient/common/utils.py index db7b042..4e77632 100644 --- a/ceilometerclient/common/utils.py +++ b/ceilometerclient/common/utils.py @@ -14,17 +14,19 @@ # under the License. from __future__ import print_function -import six + import sys import textwrap import uuid +from oslo.serialization import jsonutils +from oslo.utils import encodeutils +from oslo.utils import importutils import prettytable +import six from ceilometerclient import exc from ceilometerclient.openstack.common import cliutils -from ceilometerclient.openstack.common import importutils -from ceilometerclient.openstack.common import strutils # Decorator for cli-args @@ -83,15 +85,12 @@ def format_nested_list_of_dict(l, column_names): def print_dict(d, dict_property="Property", wrap=0): - pt = prettytable.PrettyTable([dict_property, 'Value'], - caching=False, print_empty=False) + pt = prettytable.PrettyTable([dict_property, 'Value'], print_empty=False) pt.align = 'l' for k, v in sorted(six.iteritems(d)): # convert dict to str to check length if isinstance(v, dict): - v = str(v) - if isinstance(v, six.string_types): - v = strutils.safe_encode(v) + v = jsonutils.dumps(v) # 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: @@ -106,7 +105,11 @@ def print_dict(d, dict_property="Property", wrap=0): if wrap > 0: v = textwrap.fill(str(v), wrap) pt.add_row([k, v]) - print(pt.get_string()) + encoded = encodeutils.safe_encode(pt.get_string()) + # FIXME(gordc): https://bugs.launchpad.net/oslo-incubator/+bug/1370710 + if six.PY3: + encoded = encoded.decode() + print(encoded) def find_resource(manager, name_or_id): @@ -115,20 +118,20 @@ def find_resource(manager, name_or_id): try: if isinstance(name_or_id, int) or name_or_id.isdigit(): return manager.get(int(name_or_id)) - except exc.NotFound: + except exc.HTTPNotFound: pass # now try to get entity as uuid try: uuid.UUID(str(name_or_id)) return manager.get(name_or_id) - except (ValueError, exc.NotFound): + except (ValueError, exc.HTTPNotFound): pass # finally try to find entity by name try: return manager.find(name=name_or_id) - except exc.NotFound: + except exc.HTTPNotFound: msg = "No %s with a name or ID of '%s' exists." % \ (manager.resource_class.__name__.lower(), name_or_id) raise exc.CommandError(msg) @@ -155,8 +158,7 @@ def args_array_to_dict(kwargs, key_to_convert): def args_array_to_list_of_dicts(kwargs, key_to_convert): - """Converts ['a=1;b=2','c=3;d=4'] to [{a:1,b:2},{c:3,d:4}] - """ + """Converts ['a=1;b=2','c=3;d=4'] to [{a:1,b:2},{c:3,d:4}].""" values_to_convert = kwargs.get(key_to_convert) if values_to_convert: try: diff --git a/ceilometerclient/exc.py b/ceilometerclient/exc.py index 57c60c0..c7e550b 100644 --- a/ceilometerclient/exc.py +++ b/ceilometerclient/exc.py @@ -35,11 +35,7 @@ class CommunicationError(BaseException): """Unable to communicate with server.""" -class ClientException(Exception): - """DEPRECATED.""" - - -class HTTPException(ClientException): +class HTTPException(BaseException): """Base exception for all HTTP-derived exceptions.""" code = 'N/A' @@ -47,6 +43,14 @@ class HTTPException(ClientException): self.details = details 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 "%s (HTTP %s)" % (self.__class__.__name__, self.code) @@ -60,74 +64,34 @@ class HTTPMultipleChoices(HTTPException): self.details) -class BadRequest(HTTPException): - """DEPRECATED.""" +class HTTPBadRequest(HTTPException): code = 400 -class HTTPBadRequest(BadRequest): - - 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): - """DEPRECATED.""" +class HTTPUnauthorized(HTTPException): code = 401 -class HTTPUnauthorized(Unauthorized): - pass - - -class Forbidden(HTTPException): - """DEPRECATED.""" +class HTTPForbidden(HTTPException): code = 403 -class HTTPForbidden(Forbidden): - pass - - -class NotFound(HTTPException): - """DEPRECATED.""" +class HTTPNotFound(HTTPException): code = 404 -class HTTPNotFound(NotFound): - pass - - class HTTPMethodNotAllowed(HTTPException): code = 405 -class Conflict(HTTPException): - """DEPRECATED.""" +class HTTPConflict(HTTPException): code = 409 -class HTTPConflict(Conflict): - pass - - -class OverLimit(HTTPException): - """DEPRECATED.""" +class HTTPOverLimit(HTTPException): code = 413 -class HTTPOverLimit(OverLimit): - pass - - class HTTPInternalServerError(HTTPException): code = 500 @@ -140,16 +104,11 @@ class HTTPBadGateway(HTTPException): code = 502 -class ServiceUnavailable(HTTPException): - """DEPRECATED.""" +class HTTPServiceUnavailable(HTTPException): code = 503 -class HTTPServiceUnavailable(ServiceUnavailable): - pass - - -#NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception +# NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception # classes _code_map = {} for obj_name in dir(sys.modules[__name__]): @@ -162,13 +121,3 @@ def from_response(response, details=None): """Return an instance of an HTTPException based on httplib response.""" cls = _code_map.get(response.status, HTTPException) return cls(details) - - -class NoTokenLookupException(Exception): - """DEPRECATED.""" - pass - - -class EndpointNotFound(Exception): - """DEPRECATED.""" - pass diff --git a/ceilometerclient/openstack/common/_i18n.py b/ceilometerclient/openstack/common/_i18n.py new file mode 100644 index 0000000..4b0745f --- /dev/null +++ b/ceilometerclient/openstack/common/_i18n.py @@ -0,0 +1,45 @@ +# 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. + +"""oslo.i18n integration module. + +See http://docs.openstack.org/developer/oslo.i18n/usage.html + +""" + +try: + import oslo.i18n + + # NOTE(dhellmann): This reference to o-s-l-o will be replaced by the + # application name when this module is synced into the separate + # repository. It is OK to have more than one translation function + # using the same domain, since there will still only be one message + # catalog. + _translators = oslo.i18n.TranslatorFactory(domain='ceilometerclient') + + # The primary translation function using the well-known name "_" + _ = _translators.primary + + # Translators for log levels. + # + # The abbreviated names are meant to reflect the usual use of a short + # name like '_'. The "L" is for "log" and the other letter comes from + # the level. + _LI = _translators.log_info + _LW = _translators.log_warning + _LE = _translators.log_error + _LC = _translators.log_critical +except ImportError: + # NOTE(dims): Support for cases where a project wants to use + # code from oslo-incubator, but is not ready to be internationalized + # (like tempest) + _ = _LI = _LW = _LE = _LC = lambda x: x diff --git a/ceilometerclient/openstack/common/apiclient/auth.py b/ceilometerclient/openstack/common/apiclient/auth.py index df9b674..3cf0e20 100644 --- a/ceilometerclient/openstack/common/apiclient/auth.py +++ b/ceilometerclient/openstack/common/apiclient/auth.py @@ -17,9 +17,21 @@ # E0202: An attribute inherited from %s hide this method # pylint: disable=E0202 +######################################################################## +# +# THIS MODULE IS DEPRECATED +# +# Please refer to +# https://etherpad.openstack.org/p/kilo-ceilometerclient-library-proposals for +# the discussion leading to this deprecation. +# +# We recommend checking out the python-openstacksdk project +# (https://launchpad.net/python-openstacksdk) instead. +# +######################################################################## + import abc import argparse -import logging import os import six @@ -28,9 +40,6 @@ from stevedore import extension from ceilometerclient.openstack.common.apiclient import exceptions -logger = logging.getLogger(__name__) - - _discovered_plugins = {} @@ -80,7 +89,7 @@ def load_plugin_from_args(args): alphabetical order. :type args: argparse.Namespace - :raises: AuthorizationFailure + :raises: AuthPluginOptionsMissing """ auth_system = args.os_auth_system if auth_system: @@ -217,8 +226,8 @@ class BaseAuthPlugin(object): :type service_type: string :param endpoint_type: Type of endpoint. Possible values: public or publicURL, - internal or internalURL, - admin or adminURL + internal or internalURL, + admin or adminURL :type endpoint_type: string :returns: tuple of token and endpoint strings :raises: EndpointException diff --git a/ceilometerclient/openstack/common/apiclient/base.py b/ceilometerclient/openstack/common/apiclient/base.py index f8531f8..4462e43 100644 --- a/ceilometerclient/openstack/common/apiclient/base.py +++ b/ceilometerclient/openstack/common/apiclient/base.py @@ -20,16 +20,32 @@ Base utilities to build API operation managers and objects on top of. """ +######################################################################## +# +# THIS MODULE IS DEPRECATED +# +# Please refer to +# https://etherpad.openstack.org/p/kilo-ceilometerclient-library-proposals for +# the discussion leading to this deprecation. +# +# We recommend checking out the python-openstacksdk project +# (https://launchpad.net/python-openstacksdk) instead. +# +######################################################################## + + # E1102: %s is not callable # pylint: disable=E1102 import abc +import copy +from oslo.utils import strutils import six from six.moves.urllib import parse +from ceilometerclient.openstack.common._i18n import _ from ceilometerclient.openstack.common.apiclient import exceptions -from ceilometerclient.openstack.common import strutils def getid(obj): @@ -73,8 +89,8 @@ class HookableMixin(object): :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 + :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: @@ -97,12 +113,13 @@ class BaseManager(HookableMixin): super(BaseManager, self).__init__() self.client = client - def _list(self, url, response_key, obj_class=None, json=None): + def _list(self, url, response_key=None, 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' + e.g., 'servers'. If response_key is None - all response body + will be used. :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 @@ -116,7 +133,7 @@ class BaseManager(HookableMixin): if obj_class is None: obj_class = self.resource_class - data = body[response_key] + data = body[response_key] if response_key is not None else body # NOTE(ja): keystone returns values as list as {'values': [ ... ]} # unlike other services which just return the list... try: @@ -126,15 +143,17 @@ class BaseManager(HookableMixin): return [obj_class(self, res, loaded=True) for res in data if res] - def _get(self, url, response_key): + def _get(self, url, response_key=None): """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' + e.g., 'server'. If response_key is None - all response body + will be used. """ body = self.client.get(url).json() - return self.resource_class(self, body[response_key], loaded=True) + data = body[response_key] if response_key is not None else body + return self.resource_class(self, data, loaded=True) def _head(self, url): """Retrieve request headers for an object. @@ -144,21 +163,23 @@ class BaseManager(HookableMixin): resp = self.client.head(url) return resp.status_code == 204 - def _post(self, url, json, response_key, return_raw=False): + def _post(self, url, json, response_key=None, 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' + e.g., 'server'. If response_key is None - all response body + will be used. :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() + data = body[response_key] if response_key is not None else body if return_raw: - return body[response_key] - return self.resource_class(self, body[response_key]) + return data + return self.resource_class(self, data) def _put(self, url, json=None, response_key=None): """Update an object with PUT method. @@ -167,7 +188,8 @@ class BaseManager(HookableMixin): :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' + e.g., 'servers'. If response_key is None - all response body + will be used. """ resp = self.client.put(url, json=json) # PUT requests may not return a body @@ -185,7 +207,8 @@ class BaseManager(HookableMixin): :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' + e.g., 'servers'. If response_key is None - all response body + will be used. """ body = self.client.patch(url, json=json).json() if response_key is not None: @@ -218,7 +241,10 @@ class ManagerWithFind(BaseManager): matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + msg = _("No %(name)s matching %(args)s.") % { + 'name': self.resource_class.__name__, + 'args': kwargs + } raise exceptions.NotFound(msg) elif num_matches > 1: raise exceptions.NoUniqueMatch() @@ -372,7 +398,10 @@ class CrudManager(BaseManager): num = len(rl) if num == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) + msg = _("No %(name)s matching %(args)s.") % { + 'name': self.resource_class.__name__, + 'args': kwargs + } raise exceptions.NotFound(404, msg) elif num > 1: raise exceptions.NoUniqueMatch @@ -440,8 +469,10 @@ class Resource(object): 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)) + if self.HUMAN_ID: + name = getattr(self, self.NAME_ATTR, None) + if name is not None: + return strutils.to_slug(name) return None def _add_details(self, info): @@ -455,7 +486,7 @@ class Resource(object): def __getattr__(self, k): if k not in self.__dict__: - #NOTE(bcwaldon): disallow lazy-loading if already loaded once + # NOTE(bcwaldon): disallow lazy-loading if already loaded once if not self.is_loaded(): self.get() return self.__getattr__(k) @@ -465,6 +496,11 @@ class Resource(object): return self.__dict__[k] def get(self): + """Support for lazy loading details. + + Some clients, such as novaclient have the option to lazy load the + details, details which can be loaded with this function. + """ # set_loaded() first ... so if we have to bail, we know we tried. self.set_loaded(True) if not hasattr(self.manager, 'get'): @@ -473,6 +509,8 @@ class Resource(object): new = self.manager.get(self.id) if new: self._add_details(new._info) + self._add_details( + {'x_request_id': self.manager.client.last_request_id}) def __eq__(self, other): if not isinstance(other, Resource): @@ -489,3 +527,6 @@ class Resource(object): def set_loaded(self, val): self._loaded = val + + def to_dict(self): + return copy.deepcopy(self._info) diff --git a/ceilometerclient/openstack/common/apiclient/client.py b/ceilometerclient/openstack/common/apiclient/client.py index 1b68aa9..c5d6528 100644 --- a/ceilometerclient/openstack/common/apiclient/client.py +++ b/ceilometerclient/openstack/common/apiclient/client.py @@ -25,6 +25,7 @@ OpenStack Client interface. Handles the REST calls and responses. # E0202: An attribute inherited from %s hide this method # pylint: disable=E0202 +import hashlib import logging import time @@ -33,19 +34,22 @@ try: except ImportError: import json +from oslo.utils import encodeutils +from oslo.utils import importutils import requests +from ceilometerclient.openstack.common._i18n import _ from ceilometerclient.openstack.common.apiclient import exceptions -from ceilometerclient.openstack.common import importutils - _logger = logging.getLogger(__name__) +SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',) 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; @@ -96,19 +100,32 @@ class HTTPClient(object): self.http = http or requests.Session() self.cached_token = None + self.last_request_id = None + + def _safe_header(self, name, value): + if name in SENSITIVE_HEADERS: + # because in python3 byte string handling is ... ug + v = value.encode('utf-8') + h = hashlib.sha1(v) + d = h.hexdigest() + return encodeutils.safe_decode(name), "{SHA1}%s" % d + else: + return (encodeutils.safe_decode(name), + encodeutils.safe_decode(value)) def _http_log_req(self, method, url, kwargs): if not self.debug: return string_parts = [ - "curl -i", + "curl -g -i", "-X '%s'" % method, "'%s'" % url, ] for element in kwargs['headers']: - header = "-H '%s: %s'" % (element, kwargs['headers'][element]) + header = ("-H '%s: %s'" % + self._safe_header(element, kwargs['headers'][element])) string_parts.append(header) _logger.debug("REQ: %s" % " ".join(string_parts)) @@ -151,10 +168,10 @@ class HTTPClient(object): :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` + 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.setdefault("headers", {}) kwargs["headers"]["User-Agent"] = self.user_agent if self.original_ip: kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( @@ -175,6 +192,8 @@ class HTTPClient(object): start_time, time.time())) self._http_log_resp(resp) + self.last_request_id = resp.headers.get('x-openstack-request-id') + if resp.status_code >= 400: _logger.debug( "Request returned failure status: %s", @@ -206,7 +225,7 @@ class HTTPClient(object): :param method: method of HTTP request :param url: URL of HTTP request :param kwargs: any other parameter that can be passed to -' `HTTPClient.request` + `HTTPClient.request` """ filter_args = { @@ -228,7 +247,7 @@ class HTTPClient(object): **filter_args) if not (token and endpoint): raise exceptions.AuthorizationFailure( - "Cannot find endpoint or token for request") + _("Cannot find endpoint or token for request")) old_token_endpoint = (token, endpoint) kwargs.setdefault("headers", {})["X-Auth-Token"] = token @@ -245,6 +264,10 @@ class HTTPClient(object): raise self.cached_token = None client.cached_endpoint = None + if self.auth_plugin.opts.get('token'): + self.auth_plugin.opts['token'] = None + if self.auth_plugin.opts.get('endpoint'): + self.auth_plugin.opts['endpoint'] = None self.authenticate() try: token, endpoint = self.auth_plugin.token_and_endpoint( @@ -321,6 +344,10 @@ class BaseClient(object): return self.http_client.client_request( self, method, url, **kwargs) + @property + def last_request_id(self): + return self.http_client.last_request_id + def head(self, url, **kwargs): return self.client_request("HEAD", url, **kwargs) @@ -351,8 +378,11 @@ class BaseClient(object): 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()))) + msg = _("Invalid %(api_name)s client version '%(version)s'. " + "Must be one of: %(version_map)s") % { + 'api_name': api_name, + 'version': version, + 'version_map': ', '.join(version_map.keys())} raise exceptions.UnsupportedVersion(msg) return importutils.import_class(client_path) diff --git a/ceilometerclient/openstack/common/apiclient/exceptions.py b/ceilometerclient/openstack/common/apiclient/exceptions.py index 4776d58..15a830a 100644 --- a/ceilometerclient/openstack/common/apiclient/exceptions.py +++ b/ceilometerclient/openstack/common/apiclient/exceptions.py @@ -20,11 +20,26 @@ Exception definitions. """ +######################################################################## +# +# THIS MODULE IS DEPRECATED +# +# Please refer to +# https://etherpad.openstack.org/p/kilo-ceilometerclient-library-proposals for +# the discussion leading to this deprecation. +# +# We recommend checking out the python-openstacksdk project +# (https://launchpad.net/python-openstacksdk) instead. +# +######################################################################## + import inspect import sys import six +from ceilometerclient.openstack.common._i18n import _ + class ClientException(Exception): """The base exception class for all exceptions this library raises. @@ -32,14 +47,6 @@ class ClientException(Exception): 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 @@ -60,25 +67,30 @@ class AuthorizationFailure(ClientException): pass -class ConnectionRefused(ClientException): +class ConnectionError(ClientException): """Cannot connect to API service.""" pass +class ConnectionRefused(ConnectionError): + """Connection refused while trying to connect to API service.""" + pass + + class AuthPluginOptionsMissing(AuthorizationFailure): """Auth plugin misses some options.""" def __init__(self, opt_names): super(AuthPluginOptionsMissing, self).__init__( - "Authentication failed. Missing options: %s" % + _("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.""" + """User has specified an AuthSystem that is not installed.""" def __init__(self, auth_system): super(AuthSystemNotFound, self).__init__( - "AuthSystemNotFound: %s" % repr(auth_system)) + _("AuthSystemNotFound: %s") % repr(auth_system)) self.auth_system = auth_system @@ -101,7 +113,7 @@ 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)) + _("AmbiguousEndpoints: %s") % repr(endpoints)) self.endpoints = endpoints @@ -109,7 +121,7 @@ class HttpError(ClientException): """The base exception class for all HTTP exceptions. """ http_status = 0 - message = "HTTP Error" + message = _("HTTP Error") def __init__(self, message=None, details=None, response=None, request_id=None, @@ -127,12 +139,17 @@ class HttpError(ClientException): super(HttpError, self).__init__(formatted_string) +class HTTPRedirection(HttpError): + """HTTP Redirection.""" + message = _("HTTP Redirection") + + class HTTPClientError(HttpError): """Client-side HTTP error. Exception for cases in which the client seems to have erred. """ - message = "HTTP Client Error" + message = _("HTTP Client Error") class HttpServerError(HttpError): @@ -141,7 +158,17 @@ class HttpServerError(HttpError): Exception for cases in which the server is aware that it has erred or is incapable of performing the request. """ - message = "HTTP Server Error" + message = _("HTTP Server Error") + + +class MultipleChoices(HTTPRedirection): + """HTTP 300 - Multiple Choices. + + Indicates multiple options for the resource that the client may follow. + """ + + http_status = 300 + message = _("Multiple Choices") class BadRequest(HTTPClientError): @@ -150,7 +177,7 @@ class BadRequest(HTTPClientError): The request cannot be fulfilled due to bad syntax. """ http_status = 400 - message = "Bad Request" + message = _("Bad Request") class Unauthorized(HTTPClientError): @@ -160,7 +187,7 @@ class Unauthorized(HTTPClientError): is required and has failed or has not yet been provided. """ http_status = 401 - message = "Unauthorized" + message = _("Unauthorized") class PaymentRequired(HTTPClientError): @@ -169,7 +196,7 @@ class PaymentRequired(HTTPClientError): Reserved for future use. """ http_status = 402 - message = "Payment Required" + message = _("Payment Required") class Forbidden(HTTPClientError): @@ -179,7 +206,7 @@ class Forbidden(HTTPClientError): to it. """ http_status = 403 - message = "Forbidden" + message = _("Forbidden") class NotFound(HTTPClientError): @@ -189,7 +216,7 @@ class NotFound(HTTPClientError): in the future. """ http_status = 404 - message = "Not Found" + message = _("Not Found") class MethodNotAllowed(HTTPClientError): @@ -199,7 +226,7 @@ class MethodNotAllowed(HTTPClientError): by that resource. """ http_status = 405 - message = "Method Not Allowed" + message = _("Method Not Allowed") class NotAcceptable(HTTPClientError): @@ -209,7 +236,7 @@ class NotAcceptable(HTTPClientError): acceptable according to the Accept headers sent in the request. """ http_status = 406 - message = "Not Acceptable" + message = _("Not Acceptable") class ProxyAuthenticationRequired(HTTPClientError): @@ -218,7 +245,7 @@ class ProxyAuthenticationRequired(HTTPClientError): The client must first authenticate itself with the proxy. """ http_status = 407 - message = "Proxy Authentication Required" + message = _("Proxy Authentication Required") class RequestTimeout(HTTPClientError): @@ -227,7 +254,7 @@ class RequestTimeout(HTTPClientError): The server timed out waiting for the request. """ http_status = 408 - message = "Request Timeout" + message = _("Request Timeout") class Conflict(HTTPClientError): @@ -237,7 +264,7 @@ class Conflict(HTTPClientError): in the request, such as an edit conflict. """ http_status = 409 - message = "Conflict" + message = _("Conflict") class Gone(HTTPClientError): @@ -247,7 +274,7 @@ class Gone(HTTPClientError): not be available again. """ http_status = 410 - message = "Gone" + message = _("Gone") class LengthRequired(HTTPClientError): @@ -257,7 +284,7 @@ class LengthRequired(HTTPClientError): required by the requested resource. """ http_status = 411 - message = "Length Required" + message = _("Length Required") class PreconditionFailed(HTTPClientError): @@ -267,7 +294,7 @@ class PreconditionFailed(HTTPClientError): put on the request. """ http_status = 412 - message = "Precondition Failed" + message = _("Precondition Failed") class RequestEntityTooLarge(HTTPClientError): @@ -276,7 +303,7 @@ class RequestEntityTooLarge(HTTPClientError): The request is larger than the server is willing or able to process. """ http_status = 413 - message = "Request Entity Too Large" + message = _("Request Entity Too Large") def __init__(self, *args, **kwargs): try: @@ -293,7 +320,7 @@ class RequestUriTooLong(HTTPClientError): The URI provided was too long for the server to process. """ http_status = 414 - message = "Request-URI Too Long" + message = _("Request-URI Too Long") class UnsupportedMediaType(HTTPClientError): @@ -303,7 +330,7 @@ class UnsupportedMediaType(HTTPClientError): not support. """ http_status = 415 - message = "Unsupported Media Type" + message = _("Unsupported Media Type") class RequestedRangeNotSatisfiable(HTTPClientError): @@ -313,7 +340,7 @@ class RequestedRangeNotSatisfiable(HTTPClientError): supply that portion. """ http_status = 416 - message = "Requested Range Not Satisfiable" + message = _("Requested Range Not Satisfiable") class ExpectationFailed(HTTPClientError): @@ -322,7 +349,7 @@ class ExpectationFailed(HTTPClientError): The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 - message = "Expectation Failed" + message = _("Expectation Failed") class UnprocessableEntity(HTTPClientError): @@ -332,7 +359,7 @@ class UnprocessableEntity(HTTPClientError): errors. """ http_status = 422 - message = "Unprocessable Entity" + message = _("Unprocessable Entity") class InternalServerError(HttpServerError): @@ -341,7 +368,7 @@ class InternalServerError(HttpServerError): A generic error message, given when no more specific message is suitable. """ http_status = 500 - message = "Internal Server Error" + message = _("Internal Server Error") # NotImplemented is a python keyword. @@ -352,7 +379,7 @@ class HttpNotImplemented(HttpServerError): the ability to fulfill the request. """ http_status = 501 - message = "Not Implemented" + message = _("Not Implemented") class BadGateway(HttpServerError): @@ -362,7 +389,7 @@ class BadGateway(HttpServerError): response from the upstream server. """ http_status = 502 - message = "Bad Gateway" + message = _("Bad Gateway") class ServiceUnavailable(HttpServerError): @@ -371,7 +398,7 @@ class ServiceUnavailable(HttpServerError): The server is currently unavailable. """ http_status = 503 - message = "Service Unavailable" + message = _("Service Unavailable") class GatewayTimeout(HttpServerError): @@ -381,7 +408,7 @@ class GatewayTimeout(HttpServerError): response from the upstream server. """ http_status = 504 - message = "Gateway Timeout" + message = _("Gateway Timeout") class HttpVersionNotSupported(HttpServerError): @@ -390,7 +417,7 @@ class HttpVersionNotSupported(HttpServerError): The server does not support the HTTP protocol version used in the request. """ http_status = 505 - message = "HTTP Version Not Supported" + message = _("HTTP Version Not Supported") # _code_map contains all the classes that have http_status attribute. @@ -408,12 +435,17 @@ def from_response(response, method, url): :param method: HTTP method used for request :param url: URL used for request """ + + req_id = response.headers.get("x-openstack-request-id") + # NOTE(hdd) true for older versions of nova and cinder + if not req_id: + req_id = response.headers.get("x-compute-request-id") kwargs = { "http_status": response.status_code, "response": response, "method": method, "url": url, - "request_id": response.headers.get("x-compute-request-id"), + "request_id": req_id, } if "retry-after" in response.headers: kwargs["retry_after"] = response.headers["retry-after"] @@ -425,10 +457,13 @@ def from_response(response, method, url): 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) + if isinstance(body, dict): + error = body.get(list(body)[0]) + if isinstance(error, dict): + kwargs["message"] = (error.get("message") or + error.get("faultstring")) + kwargs["details"] = (error.get("details") or + six.text_type(body)) elif content_type.startswith("text/"): kwargs["details"] = response.text diff --git a/ceilometerclient/openstack/common/apiclient/fake_client.py b/ceilometerclient/openstack/common/apiclient/fake_client.py index e99947b..289b4ba 100644 --- a/ceilometerclient/openstack/common/apiclient/fake_client.py +++ b/ceilometerclient/openstack/common/apiclient/fake_client.py @@ -21,6 +21,19 @@ wrong the tests might raise AssertionError. I've indicated in comments the places where actual behavior differs from the spec. """ +######################################################################## +# +# THIS MODULE IS DEPRECATED +# +# Please refer to +# https://etherpad.openstack.org/p/kilo-ceilometerclient-library-proposals for +# the discussion leading to this deprecation. +# +# We recommend checking out the python-openstacksdk project +# (https://launchpad.net/python-openstacksdk) instead. +# +######################################################################## + # W0102: Dangerous default value %s as argument # pylint: disable=W0102 @@ -33,7 +46,9 @@ from six.moves.urllib import parse from ceilometerclient.openstack.common.apiclient import client -def assert_has_keys(dct, required=[], optional=[]): +def assert_has_keys(dct, required=None, optional=None): + required = required or [] + optional = optional or [] for k in required: try: assert k in dct @@ -79,7 +94,7 @@ 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: + if not args and "auth_plugin" not in kwargs: args = (None, ) super(FakeHTTPClient, self).__init__(*args, **kwargs) @@ -166,6 +181,8 @@ class FakeHTTPClient(client.HTTPClient): else: status, body = resp headers = {} + self.last_request_id = headers.get('x-openstack-request-id', + 'req-test') return TestResponse({ "status_code": status, "text": body, diff --git a/ceilometerclient/openstack/common/apiclient/utils.py b/ceilometerclient/openstack/common/apiclient/utils.py new file mode 100644 index 0000000..6d94759 --- /dev/null +++ b/ceilometerclient/openstack/common/apiclient/utils.py @@ -0,0 +1,100 @@ +# +# 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. + +######################################################################## +# +# THIS MODULE IS DEPRECATED +# +# Please refer to +# https://etherpad.openstack.org/p/kilo-ceilometerclient-library-proposals for +# the discussion leading to this deprecation. +# +# We recommend checking out the python-openstacksdk project +# (https://launchpad.net/python-openstacksdk) instead. +# +######################################################################## + +from oslo.utils import encodeutils +import six + +from ceilometerclient.openstack.common._i18n import _ +from ceilometerclient.openstack.common.apiclient import exceptions +from ceilometerclient.openstack.common import uuidutils + + +def find_resource(manager, name_or_id, **find_args): + """Look for resource in a given manager. + + Used as a helper for the _find_* methods. + Example: + + .. code-block:: python + + def _find_hypervisor(cs, hypervisor): + #Get a hypervisor by name or ID. + return cliutils.find_resource(cs.hypervisors, hypervisor) + """ + # first try to get entity as integer id + try: + return manager.get(int(name_or_id)) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # now try to get entity as uuid + try: + if six.PY2: + tmp_id = encodeutils.safe_encode(name_or_id) + else: + tmp_id = encodeutils.safe_decode(name_or_id) + + if uuidutils.is_uuid_like(tmp_id): + return manager.get(tmp_id) + except (TypeError, ValueError, exceptions.NotFound): + pass + + # for str id which is not uuid + if getattr(manager, 'is_alphanum_id_allowed', False): + try: + return manager.get(name_or_id) + except exceptions.NotFound: + pass + + try: + try: + return manager.find(human_id=name_or_id, **find_args) + except exceptions.NotFound: + pass + + # finally try to find entity by name + try: + resource = getattr(manager, 'resource_class', None) + name_attr = resource.NAME_ATTR if resource else 'name' + kwargs = {name_attr: name_or_id} + kwargs.update(find_args) + return manager.find(**kwargs) + except exceptions.NotFound: + msg = _("No %(name)s with a name or " + "ID of '%(name_or_id)s' exists.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) + except exceptions.NoUniqueMatch: + msg = _("Multiple %(name)s matches found for " + "'%(name_or_id)s', use an ID to be more specific.") % \ + { + "name": manager.resource_class.__name__.lower(), + "name_or_id": name_or_id + } + raise exceptions.CommandError(msg) diff --git a/ceilometerclient/openstack/common/cliutils.py b/ceilometerclient/openstack/common/cliutils.py index 96725ed..341cd01 100644 --- a/ceilometerclient/openstack/common/cliutils.py +++ b/ceilometerclient/openstack/common/cliutils.py @@ -16,18 +16,29 @@ # W0621: Redefining name %s from outer scope # pylint: disable=W0603,W0621 +from __future__ import print_function + import getpass import inspect import os import sys import textwrap +from oslo.utils import encodeutils +from oslo.utils import strutils import prettytable import six from six import moves -from ceilometerclient.openstack.common.apiclient import exceptions -from ceilometerclient.openstack.common import strutils +from ceilometerclient.openstack.common._i18n import _ + + +class MissingArgs(Exception): + """Supplied arguments are not sufficient for calling a function.""" + def __init__(self, missing): + self.missing = missing + msg = _("Missing arguments: %s") % ", ".join(missing) + super(MissingArgs, self).__init__(msg) def validate_args(fn, *args, **kwargs): @@ -52,7 +63,7 @@ def validate_args(fn, *args, **kwargs): required_args = argspec.args[:len(argspec.args) - num_defaults] def isbound(method): - return getattr(method, 'im_self', None) is not None + return getattr(method, '__self__', None) is not None if isbound(fn): required_args.pop(0) @@ -60,7 +71,7 @@ def validate_args(fn, *args, **kwargs): missing = [arg for arg in required_args if arg not in kwargs] missing = missing[len(args):] if missing: - raise exceptions.MissingArgs(missing) + raise MissingArgs(missing) def arg(*args, **kwargs): @@ -84,7 +95,7 @@ def env(*args, **kwargs): If all are empty, defaults to '' or keyword arg `default`. """ for arg in args: - value = os.environ.get(arg, None) + value = os.environ.get(arg) if value: return value return kwargs.get('default', '') @@ -128,7 +139,7 @@ def isunauthenticated(func): def print_list(objs, fields, formatters=None, sortby_index=0, - mixed_case_fields=None): + mixed_case_fields=None, field_labels=None): """Print a list or objects as a table, one row per object. :param objs: iterable of :class:`Resource` @@ -137,14 +148,22 @@ def print_list(objs, fields, formatters=None, sortby_index=0, :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') + :param field_labels: Labels to use in the heading of the table, default to + fields. """ formatters = formatters or {} mixed_case_fields = mixed_case_fields or [] + field_labels = field_labels or fields + if len(field_labels) != len(fields): + raise ValueError(_("Field labels list %(labels)s has different number " + "of elements than fields list %(fields)s"), + {'labels': field_labels, 'fields': fields}) + if sortby_index is None: kwargs = {} else: - kwargs = {'sortby': fields[sortby_index]} - pt = prettytable.PrettyTable(fields, caching=False) + kwargs = {'sortby': field_labels[sortby_index]} + pt = prettytable.PrettyTable(field_labels) pt.align = 'l' for o in objs: @@ -161,7 +180,10 @@ def print_list(objs, fields, formatters=None, sortby_index=0, row.append(data) pt.add_row(row) - print(strutils.safe_encode(pt.get_string(**kwargs))) + if six.PY3: + print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode()) + else: + print(encodeutils.safe_encode(pt.get_string(**kwargs))) def print_dict(dct, dict_property="Property", wrap=0): @@ -171,14 +193,14 @@ def print_dict(dct, dict_property="Property", wrap=0): :param dict_property: name of the first column :param wrap: wrapping for the second column """ - pt = prettytable.PrettyTable([dict_property, 'Value'], caching=False) + pt = prettytable.PrettyTable([dict_property, 'Value']) pt.align = 'l' for k, v in six.iteritems(dct): # convert dict to str to check length if isinstance(v, dict): - v = str(v) + v = six.text_type(v) if wrap > 0: - v = textwrap.fill(str(v), wrap) + v = textwrap.fill(six.text_type(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: @@ -189,7 +211,11 @@ def print_dict(dct, dict_property="Property", wrap=0): col1 = '' else: pt.add_row([k, v]) - print(strutils.safe_encode(pt.get_string())) + + if six.PY3: + print(encodeutils.safe_encode(pt.get_string()).decode()) + else: + print(encodeutils.safe_encode(pt.get_string())) def get_password(max_password_prompts=3): @@ -199,7 +225,7 @@ def get_password(max_password_prompts=3): if hasattr(sys.stdin, "isatty") and sys.stdin.isatty(): # Check for Ctrl-D try: - for _ in moves.range(max_password_prompts): + for __ in moves.range(max_password_prompts): pw1 = getpass.getpass("OS Password: ") if verify: pw2 = getpass.getpass("Please verify: ") @@ -211,3 +237,35 @@ def get_password(max_password_prompts=3): except EOFError: pass return pw + + +def service_type(stype): + """Adds 'service_type' attribute to decorated function. + + Usage: + + .. code-block:: python + + @service_type('volume') + def mymethod(f): + ... + """ + def inner(f): + f.service_type = stype + return f + return inner + + +def get_service_type(f): + """Retrieves service type from function.""" + return getattr(f, 'service_type', None) + + +def pretty_choice_list(l): + return ', '.join("'%s'" % i for i in l) + + +def exit(msg=''): + if msg: + print (msg, file=sys.stderr) + sys.exit(1) diff --git a/ceilometerclient/openstack/common/gettextutils.py b/ceilometerclient/openstack/common/gettextutils.py deleted file mode 100644 index c28d730..0000000 --- a/ceilometerclient/openstack/common/gettextutils.py +++ /dev/null @@ -1,371 +0,0 @@ -# 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) diff --git a/ceilometerclient/openstack/common/importutils.py b/ceilometerclient/openstack/common/importutils.py deleted file mode 100644 index 4fd9ae2..0000000 --- a/ceilometerclient/openstack/common/importutils.py +++ /dev/null @@ -1,66 +0,0 @@ -# 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. - -""" -Import related utilities and helper functions. -""" - -import sys -import traceback - - -def import_class(import_str): - """Returns a class from a string including module and class.""" - mod_str, _sep, class_str = import_str.rpartition('.') - try: - __import__(mod_str) - return getattr(sys.modules[mod_str], class_str) - except (ValueError, AttributeError): - raise ImportError('Class %s cannot be found (%s)' % - (class_str, - traceback.format_exception(*sys.exc_info()))) - - -def import_object(import_str, *args, **kwargs): - """Import a class and return an instance of it.""" - return import_class(import_str)(*args, **kwargs) - - -def import_object_ns(name_space, import_str, *args, **kwargs): - """Tries to import object from default namespace. - - Imports a class and return an instance of it, first by trying - to find the class in a default namespace, then failing back to - a full path if not found in the default namespace. - """ - import_value = "%s.%s" % (name_space, import_str) - try: - return import_class(import_value)(*args, **kwargs) - except ImportError: - return import_class(import_str)(*args, **kwargs) - - -def import_module(import_str): - """Import a module.""" - __import__(import_str) - return sys.modules[import_str] - - -def try_import(import_str, default=None): - """Try to import a module and if it fails return default.""" - try: - return import_module(import_str) - except ImportError: - return default diff --git a/ceilometerclient/openstack/common/strutils.py b/ceilometerclient/openstack/common/strutils.py deleted file mode 100644 index 23b117e..0000000 --- a/ceilometerclient/openstack/common/strutils.py +++ /dev/null @@ -1,222 +0,0 @@ -# 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 _ - - -# 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): - if six.PY3: - return text.encode(encoding, errors).decode(incoming) - else: - return text.encode(encoding, errors) - elif text and encoding != incoming: - # Decode text before encoding it with `encoding` - text = safe_decode(text, incoming, errors) - if six.PY3: - return text.encode(encoding, errors).decode(incoming) - else: - 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) diff --git a/ceilometerclient/openstack/common/uuidutils.py b/ceilometerclient/openstack/common/uuidutils.py new file mode 100644 index 0000000..234b880 --- /dev/null +++ b/ceilometerclient/openstack/common/uuidutils.py @@ -0,0 +1,37 @@ +# Copyright (c) 2012 Intel Corporation. +# 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. + +""" +UUID related utilities and helper functions. +""" + +import uuid + + +def generate_uuid(): + return str(uuid.uuid4()) + + +def is_uuid_like(val): + """Returns validation of a value as a UUID. + + For our purposes, a UUID is a canonical form string: + aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa + + """ + try: + return str(uuid.UUID(val)) == val + except (TypeError, ValueError, AttributeError): + return False diff --git a/ceilometerclient/shell.py b/ceilometerclient/shell.py index 7a028ac..8057933 100644 --- a/ceilometerclient/shell.py +++ b/ceilometerclient/shell.py @@ -20,6 +20,7 @@ import argparse import logging import sys +from oslo.utils import encodeutils import six import ceilometerclient @@ -27,7 +28,20 @@ from ceilometerclient import client as ceiloclient from ceilometerclient.common import utils from ceilometerclient import exc from ceilometerclient.openstack.common import cliutils -from ceilometerclient.openstack.common import strutils + + +def _positive_non_zero_int(argument_value): + if argument_value is None: + return None + try: + value = int(argument_value) + except ValueError: + msg = "%s must be an integer" % argument_value + raise argparse.ArgumentTypeError(msg) + if value <= 0: + msg = "%s must be greater than 0" % argument_value + raise argparse.ArgumentTypeError(msg) + return value class CeilometerShell(object): @@ -62,120 +76,32 @@ class CeilometerShell(object): default=False, action="store_true", help="Print more verbose output.") - parser.add_argument('-k', '--insecure', - default=False, - action='store_true', - help="Explicitly allow ceilometerclient to " - "perform \"insecure\" SSL (https) requests. " - "The server's certificate will " - "not be verified against any certificate " - "authorities. This option should be used with " - "caution.") - - parser.add_argument('--cert-file', - help='Path of certificate file to use in SSL ' - 'connection. This file can optionally be prepended' - ' with the private key.') - - parser.add_argument('--key-file', - help='Path of client key to use in SSL connection.' - ' This option is not necessary if your key is ' - 'prepended to your cert file.') - - parser.add_argument('--os-cacert', - metavar='', - dest='os_cacert', - default=cliutils.env('OS_CACERT'), - help='Path of CA TLS certificate(s) used to verify' - 'the remote server\'s certificate. Without this ' - 'option ceilometer looks for the default system ' - 'CA certificates.') - parser.add_argument('--ca-file', - dest='os_cacert', - help='DEPRECATED! Use --os-cacert.') - parser.add_argument('--timeout', default=600, + type=_positive_non_zero_int, help='Number of seconds to wait for a response.') - parser.add_argument('--os-username', - default=cliutils.env('OS_USERNAME'), - help='Defaults to env[OS_USERNAME].') - - parser.add_argument('--os_username', - help=argparse.SUPPRESS) - - parser.add_argument('--os-password', - default=cliutils.env('OS_PASSWORD'), - help='Defaults to env[OS_PASSWORD].') - - parser.add_argument('--os_password', - help=argparse.SUPPRESS) - - parser.add_argument('--os-tenant-id', - default=cliutils.env('OS_TENANT_ID'), - help='Defaults to env[OS_TENANT_ID].') - - parser.add_argument('--os_tenant_id', - help=argparse.SUPPRESS) - - parser.add_argument('--os-tenant-name', - default=cliutils.env('OS_TENANT_NAME'), - help='Defaults to env[OS_TENANT_NAME].') - - parser.add_argument('--os_tenant_name', - help=argparse.SUPPRESS) - - parser.add_argument('--os-auth-url', - default=cliutils.env('OS_AUTH_URL'), - help='Defaults to env[OS_AUTH_URL].') - - parser.add_argument('--os_auth_url', - help=argparse.SUPPRESS) - - parser.add_argument('--os-region-name', - default=cliutils.env('OS_REGION_NAME'), - help='Defaults to env[OS_REGION_NAME].') - - parser.add_argument('--os_region_name', - help=argparse.SUPPRESS) - - parser.add_argument('--os-auth-token', - default=cliutils.env('OS_AUTH_TOKEN'), - help='Defaults to env[OS_AUTH_TOKEN].') - - parser.add_argument('--os_auth_token', - help=argparse.SUPPRESS) - - parser.add_argument('--ceilometer-url', + parser.add_argument('--ceilometer-url', metavar='', + dest='os_endpoint', default=cliutils.env('CEILOMETER_URL'), - help='Defaults to env[CEILOMETER_URL].') + help=("DEPRECATED, use --os-endpoint instead. " + "Defaults to env[CEILOMETER_URL].")) parser.add_argument('--ceilometer_url', + dest='os_endpoint', help=argparse.SUPPRESS) parser.add_argument('--ceilometer-api-version', default=cliutils.env( - 'CEILOMETER_API_VERSION', default='2'), + 'CEILOMETER_API_VERSION', default='2'), help='Defaults to env[CEILOMETER_API_VERSION] ' 'or 2.') parser.add_argument('--ceilometer_api_version', help=argparse.SUPPRESS) - parser.add_argument('--os-service-type', - default=cliutils.env('OS_SERVICE_TYPE'), - help='Defaults to env[OS_SERVICE_TYPE].') - - parser.add_argument('--os_service_type', - help=argparse.SUPPRESS) - - parser.add_argument('--os-endpoint-type', - default=cliutils.env('OS_ENDPOINT_TYPE'), - help='Defaults to env[OS_ENDPOINT_TYPE].') - - parser.add_argument('--os_endpoint_type', - help=argparse.SUPPRESS) + self.auth_plugin.add_opts(parser) + self.auth_plugin.add_common_opts(parser) return parser @@ -220,17 +146,22 @@ class CeilometerShell(object): subparser.add_argument(*args, **kwargs) subparser.set_defaults(func=callback) - def _setup_logging(self, debug): - format = '%(levelname)s (%(module)s:%(lineno)d) %(message)s' + @staticmethod + def _setup_logging(debug): + format = '%(levelname)s (%(module)s) %(message)s' if debug: logging.basicConfig(format=format, level=logging.DEBUG) else: logging.basicConfig(format=format, level=logging.WARN) + logging.getLogger('iso8601').setLevel(logging.WARNING) + logging.getLogger('urllib3.connectionpool').setLevel(logging.WARNING) def parse_args(self, argv): # Parse args once to find version + self.auth_plugin = ceiloclient.AuthPlugin() parser = self.get_base_parser() (options, args) = parser.parse_known_args(argv) + self.auth_plugin.parse_opts(options) self._setup_logging(options.debug) # build available subcommands based on version @@ -247,6 +178,15 @@ class CeilometerShell(object): # Return parsed args return api_version, subcommand_parser.parse_args(argv) + @staticmethod + def no_project_and_domain_set(args): + if not (args.os_project_id or (args.os_project_name and + (args.os_user_domain_name or args.os_user_domain_id)) or + (args.os_tenant_id or args.os_tenant_name)): + return True + else: + return False + def main(self, argv): parsed = self.parse_args(argv) if parsed == 0: @@ -261,33 +201,50 @@ class CeilometerShell(object): self.do_bash_completion(args) return 0 - if not (args.os_auth_token and args.ceilometer_url): - if not args.os_username: + if not ((self.auth_plugin.opts.get('token') + or self.auth_plugin.opts.get('auth_token')) + and self.auth_plugin.opts['endpoint']): + if not self.auth_plugin.opts['username']: raise exc.CommandError("You must provide a username via " "either --os-username or via " "env[OS_USERNAME]") - if not args.os_password: + if not self.auth_plugin.opts['password']: raise exc.CommandError("You must provide a password via " "either --os-password or via " "env[OS_PASSWORD]") - if not (args.os_tenant_id or args.os_tenant_name): + if self.no_project_and_domain_set(args): + # steer users towards Keystone V3 API + raise exc.CommandError("You must provide a project_id via " + "either --os-project-id or via " + "env[OS_PROJECT_ID] and " + "a domain_name via either " + "--os-user-domain-name or via " + "env[OS_USER_DOMAIN_NAME] or " + "a domain_id via either " + "--os-user-domain-id or via " + "env[OS_USER_DOMAIN_ID]") + + if not (self.auth_plugin.opts['tenant_id'] + or self.auth_plugin.opts['tenant_name']): raise exc.CommandError("You must provide a tenant_id via " "either --os-tenant-id or via " "env[OS_TENANT_ID]") - if not args.os_auth_url: + if not self.auth_plugin.opts['auth_url']: raise exc.CommandError("You must provide an auth url via " "either --os-auth-url or via " "env[OS_AUTH_URL]") - client = ceiloclient.get_client(api_version, **(args.__dict__)) - + client_kwargs = vars(args) + client_kwargs.update(self.auth_plugin.opts) + client_kwargs['auth_plugin'] = self.auth_plugin + client = ceiloclient.get_client(api_version, **client_kwargs) # call whatever callback was selected try: args.func(client, args) - except exc.Unauthorized: + except exc.HTTPUnauthorized: raise exc.CommandError("Invalid OpenStack Identity credentials.") def do_bash_completion(self, args): @@ -321,6 +278,11 @@ class CeilometerShell(object): class HelpFormatter(argparse.HelpFormatter): + def __init__(self, prog, indent_increment=2, max_help_position=32, + width=None): + super(HelpFormatter, self).__init__(prog, indent_increment, + max_help_position, width) + def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) @@ -338,8 +300,11 @@ def main(args=None): if '--debug' in args or '-d' in args: raise else: - print(strutils.safe_encode(six.text_type(e)), file=sys.stderr) + print(encodeutils.safe_encode(six.text_type(e)), file=sys.stderr) sys.exit(1) + except KeyboardInterrupt: + print("Stopping Ceilometer Client", file=sys.stderr) + sys.exit(130) if __name__ == "__main__": main() diff --git a/ceilometerclient/tests/fakes.py b/ceilometerclient/tests/fakes.py index ceea479..10d8d8c 100644 --- a/ceilometerclient/tests/fakes.py +++ b/ceilometerclient/tests/fakes.py @@ -29,19 +29,20 @@ def fake_headers(): 'User-Agent': 'python-ceilometerclient'} -class FakeServiceCatalog(): - def url_for(self, endpoint_type, service_type): +class FakeServiceCatalog(object): + @staticmethod + def url_for(endpoint_type, service_type): return 'http://192.168.1.5:8004/v1/f14b41234' -class FakeKeystone(): +class FakeKeystone(object): service_catalog = FakeServiceCatalog() def __init__(self, auth_token): self.auth_token = auth_token -class FakeHTTPResponse(): +class FakeHTTPResponse(object): version = 1.1 diff --git a/ceilometerclient/tests/test_client.py b/ceilometerclient/tests/test_client.py index fe9d5b0..bfcf43a 100644 --- a/ceilometerclient/tests/test_client.py +++ b/ceilometerclient/tests/test_client.py @@ -12,44 +12,140 @@ import types +import mock + from ceilometerclient import client +from ceilometerclient.tests import fakes from ceilometerclient.tests import utils from ceilometerclient.v1 import client as v1client from ceilometerclient.v2 import client as v2client -FAKE_ENV = {'os_username': 'username', - 'os_password': 'password', - 'os_tenant_name': 'tenant_name', - 'os_auth_url': 'http://no.where', - 'os_auth_token': '1234', - 'ceilometer_url': 'http://no.where'} +FAKE_ENV = { + 'username': 'username', + 'password': 'password', + 'tenant_name': 'tenant_name', + 'auth_url': 'http://no.where', + 'ceilometer_url': 'http://no.where', + 'auth_plugin': 'fake_auth', + 'token': '1234', + 'user_domain_name': 'default', + 'project_domain_name': 'default', +} class ClientTest(utils.BaseTestCase): - def create_client(self, api_version=2, exclude=[]): - env = dict((k, v) for k, v in FAKE_ENV.items() if k not in exclude) + @staticmethod + def create_client(env, api_version=2, endpoint=None, exclude=[]): + env = dict((k, v) for k, v in env.items() + if k not in exclude) + return client.get_client(api_version, **env) def setUp(self): super(ClientTest, self).setUp() def test_client_version(self): - c1 = self.create_client(api_version=1) + c1 = self.create_client(env=FAKE_ENV, api_version=1) self.assertIsInstance(c1, v1client.Client) - c2 = self.create_client(api_version=2) + c2 = self.create_client(env=FAKE_ENV, api_version=2) self.assertIsInstance(c2, v2client.Client) def test_client_auth_lambda(self): - FAKE_ENV['os_auth_token'] = lambda: FAKE_ENV['os_auth_token'] - self.assertIsInstance(FAKE_ENV['os_auth_token'], + env = FAKE_ENV.copy() + env['token'] = lambda: env['token'] + self.assertIsInstance(env['token'], types.FunctionType) - c2 = self.create_client() + c2 = self.create_client(env) self.assertIsInstance(c2, v2client.Client) def test_client_auth_non_lambda(self): - FAKE_ENV['os_auth_token'] = "1234" - self.assertIsInstance(FAKE_ENV['os_auth_token'], str) - c2 = self.create_client() + env = FAKE_ENV.copy() + env['token'] = "1234" + self.assertIsInstance(env['token'], str) + c2 = self.create_client(env) self.assertIsInstance(c2, v2client.Client) + + @mock.patch('keystoneclient.v2_0.client', fakes.FakeKeystone) + def test_client_without_auth_plugin(self): + env = FAKE_ENV.copy() + del env['auth_plugin'] + c = self.create_client(env, api_version=2, endpoint='fake_endpoint') + self.assertIsInstance(c.auth_plugin, client.AuthPlugin) + + def test_client_without_auth_plugin_keystone_v3(self): + env = FAKE_ENV.copy() + del env['auth_plugin'] + expected = { + 'username': 'username', + 'endpoint': 'http://no.where', + 'tenant_name': 'tenant_name', + 'service_type': None, + 'token': '1234', + 'endpoint_type': None, + 'auth_url': 'http://no.where', + 'tenant_id': None, + 'cacert': None, + 'password': 'password', + 'user_domain_name': 'default', + 'user_domain_id': None, + 'project_domain_name': 'default', + 'project_domain_id': None, + } + with mock.patch('ceilometerclient.client.AuthPlugin') as auth_plugin: + self.create_client(env, api_version=2) + auth_plugin.assert_called_with(**expected) + + def test_client_with_auth_plugin(self): + c = self.create_client(FAKE_ENV, api_version=2) + self.assertIsInstance(c.auth_plugin, str) + + def test_v2_client_timeout_invalid_value(self): + env = FAKE_ENV.copy() + env['timeout'] = 'abc' + self.assertRaises(ValueError, self.create_client, env) + env['timeout'] = '1.5' + self.assertRaises(ValueError, self.create_client, env) + + def _test_v2_client_timeout_integer(self, timeout, expected_value): + env = FAKE_ENV.copy() + env['timeout'] = timeout + expected = { + 'auth_plugin': 'fake_auth', + 'timeout': expected_value, + 'original_ip': None, + 'http': None, + 'region_name': None, + 'verify': True, + 'timings': None, + 'keyring_saver': None, + 'cert': None, + 'endpoint_type': None, + 'user_agent': None, + 'debug': None, + } + cls = 'ceilometerclient.openstack.common.apiclient.client.HTTPClient' + with mock.patch(cls) as mocked: + self.create_client(env) + mocked.assert_called_with(**expected) + + def test_v2_client_timeout_zero(self): + self._test_v2_client_timeout_integer(0, None) + + def test_v2_client_timeout_valid_value(self): + self._test_v2_client_timeout_integer(30, 30) + + def test_v2_client_cacert_in_verify(self): + env = FAKE_ENV.copy() + env['cacert'] = '/path/to/cacert' + client = self.create_client(env) + self.assertEqual('/path/to/cacert', client.client.verify) + + def test_v2_client_certfile_and_keyfile(self): + env = FAKE_ENV.copy() + env['cert_file'] = '/path/to/cert' + env['key_file'] = '/path/to/keycert' + client = self.create_client(env) + self.assertEqual(('/path/to/cert', '/path/to/keycert'), + client.client.cert) diff --git a/ceilometerclient/tests/test_exc.py b/ceilometerclient/tests/test_exc.py index 53ae512..9c8f146 100644 --- a/ceilometerclient/tests/test_exc.py +++ b/ceilometerclient/tests/test_exc.py @@ -16,36 +16,56 @@ import json from ceilometerclient import exc - from ceilometerclient.tests import utils +HTTPEXCEPTIONS = {'HTTPBadRequest': exc.HTTPBadRequest, + 'HTTPUnauthorized': exc.HTTPUnauthorized, + 'HTTPForbidden': exc.HTTPForbidden, + 'HTTPNotFound': exc.HTTPNotFound, + 'HTTPMethodNotAllowed': exc.HTTPMethodNotAllowed, + 'HTTPConflict': exc.HTTPConflict, + 'HTTPOverLimit': exc.HTTPOverLimit, + 'HTTPInternalServerError': exc.HTTPInternalServerError, + 'HTTPNotImplemented': exc.HTTPNotImplemented, + 'HTTPBadGateway': exc.HTTPBadGateway, + 'HTTPServiceUnavailable': exc.HTTPServiceUnavailable} -class HTTPBadRequestTest(utils.BaseTestCase): +class HTTPExceptionsTest(utils.BaseTestCase): def test_str_no_details(self): - exception = exc.HTTPBadRequest() - self.assertEqual("HTTPBadRequest (HTTP 400)", str(exception)) + for k, v in HTTPEXCEPTIONS.items(): + exception = v() + ret_str = k + " (HTTP " + str(exception.code) + ")" + self.assertEqual(ret_str, str(exception)) def test_str_no_json(self): - exception = exc.HTTPBadRequest(details="foo") - self.assertEqual("HTTPBadRequest (HTTP 400)", str(exception)) + for k, v in HTTPEXCEPTIONS.items(): + exception = v(details="foo") + ret_str = k + " (HTTP " + str(exception.code) + ")" + self.assertEqual(ret_str, str(exception)) def test_str_no_error_message(self): - exception = exc.HTTPBadRequest(details=json.dumps({})) - self.assertEqual("HTTPBadRequest (HTTP 400)", str(exception)) + for k, v in HTTPEXCEPTIONS.items(): + exception = v(details=json.dumps({})) + ret_str = k + " (HTTP " + str(exception.code) + ")" + self.assertEqual(ret_str, 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)) + for k, v in HTTPEXCEPTIONS.items(): + exception = v( + details=json.dumps({"error_message": {"foo": "bar"}})) + ret_str = k + " (HTTP " + str(exception.code) + ")" + self.assertEqual(ret_str, 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)) + for k, v in HTTPEXCEPTIONS.items(): + exception = v(details=json.dumps({"error_message": "oops"})) + ret_str = k + " (HTTP " + str(exception.code) + ")" + self.assertEqual(ret_str, 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)) + for k, v in HTTPEXCEPTIONS.items(): + exception = v(details=json.dumps( + {"error_message": {"faultstring": "oops"}})) + ret_str = k + " (HTTP " + str(exception.code) + ") ERROR oops" + self.assertEqual(ret_str, str(exception)) diff --git a/ceilometerclient/tests/test_http.py b/ceilometerclient/tests/test_http.py deleted file mode 100644 index 239bb86..0000000 --- a/ceilometerclient/tests/test_http.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright 2012 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. - -import mock - -from ceilometerclient.common import http -from ceilometerclient.tests import utils - - -class HttpClientTest(utils.BaseTestCase): - url = 'http://localhost' - - def test_url_generation_trailing_slash_in_base(self): - client = http.HTTPClient("%s/" % self.url) - url = client._make_connection_url('/v1/resources') - self.assertEqual(url, '/v1/resources') - - def test_url_generation_without_trailing_slash_in_base(self): - client = http.HTTPClient(self.url) - url = client._make_connection_url('/v1/resources') - self.assertEqual(url, '/v1/resources') - - def test_url_generation_prefix_slash_in_path(self): - client = http.HTTPClient("%s/" % self.url) - url = client._make_connection_url('/v1/resources') - self.assertEqual(url, '/v1/resources') - - def test_url_generation_without_prefix_slash_in_path(self): - client = http.HTTPClient(self.url) - url = client._make_connection_url('v1/resources') - self.assertEqual(url, '/v1/resources') - - def test_get_connection(self): - client = http.HTTPClient(self.url) - self.assertIsNotNone(client.get_connection()) - - @mock.patch.object(http.HTTPClient, 'get_connection') - def test_url_generation_with_proxy(self, get_conn): - client = http.HTTPClient(self.url, token=lambda: 'token') - client.proxy_url = "http://localhost:3128/" - conn = mock.MagicMock() - conn.request.side_effect = Exception("stop") - get_conn.return_value = conn - try: - client._http_request('/v1/resources', 'GET') - except Exception: - pass - conn.request.assert_called_once_with('GET', (self.url.rstrip('/') + - '/v1/resources'), - headers=mock.ANY) - - -class HttpsClientTest(HttpClientTest): - url = 'https://localhost' - - -class HttpEndingSlashClientTest(HttpClientTest): - url = 'http://localhost/' diff --git a/ceilometerclient/tests/test_shell.py b/ceilometerclient/tests/test_shell.py index 1536abc..6d6403b 100644 --- a/ceilometerclient/tests/test_shell.py +++ b/ceilometerclient/tests/test_shell.py @@ -11,55 +11,56 @@ # under the License. import re -import six import sys import fixtures +from keystoneclient import session as ks_session import mock +import six from testtools import matchers -from keystoneclient.v2_0 import client as ksclient - from ceilometerclient import exc +from ceilometerclient.openstack.common.apiclient import client as api_client from ceilometerclient import shell as ceilometer_shell from ceilometerclient.tests import utils -from ceilometerclient.v1 import client as v1client +from ceilometerclient.v2 import client as v2client -FAKE_ENV = {'OS_USERNAME': 'username', - 'OS_PASSWORD': 'password', - 'OS_TENANT_NAME': 'tenant_name', - 'OS_AUTH_URL': 'http://no.where'} +FAKE_V2_ENV = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_TENANT_NAME': 'tenant_name', + 'OS_AUTH_URL': 'http://localhost:5000/v2.0'} + +FAKE_V3_ENV = {'OS_USERNAME': 'username', + 'OS_PASSWORD': 'password', + 'OS_USER_DOMAIN_NAME': 'domain_name', + 'OS_PROJECT_ID': '1234567890', + 'OS_AUTH_URL': 'http://localhost:5000/v3'} -class ShellTest(utils.BaseTestCase): - re_options = re.DOTALL | re.MULTILINE +class ShellTestBase(utils.BaseTestCase): # Patch os.environ to avoid required auth info. - def make_env(self, exclude=None): - env = dict((k, v) for k, v in FAKE_ENV.items() if k != exclude) + def make_env(self, env_version, exclude=None): + env = dict((k, v) for k, v in env_version.items() if k != exclude) self.useFixture(fixtures.MonkeyPatch('os.environ', env)) - def setUp(self): - super(ShellTest, self).setUp() - @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 +class ShellHelpTest(ShellTestBase): + RE_OPTIONS = re.DOTALL | re.MULTILINE + + @mock.patch('sys.stdout', new=six.StringIO()) + @mock.patch.object(ks_session, 'Session', mock.MagicMock()) + @mock.patch.object(v2client.client.HTTPClient, + 'client_request', mock.MagicMock()) + def shell(self, argstr): try: - sys.stdout = six.StringIO() _shell = ceilometer_shell.CeilometerShell() _shell.main(argstr.split()) except SystemExit: exc_type, exc_value, exc_traceback = sys.exc_info() self.assertEqual(exc_value.code, 0) - finally: - out = sys.stdout.getvalue() - sys.stdout.close() - sys.stdout = orig - return out + return sys.stdout.getvalue() def test_help_unknown_command(self): self.assertRaises(exc.CommandError, self.shell, 'help foofoo') @@ -75,7 +76,7 @@ class ShellTest(utils.BaseTestCase): for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, - self.re_options)) + self.RE_OPTIONS)) def test_help_on_subcommand(self): required = [ @@ -89,29 +90,136 @@ class ShellTest(utils.BaseTestCase): help_text = self.shell(argstr) for r in required: self.assertThat(help_text, - matchers.MatchesRegex(r, self.re_options)) + matchers.MatchesRegex(r, self.RE_OPTIONS)) - def test_auth_param(self): - self.make_env(exclude='OS_USERNAME') - self.test_help() - @mock.patch.object(ksclient, 'Client') +class ShellKeystoneV2Test(ShellTestBase): + + @mock.patch.object(ks_session, 'Session') def test_debug_switch_raises_error(self, mock_ksclient): - mock_ksclient.side_effect = exc.Unauthorized - self.make_env() + mock_ksclient.side_effect = exc.HTTPUnauthorized + self.make_env(FAKE_V2_ENV) args = ['--debug', 'event-list'] - self.assertRaises(exc.Unauthorized, ceilometer_shell.main, args) + self.assertRaises(exc.CommandError, ceilometer_shell.main, args) - @mock.patch.object(ksclient, 'Client') + @mock.patch.object(ks_session, 'Session') def test_dash_d_switch_raises_error(self, mock_ksclient): mock_ksclient.side_effect = exc.CommandError("FAIL") - self.make_env() + self.make_env(FAKE_V2_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() + @mock.patch('sys.stderr') + @mock.patch.object(ks_session, 'Session') + def test_no_debug_switch_no_raises_errors(self, mock_ksclient, __): + mock_ksclient.side_effect = exc.HTTPUnauthorized("FAIL") + self.make_env(FAKE_V2_ENV) args = ['event-list'] self.assertRaises(SystemExit, ceilometer_shell.main, args) + + +class ShellKeystoneV3Test(ShellTestBase): + + @mock.patch.object(ks_session, 'Session') + def test_debug_switch_raises_error(self, mock_ksclient): + mock_ksclient.side_effect = exc.HTTPUnauthorized + self.make_env(FAKE_V3_ENV) + args = ['--debug', 'event-list'] + self.assertRaises(exc.CommandError, ceilometer_shell.main, args) + + @mock.patch.object(ks_session, 'Session') + def test_dash_d_switch_raises_error(self, mock_ksclient): + mock_ksclient.side_effect = exc.CommandError("FAIL") + self.make_env(FAKE_V3_ENV) + args = ['-d', 'event-list'] + self.assertRaises(exc.CommandError, ceilometer_shell.main, args) + + @mock.patch('sys.stderr') + @mock.patch.object(ks_session, 'Session') + def test_no_debug_switch_no_raises_errors(self, mock_ksclient, __): + mock_ksclient.side_effect = exc.HTTPUnauthorized("FAIL") + self.make_env(FAKE_V3_ENV) + args = ['event-list'] + self.assertRaises(SystemExit, ceilometer_shell.main, args) + + +class ShellTimeoutTest(ShellTestBase): + + @mock.patch('sys.stderr', new=six.StringIO()) + def _test_timeout(self, timeout, expected_msg): + args = ['--timeout', timeout, 'alarm-list'] + self.assertRaises(SystemExit, ceilometer_shell.main, args) + self.assertEqual(expected_msg, sys.stderr.getvalue().splitlines()[-1]) + + def test_timeout_invalid_value(self): + expected_msg = ('ceilometer: error: argument --timeout: ' + 'abc must be an integer') + self._test_timeout('abc', expected_msg) + + def test_timeout_negative_value(self): + expected_msg = ('ceilometer: error: argument --timeout: ' + '-1 must be greater than 0') + self._test_timeout('-1', expected_msg) + + def test_timeout_float_value(self): + expected_msg = ('ceilometer: error: argument --timeout: ' + '1.5 must be an integer') + self._test_timeout('1.5', expected_msg) + + def test_timeout_zero(self): + expected_msg = ('ceilometer: error: argument --timeout: ' + '0 must be greater than 0') + self._test_timeout('0', expected_msg) + + +class ShellInsecureTest(ShellTestBase): + + @mock.patch.object(api_client, 'HTTPClient') + def test_insecure_true_ceilometer(self, mocked_client): + self.make_env(FAKE_V2_ENV) + args = ['--debug', '--os-insecure', 'true', 'alarm-list'] + self.assertIsNone(ceilometer_shell.main(args)) + args, kwargs = mocked_client.call_args + self.assertEqual(False, kwargs.get('verify')) + + @mock.patch.object(ks_session, 'Session') + def test_insecure_true_keystone(self, mocked_session): + mocked_session.side_effect = exc.HTTPUnauthorized("FAIL") + self.make_env(FAKE_V2_ENV) + args = ['--debug', '--os-insecure', 'true', 'alarm-list'] + self.assertRaises(exc.CommandError, ceilometer_shell.main, args) + mocked_session.assert_called_with(verify=False, cert='') + + @mock.patch.object(api_client, 'HTTPClient') + def test_insecure_false_ceilometer(self, mocked_client): + self.make_env(FAKE_V2_ENV) + args = ['--debug', '--os-insecure', 'false', 'alarm-list'] + self.assertIsNone(ceilometer_shell.main(args)) + args, kwargs = mocked_client.call_args + self.assertEqual(True, kwargs.get('verify')) + + @mock.patch.object(ks_session, 'Session') + def test_insecure_false_keystone(self, mocked_session): + mocked_session.side_effect = exc.HTTPUnauthorized("FAIL") + self.make_env(FAKE_V2_ENV) + args = ['--debug', '--os-insecure', 'false', 'alarm-list'] + self.assertRaises(exc.CommandError, ceilometer_shell.main, args) + mocked_session.assert_called_with(verify=True, cert='') + + +class ShellEndpointTest(ShellTestBase): + + @mock.patch('ceilometerclient.v2.client.Client') + def _test_endpoint_and_token(self, token_name, endpoint_name, mocked): + args = ['--debug', token_name, 'fake-token', + endpoint_name, 'http://fake-url', 'alarm-list'] + self.assertEqual(None, ceilometer_shell.main(args)) + args, kwargs = mocked.call_args + self.assertEqual('http://fake-url', kwargs.get('endpoint')) + self.assertEqual('fake-token', kwargs.get('token')) + + def test_endpoint_and_token(self): + self._test_endpoint_and_token('--os-auth-token', '--ceilometer-url') + self._test_endpoint_and_token('--os-auth-token', '--os-endpoint') + self._test_endpoint_and_token('--os-token', '--ceilometer-url') + self._test_endpoint_and_token('--os-token', '--os-endpoint') diff --git a/ceilometerclient/tests/test_utils.py b/ceilometerclient/tests/test_utils.py index 9082328..6ef1fd1 100644 --- a/ceilometerclient/tests/test_utils.py +++ b/ceilometerclient/tests/test_utils.py @@ -13,11 +13,10 @@ # License for the specific language governing permissions and limitations # under the License. - import itertools + import mock import six -import sys from ceilometerclient.common import utils from ceilometerclient.tests import utils as test_utils @@ -26,30 +25,56 @@ from ceilometerclient.tests import utils as test_utils class UtilsTest(test_utils.BaseTestCase): def test_prettytable(self): - class Struct: + class Struct(object): def __init__(self, **entries): self.__dict__.update(entries) # test that the prettytable output is wellformatted (left-aligned) - saved_stdout = sys.stdout - try: - sys.stdout = output_dict = six.StringIO() + with mock.patch('sys.stdout', new=six.StringIO()) as stdout: utils.print_dict({'K': 'k', 'Key': 'Value'}) - - finally: - sys.stdout = saved_stdout - - self.assertEqual(output_dict.getvalue(), '''\ + self.assertEqual('''\ +----------+-------+ | Property | Value | +----------+-------+ | K | k | | Key | Value | +----------+-------+ -''') +''', stdout.getvalue()) + + with mock.patch('sys.stdout', new=six.StringIO()) as stdout: + utils.print_dict({'alarm_id': '262567fd-d79a-4bbb-a9d0-59d879b6', + 'description': 'test alarm', + 'state': 'insufficient data', + 'repeat_actions': 'False', + 'type': 'threshold', + 'threshold': '1.0', + 'statistic': 'avg', + 'time_constraints': '[{name: c1,' + '\\n description: test,' + '\\n start: 0 18 * * *,' + '\\n duration: 1,' + '\\n timezone: US}]'}) + self.assertEqual('''\ ++------------------+----------------------------------+ +| Property | Value | ++------------------+----------------------------------+ +| alarm_id | 262567fd-d79a-4bbb-a9d0-59d879b6 | +| description | test alarm | +| repeat_actions | False | +| state | insufficient data | +| statistic | avg | +| threshold | 1.0 | +| time_constraints | [{name: c1, | +| | description: test, | +| | start: 0 18 * * *, | +| | duration: 1, | +| | timezone: US}] | +| type | threshold | ++------------------+----------------------------------+ +''', stdout.getvalue()) def test_print_list(self): - class Foo: + class Foo(object): def __init__(self, one, two, three): self.one = one self.two = two @@ -61,17 +86,13 @@ class UtilsTest(test_utils.BaseTestCase): Foo(12, '0', 'Z')] def do_print_list(sortby): - saved_stdout = sys.stdout - try: - sys.stdout = output = six.StringIO() + with mock.patch('sys.stdout', new=six.StringIO()) as stdout: 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() + return stdout.getvalue() printed = do_print_list(None) self.assertEqual(printed, '''\ diff --git a/ceilometerclient/tests/utils.py b/ceilometerclient/tests/utils.py index dae7ea2..57bc276 100644 --- a/ceilometerclient/tests/utils.py +++ b/ceilometerclient/tests/utils.py @@ -13,54 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. -import copy import fixtures -import six import testtools -from ceilometerclient.common import http - class BaseTestCase(testtools.TestCase): def setUp(self): super(BaseTestCase, self).setUp() self.useFixture(fixtures.FakeLogger()) - - -class FakeAPI(object): - def __init__(self, fixtures): - self.fixtures = fixtures - self.calls = [] - - def _request(self, method, url, headers=None, body=None): - call = (method, url, headers or {}, body) - self.calls.append(call) - return self.fixtures[url][method] - - def raw_request(self, *args, **kwargs): - fixture = self._request(*args, **kwargs) - body_iter = http.ResponseBodyIterator(six.StringIO(fixture[1])) - return FakeResponse(fixture[0]), body_iter - - def json_request(self, *args, **kwargs): - fixture = self._request(*args, **kwargs) - return FakeResponse(fixture[0]), fixture[1] - - -class FakeResponse(object): - def __init__(self, headers, body=None, version=None): - """:param headers: dict representing HTTP response headers - :param body: file-like object - """ - self.headers = headers - self.body = body - - def getheaders(self): - return copy.deepcopy(self.headers).items() - - def getheader(self, key, default): - return self.headers.get(key, default) - - def read(self, amt): - return self.body.read(amt) diff --git a/ceilometerclient/tests/v1/test_meters.py b/ceilometerclient/tests/v1/test_meters.py index f97c18b..1c883b7 100644 --- a/ceilometerclient/tests/v1/test_meters.py +++ b/ceilometerclient/tests/v1/test_meters.py @@ -12,7 +12,8 @@ # 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.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v1.meters @@ -109,15 +110,16 @@ class MeterManagerTest(utils.BaseTestCase): def setUp(self): super(MeterManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = ceilometerclient.v1.meters.MeterManager(self.api) def test_list_all(self): resources = list(self.mgr.list()) expect = [ - ('GET', '/v1/meters', {}, None), + 'GET', '/v1/meters' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 2) self.assertEqual(resources[0].resource_id, 'a') self.assertEqual(resources[1].resource_id, 'b') @@ -125,9 +127,9 @@ class MeterManagerTest(utils.BaseTestCase): def test_list_by_source(self): resources = list(self.mgr.list(source='openstack')) expect = [ - ('GET', '/v1/sources/openstack/meters', {}, None), + 'GET', '/v1/sources/openstack/meters' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 2) self.assertEqual(resources[0].resource_id, 'b') self.assertEqual(resources[1].resource_id, 'q') @@ -135,26 +137,26 @@ class MeterManagerTest(utils.BaseTestCase): def test_list_by_user(self): resources = list(self.mgr.list(user_id='joey')) expect = [ - ('GET', '/v1/users/joey/meters', {}, None), + 'GET', '/v1/users/joey/meters' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 1) self.assertEqual(resources[0].resource_id, 'b') def test_list_by_project(self): resources = list(self.mgr.list(project_id='dig_the_ditch')) expect = [ - ('GET', '/v1/projects/dig_the_ditch/meters', {}, None), + 'GET', '/v1/projects/dig_the_ditch/meters' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 1) self.assertEqual(resources[0].resource_id, 'b') def test_list_by_metaquery(self): resources = list(self.mgr.list(metaquery='metadata.zxc_id=foo')) expect = [ - ('GET', '/v1/meters?metadata.zxc_id=foo', {}, None), + 'GET', '/v1/meters?metadata.zxc_id=foo' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 1) self.assertEqual(resources[0].resource_id, 'b') diff --git a/ceilometerclient/tests/v1/test_projects.py b/ceilometerclient/tests/v1/test_projects.py index a1faaf0..2f34a5c 100644 --- a/ceilometerclient/tests/v1/test_projects.py +++ b/ceilometerclient/tests/v1/test_projects.py @@ -12,7 +12,8 @@ # 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.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v1.meters @@ -40,15 +41,16 @@ class ProjectManagerTest(utils.BaseTestCase): def setUp(self): super(ProjectManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = ceilometerclient.v1.meters.ProjectManager(self.api) def test_list_all(self): projects = list(self.mgr.list()) expect = [ - ('GET', '/v1/projects', {}, None), + 'GET', '/v1/projects' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(projects), 2) self.assertEqual(projects[0].project_id, 'a') self.assertEqual(projects[1].project_id, 'b') @@ -56,8 +58,8 @@ class ProjectManagerTest(utils.BaseTestCase): def test_list_by_source(self): projects = list(self.mgr.list(source='source_b')) expect = [ - ('GET', '/v1/sources/source_b/projects', {}, None), + 'GET', '/v1/sources/source_b/projects' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(projects), 1) self.assertEqual(projects[0].project_id, 'b') diff --git a/ceilometerclient/tests/v1/test_resources.py b/ceilometerclient/tests/v1/test_resources.py index 00cea8c..4eac3fe 100644 --- a/ceilometerclient/tests/v1/test_resources.py +++ b/ceilometerclient/tests/v1/test_resources.py @@ -12,7 +12,8 @@ # 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.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v1.meters @@ -108,15 +109,16 @@ class ResourceManagerTest(utils.BaseTestCase): def setUp(self): super(ResourceManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = ceilometerclient.v1.meters.ResourceManager(self.api) def test_list_all(self): resources = list(self.mgr.list()) expect = [ - ('GET', '/v1/resources', {}, None), + 'GET', '/v1/resources' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 2) self.assertEqual(resources[0].resource_id, 'a') self.assertEqual(resources[1].resource_id, 'b') @@ -124,27 +126,27 @@ class ResourceManagerTest(utils.BaseTestCase): def test_list_by_user(self): resources = list(self.mgr.list(user_id='joey')) expect = [ - ('GET', '/v1/users/joey/resources', {}, None), + 'GET', '/v1/users/joey/resources' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 1) self.assertEqual(resources[0].resource_id, 'b') def test_list_by_metaquery(self): resources = list(self.mgr.list(metaquery='metadata.zxc_id=foo')) expect = [ - ('GET', '/v1/resources?metadata.zxc_id=foo', {}, None), + 'GET', '/v1/resources?metadata.zxc_id=foo' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 1) self.assertEqual(resources[0].resource_id, 'b') def test_list_by_project(self): resources = list(self.mgr.list(project_id='project_bla')) expect = [ - ('GET', '/v1/projects/project_bla/resources', {}, None), + 'GET', '/v1/projects/project_bla/resources' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 1) self.assertEqual(resources[0].resource_id, 'a') @@ -152,9 +154,8 @@ class ResourceManagerTest(utils.BaseTestCase): resources = list(self.mgr.list(start_timestamp='now', end_timestamp='now')) expect = [ - ('GET', '/v1/resources?start_timestamp=now&end_timestamp=now', - {}, None), + 'GET', '/v1/resources?start_timestamp=now&end_timestamp=now' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 1) self.assertEqual(resources[0].resource_id, 'b') diff --git a/ceilometerclient/tests/v1/test_samples.py b/ceilometerclient/tests/v1/test_samples.py index 9e59b46..61f064e 100644 --- a/ceilometerclient/tests/v1/test_samples.py +++ b/ceilometerclient/tests/v1/test_samples.py @@ -12,7 +12,8 @@ # 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.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v1.meters @@ -122,24 +123,25 @@ class SampleManagerTest(utils.BaseTestCase): def setUp(self): super(SampleManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = ceilometerclient.v1.meters.SampleManager(self.api) def test_list_all(self): samples = list(self.mgr.list(counter_name=None)) expect = [ - ('GET', '/v1/meters', {}, None), + 'GET', '/v1/meters' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(samples), 0) def test_list_by_source(self): samples = list(self.mgr.list(source='openstack', counter_name='this')) expect = [ - ('GET', '/v1/sources/openstack/meters/this', {}, None), + 'GET', '/v1/sources/openstack/meters/this' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(samples), 1) self.assertEqual(samples[0].resource_id, 'b') @@ -147,9 +149,9 @@ class SampleManagerTest(utils.BaseTestCase): samples = list(self.mgr.list(user_id='freddy', counter_name='balls')) expect = [ - ('GET', '/v1/users/freddy/meters/balls', {}, None), + 'GET', '/v1/users/freddy/meters/balls' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(samples), 1) self.assertEqual(samples[0].project_id, 'melbourne_open') self.assertEqual(samples[0].user_id, 'freddy') @@ -159,9 +161,9 @@ class SampleManagerTest(utils.BaseTestCase): samples = list(self.mgr.list(project_id='dig_the_ditch', counter_name='meters')) expect = [ - ('GET', '/v1/projects/dig_the_ditch/meters/meters', {}, None), + 'GET', '/v1/projects/dig_the_ditch/meters/meters' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(samples), 1) self.assertEqual(samples[0].project_id, 'dig_the_ditch') self.assertEqual(samples[0].volume, 345) @@ -171,9 +173,9 @@ class SampleManagerTest(utils.BaseTestCase): samples = list(self.mgr.list(metaquery='metadata.zxc_id=foo', counter_name='this')) expect = [ - ('GET', '/v1/meters?metadata.zxc_id=foo', {}, None), + 'GET', '/v1/meters?metadata.zxc_id=foo' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(samples), 1) self.assertEqual(samples[0].resource_metadata['zxc_id'], 'foo') @@ -183,12 +185,11 @@ class SampleManagerTest(utils.BaseTestCase): start_timestamp='now', end_timestamp='now')) expect = [ - ('GET', - '/v1/users/freddy/meters/balls?' + - 'start_timestamp=now&end_timestamp=now', - {}, None), + 'GET', + '/v1/users/freddy/meters/balls?' + + 'start_timestamp=now&end_timestamp=now' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(samples), 1) self.assertEqual(samples[0].project_id, 'melbourne_open') self.assertEqual(samples[0].user_id, 'freddy') diff --git a/ceilometerclient/tests/v1/test_users.py b/ceilometerclient/tests/v1/test_users.py index df208c5..541139b 100644 --- a/ceilometerclient/tests/v1/test_users.py +++ b/ceilometerclient/tests/v1/test_users.py @@ -12,7 +12,8 @@ # 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.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v1.meters @@ -40,15 +41,16 @@ class UserManagerTest(utils.BaseTestCase): def setUp(self): super(UserManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = ceilometerclient.v1.meters.UserManager(self.api) def test_list_all(self): users = list(self.mgr.list()) expect = [ - ('GET', '/v1/users', {}, None), + 'GET', '/v1/users' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(users), 2) self.assertEqual(users[0].user_id, 'a') self.assertEqual(users[1].user_id, 'b') @@ -56,8 +58,8 @@ class UserManagerTest(utils.BaseTestCase): def test_list_by_source(self): users = list(self.mgr.list(source='source_b')) expect = [ - ('GET', '/v1/sources/source_b/users', {}, None), + 'GET', '/v1/sources/source_b/users' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(users), 1) self.assertEqual(users[0].user_id, 'b') diff --git a/ceilometerclient/tests/v2/test_alarms.py b/ceilometerclient/tests/v2/test_alarms.py index 1644f20..4239d2f 100644 --- a/ceilometerclient/tests/v2/test_alarms.py +++ b/ceilometerclient/tests/v2/test_alarms.py @@ -1,8 +1,5 @@ -# -*- encoding: utf-8 -*- # -# Copyright © 2013 Red Hat, Inc -# -# Author: Eoghan Glynn +# Copyright 2013 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 @@ -22,13 +19,16 @@ import six from six.moves import xrange # noqa import testtools -from ceilometerclient.tests import utils +from ceilometerclient import exc +from ceilometerclient.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.v2 import alarms AN_ALARM = {u'alarm_actions': [u'http://site:8000/alarm'], u'ok_actions': [u'http://site:8000/ok'], u'description': u'An alarm', u'type': u'threshold', + u'severity': 'low', u'threshold_rule': { u'meter_name': u'storage.objects', u'query': [{u'field': u'key_name', @@ -38,18 +38,20 @@ AN_ALARM = {u'alarm_actions': [u'http://site:8000/alarm'], u'period': 240.0, u'statistic': u'avg', u'threshold': 200.0, - u'comparison_operator': 'gt', - }, - u'time_constraints': [{u'name': u'cons1', - u'description': u'desc1', - u'start': u'0 11 * * *', - u'duration': 300, - u'timezone': u''}, - {u'name': u'cons2', - u'description': u'desc2', - u'start': u'0 23 * * *', - u'duration': 600, - u'timezone': ''}], + u'comparison_operator': 'gt'}, + u'time_constraints': [ + { + u'name': u'cons1', + u'description': u'desc1', + u'start': u'0 11 * * *', + u'duration': 300, + u'timezone': u''}, + { + u'name': u'cons2', + u'description': u'desc2', + u'start': u'0 23 * * *', + u'duration': 600, + u'timezone': ''}], u'timestamp': u'2013-05-09T13:41:23.085000', u'enabled': True, u'alarm_id': u'alarm-id', @@ -106,6 +108,7 @@ AN_LEGACY_ALARM = {u'alarm_actions': [u'http://site:8000/alarm'], u'period': 240.0, u'alarm_id': u'alarm-id', u'state': u'ok', + u'severity': u'low', u'insufficient_data_actions': [u'http://site:8000/nodata'], u'statistic': u'avg', u'threshold': 200.0, @@ -200,16 +203,31 @@ fixtures = { {}, UPDATED_ALARM, ), + 'DELETE': ( + {}, + None, + ), + }, + '/v2/alarms/unk-alarm-id': + { + 'GET': ( + {}, + None, + ), + 'PUT': ( + {}, + None, + ), }, '/v2/alarms/alarm-id/state': { 'PUT': ( {}, - 'alarm' + {'alarm': 'alarm'} ), 'GET': ( {}, - 'alarm' + {'alarm': 'alarm'} ), }, @@ -249,15 +267,16 @@ class AlarmManagerTest(testtools.TestCase): def setUp(self): super(AlarmManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = alarms.AlarmManager(self.api) def test_list_all(self): alarms = list(self.mgr.list()) expect = [ - ('GET', '/v2/alarms', {}, None), + 'GET', '/v2/alarms' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(alarms), 1) self.assertEqual(alarms[0].alarm_id, 'alarm-id') @@ -267,53 +286,58 @@ class AlarmManagerTest(testtools.TestCase): {"field": "name", "value": "SwiftObjectAlarm"}])) expect = [ - ('GET', - '/v2/alarms?q.field=project_id&q.field=name&q.op=&q.op=' - '&q.type=&q.type=&q.value=project-id&q.value=SwiftObjectAlarm', - {}, None), + 'GET', + '/v2/alarms?q.field=project_id&q.field=name&q.op=&q.op=' + '&q.type=&q.type=&q.value=project-id&q.value=SwiftObjectAlarm', ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(alarms), 1) self.assertEqual(alarms[0].alarm_id, 'alarm-id') def test_get(self): alarm = self.mgr.get(alarm_id='alarm-id') expect = [ - ('GET', '/v2/alarms/alarm-id', {}, None), + 'GET', '/v2/alarms/alarm-id' ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(alarm) + self.http_client.assert_called(*expect) + self.assertIsNotNone(alarm) self.assertEqual(alarm.alarm_id, 'alarm-id') self.assertEqual(alarm.rule, alarm.threshold_rule) def test_create(self): alarm = self.mgr.create(**CREATE_ALARM) expect = [ - ('POST', '/v2/alarms', {}, CREATE_ALARM), + 'POST', '/v2/alarms' ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(alarm) + self.http_client.assert_called(*expect, body=CREATE_ALARM) + self.assertIsNotNone(alarm) def test_update(self): alarm = self.mgr.update(alarm_id='alarm-id', **UPDATE_ALARM) - expect = [ - ('GET', '/v2/alarms/alarm-id', {}, None), - ('PUT', '/v2/alarms/alarm-id', {}, UPDATED_ALARM), + expect_get = [ + 'GET', '/v2/alarms/alarm-id' ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(alarm) + expect_put = [ + 'PUT', '/v2/alarms/alarm-id', UPDATED_ALARM + ] + self.http_client.assert_called(*expect_get, pos=0) + self.http_client.assert_called(*expect_put, pos=1) + self.assertIsNotNone(alarm) self.assertEqual(alarm.alarm_id, 'alarm-id') for (key, value) in six.iteritems(UPDATED_ALARM): self.assertEqual(getattr(alarm, key), value) def test_update_delta(self): alarm = self.mgr.update(alarm_id='alarm-id', **DELTA_ALARM) - expect = [ - ('GET', '/v2/alarms/alarm-id', {}, None), - ('PUT', '/v2/alarms/alarm-id', {}, UPDATED_ALARM), + expect_get = [ + 'GET', '/v2/alarms/alarm-id' ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(alarm) + expect_put = [ + 'PUT', '/v2/alarms/alarm-id', UPDATED_ALARM + ] + self.http_client.assert_called(*expect_get, pos=0) + self.http_client.assert_called(*expect_put, pos=1) + self.assertIsNotNone(alarm) self.assertEqual(alarm.alarm_id, 'alarm-id') for (key, value) in six.iteritems(UPDATED_ALARM): self.assertEqual(getattr(alarm, key), value) @@ -321,31 +345,79 @@ class AlarmManagerTest(testtools.TestCase): def test_set_state(self): state = self.mgr.set_state(alarm_id='alarm-id', state='alarm') expect = [ - ('PUT', '/v2/alarms/alarm-id/state', {}, 'alarm'), + 'PUT', '/v2/alarms/alarm-id/state' ] - self.assertEqual(self.api.calls, expect) - self.assertEqual(state, 'alarm') + self.http_client.assert_called(*expect, body='alarm') + self.assertEqual(state, {'alarm': 'alarm'}) def test_get_state(self): state = self.mgr.get_state(alarm_id='alarm-id') expect = [ - ('GET', '/v2/alarms/alarm-id/state', {}, None), + 'GET', '/v2/alarms/alarm-id/state' ] - self.assertEqual(self.api.calls, expect) - self.assertEqual(state, 'alarm') + self.http_client.assert_called(*expect) + self.assertEqual(state, {'alarm': 'alarm'}) def test_delete(self): deleted = self.mgr.delete(alarm_id='victim-id') expect = [ - ('DELETE', '/v2/alarms/victim-id', {}, None), + 'DELETE', '/v2/alarms/victim-id' ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(deleted is None) + self.http_client.assert_called(*expect) + self.assertIsNone(deleted) + + def test_get_from_alarm_class(self): + alarm = self.mgr.get(alarm_id='alarm-id') + self.assertIsNotNone(alarm) + alarm.get() + expect = [ + 'GET', '/v2/alarms/alarm-id' + ] + self.http_client.assert_called(*expect, pos=0) + self.http_client.assert_called(*expect, pos=1) + self.assertEqual('alarm-id', alarm.alarm_id) + self.assertEqual(alarm.threshold_rule, alarm.rule) + + def test_get_state_from_alarm_class(self): + alarm = self.mgr.get(alarm_id='alarm-id') + self.assertIsNotNone(alarm) + state = alarm.get_state() + expect_get_1 = [ + 'GET', '/v2/alarms/alarm-id' + ] + expect_get_2 = [ + 'GET', '/v2/alarms/alarm-id/state' + ] + self.http_client.assert_called(*expect_get_1, pos=0) + self.http_client.assert_called(*expect_get_2, pos=1) + self.assertEqual('alarm', state) + + def test_update_missing(self): + alarm = None + try: + alarm = self.mgr.update(alarm_id='unk-alarm-id', **UPDATE_ALARM) + except exc.CommandError: + pass + self.assertEqual(alarm, None) + + def test_delete_from_alarm_class(self): + alarm = self.mgr.get(alarm_id='alarm-id') + self.assertIsNotNone(alarm) + deleted = alarm.delete() + expect_get = [ + 'GET', '/v2/alarms/alarm-id' + ] + expect_delete = [ + 'DELETE', '/v2/alarms/alarm-id' + ] + self.http_client.assert_called(*expect_get, pos=0) + self.http_client.assert_called(*expect_delete, pos=1) + self.assertIsNone(deleted) def _do_test_get_history(self, q, url): history = self.mgr.get_history(q=q, alarm_id='alarm-id') - expect = [('GET', url, {}, None)] - self.assertEqual(self.api.calls, expect) + expect = ['GET', url] + self.http_client.assert_called(*expect) for i in xrange(len(history)): change = history[i] self.assertIsInstance(change, alarms.AlarmChange) @@ -367,16 +439,17 @@ class AlarmLegacyManagerTest(testtools.TestCase): def setUp(self): super(AlarmLegacyManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = alarms.AlarmManager(self.api) def test_create(self): alarm = self.mgr.create(**CREATE_LEGACY_ALARM) expect = [ - ('POST', '/v2/alarms', {}, CREATE_ALARM_WITHOUT_TC), + 'POST', '/v2/alarms', CREATE_ALARM_WITHOUT_TC, ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(alarm) + self.http_client.assert_called(*expect) + self.assertIsNotNone(alarm) def test_create_counter_name(self): create = {} @@ -385,19 +458,18 @@ class AlarmLegacyManagerTest(testtools.TestCase): del create['meter_name'] alarm = self.mgr.create(**create) expect = [ - ('POST', '/v2/alarms', {}, CREATE_ALARM_WITHOUT_TC), + 'POST', '/v2/alarms', CREATE_ALARM_WITHOUT_TC, ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(alarm) + self.http_client.assert_called(*expect) + self.assertIsNotNone(alarm) def test_update(self): alarm = self.mgr.update(alarm_id='alarm-id', **DELTA_LEGACY_ALARM) - expect = [ - ('GET', '/v2/alarms/alarm-id', {}, None), - ('PUT', '/v2/alarms/alarm-id', {}, UPDATED_ALARM), + expect_put = [ + 'PUT', '/v2/alarms/alarm-id', UPDATED_ALARM ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(alarm) + self.http_client.assert_called(*expect_put) + self.assertIsNotNone(alarm) self.assertEqual(alarm.alarm_id, 'alarm-id') for (key, value) in six.iteritems(UPDATED_ALARM): self.assertEqual(getattr(alarm, key), value) @@ -408,12 +480,11 @@ class AlarmLegacyManagerTest(testtools.TestCase): updated['counter_name'] = UPDATED_LEGACY_ALARM['meter_name'] del updated['meter_name'] alarm = self.mgr.update(alarm_id='alarm-id', **updated) - expect = [ - ('GET', '/v2/alarms/alarm-id', {}, None), - ('PUT', '/v2/alarms/alarm-id', {}, UPDATED_ALARM), + expect_put = [ + 'PUT', '/v2/alarms/alarm-id', UPDATED_ALARM ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(alarm) + self.http_client.assert_called(*expect_put) + self.assertIsNotNone(alarm) self.assertEqual(alarm.alarm_id, 'alarm-id') for (key, value) in six.iteritems(UPDATED_ALARM): self.assertEqual(getattr(alarm, key), value) @@ -423,7 +494,8 @@ class AlarmTimeConstraintTest(testtools.TestCase): def setUp(self): super(AlarmTimeConstraintTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = alarms.AlarmManager(self.api) def test_add_new(self): @@ -432,26 +504,37 @@ class AlarmTimeConstraintTest(testtools.TestCase): duration=500) kwargs = dict(time_constraints=[new_constraint]) self.mgr.update(alarm_id='alarm-id', **kwargs) - actual = self.api.calls[1][3]['time_constraints'] - expected = AN_ALARM[u'time_constraints'] + [new_constraint] - self.assertEqual(expected, actual) + body = copy.deepcopy(AN_ALARM) + body[u'time_constraints'] = \ + AN_ALARM[u'time_constraints'] + [new_constraint] + expect = [ + 'PUT', '/v2/alarms/alarm-id', body + ] + self.http_client.assert_called(*expect) def test_update_existing(self): updated_constraint = dict(name='cons2', duration=500) kwargs = dict(time_constraints=[updated_constraint]) self.mgr.update(alarm_id='alarm-id', **kwargs) - actual = self.api.calls[1][3]['time_constraints'] - expected = [AN_ALARM[u'time_constraints'][0], dict(name='cons2', - description='desc2', - start='0 23 * * *', - duration=500, - timezone='')] - self.assertEqual(expected, actual) + body = copy.deepcopy(AN_ALARM) + body[u'time_constraints'][1] = dict(name='cons2', + description='desc2', + start='0 23 * * *', + duration=500, + timezone='') + + expect = [ + 'PUT', '/v2/alarms/alarm-id', body + ] + self.http_client.assert_called(*expect) def test_remove(self): kwargs = dict(remove_time_constraints=['cons2']) self.mgr.update(alarm_id='alarm-id', **kwargs) - actual = self.api.calls[1][3]['time_constraints'] - expected = [AN_ALARM[u'time_constraints'][0]] - self.assertEqual(expected, actual) + body = copy.deepcopy(AN_ALARM) + body[u'time_constraints'] = AN_ALARM[u'time_constraints'][:1] + expect = [ + 'PUT', '/v2/alarms/alarm-id', body + ] + self.http_client.assert_called(*expect) diff --git a/ceilometerclient/tests/v2/test_event_types.py b/ceilometerclient/tests/v2/test_event_types.py index 85c59ec..8d5e4a0 100644 --- a/ceilometerclient/tests/v2/test_event_types.py +++ b/ceilometerclient/tests/v2/test_event_types.py @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -13,6 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. + +from ceilometerclient.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v2.event_types @@ -31,15 +33,16 @@ class EventTypesManagerTest(utils.BaseTestCase): def setUp(self): super(EventTypesManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) 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), + 'GET', '/v2/event_types/' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(event_types), 4) self.assertEqual(event_types[0].event_type, "Foo") self.assertEqual(event_types[1].event_type, "Bar") diff --git a/ceilometerclient/tests/v2/test_events.py b/ceilometerclient/tests/v2/test_events.py index ee90a14..6b1e74b 100644 --- a/ceilometerclient/tests/v2/test_events.py +++ b/ceilometerclient/tests/v2/test_events.py @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -12,7 +11,8 @@ # 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.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v2.events @@ -23,22 +23,22 @@ fixtures = { {}, [ { + 'message_id': '1', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', - 'traits': {'trait_A': 'abc', - 'message_id': '1'}, + 'traits': {'trait_A': 'abc'}, }, { + 'message_id': '2', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', - 'traits': {'trait_A': 'def', - 'message_id': '2'}, + 'traits': {'trait_A': 'def'}, }, { + 'message_id': '3', 'event_type': 'Bar', 'generated': '1970-01-01T00:00:00', - 'traits': {'trait_B': 'bartrait', - 'message_id': '3'}, + 'traits': {'trait_B': 'bartrait'}, }, ] ), @@ -49,18 +49,18 @@ fixtures = { {}, [ { + 'message_id': '1', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'abc', - 'hostname': 'localhost', - 'message_id': '1'}, + 'hostname': 'localhost'}, }, { + 'message_id': '2', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'def', - 'hostname': 'localhost', - 'message_id': '2'}, + 'hostname': 'localhost'}, } ] ), @@ -71,18 +71,18 @@ fixtures = { {}, [ { + 'message_id': '1', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'abc', - 'hostname': 'foreignhost', - 'message_id': '1'}, + 'hostname': 'foreignhost'}, }, { + 'message_id': '2', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'def', - 'hostname': 'foreignhost', - 'message_id': '2'}, + 'hostname': 'foreignhost'}, } ] ), @@ -94,12 +94,12 @@ fixtures = { {}, [ { + 'message_id': '1', 'event_type': 'Bar', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'abc', 'hostname': 'localhost', - 'num_cpus': '5', - 'message_id': '1'}, + 'num_cpus': '5'}, }, ] ), @@ -110,10 +110,10 @@ fixtures = { 'GET': ( {}, { + 'message_id': '2', 'event_type': 'Foo', 'generated': '1970-01-01T00:00:00', 'traits': {'trait_A': 'def', - 'message_id': '2', 'intTrait': '42'}, } ), @@ -125,15 +125,16 @@ class EventManagerTest(utils.BaseTestCase): def setUp(self): super(EventManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = ceilometerclient.v2.events.EventManager(self.api) def test_list_all(self): events = list(self.mgr.list()) expect = [ - ('GET', '/v2/events', {}, None), + 'GET', '/v2/events' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(events), 3) self.assertEqual(events[0].event_type, 'Foo') self.assertEqual(events[1].event_type, 'Foo') @@ -142,10 +143,10 @@ class EventManagerTest(utils.BaseTestCase): def test_list_one(self): event = self.mgr.get(2) expect = [ - ('GET', '/v2/events/2', {}, None), + 'GET', '/v2/events/2' ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(event) + self.http_client.assert_called(*expect) + self.assertIsNotNone(event) self.assertEqual(event.event_type, 'Foo') def test_list_with_query(self): @@ -153,11 +154,10 @@ class EventManagerTest(utils.BaseTestCase): "value": "localhost", "type": "string"}])) expect = [ - ('GET', '/v2/events?q.field=hostname&q.op=&q.type=string' - '&q.value=localhost', - {}, None), + 'GET', '/v2/events?q.field=hostname&q.op=&q.type=string' + '&q.value=localhost' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(events), 2) self.assertEqual(events[0].event_type, 'Foo') @@ -165,11 +165,10 @@ class EventManagerTest(utils.BaseTestCase): 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), + 'GET', '/v2/events?q.field=hostname&q.op=' + '&q.type=&q.value=foreignhost' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(events), 2) self.assertEqual(events[0].event_type, 'Foo') @@ -181,9 +180,19 @@ class EventManagerTest(utils.BaseTestCase): "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), + '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' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(events), 1) + + def test_get_from_event_class(self): + event = self.mgr.get(2) + self.assertIsNotNone(event) + event.get() + expect = [ + 'GET', '/v2/events/2' + ] + self.http_client.assert_called(*expect, pos=0) + self.http_client.assert_called(*expect, pos=1) + self.assertEqual('Foo', event.event_type) diff --git a/ceilometerclient/tests/v2/test_options.py b/ceilometerclient/tests/v2/test_options.py index 8cbf73a..6318d5c 100644 --- a/ceilometerclient/tests/v2/test_options.py +++ b/ceilometerclient/tests/v2/test_options.py @@ -10,7 +10,6 @@ # 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 from ceilometerclient.v2 import options @@ -83,9 +82,57 @@ class CliTest(utils.BaseTestCase): 'op': 'le', 'value': '283.347', 'type': ''}]) - def test_invalid_seperator(self): - self.assertRaises(ValueError, options.cli_to_array, - 'this=2.4,fooo=doof') + def test_comma(self): + ar = options.cli_to_array('this=2.4,fooo=doof') + self.assertEqual([{'field': 'this', + 'op': 'eq', + 'value': '2.4,fooo=doof', + 'type': ''}], + ar) + + def test_special_character(self): + ar = options.cli_to_array('key~123=value!123') + self.assertEqual([{'field': 'key~123', + 'op': 'eq', + 'value': 'value!123', + 'type': ''}], + ar) + + def _do_test_typed_float_op(self, op, op_str): + ar = options.cli_to_array('that%sfloat::283.347' % op) + self.assertEqual([{'field': 'that', + 'type': 'float', + 'value': '283.347', + 'op': op_str}], + ar) + + def test_typed_float_eq(self): + self._do_test_typed_float_op('<', 'lt') + + def test_typed_float_le(self): + self._do_test_typed_float_op('<=', 'le') + + def test_typed_string_whitespace(self): + ar = options.cli_to_array('state=string::insufficient data') + self.assertEqual([{'field': 'state', + 'op': 'eq', + 'type': 'string', + 'value': 'insufficient data'}], + ar) + + def test_typed_string_whitespace_complex(self): + ar = options.cli_to_array( + 'that>=float::99.9999;state=string::insufficient data' + ) + self.assertEqual([{'field': 'that', + 'op': 'ge', + 'type': 'float', + 'value': '99.9999'}, + {'field': 'state', + 'op': 'eq', + 'type': 'string', + 'value': 'insufficient data'}], + ar) def test_invalid_operator(self): self.assertRaises(ValueError, options.cli_to_array, @@ -97,6 +144,22 @@ class CliTest(utils.BaseTestCase): 'op': 'le', 'value': '34', 'type': ''}]) + def test_single_char_field_or_value(self): + ar = options.cli_to_array('m<=34;large.thing>s;x!=y') + self.assertEqual([{'field': 'm', + 'op': 'le', + 'value': '34', + 'type': ''}, + {'field': 'large.thing', + 'op': 'gt', + 'value': 's', + 'type': ''}, + {'field': 'x', + 'op': 'ne', + 'value': 'y', + 'type': ''}], + ar) + def test_without_data_type(self): ar = options.cli_to_array('hostname=localhost') self.assertEqual(ar, [{'field': 'hostname', @@ -152,3 +215,25 @@ class CliTest(utils.BaseTestCase): 'op': 'eq', 'type': '', 'value': 'datetime:sometimestamp'}]) + + def test_missing_key(self): + self.assertRaises(ValueError, options.cli_to_array, + 'average=float::1234.0;>=string::hello') + + def test_missing_value(self): + self.assertRaises(ValueError, options.cli_to_array, + 'average=float::1234.0;house>=') + + def test_timestamp_value(self): + ar = options.cli_to_array( + 'project=cow;timestamp>=datetime::2014-03-11T16:02:58' + ) + self.assertEqual([{'field': 'project', + 'op': 'eq', + 'type': '', + 'value': 'cow'}, + {'field': 'timestamp', + 'op': 'ge', + 'type': 'datetime', + 'value': '2014-03-11T16:02:58'}], + ar) diff --git a/ceilometerclient/tests/v2/test_query_alarm_history.py b/ceilometerclient/tests/v2/test_query_alarm_history.py index 2a6cfb4..60d437d 100644 --- a/ceilometerclient/tests/v2/test_query_alarm_history.py +++ b/ceilometerclient/tests/v2/test_query_alarm_history.py @@ -14,6 +14,8 @@ # License for the specific language governing permissions and limitations # under the License. +from ceilometerclient.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils from ceilometerclient.v2 import query @@ -49,13 +51,16 @@ class QueryAlarmsManagerTest(utils.BaseTestCase): def setUp(self): super(QueryAlarmsManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = query.QueryAlarmHistoryManager(self.api) def test_query(self): alarm_history = self.mgr.query(**QUERY) expect = [ - ('POST', '/v2/query/alarms/history', {}, QUERY), + + 'POST', '/v2/query/alarms/history', QUERY, + ] - self.assertEqual(expect, self.api.calls) + self.http_client.assert_called(*expect) self.assertEqual(1, len(alarm_history)) diff --git a/ceilometerclient/tests/v2/test_query_alarms.py b/ceilometerclient/tests/v2/test_query_alarms.py index 199caa7..7897ad1 100644 --- a/ceilometerclient/tests/v2/test_query_alarms.py +++ b/ceilometerclient/tests/v2/test_query_alarms.py @@ -1,7 +1,5 @@ # Copyright Ericsson AB 2014. All rights reserved # -# Author: Balazs Gibizer -# # 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 @@ -14,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +from ceilometerclient.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils from ceilometerclient.v2 import query @@ -60,13 +60,15 @@ class QueryAlarmsManagerTest(utils.BaseTestCase): def setUp(self): super(QueryAlarmsManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = query.QueryAlarmsManager(self.api) def test_query(self): alarms = self.mgr.query(**QUERY) expect = [ - ('POST', '/v2/query/alarms', {}, QUERY), + 'POST', '/v2/query/alarms', QUERY, ] - self.assertEqual(expect, self.api.calls) + + self.http_client.assert_called(*expect) self.assertEqual(1, len(alarms)) diff --git a/ceilometerclient/tests/v2/test_query_samples.py b/ceilometerclient/tests/v2/test_query_samples.py index b12a508..e747717 100644 --- a/ceilometerclient/tests/v2/test_query_samples.py +++ b/ceilometerclient/tests/v2/test_query_samples.py @@ -1,7 +1,5 @@ # Copyright Ericsson AB 2014. All rights reserved # -# Author: Balazs Gibizer -# # 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 @@ -14,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +from ceilometerclient.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils from ceilometerclient.v2 import query @@ -53,13 +53,15 @@ class QuerySamplesManagerTest(utils.BaseTestCase): def setUp(self): super(QuerySamplesManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = query.QuerySamplesManager(self.api) def test_query(self): samples = self.mgr.query(**QUERY) expect = [ - ('POST', '/v2/query/samples', {}, QUERY), + + 'POST', '/v2/query/samples', QUERY, ] - self.assertEqual(expect, self.api.calls) + self.http_client.assert_called(*expect) self.assertEqual(1, len(samples)) diff --git a/ceilometerclient/tests/v2/test_resources.py b/ceilometerclient/tests/v2/test_resources.py index 13a29fe..090ce8c 100644 --- a/ceilometerclient/tests/v2/test_resources.py +++ b/ceilometerclient/tests/v2/test_resources.py @@ -12,7 +12,8 @@ # 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.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v2.resources @@ -70,15 +71,16 @@ class ResourceManagerTest(utils.BaseTestCase): def setUp(self): super(ResourceManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = ceilometerclient.v2.resources.ResourceManager(self.api) def test_list_all(self): resources = list(self.mgr.list()) expect = [ - ('GET', '/v2/resources', {}, None), + 'GET', '/v2/resources' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 2) self.assertEqual(resources[0].resource_id, 'a') self.assertEqual(resources[1].resource_id, 'b') @@ -86,10 +88,10 @@ class ResourceManagerTest(utils.BaseTestCase): def test_list_one(self): resource = self.mgr.get(resource_id='a') expect = [ - ('GET', '/v2/resources/a', {}, None), + 'GET', '/v2/resources/a' ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(resource) + self.http_client.assert_called(*expect) + self.assertIsNotNone(resource) self.assertEqual(resource.resource_id, 'a') def test_list_by_query(self): @@ -97,10 +99,20 @@ class ResourceManagerTest(utils.BaseTestCase): "value": "a"}, ])) expect = [ - ('GET', '/v2/resources?q.field=resource_id&q.op=' - '&q.type=&q.value=a', - {}, None), + 'GET', '/v2/resources?q.field=resource_id&q.op=' + '&q.type=&q.value=a' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(resources), 1) self.assertEqual(resources[0].resource_id, 'a') + + def test_get_from_resource_class(self): + resource = self.mgr.get(resource_id='a') + self.assertIsNotNone(resource) + resource.get() + expect = [ + 'GET', '/v2/resources/a' + ] + self.http_client.assert_called(*expect, pos=0) + self.http_client.assert_called(*expect, pos=1) + self.assertEqual('a', resource.resource_id) diff --git a/ceilometerclient/tests/v2/test_samples.py b/ceilometerclient/tests/v2/test_samples.py index 7b0b878..aee2220 100644 --- a/ceilometerclient/tests/v2/test_samples.py +++ b/ceilometerclient/tests/v2/test_samples.py @@ -15,71 +15,115 @@ import copy +from ceilometerclient.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v2.samples -GET_SAMPLE = {u'counter_name': u'instance', - u'user_id': u'user-id', - u'resource_id': u'resource-id', - u'timestamp': u'2012-07-02T10:40:00', - u'source': u'test_source', - u'message_id': u'54558a1c-6ef3-11e2-9875-5453ed1bbb5f', - u'counter_unit': u'', - u'counter_volume': 1.0, - u'project_id': u'project1', - u'resource_metadata': {u'tag': u'self.counter', - u'display_name': u'test-server'}, - u'counter_type': u'cumulative'} -CREATE_SAMPLE = copy.deepcopy(GET_SAMPLE) +GET_OLD_SAMPLE = {u'counter_name': u'instance', + u'user_id': u'user-id', + u'resource_id': u'resource-id', + u'timestamp': u'2012-07-02T10:40:00', + u'source': u'test_source', + u'message_id': u'54558a1c-6ef3-11e2-9875-5453ed1bbb5f', + u'counter_unit': u'', + u'counter_volume': 1.0, + u'project_id': u'project1', + u'resource_metadata': {u'tag': u'self.counter', + u'display_name': u'test-server'}, + u'counter_type': u'cumulative'} +CREATE_SAMPLE = copy.deepcopy(GET_OLD_SAMPLE) del CREATE_SAMPLE['message_id'] del CREATE_SAMPLE['source'] -base_url = '/v2/meters/instance' -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 = { - base_url: - { +GET_SAMPLE = { + "user_id": None, + "resource_id": "9b651dfd-7d30-402b-972e-212b2c4bfb05", + "timestamp": "2014-11-03T13:37:46", + "meter": "image", + "volume": 1.0, + "source": "openstack", + "recorded_at": "2014-11-03T13:37:46.994458", + "project_id": "2cc3a7bb859b4bacbeab0aa9ca673033", + "type": "gauge", + "id": "98b5f258-635e-11e4-8bdd-0025647390c1", + "unit": "image", + "resource_metadata": {}, +} + +METER_URL = '/v2/meters/instance' +SAMPLE_URL = '/v2/samples' +QUERIES = ('q.field=resource_id&q.field=source&q.op=&q.op=' + '&q.type=&q.type=&q.value=foo&q.value=bar') +LIMIT = 'limit=1' + +OLD_SAMPLE_FIXTURES = { + METER_URL: { 'GET': ( {}, - [GET_SAMPLE] + [GET_OLD_SAMPLE] ), 'POST': ( {}, [CREATE_SAMPLE], ), }, - '%s?%s' % (base_url, args): - { + '%s?%s' % (METER_URL, QUERIES): { 'GET': ( {}, [], ), }, - '%s?%s' % (base_url, args_limit): - { + '%s?%s' % (METER_URL, LIMIT): { 'GET': ( {}, - [GET_SAMPLE] + [GET_OLD_SAMPLE] ), } } +SAMPLE_FIXTURES = { + SAMPLE_URL: { + 'GET': ( + (), + [GET_SAMPLE] + ), + }, + '%s?%s' % (SAMPLE_URL, QUERIES): { + 'GET': ( + {}, + [], + ), + }, + '%s?%s' % (SAMPLE_URL, LIMIT): { + 'GET': ( + {}, + [GET_SAMPLE], + ), + }, + '%s/%s' % (SAMPLE_URL, GET_SAMPLE['id']): { + 'GET': ( + {}, + GET_SAMPLE, + ), + }, +} -class SampleManagerTest(utils.BaseTestCase): +class OldSampleManagerTest(utils.BaseTestCase): def setUp(self): - super(SampleManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) - self.mgr = ceilometerclient.v2.samples.SampleManager(self.api) + super(OldSampleManagerTest, self).setUp() + self.http_client = fake_client.FakeHTTPClient( + fixtures=OLD_SAMPLE_FIXTURES) + self.api = client.BaseClient(self.http_client) + self.mgr = ceilometerclient.v2.samples.OldSampleManager(self.api) def test_list_by_meter_name(self): samples = list(self.mgr.list(meter_name='instance')) expect = [ - ('GET', '/v2/meters/instance', {}, None), + 'GET', '/v2/meters/instance' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(samples), 1) self.assertEqual(samples[0].resource_id, 'resource-id') @@ -91,20 +135,64 @@ class SampleManagerTest(utils.BaseTestCase): {"field": "source", "value": "bar"}, ])) - expect = [('GET', '%s?%s' % (base_url, args), {}, None)] - self.assertEqual(self.api.calls, expect) + expect = ['GET', '%s?%s' % (METER_URL, QUERIES)] + self.http_client.assert_called(*expect) self.assertEqual(len(samples), 0) def test_create(self): sample = self.mgr.create(**CREATE_SAMPLE) expect = [ - ('POST', '/v2/meters/instance', {}, [CREATE_SAMPLE]), + 'POST', '/v2/meters/instance' ] - self.assertEqual(self.api.calls, expect) - self.assertTrue(sample) + self.http_client.assert_called(*expect, body=[CREATE_SAMPLE]) + self.assertIsNotNone(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) + expect = ['GET', '/v2/meters/instance?limit=1'] + self.http_client.assert_called(*expect) self.assertEqual(len(samples), 1) + + +class SampleManagerTest(utils.BaseTestCase): + + def setUp(self): + super(SampleManagerTest, self).setUp() + self.http_client = fake_client.FakeHTTPClient( + fixtures=SAMPLE_FIXTURES) + self.api = client.BaseClient(self.http_client) + self.mgr = ceilometerclient.v2.samples.SampleManager(self.api) + + def test_sample_list(self): + samples = list(self.mgr.list()) + expect = [ + 'GET', '/v2/samples' + ] + self.http_client.assert_called(*expect) + self.assertEqual(1, len(samples)) + self.assertEqual('9b651dfd-7d30-402b-972e-212b2c4bfb05', + samples[0].resource_id) + + def test_sample_list_with_queries(self): + queries = [ + {"field": "resource_id", + "value": "foo"}, + {"field": "source", + "value": "bar"}, + ] + samples = list(self.mgr.list(q=queries)) + expect = ['GET', '%s?%s' % (SAMPLE_URL, QUERIES)] + self.http_client.assert_called(*expect) + self.assertEqual(0, len(samples)) + + def test_sample_list_with_limit(self): + samples = list(self.mgr.list(limit=1)) + expect = ['GET', '/v2/samples?limit=1'] + self.http_client.assert_called(*expect) + self.assertEqual(1, len(samples)) + + def test_sample_get(self): + sample = self.mgr.get(GET_SAMPLE['id']) + expect = ['GET', '/v2/samples/' + GET_SAMPLE['id']] + self.http_client.assert_called(*expect) + self.assertEqual(GET_SAMPLE, sample.to_dict()) diff --git a/ceilometerclient/tests/v2/test_shell.py b/ceilometerclient/tests/v2/test_shell.py index 80c57c8..4030439 100644 --- a/ceilometerclient/tests/v2/test_shell.py +++ b/ceilometerclient/tests/v2/test_shell.py @@ -15,16 +15,18 @@ # License for the specific language governing permissions and limitations # under the License. -import mock import re -import six import sys +import mock +import six from testtools import matchers +from ceilometerclient import exc from ceilometerclient import shell as base_shell from ceilometerclient.tests import utils from ceilometerclient.v2 import alarms +from ceilometerclient.v2 import events from ceilometerclient.v2 import samples from ceilometerclient.v2 import shell as ceilometer_shell from ceilometerclient.v2 import statistics @@ -107,32 +109,27 @@ class ShellAlarmHistoryCommandTest(utils.BaseTestCase): self.args = mock.Mock() self.args.alarm_id = self.ALARM_ID + @mock.patch('sys.stdout', new=six.StringIO()) def _do_test_alarm_history(self, raw_query=None, parsed_query=None): self.args.query = raw_query - orig = sys.stdout - sys.stdout = six.StringIO() history = [alarms.AlarmChange(mock.Mock(), change) for change in self.ALARM_HISTORY] self.cc.alarms.get_history.return_value = history - try: - ceilometer_shell.do_alarm_history(self.cc, self.args) - self.cc.alarms.get_history.assert_called_once_with( - q=parsed_query, - alarm_id=self.ALARM_ID - ) - out = sys.stdout.getvalue() - required = [ - '.*creation%sname: scombo.*' % self.TIMESTAMP_RE, - '.*rule change%sdescription: combination of one.*' % - self.TIMESTAMP_RE, - '.*state transition%sstate: alarm.*' % self.TIMESTAMP_RE, - ] - for r in required: - self.assertThat(out, matchers.MatchesRegex(r, re.DOTALL)) - finally: - sys.stdout.close() - sys.stdout = orig + ceilometer_shell.do_alarm_history(self.cc, self.args) + self.cc.alarms.get_history.assert_called_once_with( + q=parsed_query, + alarm_id=self.ALARM_ID + ) + out = sys.stdout.getvalue() + required = [ + '.*creation%sname: scombo.*' % self.TIMESTAMP_RE, + '.*rule change%sdescription: combination of one.*' % + self.TIMESTAMP_RE, + '.*state transition%sstate: alarm.*' % self.TIMESTAMP_RE, + ] + for r in required: + self.assertThat(out, matchers.MatchesRegex(r, re.DOTALL)) def test_alarm_all_history(self): self._do_test_alarm_history() @@ -176,6 +173,7 @@ class ShellAlarmCommandTest(utils.BaseTestCase): "timezone": ""}], "alarm_id": ALARM_ID, "state": "insufficient data", + "severity": "low", "insufficient_data_actions": [], "repeat_actions": True, "user_id": "528d9b68fa774689834b5c04b4564f8a", @@ -183,6 +181,20 @@ class ShellAlarmCommandTest(utils.BaseTestCase): "type": "threshold", "name": "cpu_high"} + THRESHOLD_ALARM_CLI_ARGS = [ + '--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' + ] + def setUp(self): super(ShellAlarmCommandTest, self).setUp() self.cc = mock.Mock() @@ -190,28 +202,23 @@ class ShellAlarmCommandTest(utils.BaseTestCase): self.args = mock.Mock() self.args.alarm_id = self.ALARM_ID + @mock.patch('sys.stdout', new=six.StringIO()) 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 + 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.assertNotIn('repeat_actions', kwargs) def test_alarm_update_repeat_actions_untouched(self): method = ceilometer_shell.do_alarm_update @@ -249,49 +256,46 @@ class ShellAlarmCommandTest(utils.BaseTestCase): method = ceilometer_shell.do_alarm_threshold_update self._do_test_alarm_update_repeat_actions(method, False) + @mock.patch('sys.stdout', new=six.StringIO()) def test_alarm_threshold_create_args(self): + argv = ['alarm-threshold-create'] + self.THRESHOLD_ALARM_CLI_ARGS + self._test_alarm_threshold_action_args('create', argv) + + def test_alarm_threshold_update_args(self): + argv = ['alarm-threshold-update', 'x'] + self.THRESHOLD_ALARM_CLI_ARGS + self._test_alarm_threshold_action_args('update', argv) + + @mock.patch('sys.stdout', new=six.StringIO()) + def _test_alarm_threshold_action_args(self, action, argv): 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 + getattr(self.cc.alarms, action).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 + func = getattr(ceilometer_shell, 'do_alarm_threshold_' + action) + func(self.cc, args) + _, kwargs = getattr(self.cc.alarms, action).call_args + self._check_alarm_threshold_args(kwargs) + def _check_alarm_threshold_args(self, kwargs): + 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.assertIn('threshold_rule', 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']) + + @mock.patch('sys.stdout', new=six.StringIO()) def test_alarm_create_time_constraints(self): shell = base_shell.CeilometerShell() argv = ['alarm-threshold-create', @@ -305,78 +309,96 @@ class ShellAlarmCommandTest(utils.BaseTestCase): ] _, 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 - time_constraints = [dict(name='cons1', start='0 11 * * *', - duration='300'), - dict(name='cons2', start='0 23 * * *', - duration='600')] - self.assertEqual(time_constraints, kwargs['time_constraints']) - finally: - sys.stdout.close() - sys.stdout = orig + ceilometer_shell.do_alarm_threshold_create(self.cc, args) + _, kwargs = self.cc.alarms.create.call_args + time_constraints = [dict(name='cons1', start='0 11 * * *', + duration='300'), + dict(name='cons2', start='0 23 * * *', + duration='600')] + self.assertEqual(time_constraints, kwargs['time_constraints']) 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"}] + SAMPLE_VALUES = ( + ("cpu_util", + "5dcf5537-3161-4e25-9235-407e1385bd35", + "2013-10-15T05:50:30", + "%", + 0.261666666667, + "gauge", + "86536501-b2c9-48f6-9c6a-7a5b14ba7482"), + ("cpu_util", + "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f", + "2013-10-15T05:50:29", + "%", + 0.261666666667, + "gauge", + "fe2a91ec-602b-4b55-8cba-5302ce3b916e",), + ("cpu_util", + "5dcf5537-3161-4e25-9235-407e1385bd35", + "2013-10-15T05:40:30", + "%", + 0.251247920133, + "gauge", + "52768bcb-b4e9-4db9-a30c-738c758b6f43"), + ("cpu_util", + "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f", + "2013-10-15T05:40:29", + "%", + 0.26, + "gauge", + "31ae614a-ac6b-4fb9-b106-4667bae03308"), + ) + + OLD_SAMPLES = [ + dict(counter_name=s[0], + resource_id=s[1], + timestamp=s[2], + counter_unit=s[3], + counter_volume=s[4], + counter_type=s[5]) + for s in SAMPLE_VALUES + ] + + SAMPLES = [ + dict(meter=s[0], + resource_id=s[1], + timestamp=s[2], + unit=s[3], + volume=s[4], + type=s[5], + id=s[6]) + for s in SAMPLE_VALUES + ] def setUp(self): super(ShellSampleListCommandTest, self).setUp() self.cc = mock.Mock() + self.cc.samples = mock.Mock() + self.cc.new_samples = 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] + @mock.patch('sys.stdout', new=six.StringIO()) + def test_old_sample_list(self): + self.args.meter = self.METER + sample_list = [samples.OldSample(mock.Mock(), sample) + for sample in self.OLD_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 + 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) - self.assertEqual(output.getvalue(), '''\ + self.assertEqual('''\ +--------------------------------------+----------+-------+----------------\ +------+---------------------+ | Resource ID | Name | Type | Volume \ @@ -393,7 +415,93 @@ class ShellSampleListCommandTest(utils.BaseTestCase): | % | 2013-10-15T05:40:29 | +--------------------------------------+----------+-------+----------------\ +------+---------------------+ -''') +''', sys.stdout.getvalue()) + + @mock.patch('sys.stdout', new=six.StringIO()) + def test_sample_list(self): + self.args.meter = None + sample_list = [samples.Sample(mock.Mock(), sample) + for sample in self.SAMPLES] + self.cc.new_samples.list.return_value = sample_list + + ceilometer_shell.do_sample_list(self.cc, self.args) + self.cc.new_samples.list.assert_called_once_with( + q=None, + limit=None) + + self.assertEqual('''\ ++--------------------------------------+--------------------------------------\ ++----------+-------+----------------+------+---------------------+ +| ID | Resource ID \ +| Name | Type | Volume | Unit | Timestamp | ++--------------------------------------+--------------------------------------\ ++----------+-------+----------------+------+---------------------+ +| 86536501-b2c9-48f6-9c6a-7a5b14ba7482 | 5dcf5537-3161-4e25-9235-407e1385bd35 \ +| cpu_util | gauge | 0.261666666667 | % | 2013-10-15T05:50:30 | +| fe2a91ec-602b-4b55-8cba-5302ce3b916e | 87d197e9-9cf6-4c25-bc66-1b1f4cedb52f \ +| cpu_util | gauge | 0.261666666667 | % | 2013-10-15T05:50:29 | +| 52768bcb-b4e9-4db9-a30c-738c758b6f43 | 5dcf5537-3161-4e25-9235-407e1385bd35 \ +| cpu_util | gauge | 0.251247920133 | % | 2013-10-15T05:40:30 | +| 31ae614a-ac6b-4fb9-b106-4667bae03308 | 87d197e9-9cf6-4c25-bc66-1b1f4cedb52f \ +| cpu_util | gauge | 0.26 | % | 2013-10-15T05:40:29 | ++--------------------------------------+--------------------------------------\ ++----------+-------+----------------+------+---------------------+ +''', sys.stdout.getvalue()) + + +class ShellSampleShowCommandTest(utils.BaseTestCase): + + SAMPLE = { + "user_id": None, + "resource_id": "9b651dfd-7d30-402b-972e-212b2c4bfb05", + "timestamp": "2014-11-03T13:37:46", + "meter": "image", + "volume": 1.0, + "source": "openstack", + "recorded_at": "2014-11-03T13:37:46.994458", + "project_id": "2cc3a7bb859b4bacbeab0aa9ca673033", + "type": "gauge", + "id": "98b5f258-635e-11e4-8bdd-0025647390c1", + "unit": "image", + "metadata": { + "name": "cirros-0.3.2-x86_64-uec", + } + } + + def setUp(self): + super(ShellSampleShowCommandTest, self).setUp() + self.cc = mock.Mock() + self.cc.new_samples = mock.Mock() + self.args = mock.Mock() + self.args.sample_id = "98b5f258-635e-11e4-8bdd-0025647390c1" + + @mock.patch('sys.stdout', new=six.StringIO()) + def test_sample_show(self): + sample = samples.Sample(mock.Mock(), self.SAMPLE) + self.cc.new_samples.get.return_value = sample + + ceilometer_shell.do_sample_show(self.cc, self.args) + self.cc.new_samples.get.assert_called_once_with( + "98b5f258-635e-11e4-8bdd-0025647390c1") + + self.assertEqual('''\ ++-------------+--------------------------------------+ +| Property | Value | ++-------------+--------------------------------------+ +| id | 98b5f258-635e-11e4-8bdd-0025647390c1 | +| metadata | {"name": "cirros-0.3.2-x86_64-uec"} | +| meter | image | +| project_id | 2cc3a7bb859b4bacbeab0aa9ca673033 | +| recorded_at | 2014-11-03T13:37:46.994458 | +| resource_id | 9b651dfd-7d30-402b-972e-212b2c4bfb05 | +| source | openstack | +| timestamp | 2014-11-03T13:37:46 | +| type | gauge | +| unit | image | +| user_id | None | +| volume | 1.0 | ++-------------+--------------------------------------+ +''', sys.stdout.getvalue()) class ShellSampleCreateCommandTest(utils.BaseTestCase): @@ -420,6 +528,7 @@ class ShellSampleCreateCommandTest(utils.BaseTestCase): def setUp(self): super(ShellSampleCreateCommandTest, self).setUp() self.cc = mock.Mock() + self.cc.samples = mock.Mock() self.args = mock.Mock() self.args.meter_name = self.METER self.args.meter_type = self.METER_TYPE @@ -427,19 +536,15 @@ class ShellSampleCreateCommandTest(utils.BaseTestCase): self.args.resource_id = self.RESOURCE_ID self.args.sample_volume = self.SAMPLE_VOLUME + @mock.patch('sys.stdout', new=six.StringIO()) def test_sample_create(self): - - ret_sample = [samples.Sample(mock.Mock(), sample) + ret_sample = [samples.OldSample(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(), '''\ + ceilometer_shell.do_sample_create(self.cc, self.args) + + self.assertEqual('''\ +-------------------+---------------------------------------------+ | Property | Value | +-------------------+---------------------------------------------+ @@ -455,7 +560,7 @@ class ShellSampleCreateCommandTest(utils.BaseTestCase): | user_id | 21b442b8101d407d8242b6610e0ed0eb | | volume | 1.0 | +-------------------+---------------------------------------------+ -''') +''', sys.stdout.getvalue()) class ShellQuerySamplesCommandTest(utils.BaseTestCase): @@ -487,17 +592,13 @@ class ShellQuerySamplesCommandTest(utils.BaseTestCase): self.args.orderby = self.QUERY["orderby"] self.args.limit = self.QUERY["limit"] + @mock.patch('sys.stdout', new=six.StringIO()) def test_query(self): - ret_sample = [samples.Sample(mock.Mock(), sample) for sample in self.SAMPLE] self.cc.query_samples.query.return_value = ret_sample - org_stdout = sys.stdout - try: - sys.stdout = output = six.StringIO() - ceilometer_shell.do_query_samples(self.cc, self.args) - finally: - sys.stdout = org_stdout + + ceilometer_shell.do_query_samples(self.cc, self.args) self.assertEqual('''\ +--------------------------------------+----------+-------+--------+---------\ @@ -510,7 +611,7 @@ class ShellQuerySamplesCommandTest(utils.BaseTestCase): | 2014-02-19T05:50:16.673604 | +--------------------------------------+----------+-------+--------+---------\ -+----------------------------+ -''', output.getvalue()) +''', sys.stdout.getvalue()) class ShellQueryAlarmsCommandTest(utils.BaseTestCase): @@ -530,9 +631,12 @@ class ShellQueryAlarmsCommandTest(utils.BaseTestCase): "project_id": "c96c887c216949acbdfbd8b494863567", "repeat_actions": False, "state": "ok", + "severity": "critical", "state_timestamp": "2014-02-20T10:37:15.589860", "threshold_rule": None, "timestamp": "2014-02-20T10:37:15.589856", + "time_constraints": [{"name": "test", "start": "0 23 * * *", + "duration": 10800}], "type": "combination", "user_id": "c96c887c216949acbdfbd8b494863567"}] @@ -549,35 +653,69 @@ class ShellQueryAlarmsCommandTest(utils.BaseTestCase): self.args.orderby = self.QUERY["orderby"] self.args.limit = self.QUERY["limit"] + @mock.patch('sys.stdout', new=six.StringIO()) def test_query(self): - ret_alarm = [alarms.Alarm(mock.Mock(), alarm) for alarm in self.ALARM] self.cc.query_alarms.query.return_value = ret_alarm - org_stdout = sys.stdout - try: - sys.stdout = output = six.StringIO() - ceilometer_shell.do_query_alarms(self.cc, self.args) - finally: - sys.stdout = org_stdout + + ceilometer_shell.do_query_alarms(self.cc, self.args) self.assertEqual('''\ -+--------------------------------------+------------------+-------+---------\ -+------------+--------------------------------------------------------------\ -----------------------------------------+ -| Alarm ID | Name | State | Enabled \ -| Continuous | Alarm condition \ - | -+--------------------------------------+------------------+-------+---------\ -+------------+--------------------------------------------------------------\ -----------------------------------------+ -| 768ff714-8cfb-4db9-9753-d484cb33a1cc | SwiftObjectAlarm | ok | True \ -| False | combinated states (OR) of 739e99cb-c2ec-4718-b900-332502355f3\ -8, 153462d0-a9b8-4b5b-8175-9e4b05e9b856 | -+--------------------------------------+------------------+-------+---------\ -+------------+--------------------------------------------------------------\ -----------------------------------------+ -''', output.getvalue()) ++--------------------------------------+------------------+-------+----------+\ +---------+------------+-------------------------------------------------------\ +-----------------------------------------------+-------------------------------\ +-+ +| Alarm ID | Name | State | Severity \ +| Enabled | Continuous | Alarm condition \ + | Time constraints \ + | ++--------------------------------------+------------------+-------+----------+\ +---------+------------+-------------------------------------------------------\ +-----------------------------------------------+--------------------------------+ +| 768ff714-8cfb-4db9-9753-d484cb33a1cc | SwiftObjectAlarm | ok | critical \ +| True | False | combinated states (OR) of \ +739e99cb-c2ec-4718-b900-332502355f38, 153462d0-a9b8-4b5b-8175-9e4b05e9b856 |\ + test at 0 23 * * * for 10800s | ++--------------------------------------+------------------+-------+----------+\ +---------+------------+-------------------------------------------------------\ +-----------------------------------------------+------------------------------\ +--+ +''', sys.stdout.getvalue()) + + @mock.patch('sys.stdout', new=six.StringIO()) + def test_time_constraints_compatibility(self): + # client should be backwards compatible + alarm_without_tc = dict(self.ALARM[0]) + del alarm_without_tc['time_constraints'] + + # NOTE(nsaje): Since we're accessing a nonexisting key in the resource, + # the resource is looking it up with the manager (which is a mock). + manager_mock = mock.Mock() + del manager_mock.get + ret_alarm = [alarms.Alarm(manager_mock, alarm_without_tc)] + self.cc.query_alarms.query.return_value = ret_alarm + + ceilometer_shell.do_query_alarms(self.cc, self.args) + + self.assertEqual('''\ ++--------------------------------------+------------------+-------+----------+\ +---------+------------+-------------------------------------------------------\ +-----------------------------------------------+------------------+ +| Alarm ID | Name | State | Severity \ +| Enabled | Continuous | Alarm condition \ + | Time constraints | ++--------------------------------------+------------------+-------+----------+\ +---------+------------+-------------------------------------------------------\ +-----------------------------------------------+------------------+ +| 768ff714-8cfb-4db9-9753-d484cb33a1cc | SwiftObjectAlarm | ok | critical \ +| True | False | combinated states (OR) of \ +739e99cb-c2ec-4718-b900-332502355f38, 153462d0-a9b8-4b5b-8175-9e4b05e9b856 \ +| None | ++--------------------------------------+------------------+-------+----------+\ +---------+------------+-------------------------------------------------------\ +-----------------------------------------------+------------------+ +''', sys.stdout.getvalue()) class ShellQueryAlarmHistoryCommandTest(utils.BaseTestCase): @@ -606,17 +744,13 @@ class ShellQueryAlarmHistoryCommandTest(utils.BaseTestCase): self.args.orderby = self.QUERY["orderby"] self.args.limit = self.QUERY["limit"] + @mock.patch('sys.stdout', new=six.StringIO()) def test_query(self): - ret_alarm_history = [alarms.AlarmChange(mock.Mock(), alarm_history) for alarm_history in self.ALARM_HISTORY] self.cc.query_alarm_history.query.return_value = ret_alarm_history - org_stdout = sys.stdout - try: - sys.stdout = output = six.StringIO() - ceilometer_shell.do_query_alarm_history(self.cc, self.args) - finally: - sys.stdout = org_stdout + + ceilometer_shell.do_query_alarm_history(self.cc, self.args) self.assertEqual('''\ +----------------------------------+--------------------------------------+-\ @@ -634,7 +768,7 @@ rule change | {"threshold": 42.0, "evaluation_periods": 4} | 2014-03-11T16:0\ +----------------------------------+--------------------------------------+-\ ------------+----------------------------------------------+----------------\ ------------+ -''', output.getvalue()) +''', sys.stdout.getvalue()) class ShellStatisticsTest(utils.BaseTestCase): @@ -788,3 +922,200 @@ class ShellStatisticsTest(utils.BaseTestCase): fields, [self.displays.get(f, f) for f in fields], ) + + +class ShellEmptyIdTest(utils.BaseTestCase): + """Test empty field which will cause calling incorrect rest uri.""" + + def _test_entity_action_with_empty_values(self, entity, + *args, **kwargs): + positional = kwargs.pop('positional', False) + for value in ('', ' ', ' ', '\t'): + self._test_entity_action_with_empty_value(entity, value, + positional, *args) + + def _test_entity_action_with_empty_value(self, entity, value, + positional, *args): + new_args = [value] if positional else ['--%s' % entity, value] + argv = list(args) + new_args + shell = base_shell.CeilometerShell() + with mock.patch('ceilometerclient.exc.CommandError') as e: + e.return_value = exc.BaseException() + self.assertRaises(exc.BaseException, shell.parse_args, argv) + entity = entity.replace('-', '_') + e.assert_called_with('%s should not be empty' % entity) + + def _test_alarm_action_with_empty_ids(self, method, *args): + args = [method] + list(args) + self._test_entity_action_with_empty_values('alarm_id', + positional=True, *args) + + def test_alarm_show_with_empty_id(self): + self._test_alarm_action_with_empty_ids('alarm-show') + + def test_alarm_update_with_empty_id(self): + self._test_alarm_action_with_empty_ids('alarm-update') + + def test_alarm_threshold_update_with_empty_id(self): + self._test_alarm_action_with_empty_ids('alarm-threshold-update') + + def test_alarm_combination_update_with_empty_id(self): + self._test_alarm_action_with_empty_ids('alarm-combination-update') + + def test_alarm_delete_with_empty_id(self): + self._test_alarm_action_with_empty_ids('alarm-delete') + + def test_alarm_state_get_with_empty_id(self): + self._test_alarm_action_with_empty_ids('alarm-state-get') + + def test_alarm_state_set_with_empty_id(self): + args = ['alarm-state-set', '--state', 'ok'] + self._test_alarm_action_with_empty_ids(*args) + + def test_alarm_history_with_empty_id(self): + self._test_alarm_action_with_empty_ids('alarm-history') + + def test_event_show_with_empty_message_id(self): + args = ['event-show'] + self._test_entity_action_with_empty_values('message_id', *args) + + def test_resource_show_with_empty_id(self): + args = ['resource-show'] + self._test_entity_action_with_empty_values('resource_id', *args) + + def test_sample_list_with_empty_meter(self): + args = ['sample-list'] + self._test_entity_action_with_empty_values('meter', *args) + + def test_sample_create_with_empty_meter(self): + args = ['sample-create', '-r', 'x', '--meter-type', 'gauge', + '--meter-unit', 'B', '--sample-volume', '1'] + self._test_entity_action_with_empty_values('meter-name', *args) + + def test_statistics_with_empty_meter(self): + args = ['statistics'] + self._test_entity_action_with_empty_values('meter', *args) + + def test_trait_description_list_with_empty_event_type(self): + args = ['trait-description-list'] + self._test_entity_action_with_empty_values('event_type', *args) + + def test_trait_list_with_empty_event_type(self): + args = ['trait-list', '--trait_name', 'x'] + self._test_entity_action_with_empty_values('event_type', *args) + + def test_trait_list_with_empty_trait_name(self): + args = ['trait-list', '--event_type', 'x'] + self._test_entity_action_with_empty_values('trait_name', *args) + + +class ShellObsoletedArgsTest(utils.BaseTestCase): + """Test arguments that have been obsoleted.""" + + def _test_entity_obsoleted(self, entity, value, positional, *args): + new_args = [value] if positional else ['--%s' % entity, value] + argv = list(args) + new_args + shell = base_shell.CeilometerShell() + with mock.patch('sys.stdout', new_callable=six.StringIO) as stdout: + shell.parse_args(argv) + self.assertIn('obsolete', stdout.getvalue()) + + def test_obsolete_alarm_id(self): + for method in ['alarm-show', 'alarm-update', 'alarm-threshold-update', + 'alarm-combination-update', 'alarm-delete', + 'alarm-state-get', 'alarm-history']: + self._test_entity_obsoleted('alarm_id', 'abcde', False, method) + + +class ShellEventListCommandTest(utils.BaseTestCase): + + EVENTS = [ + { + "traits": [], + "generated": "2015-01-12T04:03:25.741471", + "message_id": "fb2bef58-88af-4380-8698-e0f18fcf452d", + "event_type": "compute.instance.create.start", + "traits": [{ + "name": "state", + "type": "string", + "value": "building", + }], + }, + { + "traits": [], + "generated": "2015-01-12T04:03:28.452495", + "message_id": "9b20509a-576b-4995-acfa-1a24ee5cf49f", + "event_type": "compute.instance.create.end", + "traits": [{ + "name": "state", + "type": "string", + "value": "active", + }], + }, + ] + + def setUp(self): + super(ShellEventListCommandTest, self).setUp() + self.cc = mock.Mock() + self.args = mock.Mock() + self.args.query = None + self.args.no_traits = None + + @mock.patch('sys.stdout', new=six.StringIO()) + def test_event_list(self): + ret_events = [events.Event(mock.Mock(), event) + for event in self.EVENTS] + self.cc.events.list.return_value = ret_events + ceilometer_shell.do_event_list(self.cc, self.args) + self.assertEqual('''\ ++--------------------------------------+-------------------------------+\ +----------------------------+-------------------------------+ +| Message ID | Event Type |\ + Generated | Traits | ++--------------------------------------+-------------------------------+\ +----------------------------+-------------------------------+ +| fb2bef58-88af-4380-8698-e0f18fcf452d | compute.instance.create.start |\ + 2015-01-12T04:03:25.741471 | +-------+--------+----------+ | +| | |\ + | | name | type | value | | +| | |\ + | +-------+--------+----------+ | +| | |\ + | | state | string | building | | +| | |\ + | +-------+--------+----------+ | +| 9b20509a-576b-4995-acfa-1a24ee5cf49f | compute.instance.create.end |\ + 2015-01-12T04:03:28.452495 | +-------+--------+--------+ | +| | |\ + | | name | type | value | | +| | |\ + | +-------+--------+--------+ | +| | |\ + | | state | string | active | | +| | |\ + | +-------+--------+--------+ | ++--------------------------------------+-------------------------------+\ +----------------------------+-------------------------------+ +''', sys.stdout.getvalue()) + + @mock.patch('sys.stdout', new=six.StringIO()) + def test_event_list_no_traits(self): + self.args.no_traits = True + ret_events = [events.Event(mock.Mock(), event) + for event in self.EVENTS] + self.cc.events.list.return_value = ret_events + ceilometer_shell.do_event_list(self.cc, self.args) + self.assertEqual('''\ ++--------------------------------------+-------------------------------\ ++----------------------------+ +| Message ID | Event Type \ +| Generated | ++--------------------------------------+-------------------------------\ ++----------------------------+ +| fb2bef58-88af-4380-8698-e0f18fcf452d | compute.instance.create.start \ +| 2015-01-12T04:03:25.741471 | +| 9b20509a-576b-4995-acfa-1a24ee5cf49f | compute.instance.create.end \ +| 2015-01-12T04:03:28.452495 | ++--------------------------------------+-------------------------------\ ++----------------------------+ +''', sys.stdout.getvalue()) diff --git a/ceilometerclient/tests/v2/test_statistics.py b/ceilometerclient/tests/v2/test_statistics.py index dde4dd6..65f633e 100644 --- a/ceilometerclient/tests/v2/test_statistics.py +++ b/ceilometerclient/tests/v2/test_statistics.py @@ -12,7 +12,8 @@ # 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.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v2.statistics @@ -114,15 +115,16 @@ class StatisticsManagerTest(utils.BaseTestCase): def setUp(self): super(StatisticsManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) self.mgr = ceilometerclient.v2.statistics.StatisticsManager(self.api) def test_list_by_meter_name(self): stats = list(self.mgr.list(meter_name='instance')) expect = [ - ('GET', '/v2/meters/instance/statistics', {}, None), + 'GET', '/v2/meters/instance/statistics' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(stats), 1) self.assertEqual(stats[0].count, 135) @@ -135,10 +137,9 @@ class StatisticsManagerTest(utils.BaseTestCase): "value": "bar"}, ])) expect = [ - ('GET', - '%s?%s' % (base_url, qry), {}, None), + 'GET', '%s?%s' % (base_url, qry) ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(stats), 1) self.assertEqual(stats[0].count, 135) @@ -152,10 +153,9 @@ class StatisticsManagerTest(utils.BaseTestCase): ], period=60)) expect = [ - ('GET', - '%s?%s%s' % (base_url, qry, period), {}, None), + 'GET', '%s?%s%s' % (base_url, qry, period) ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(stats), 1) self.assertEqual(stats[0].count, 135) @@ -169,16 +169,36 @@ class StatisticsManagerTest(utils.BaseTestCase): ], groupby=['resource_id'])) expect = [ - ('GET', - '%s?%s%s' % (base_url, qry, groupby), {}, None), + 'GET', + '%s?%s%s' % (base_url, qry, groupby) ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*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') + def test_list_by_meter_name_with_groupby_as_str(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) + ] + self.http_client.assert_called(*expect) + self.assertEqual(2, len(stats)) + self.assertEqual(135, stats[0].count) + self.assertEqual(12, stats[1].count) + self.assertEqual('foo', stats[0].groupby.get('resource_id')) + self.assertEqual('bar', stats[1].groupby.get('resource_id')) + def test_list_by_meter_name_with_aggregates(self): aggregates = [ { @@ -192,10 +212,10 @@ class StatisticsManagerTest(utils.BaseTestCase): stats = list(self.mgr.list(meter_name='instance', aggregates=aggregates)) expect = [ - ('GET', - '%s?%s' % (base_url, aggregate_query), {}, None), + 'GET', + '%s?%s' % (base_url, aggregate_query) ] - self.assertEqual(expect, self.api.calls) + self.http_client.assert_called(*expect) self.assertEqual(1, len(stats)) self.assertEqual(2, stats[0].count) self.assertEqual(2.0, stats[0].aggregate.get('count')) diff --git a/ceilometerclient/tests/v2/test_trait_descriptions.py b/ceilometerclient/tests/v2/test_trait_descriptions.py index 4c84f3c..7913124 100644 --- a/ceilometerclient/tests/v2/test_trait_descriptions.py +++ b/ceilometerclient/tests/v2/test_trait_descriptions.py @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -12,7 +11,8 @@ # 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.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v2.trait_descriptions @@ -35,16 +35,17 @@ class TraitDescriptionManagerTest(utils.BaseTestCase): def setUp(self): super(TraitDescriptionManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) 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), + 'GET', '/v2/event_types/Foo/traits' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(trait_descriptions), 3) for i, vals in enumerate([('trait_1', 'string'), ('trait_2', 'integer'), diff --git a/ceilometerclient/tests/v2/test_traits.py b/ceilometerclient/tests/v2/test_traits.py index 697acf1..1d7dde0 100644 --- a/ceilometerclient/tests/v2/test_traits.py +++ b/ceilometerclient/tests/v2/test_traits.py @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -12,7 +11,8 @@ # 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.openstack.common.apiclient import client +from ceilometerclient.openstack.common.apiclient import fake_client from ceilometerclient.tests import utils import ceilometerclient.v2.traits @@ -38,15 +38,16 @@ class TraitManagerTest(utils.BaseTestCase): def setUp(self): super(TraitManagerTest, self).setUp() - self.api = utils.FakeAPI(fixtures) + self.http_client = fake_client.FakeHTTPClient(fixtures=fixtures) + self.api = client.BaseClient(self.http_client) 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), + 'GET', '/v2/event_types/Foo/traits/trait_1' ] - self.assertEqual(self.api.calls, expect) + self.http_client.assert_called(*expect) self.assertEqual(len(traits), 2) for i, vals in enumerate([('trait_1', 'datetime', diff --git a/ceilometerclient/v1/client.py b/ceilometerclient/v1/client.py index 89a5b44..4f531bf 100644 --- a/ceilometerclient/v1/client.py +++ b/ceilometerclient/v1/client.py @@ -13,7 +13,9 @@ # License for the specific language governing permissions and limitations # under the License. -from ceilometerclient.common import http + +from ceilometerclient import client as ceiloclient +from ceilometerclient.openstack.common.apiclient import client from ceilometerclient.v1 import meters @@ -29,7 +31,24 @@ class Client(object): def __init__(self, *args, **kwargs): """Initialize a new client for the Ceilometer v1 API.""" - self.http_client = http.HTTPClient(*args, **kwargs) + self.auth_plugin = kwargs.get('auth_plugin') \ + or ceiloclient.get_auth_plugin(*args, **kwargs) + self.client = client.HTTPClient( + auth_plugin=self.auth_plugin, + region_name=kwargs.get('region_name'), + endpoint_type=kwargs.get('endpoint_type'), + original_ip=kwargs.get('original_ip'), + verify=kwargs.get('verify'), + cert=kwargs.get('cert'), + timeout=kwargs.get('timeout'), + timings=kwargs.get('timings'), + keyring_saver=kwargs.get('keyring_saver'), + debug=kwargs.get('debug'), + user_agent=kwargs.get('user_agent'), + http=kwargs.get('http') + ) + + self.http_client = client.BaseClient(self.client) self.meters = meters.MeterManager(self.http_client) self.samples = meters.SampleManager(self.http_client) self.users = meters.UserManager(self.http_client) diff --git a/ceilometerclient/v1/meters.py b/ceilometerclient/v1/meters.py index 1d64fd3..c8a784b 100644 --- a/ceilometerclient/v1/meters.py +++ b/ceilometerclient/v1/meters.py @@ -20,12 +20,12 @@ from ceilometerclient.common import base def _get_opt_path(simple_params=[], **kwargs): l = [] - #get simple paramters + # get simple paramters for key in simple_params: val = kwargs.get(key) if val: l.append(key + '=' + val) - #get metadata query paramters + # get metadata query paramters metaquery = kwargs.get('metaquery') if metaquery: l.extend(metaquery.split(':')) @@ -51,7 +51,7 @@ class UserManager(base.Manager): def list(self, **kwargs): s = kwargs.get('source') if s: - path = '/sources/%s/users' % (s) + path = '/sources/%s/users' % s else: path = '/users' return self._list('/v1%s' % path, 'users') @@ -100,17 +100,17 @@ class ResourceManager(base.Manager): opts_path = _get_opt_path(['start_timestamp', 'end_timestamp'], **kwargs) if u: - path = '/users/%s/resources' % (u) + path = '/users/%s/resources' % u elif s: - path = '/sources/%s/resources' % (s) + path = '/sources/%s/resources' % s elif p: - path = '/projects/%s/resources' % (p) + path = '/projects/%s/resources' % p else: path = '/resources' if opts_path: path = '/v1%s?%s' % (path, opts_path) else: - path = '/v1%s' % (path) + path = '/v1%s' % path return self._list(path, 'resources') @@ -152,7 +152,7 @@ class SampleManager(base.Manager): if opts_path: path = '/v1%s?%s' % (path, opts_path) else: - path = '/v1%s' % (path) + path = '/v1%s' % path return self._list(path, 'events') @@ -186,5 +186,5 @@ class MeterManager(base.Manager): if opts_path: path = '/v1%s?%s' % (path, opts_path) else: - path = '/v1%s' % (path) + path = '/v1%s' % path return self._list(path, 'meters') diff --git a/ceilometerclient/v1/shell.py b/ceilometerclient/v1/shell.py index 1edfc2d..395f2bc 100644 --- a/ceilometerclient/v1/shell.py +++ b/ceilometerclient/v1/shell.py @@ -68,7 +68,7 @@ def do_sample_list(cc, args): @utils.arg('-p', '--project_id', metavar='', help='ID of the project to show samples for.') def do_meter_list(cc, args={}): - '''List the user's meter''' + '''List the user's meter.''' fields = {'resource_id': args.resource_id, 'user_id': args.user_id, 'project_id': args.project_id, @@ -108,7 +108,7 @@ def do_user_list(cc, args={}): help='ISO date in UTC which limits resouces by ' 'last update time <= this value') def do_resource_list(cc, args={}): - '''List the resources.''' + """List the resources.""" kwargs = {'source': args.source, 'user_id': args.user_id, 'project_id': args.project_id, @@ -126,7 +126,7 @@ def do_resource_list(cc, args={}): @utils.arg('-s', '--source', metavar='', help='ID of the resource to show projects for.') def do_project_list(cc, args={}): - '''List the projects.''' + """List the projects.""" kwargs = {'source': args.source} projects = cc.projects.list(**kwargs) diff --git a/ceilometerclient/v2/alarms.py b/ceilometerclient/v2/alarms.py index 837904c..18cdbb0 100644 --- a/ceilometerclient/v2/alarms.py +++ b/ceilometerclient/v2/alarms.py @@ -1,8 +1,5 @@ -# -*- encoding: utf-8 -*- # -# Copyright © 2013 Red Hat, Inc -# -# Author: Eoghan Glynn +# Copyright 2013 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 @@ -20,6 +17,7 @@ import warnings from ceilometerclient.common import base from ceilometerclient.common import utils +from ceilometerclient import exc from ceilometerclient.v2 import options @@ -28,13 +26,12 @@ UPDATABLE_ATTRIBUTES = [ 'description', 'type', 'state', + 'severity', 'enabled', 'alarm_actions', 'ok_actions', 'insufficient_data_actions', 'repeat_actions', - 'threshold_rule', - 'combination_rule', ] CREATION_ATTRIBUTES = UPDATABLE_ATTRIBUTES + ['project_id', 'user_id', 'time_constraints'] @@ -49,8 +46,17 @@ class Alarm(base.Resource): # that look like the Alarm storage object if k == 'rule': k = '%s_rule' % self.type + if k == 'id': + return self.alarm_id return super(Alarm, self).__getattr__(k) + def delete(self): + return self.manager.delete(self.alarm_id) + + def get_state(self): + state = self.manager.get_state(self.alarm_id) + return state.get('alarm') + class AlarmChange(base.Resource): def __repr__(self): @@ -76,6 +82,14 @@ class AlarmManager(base.Manager): except IndexError: return None + except exc.HTTPNotFound: + # When we try to get deleted alarm HTTPNotFound occurs + # or when alarm doesn't exists this exception don't must + # go deeper because cleanUp() (method which remove all + # created things like instance, alarm, etc.) at scenario + # tests doesn't know how to process it + return None + @classmethod def _compat_legacy_alarm_kwargs(cls, kwargs, create=False): cls._compat_counter_rename_kwargs(kwargs, create) @@ -135,16 +149,21 @@ class AlarmManager(base.Manager): def create(self, **kwargs): self._compat_legacy_alarm_kwargs(kwargs, create=True) new = dict((key, value) for (key, value) in kwargs.items() - if key in CREATION_ATTRIBUTES) + if (key in CREATION_ATTRIBUTES + or key.endswith('_rule'))) return self._create(self._path(), new) def update(self, alarm_id, **kwargs): self._compat_legacy_alarm_kwargs(kwargs) - updated = self.get(alarm_id).to_dict() + alarm = self.get(alarm_id) + if alarm is None: + raise exc.CommandError('Alarm not found: %s' % alarm_id) + updated = alarm.to_dict() updated['time_constraints'] = self._merge_time_constraints( updated.get('time_constraints', []), kwargs) kwargs = dict((k, v) for k, v in kwargs.items() - if k in updated and k in UPDATABLE_ATTRIBUTES) + if k in updated and (k in UPDATABLE_ATTRIBUTES + or k.endswith('_rule'))) utils.merge_nested_dict(updated, kwargs, depth=1) return self._update(self._path(alarm_id), updated) @@ -152,14 +171,12 @@ class AlarmManager(base.Manager): return self._delete(self._path(alarm_id)) def set_state(self, alarm_id, state): - resp, body = self.api.json_request('PUT', - "%s/state" % self._path(alarm_id), - body=state) + body = self.api.put("%s/state" % self._path(alarm_id), + json=state).json() return body def get_state(self, alarm_id): - resp, body = self.api.json_request('GET', - "%s/state" % self._path(alarm_id)) + body = self.api.get("%s/state" % self._path(alarm_id)).json() return body def get_history(self, alarm_id, q=None): diff --git a/ceilometerclient/v2/client.py b/ceilometerclient/v2/client.py index b9ead19..a4a3619 100644 --- a/ceilometerclient/v2/client.py +++ b/ceilometerclient/v2/client.py @@ -15,7 +15,8 @@ # License for the specific language governing permissions and limitations # under the License. -from ceilometerclient.common import http +from ceilometerclient import client as ceiloclient +from ceilometerclient.openstack.common.apiclient import client from ceilometerclient.v2 import alarms from ceilometerclient.v2 import event_types from ceilometerclient.v2 import events @@ -31,26 +32,48 @@ from ceilometerclient.v2 import traits class Client(object): """Client for the Ceilometer v2 API. - :param string endpoint: A user-supplied endpoint URL for the ceilometer + :param endpoint: A user-supplied endpoint URL for the ceilometer service. - :param function token: Provides token for authentication. - :param integer timeout: Allows customization of the timeout for client - http requests. (optional) + :type endpoint: string + :param token: Provides token for authentication. + :type token: function + :param timeout: Allows customization of the timeout for client + http requests. (optional) + :type timeout: integer """ def __init__(self, *args, **kwargs): """Initialize a new client for the Ceilometer v2 API.""" - self.http_client = http.HTTPClient(*args, **kwargs) + self.auth_plugin = kwargs.get('auth_plugin') \ + or ceiloclient.get_auth_plugin(*args, **kwargs) + self.client = client.HTTPClient( + auth_plugin=self.auth_plugin, + region_name=kwargs.get('region_name'), + endpoint_type=kwargs.get('endpoint_type'), + original_ip=kwargs.get('original_ip'), + verify=kwargs.get('verify'), + cert=kwargs.get('cert'), + timeout=kwargs.get('timeout'), + timings=kwargs.get('timings'), + keyring_saver=kwargs.get('keyring_saver'), + debug=kwargs.get('debug'), + user_agent=kwargs.get('user_agent'), + http=kwargs.get('http') + ) + + self.http_client = client.BaseClient(self.client) self.meters = meters.MeterManager(self.http_client) - self.samples = samples.SampleManager(self.http_client) + self.samples = samples.OldSampleManager(self.http_client) + self.new_samples = samples.SampleManager(self.http_client) self.statistics = statistics.StatisticsManager(self.http_client) self.resources = resources.ResourceManager(self.http_client) 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.\ + self.trait_descriptions = trait_descriptions.\ TraitDescriptionManager(self.http_client) + self.query_samples = query.QuerySamplesManager( self.http_client) self.query_alarms = query.QueryAlarmsManager( diff --git a/ceilometerclient/v2/event_types.py b/ceilometerclient/v2/event_types.py index 036eb3e..631f0b4 100644 --- a/ceilometerclient/v2/event_types.py +++ b/ceilometerclient/v2/event_types.py @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may diff --git a/ceilometerclient/v2/events.py b/ceilometerclient/v2/events.py index 0e245bb..32380d6 100644 --- a/ceilometerclient/v2/events.py +++ b/ceilometerclient/v2/events.py @@ -1,4 +1,3 @@ -# -*- encoding: utf-8 -*- # Copyright 2014 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); you may @@ -21,6 +20,11 @@ class Event(base.Resource): def __repr__(self): return "" % self._info + def __getattr__(self, k): + if k == 'id': + return self.message_id + return super(Event, self).__getattr__(k) + class EventManager(base.Manager): resource_class = Event diff --git a/ceilometerclient/v2/meters.py b/ceilometerclient/v2/meters.py index baa7cbd..938854d 100644 --- a/ceilometerclient/v2/meters.py +++ b/ceilometerclient/v2/meters.py @@ -1,6 +1,5 @@ -# -*- encoding: utf-8 -*- # -# Copyright © 2013 Red Hat, Inc +# Copyright 2013 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 diff --git a/ceilometerclient/v2/options.py b/ceilometerclient/v2/options.py index 6649971..bf80ce5 100644 --- a/ceilometerclient/v2/options.py +++ b/ceilometerclient/v2/options.py @@ -13,19 +13,35 @@ import re -from six.moves.urllib import parse +from six.moves import urllib + +OP_LOOKUP = {'!=': 'ne', + '>=': 'ge', + '<=': 'le', + '>': 'gt', + '<': 'lt', + '=': 'eq'} + +OP_LOOKUP_KEYS = '|'.join(sorted(OP_LOOKUP.keys(), key=len, reverse=True)) +OP_SPLIT_RE = re.compile(r'(%s)' % OP_LOOKUP_KEYS) + +DATA_TYPE_RE = re.compile(r'^(string|integer|float|datetime|boolean)(::)(.+)$') def build_url(path, q, params=None): - '''This converts from a list of dicts and a list of params to - what the rest api needs, so from: - "[{field=this,op=le,value=34},{field=that,op=eq,value=foo}], - ['foo=bar','sna=fu']" + """Convert list of dicts and a list of params to query url format. + + This will convert the following: + "[{field=this,op=le,value=34}, + {field=that,op=eq,value=foo,type=string}], + ['foo=bar','sna=fu']" to: - "?q.field=this&q.op=le&q.value=34& - q.field=that&q.op=eq&q.value=foo& - foo=bar&sna=fu" - ''' + "?q.field=this&q.field=that& + q.op=le&q.op=eq& + q.type=&q.type=string& + q.value=34&q.value=foo& + foo=bar&sna=fu" + """ if q: query_params = {'q.field': [], 'q.value': [], @@ -39,7 +55,7 @@ def build_url(path, q, params=None): # 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 += "?" + parse.urlencode(new_qparams, doseq=True) + path += "?" + urllib.parse.urlencode(new_qparams, doseq=True) if params: for p in params: @@ -52,56 +68,53 @@ def build_url(path, q, params=None): def cli_to_array(cli_query): - """This converts from the cli list of queries to what is required - by the python api. - so from: - "this<=34;that=foo" + """Convert CLI list of queries to the Python API format. + + This will convert the following: + "this<=34;that=string::foo" to - "[{field=this,op=le,value=34},{field=that,op=eq,value=foo}]" + "[{field=this,op=le,value=34,type=''}, + {field=that,op=eq,value=foo,type=string}]" """ if cli_query is None: return None - op_lookup = {'!=': 'ne', - '>=': 'ge', - '<=': 'le', - '>': 'gt', - '<': 'lt', - '=': 'eq'} + def split_by_op(query): + """Split a single query string to field, operator, value.""" - def split_by_op(string): - # two character split (<=,!=) - frags = re.findall(r'([[a-zA-Z0-9_.]+)([><=])([^ -,\t\n\r\f\v]+)', - string) - return frags + def _value_error(message): + raise ValueError('invalid query %(query)s: missing %(message)s' % + {'query': query, 'message': message}) - def split_by_data_type(string): - frags = re.findall(r'^(string|integer|float|datetime|boolean)(::)' - r'([^ -,\t\n\r\f\v]+)$', string) + try: + field, operator, value = OP_SPLIT_RE.split(query, maxsplit=1) + except ValueError: + _value_error('operator') - # 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 + if not len(field): + _value_error('field') + + if not len(value): + _value_error('value') + + return field, operator, value + + def split_by_data_type(query_value): + frags = DATA_TYPE_RE.match(query_value) + + # The second match is the separator. Return a list without it if + # a type identifier was found. + return frags.group(1, 3) if frags else None opts = [] queries = cli_query.split(';') for q in queries: - frag = split_by_op(q) - if len(frag) > 1: - raise ValueError('incorrect separator %s in query "%s"' % - ('(should be ";")', q)) - if len(frag) == 0: - raise ValueError('invalid query %s' % q) - query = frag[0] + query = split_by_op(q) opt = {} opt['field'] = query[0] - opt['op'] = op_lookup[query[1]] + opt['op'] = OP_LOOKUP[query[1]] # Allow the data type of the value to be specified via ::, # where type can be one of integer, string, float, datetime, boolean diff --git a/ceilometerclient/v2/query.py b/ceilometerclient/v2/query.py index a615e2f..a11a56d 100644 --- a/ceilometerclient/v2/query.py +++ b/ceilometerclient/v2/query.py @@ -23,7 +23,7 @@ from ceilometerclient.v2 import samples class QueryManager(base.Manager): path_suffix = None - def query(self, filter, orderby, limit): + def query(self, filter=None, orderby=None, limit=None): query = {} if filter: query["filter"] = filter @@ -33,9 +33,9 @@ class QueryManager(base.Manager): query["limit"] = limit url = '/v2/query%s' % self.path_suffix - resp, body = self.api.json_request('POST', - url, - body=query) + + body = self.api.post(url, json=query).json() + if body: return [self.resource_class(self, b) for b in body] else: diff --git a/ceilometerclient/v2/resources.py b/ceilometerclient/v2/resources.py index 59adb95..99e6fd5 100644 --- a/ceilometerclient/v2/resources.py +++ b/ceilometerclient/v2/resources.py @@ -1,6 +1,5 @@ -# -*- encoding: utf-8 -*- # -# Copyright © 2013 Red Hat, Inc +# Copyright 2013 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 @@ -22,6 +21,11 @@ class Resource(base.Resource): def __repr__(self): return "" % self._info + def __getattr__(self, k): + if k == 'id': + return self.resource_id + return super(Resource, self).__getattr__(k) + class ResourceManager(base.Manager): resource_class = Resource diff --git a/ceilometerclient/v2/samples.py b/ceilometerclient/v2/samples.py index cd0c3c2..c046051 100644 --- a/ceilometerclient/v2/samples.py +++ b/ceilometerclient/v2/samples.py @@ -26,13 +26,18 @@ CREATION_ATTRIBUTES = ('source', 'resource_metadata') -class Sample(base.Resource): +class OldSample(base.Resource): + """Represents API v2 OldSample object. + + Model definition: + http://docs.openstack.org/developer/ceilometer/webapi/v2.html#OldSample + """ def __repr__(self): - return "" % self._info + return "" % self._info -class SampleManager(base.Manager): - resource_class = Sample +class OldSampleManager(base.Manager): + resource_class = OldSample @staticmethod def _path(counter_name=None): @@ -47,8 +52,31 @@ class SampleManager(base.Manager): new = dict((key, value) for (key, value) in kwargs.items() if key in CREATION_ATTRIBUTES) url = self._path(counter_name=kwargs['counter_name']) - resp, body = self.api.json_request('POST', - url, - body=[new]) + body = self.api.post(url, json=[new]).json() if body: - return [Sample(self, b) for b in body] + return [OldSample(self, b) for b in body] + + +class Sample(base.Resource): + """Represents API v2 Sample object. + + Model definition: + http://docs.openstack.org/developer/ceilometer/webapi/v2.html#Sample + """ + def __repr__(self): + return "" % self._info + + +class SampleManager(base.Manager): + resource_class = Sample + + def list(self, q=None, limit=None): + params = ['limit=%s' % str(limit)] if limit else None + return self._list(options.build_url("/v2/samples", q, params)) + + def get(self, sample_id): + path = "/v2/samples/" + sample_id + try: + return self._list(path, expect_single=True)[0] + except IndexError: + return None diff --git a/ceilometerclient/v2/shell.py b/ceilometerclient/v2/shell.py index de44c51..67a7646 100644 --- a/ceilometerclient/v2/shell.py +++ b/ceilometerclient/v2/shell.py @@ -1,6 +1,5 @@ -# -*- encoding: utf-8 -*- # -# Copyright © 2013 Red Hat, Inc +# Copyright 2013 Red Hat, Inc # Copyright Ericsson AB 2014. All rights reserved # # Authors: Angus Salkeld @@ -19,17 +18,20 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import functools import json + +from oslo.utils import strutils import six from ceilometerclient.common import utils from ceilometerclient import exc -from ceilometerclient.openstack.common import strutils from ceilometerclient.v2 import options -ALARM_STATES = ['ok', 'alarm', 'insufficient_data'] +ALARM_STATES = ['ok', 'alarm', 'insufficient data'] +ALARM_SEVERITY = ['low', 'moderate', 'critical'] ALARM_OPERATORS = ['lt', 'le', 'eq', 'ne', 'ge', 'gt'] ALARM_COMBINATION_OPERATORS = ['and', 'or'] STATISTICS = ['max', 'min', 'avg', 'sum', 'count'] @@ -48,21 +50,38 @@ COMPLEX_OPERATORS = ['and', 'or'] SIMPLE_OPERATORS = ["=", "!=", "<", "<=", '>', '>='] +class NotEmptyAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + values = values or getattr(namespace, self.dest) + if not values or values.isspace(): + raise exc.CommandError('%s should not be empty' % self.dest) + setattr(namespace, self.dest, values) + + +def obsoleted_by(new_dest): + class ObsoletedByAction(argparse.Action): + def __call__(self, parser, namespace, values, option_string=None): + old_dest = option_string or self.dest + print('%s is obsolete! See help for more details.' % old_dest) + setattr(namespace, new_dest, values) + return ObsoletedByAction + + @utils.arg('-q', '--query', metavar='', 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='', required=True, - help='Name of meter to show samples for.') + action=NotEmptyAction, help='Name of meter to list statistics for.') @utils.arg('-p', '--period', metavar='', help='Period in seconds over which to group samples.') @utils.arg('-g', '--groupby', metavar='', action='append', help='Field for group by.') @utils.arg('-a', '--aggregate', metavar='[<-]', action='append', default=[], help=('Function for data aggregation. ' - 'Available aggregates are: ' - '%s.' % ", ".join(AGGREGATES.keys()))) + 'Available aggregates are: ' + '%s.' % ", ".join(AGGREGATES.keys()))) def do_statistics(cc, args): - '''List the statistics for a meter.''' + """List the statistics for a meter.""" aggregates = [] for a in args.aggregate: aggregates.append(dict(zip(('func', 'param'), a.split("<-")))) @@ -105,12 +124,19 @@ def do_statistics(cc, args): @utils.arg('-q', '--query', metavar='', 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='', required=True, - help='Name of meter to show samples for.') +@utils.arg('-m', '--meter', metavar='', + action=NotEmptyAction, help='Name of meter to show samples for.') @utils.arg('-l', '--limit', metavar='', help='Maximum number of samples to return.') def do_sample_list(cc, args): - '''List the samples for a meter.''' + """List the samples (return OldSample objects if -m/--meter is set).""" + if not args.meter: + return _do_sample_list(cc, args) + else: + return _do_old_sample_list(cc, args) + + +def _do_old_sample_list(cc, args): fields = {'meter_name': args.meter, 'q': options.cli_to_array(args.query), 'limit': args.limit} @@ -123,8 +149,36 @@ def do_sample_list(cc, args): 'Timestamp'] fields = ['resource_id', 'counter_name', 'counter_type', 'counter_volume', 'counter_unit', 'timestamp'] - utils.print_list(samples, fields, field_labels, - sortby=None) + utils.print_list(samples, fields, field_labels, sortby=None) + + +def _do_sample_list(cc, args): + fields = { + 'q': options.cli_to_array(args.query), + 'limit': args.limit + } + samples = cc.new_samples.list(**fields) + field_labels = ['ID', 'Resource ID', 'Name', 'Type', 'Volume', 'Unit', + 'Timestamp'] + fields = ['id', 'resource_id', 'meter', 'type', 'volume', 'unit', + 'timestamp'] + utils.print_list(samples, fields, field_labels, sortby=None) + + +@utils.arg('sample_id', metavar='', action=NotEmptyAction, + help='ID (aka message ID) of the sample to show.') +def do_sample_show(cc, args): + '''Show an sample.''' + sample = cc.new_samples.get(args.sample_id) + + if sample is None: + raise exc.CommandError('Sample not found: %s' % args.sample_id) + + fields = ['id', 'meter', 'volume', 'type', 'unit', 'source', + 'resource_id', 'user_id', 'project_id', + 'timestamp', 'recorded_at', 'metadata'] + data = dict((f, getattr(sample, f, '')) for f in fields) + utils.print_dict(data, wrap=72) @utils.arg('--project-id', metavar='', @@ -136,7 +190,7 @@ def do_sample_list(cc, args): @utils.arg('-r', '--resource-id', metavar='', required=True, help='ID of the resource.') @utils.arg('-m', '--meter-name', metavar='', required=True, - help='The meter name.') + action=NotEmptyAction, help='The meter name.') @utils.arg('--meter-type', metavar='', required=True, help='The meter type.') @utils.arg('--meter-unit', metavar='', required=True, @@ -144,11 +198,12 @@ def do_sample_list(cc, args): @utils.arg('--sample-volume', metavar='', required=True, help='The sample volume.') @utils.arg('--resource-metadata', metavar='', - help='Resource metadata.') + help='Resource metadata. Provided value should be a set of ' + 'key-value pairs e.g. {"key":"value"}.') @utils.arg('--timestamp', metavar='', help='The sample timestamp.') def do_sample_create(cc, args={}): - '''Create a sample.''' + """Create a sample.""" arg_to_field_mapping = {'meter_name': 'counter_name', 'meter_unit': 'counter_unit', 'meter_type': 'counter_type', @@ -175,7 +230,7 @@ def do_sample_create(cc, args={}): 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={}): - '''List the user's meters.''' + """List the user's meters.""" meters = cc.meters.list(q=options.cli_to_array(args.query)) field_labels = ['Name', 'Type', 'Unit', 'Resource ID', 'User ID', 'Project ID'] @@ -185,6 +240,20 @@ def do_meter_list(cc, args={}): sortby=0) +def _display_alarm_list(alarms, sortby=None): + # omit action initially to keep output width sane + # (can switch over to vertical formatting when available from CLIFF) + field_labels = ['Alarm ID', 'Name', 'State', 'Severity', 'Enabled', + 'Continuous', 'Alarm condition', 'Time constraints'] + fields = ['alarm_id', 'name', 'state', 'severity', 'enabled', + 'repeat_actions', 'rule', 'time_constraints'] + utils.print_list( + alarms, fields, field_labels, + formatters={'rule': alarm_rule_formatter, + 'time_constraints': time_constraints_formatter_brief}, + sortby=sortby) + + def _display_rule(type, rule): if type == 'threshold': return ('%(meter_name)s %(comparison_operator)s ' @@ -204,14 +273,14 @@ def _display_rule(type, rule): else: # just dump all return "\n".join(["%s: %s" % (f, v) - for f, v in rule.iteritems()]) + for f, v in six.iteritems(rule)]) def alarm_rule_formatter(alarm): return _display_rule(alarm.type, alarm.rule) -def _display_time_constraints(time_constraints): +def _display_time_constraints_brief(time_constraints): if time_constraints: return ', '.join('%(name)s at %(start)s %(timezone)s for %(duration)ss' % { @@ -225,8 +294,10 @@ def _display_time_constraints(time_constraints): return 'None' -def time_constraints_formatter(alarm): - return _display_time_constraints(alarm.time_constraints) +def time_constraints_formatter_brief(alarm): + return _display_time_constraints_brief(getattr(alarm, + 'time_constraints', + None)) def _infer_type(detail): @@ -254,7 +325,7 @@ def alarm_change_detail_formatter(change): fields.append('%s: %s' % (k, detail[k])) if 'time_constraints' in detail: fields.append('time_constraints: %s' % - _display_time_constraints( + _display_time_constraints_brief( detail['time_constraints'])) elif change.type == 'rule change': for k, v in six.iteritems(detail): @@ -270,18 +341,9 @@ def alarm_change_detail_formatter(change): 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={}): - '''List the user's alarms.''' + """List the user's alarms.""" alarms = cc.alarms.list(q=options.cli_to_array(args.query)) - # omit action initially to keep output width sane - # (can switch over to vertical formatting when available from CLIFF) - field_labels = ['Alarm ID', 'Name', 'State', 'Enabled', 'Continuous', - 'Alarm condition', 'Time constraints'] - fields = ['alarm_id', 'name', 'state', 'enabled', 'repeat_actions', - 'rule', 'time_constraints'] - utils.print_list( - alarms, fields, field_labels, - formatters={'rule': alarm_rule_formatter, - 'time_constraints': time_constraints_formatter}, sortby=0) + _display_alarm_list(alarms, sortby=0) def alarm_query_formater(alarm): @@ -292,7 +354,7 @@ def alarm_query_formater(alarm): return r' AND\n'.join(qs) -def alarm_time_constraints_formatter(alarm): +def time_constraints_formatter_full(alarm): time_constraints = [] for tc in alarm.time_constraints: lines = [] @@ -305,25 +367,27 @@ def alarm_time_constraints_formatter(alarm): def _display_alarm(alarm): fields = ['name', 'description', 'type', - 'state', 'enabled', 'alarm_id', 'user_id', 'project_id', - 'alarm_actions', 'ok_actions', 'insufficient_data_actions', - 'repeat_actions'] + 'state', 'severity', 'enabled', 'alarm_id', 'user_id', + 'project_id', 'alarm_actions', 'ok_actions', + 'insufficient_data_actions', 'repeat_actions'] data = dict([(f, getattr(alarm, f, '')) for f in fields]) data.update(alarm.rule) if alarm.type == 'threshold': data['query'] = alarm_query_formater(alarm) if alarm.time_constraints: - data['time_constraints'] = alarm_time_constraints_formatter(alarm) + data['time_constraints'] = time_constraints_formatter_full(alarm) utils.print_dict(data, wrap=72) -@utils.arg('-a', '--alarm_id', metavar='', required=True, - help='ID of the alarm to show.') +@utils.arg('-a', '--alarm_id', metavar='', + action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS, + dest='alarm_id_deprecated') +@utils.arg('alarm_id', metavar='', nargs='?', + action=NotEmptyAction, help='ID of the alarm to show.') def do_alarm_show(cc, args={}): - '''Show an alarm.''' - try: - alarm = cc.alarms.get(args.alarm_id) - except exc.HTTPNotFound: + """Show an alarm.""" + alarm = cc.alarms.get(args.alarm_id) + if alarm is None: raise exc.CommandError('Alarm not found: %s' % args.alarm_id) else: _display_alarm(alarm) @@ -343,6 +407,9 @@ def common_alarm_arguments(create=False): help='Free text description of the alarm.') @utils.arg('--state', metavar='', help='State of the alarm, one of: ' + str(ALARM_STATES)) + @utils.arg('--severity', metavar='', + help='Severity of the alarm, one of: ' + + str(ALARM_SEVERITY)) @utils.arg('--enabled', type=strutils.bool_from_string, metavar='{True|False}', help='True if alarm evaluation/actioning is enabled.') @@ -358,13 +425,13 @@ def common_alarm_arguments(create=False): dest='insufficient_data_actions', metavar='', action='append', default=None, help=('URL to invoke when state transitions to ' - 'insufficient_data. May be used multiple times.')) + 'insufficient data. May be used multiple times.')) @utils.arg('--time-constraint', dest='time_constraints', metavar='