Merge tag '1.0.13' into debian/kilo

python-ceilometerclient 1.0.13

Includes the following changes:

 * Corrected the errors in sample-show and sample-create
 * Allow create/update gnocchi alarm rule
 * Don't enforce a kind of alarm rules
 * Update get_client() parameters with correct variable names
 * Add Sample API support
 * add --no-traits for event-list
 * Add severity field to alarm CLI
 * Remove trailing space before , in the help string of --time-constraint
 * Fix improper parameter setup for cacert and client certs
 * event-list should sort by timestamp, not id
 * Allow all pep8 checks
 * Fix H105 pep8 error
 * Triple double-quoted strings should be used for docstrings
 * Support ceilometer-url and os-endpoint
 * sync to latest oslo-incubator code
 * Add apiclient to openstack-common.conf
 * Add client property for common.base.Manager
 * Allow graceful shutdown on Ctrl+C
 * Make methods static where it's possible
 * Fix old-style classes declaration
 * Remove redundant parentheses (except openstack.common)
 * Enable --os-insecure CLI option
 * sync with oslo and use oslo.i18n
 * Workflow documentation is now in infra-manual
 * Support os-endpoint-type
 * Alarm TimeConstraint display incorrect
 * Add `requests` to requirements
 * Fix timeout argument not treated as integer
 * Refactor tests/test_shell.py
 * Add --slowest option for testr
 * Fix wrong initialization of AuthPlugin for keystone v3
 * Add CONTRIBUTING.rst
This commit is contained in:
Thomas Goirand
2015-04-15 10:57:34 +02:00
74 changed files with 3159 additions and 2243 deletions

2
.gitignore vendored
View File

@@ -6,6 +6,8 @@ build
cover
.testrepository
.venv
doc/build
doc/source/ref
subunit.log
AUTHORS
ChangeLog

16
CONTRIBUTING.rst Normal file
View File

@@ -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

View File

@@ -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 <http://wiki.openstack.org/HowToContribute>`_. The master
repository is on `GitHub <http://github.com/openstack/python-ceilometerclient>`_.
`developer guide <http://docs.openstack.org/infra/manual/developers.html>`_. The master
repository is in `Git <http://git.openstack.org/cgit/openstack/python-ceilometerclient>`_.
See release notes and more at `<http://docs.openstack.org/developer/python-ceilometerclient/>`_.

View File

@@ -10,68 +10,272 @@
# 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.
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
: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
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_endpoint(ks_session, **kwargs):
"""Get an endpoint using the provided keystone session."""
# set service specific endpoint types
endpoint_type = kwargs.get('endpoint_type') or 'publicURL'
service_type = kwargs.get('service_type') or 'metering'
endpoint = ks_session.get_endpoint(service_type=service_type,
interface=endpoint_type,
region_name=kwargs.get('region_name'))
return endpoint
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']
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
"""
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'))
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 _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 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 get_client(api_version, **kwargs):
"""Get an authtenticated client, based on the credentials
in the keyword args.
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: pre-existing token to re-use
* ceilometer_url: ceilometer API endpoint
* 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_cacert: path of CA TLS certificate
* os_cert|os_cacert: path of CA TLS certificate
* os_key: SSL private key
* 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)
endpoint = kwargs.get('os_endpoint') or kwargs.get('ceilometer_url')
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'))):
ks_kwargs = {
cli_kwargs = {
'username': kwargs.get('os_username'),
'password': kwargs.get('os_password'),
'tenant_id': kwargs.get('os_tenant_id'),
@@ -81,27 +285,36 @@ def get_client(api_version, **kwargs):
'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)
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'),
'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'),
}
return Client(api_version, endpoint, **cli_kwargs)
cli_kwargs.update(kwargs)
cli_kwargs.update(_adjust_params(cli_kwargs))
return Client(version, endpoint, **cli_kwargs)
def Client(version, *args, **kwargs):
module = utils.import_versioned_module(version, 'client')
client_class = getattr(module, 'Client')
return client_class(*args, **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

View File

@@ -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

View File

@@ -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()

View File

@@ -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:

View File

@@ -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,15 +104,10 @@ 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
# classes
_code_map = {}
@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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):
@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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,

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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,96 +76,19 @@ 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='<ca-certificate-file>',
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='<CEILOMETER_URL>',
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',
@@ -163,19 +100,8 @@ class CeilometerShell(object):
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()

View File

@@ -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

View File

@@ -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)

View File

@@ -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(
for k, v in HTTPEXCEPTIONS.items():
exception = v(
details=json.dumps({"error_message": {"foo": "bar"}}))
self.assertEqual("HTTPBadRequest (HTTP 400)", str(exception))
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))

View File

@@ -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/'

View File

@@ -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',
FAKE_V2_ENV = {'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': 'http://no.where'}
'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')

View File

@@ -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, '''\

View File

@@ -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)

View File

@@ -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')

View File

@@ -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')

View File

@@ -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')

View File

@@ -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',
'GET',
'/v1/users/freddy/meters/balls?' +
'start_timestamp=now&end_timestamp=now',
{}, None),
'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')

View File

@@ -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')

View File

@@ -1,8 +1,5 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 Red Hat, Inc
#
# Author: Eoghan Glynn <eglynn@redhat.com>
# 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,14 +38,16 @@ 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'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'name': u'cons2',
u'description': u'desc2',
u'start': u'0 23 * * *',
u'duration': 600,
@@ -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',
'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),
]
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',
body = copy.deepcopy(AN_ALARM)
body[u'time_constraints'][1] = dict(name='cons2',
description='desc2',
start='0 23 * * *',
duration=500,
timezone='')]
self.assertEqual(expected, actual)
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)

View File

@@ -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")

View File

@@ -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)

View File

@@ -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)

View File

@@ -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))

View File

@@ -1,7 +1,5 @@
# Copyright Ericsson AB 2014. All rights reserved
#
# Author: Balazs Gibizer <balazs.gibizer@ericsson.com>
#
# 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))

View File

@@ -1,7 +1,5 @@
# Copyright Ericsson AB 2014. All rights reserved
#
# Author: Balazs Gibizer <balazs.gibizer@ericsson.com>
#
# 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))

View File

@@ -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)

View File

@@ -15,10 +15,12 @@
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',
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',
@@ -30,56 +32,98 @@ GET_SAMPLE = {u'counter_name': u'instance',
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)
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='
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')
args_limit = 'limit=1'
fixtures = {
base_url:
{
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())

View File

@@ -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,15 +109,13 @@ 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,
@@ -130,9 +130,6 @@ class ShellAlarmHistoryCommandTest(utils.BaseTestCase):
]
for r in required:
self.assertThat(out, matchers.MatchesRegex(r, re.DOTALL))
finally:
sys.stdout.close()
sys.stdout = orig
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,17 +202,15 @@ 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])
@@ -208,10 +218,7 @@ class ShellAlarmCommandTest(utils.BaseTestCase):
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
self.assertNotIn('repeat_actions', kwargs)
def test_alarm_update_repeat_actions_untouched(self):
method = ceilometer_shell.do_alarm_update
@@ -249,35 +256,34 @@ 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
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.assertTrue('threshold_rule' in kwargs)
self.assertIn('threshold_rule', kwargs)
rule = kwargs['threshold_rule']
self.assertEqual('cpu_util', rule.get('meter_name'))
self.assertEqual(70.0, rule.get('threshold'))
@@ -288,10 +294,8 @@ class ShellAlarmCommandTest(utils.BaseTestCase):
query = dict(field='resource_id', type='',
value='INSTANCE_ID', op='eq')
self.assertEqual([query], rule['query'])
finally:
sys.stdout.close()
sys.stdout = orig
@mock.patch('sys.stdout', new=six.StringIO())
def test_alarm_create_time_constraints(self):
shell = base_shell.CeilometerShell()
argv = ['alarm-threshold-create',
@@ -305,12 +309,9 @@ 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 * * *',
@@ -318,65 +319,86 @@ class ShellAlarmCommandTest(utils.BaseTestCase):
dict(name='cons2', start='0 23 * * *',
duration='600')]
self.assertEqual(time_constraints, kwargs['time_constraints'])
finally:
sys.stdout.close()
sys.stdout = orig
class ShellSampleListCommandTest(utils.BaseTestCase):
METER = 'cpu_util'
SAMPLES = [{"counter_name": "cpu_util",
"resource_id": "5dcf5537-3161-4e25-9235-407e1385bd35",
"timestamp": "2013-10-15T05:50:30",
"counter_unit": "%",
"counter_volume": 0.261666666667,
"counter_type": "gauge"},
{"counter_name": "cpu_util",
"resource_id": "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f",
"timestamp": "2013-10-15T05:50:29",
"counter_unit": "%",
"counter_volume": 0.261666666667,
"counter_type": "gauge"},
{"counter_name": "cpu_util",
"resource_id": "5dcf5537-3161-4e25-9235-407e1385bd35",
"timestamp": "2013-10-15T05:40:30",
"counter_unit": "%",
"counter_volume": 0.251247920133,
"counter_type": "gauge"},
{"counter_name": "cpu_util",
"resource_id": "87d197e9-9cf6-4c25-bc66-1b1f4cedb52f",
"timestamp": "2013-10-15T05:40:29",
"counter_unit": "%",
"counter_volume": 0.26,
"counter_type": "gauge"}]
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
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
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
self.assertEqual('''\
+--------------------------------------+------------------+-------+---------\
+------------+--------------------------------------------------------------\
----------------------------------------+
| Alarm ID | Name | State | Enabled \
| Continuous | Alarm condition \
+--------------------------------------+------------------+-------+----------+\
---------+------------+-------------------------------------------------------\
-----------------------------------------------+-------------------------------\
-+
| Alarm ID | Name | State | Severity \
| Enabled | Continuous | Alarm condition \
| Time constraints \
|
+--------------------------------------+------------------+-------+---------\
+------------+--------------------------------------------------------------\
----------------------------------------+
| 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())
+--------------------------------------+------------------+-------+----------+\
---------+------------+-------------------------------------------------------\
-----------------------------------------------+--------------------------------+
| 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
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())

View File

@@ -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'))

View File

@@ -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'),

View File

@@ -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',

View File

@@ -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)

View File

@@ -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')

View File

@@ -68,7 +68,7 @@ def do_sample_list(cc, args):
@utils.arg('-p', '--project_id', metavar='<PROJECT_ID>',
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='<SOURCE>',
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)

View File

@@ -1,8 +1,5 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 Red Hat, Inc
#
# Author: Eoghan Glynn <eglynn@redhat.com>
# 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):

View File

@@ -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
: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(

View File

@@ -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

View File

@@ -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 "<Event %s>" % 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

View File

@@ -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

View File

@@ -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}],
"""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&
"?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)
if len(frags) == 0:
#single char 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 <type>::<value>,
# where type can be one of integer, string, float, datetime, boolean

View File

@@ -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:

View File

@@ -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 "<Resource %s>" % 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

View File

@@ -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 "<Sample %s>" % self._info
return "<OldSample %s>" % 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 "<Sample %s>" % 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

View File

@@ -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 <asalkeld@redhat.com>
@@ -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,11 +50,28 @@ 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='<QUERY>',
help='key[op]data_type::value; list. data_type is optional, '
'but if supplied must be string, integer, float, or boolean.')
@utils.arg('-m', '--meter', metavar='<NAME>', required=True,
help='Name of meter to show samples for.')
action=NotEmptyAction, help='Name of meter to list statistics for.')
@utils.arg('-p', '--period', metavar='<PERIOD>',
help='Period in seconds over which to group samples.')
@utils.arg('-g', '--groupby', metavar='<FIELD>', action='append',
@@ -62,7 +81,7 @@ SIMPLE_OPERATORS = ["=", "!=", "<", "<=", '>', '>=']
'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='<QUERY>',
help='key[op]data_type::value; list. data_type is optional, '
'but if supplied must be string, integer, float, or boolean.')
@utils.arg('-m', '--meter', metavar='<NAME>', required=True,
help='Name of meter to show samples for.')
@utils.arg('-m', '--meter', metavar='<NAME>',
action=NotEmptyAction, help='Name of meter to show samples for.')
@utils.arg('-l', '--limit', metavar='<NUMBER>',
help='Maximum number of samples to return.')
def do_sample_list(cc, args):
'''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='<SAMPLE_ID>', 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='<PROJECT_ID>',
@@ -136,7 +190,7 @@ def do_sample_list(cc, args):
@utils.arg('-r', '--resource-id', metavar='<RESOURCE_ID>', required=True,
help='ID of the resource.')
@utils.arg('-m', '--meter-name', metavar='<METER_NAME>', required=True,
help='The meter name.')
action=NotEmptyAction, help='The meter name.')
@utils.arg('--meter-type', metavar='<METER_TYPE>', required=True,
help='The meter type.')
@utils.arg('--meter-unit', metavar='<METER_UNIT>', required=True,
@@ -144,11 +198,12 @@ def do_sample_list(cc, args):
@utils.arg('--sample-volume', metavar='<SAMPLE_VOLUME>', required=True,
help='The sample volume.')
@utils.arg('--resource-metadata', metavar='<RESOURCE_METADATA>',
help='Resource metadata.')
help='Resource metadata. Provided value should be a set of '
'key-value pairs e.g. {"key":"value"}.')
@utils.arg('--timestamp', metavar='<TIMESTAMP>',
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='<ALARM_ID>', required=True,
help='ID of the alarm to show.')
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS,
dest='alarm_id_deprecated')
@utils.arg('alarm_id', metavar='<ALARM_ID>', nargs='?',
action=NotEmptyAction, help='ID of the alarm to show.')
def do_alarm_show(cc, args={}):
'''Show an alarm.'''
try:
"""Show an alarm."""
alarm = cc.alarms.get(args.alarm_id)
except exc.HTTPNotFound:
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='<STATE>',
help='State of the alarm, one of: ' + str(ALARM_STATES))
@utils.arg('--severity', metavar='<SEVERITY>',
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,7 +425,7 @@ def common_alarm_arguments(create=False):
dest='insufficient_data_actions',
metavar='<Webhook URL>', 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='<Time Constraint>', action='append',
default=None,
@@ -378,6 +445,73 @@ def common_alarm_arguments(create=False):
return _wrapper
def common_alarm_gnocchi_arguments(rule_namespace, create=False):
def _wrapper(func):
@utils.arg('--granularity', type=int, metavar='<GRANULARITY>',
dest=rule_namespace + '/granularity',
help='Length of each period (seconds) to evaluate over.')
@utils.arg('--evaluation-periods', type=int, metavar='<COUNT>',
dest=rule_namespace + '/evaluation_periods',
help='Number of periods to evaluate over.')
@utils.arg('--aggregation-method', metavar='<AGGREATION>',
dest=rule_namespace + '/aggregation_method',
help=('Aggregation method to use, one of: ' +
str(STATISTICS) + '.'))
@utils.arg('--comparison-operator', metavar='<OPERATOR>',
dest=rule_namespace + '/comparison_operator',
help=('Operator to compare with, one of: ' +
str(ALARM_OPERATORS) + '.'))
@utils.arg('--threshold', type=float, metavar='<THRESHOLD>',
dest=rule_namespace + '/threshold',
required=create,
help='Threshold to evaluate against.')
@utils.arg('--repeat-actions', dest='repeat_actions',
metavar='{True|False}', type=strutils.bool_from_string,
default=False,
help=('True if actions should be repeatedly notified '
'while alarm remains in target state.'))
@functools.wraps(func)
def _wrapped(*args, **kwargs):
return func(*args, **kwargs)
return _wrapped
return _wrapper
def common_alarm_gnocchi_metrics_arguments(create=False):
def _wrapper(func):
@utils.arg('-m', '--metrics', metavar='<METRICS>',
dest='gnocchi_metrics_threshold_rule/meter_name',
action='append', required=create,
help='Metric to evaluate against.')
@functools.wraps(func)
def _wrapped(*args, **kwargs):
return func(*args, **kwargs)
return _wrapped
return _wrapper
def common_alarm_gnocchi_resources_arguments(create=False):
def _wrapper(func):
@utils.arg('-m', '--metric', metavar='<METRIC>',
dest='gnocchi_resources_threshold_rule/metric',
required=create,
help='Metric to evaluate against.')
@utils.arg('--resource-type', metavar='<RESOURCE_TYPE>',
dest='gnocchi_resources_threshold_rule/resource_type',
required=create,
help='Resource_type to evaluate against.')
@utils.arg('--resource-constraint', metavar='<RESOURCE_CONSTRAINT>',
dest='gnocchi_resources_threshold_rule/resource_constraint',
required=create,
help=('Resources to evaluate against or a expression '
'to select multiple resources.'))
@functools.wraps(func)
def _wrapped(*args, **kwargs):
return func(*args, **kwargs)
return _wrapped
return _wrapper
@common_alarm_arguments(create=True)
@utils.arg('--period', type=int, metavar='<PERIOD>',
help='Length of each period (seconds) to evaluate over.')
@@ -402,7 +536,7 @@ def common_alarm_arguments(create=False):
help=('True if actions should be repeatedly notified '
'while alarm remains in target state.'))
def do_alarm_create(cc, args={}):
'''Create a new alarm (Deprecated). Use alarm-threshold-create instead.'''
"""Create a new alarm (Deprecated). Use alarm-threshold-create instead."""
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, "time_constraints")
fields = utils.args_array_to_dict(fields, "matching_metadata")
@@ -410,6 +544,33 @@ def do_alarm_create(cc, args={}):
_display_alarm(alarm)
@common_alarm_arguments(create=True)
@common_alarm_gnocchi_arguments('gnocchi_resources_threshold_rule',
create=True)
@common_alarm_gnocchi_resources_arguments(create=True)
def do_alarm_gnocchi_resources_threshold_create(cc, args={}):
"""Create a new alarm based on computed statistics."""
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
fields['type'] = 'gnocchi_resources_threshold'
alarm = cc.alarms.create(**fields)
_display_alarm(alarm)
@common_alarm_arguments(create=True)
@common_alarm_gnocchi_arguments('gnocchi_metrics_threshold_rule', create=True)
@common_alarm_gnocchi_metrics_arguments(create=True)
def do_alarm_gnocchi_metrics_threshold_create(cc, args={}):
"""Create a new alarm based on computed statistics."""
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
fields['type'] = 'gnocchi_metrics_threshold'
alarm = cc.alarms.create(**fields)
_display_alarm(alarm)
@common_alarm_arguments(create=True)
@utils.arg('-m', '--meter-name', metavar='<METRIC>', required=True,
dest='threshold_rule/meter_name',
@@ -440,7 +601,7 @@ def do_alarm_create(cc, args={}):
help=('True if actions should be repeatedly notified '
'while alarm remains in target state.'))
def do_alarm_threshold_create(cc, args={}):
'''Create a new alarm based on computed statistics.'''
"""Create a new alarm based on computed statistics."""
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
@@ -455,7 +616,7 @@ def do_alarm_threshold_create(cc, args={}):
@common_alarm_arguments(create=True)
@utils.arg('--alarm_ids', action='append', metavar='<ALARM IDS>',
required=True, dest='combination_rule/alarm_ids',
help='List of alarm ids.')
help='List of alarm IDs.')
@utils.arg('--operator', metavar='<OPERATOR>',
dest='combination_rule/operator',
help='Operator to compare with, one of: ' + str(
@@ -466,7 +627,7 @@ def do_alarm_threshold_create(cc, args={}):
help=('True if actions should be repeatedly notified '
'while alarm remains in target state.'))
def do_alarm_combination_create(cc, args={}):
'''Create a new alarm based on state of other alarms.'''
"""Create a new alarm based on state of other alarms."""
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
@@ -475,8 +636,11 @@ def do_alarm_combination_create(cc, args={}):
_display_alarm(alarm)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True,
help='ID of the alarm to update.')
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS,
dest='alarm_id_deprecated')
@utils.arg('alarm_id', metavar='<ALARM_ID>', nargs='?',
action=NotEmptyAction, help='ID of the alarm to update.')
@common_alarm_arguments()
@utils.arg('--remove-time-constraint', action='append',
metavar='<Constraint names>',
@@ -504,7 +668,7 @@ def do_alarm_combination_create(cc, args={}):
help=('True if actions should be repeatedly notified '
'while alarm remains in target state.'))
def do_alarm_update(cc, args={}):
'''Update an existing alarm (Deprecated).'''
"""Update an existing alarm (Deprecated)."""
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, "time_constraints")
fields = utils.args_array_to_dict(fields, "matching_metadata")
@@ -516,8 +680,11 @@ def do_alarm_update(cc, args={}):
_display_alarm(alarm)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True,
help='ID of the alarm to update.')
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS,
dest='alarm_id_deprecated')
@utils.arg('alarm_id', metavar='<ALARM_ID>', nargs='?',
action=NotEmptyAction, help='ID of the alarm to update.')
@common_alarm_arguments()
@utils.arg('--remove-time-constraint', action='append',
metavar='<Constraint names>',
@@ -544,6 +711,7 @@ def do_alarm_update(cc, args={}):
dest='threshold_rule/threshold',
help='Threshold to evaluate against.')
@utils.arg('-q', '--query', metavar='<QUERY>',
dest='threshold_rule/query',
help='key[op]data_type::value; list. data_type is optional, '
'but if supplied must be string, integer, float, or boolean.')
@utils.arg('--repeat-actions', dest='repeat_actions',
@@ -551,7 +719,7 @@ def do_alarm_update(cc, args={}):
help=('True if actions should be repeatedly notified '
'while alarm remains in target state.'))
def do_alarm_threshold_update(cc, args={}):
'''Update an existing alarm based on computed statistics.'''
"""Update an existing alarm based on computed statistics."""
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
@@ -567,8 +735,63 @@ def do_alarm_threshold_update(cc, args={}):
_display_alarm(alarm)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True,
help='ID of the alarm to update.')
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS,
dest='alarm_id_deprecated')
@utils.arg('alarm_id', metavar='<ALARM_ID>', nargs='?',
action=NotEmptyAction, help='ID of the alarm to update.')
@common_alarm_arguments()
@common_alarm_gnocchi_arguments('gnocchi_resources_threshold')
@common_alarm_gnocchi_resources_arguments()
@utils.arg('--remove-time-constraint', action='append',
metavar='<Constraint names>',
dest='remove_time_constraints',
help='Name or list of names of the time constraints to remove.')
def do_alarm_gnocchi_resources_threshold_update(cc, args={}):
"""Update an existing alarm based on computed statistics."""
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
fields.pop('alarm_id')
fields['type'] = 'gnocchi_resources_threshold'
try:
alarm = cc.alarms.update(args.alarm_id, **fields)
except exc.HTTPNotFound:
raise exc.CommandError('Alarm not found: %s' % args.alarm_id)
_display_alarm(alarm)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS,
dest='alarm_id_deprecated')
@utils.arg('alarm_id', metavar='<ALARM_ID>', nargs='?',
action=NotEmptyAction, help='ID of the alarm to update.')
@common_alarm_arguments()
@common_alarm_gnocchi_arguments('gnocchi_metrics_threshold')
@common_alarm_gnocchi_metrics_arguments()
@utils.arg('--remove-time-constraint', action='append',
metavar='<Constraint names>',
dest='remove_time_constraints',
help='Name or list of names of the time constraints to remove.')
def do_alarm_gnocchi_metrics_threshold_update(cc, args={}):
"""Update an existing alarm based on computed statistics."""
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
fields.pop('alarm_id')
fields['type'] = 'gnocchi_metrics_threshold'
try:
alarm = cc.alarms.update(args.alarm_id, **fields)
except exc.HTTPNotFound:
raise exc.CommandError('Alarm not found: %s' % args.alarm_id)
_display_alarm(alarm)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS,
dest='alarm_id_deprecated')
@utils.arg('alarm_id', metavar='<ALARM_ID>', nargs='?',
action=NotEmptyAction, help='ID of the alarm to update.')
@common_alarm_arguments()
@utils.arg('--remove-time-constraint', action='append',
metavar='<Constraint names>',
@@ -576,7 +799,7 @@ def do_alarm_threshold_update(cc, args={}):
help='Name or list of names of the time constraints to remove.')
@utils.arg('--alarm_ids', action='append', metavar='<ALARM IDS>',
dest='combination_rule/alarm_ids',
help='List of alarm id.')
help='List of alarm IDs.')
@utils.arg('--operator', metavar='<OPERATOR>',
dest='combination_rule/operator',
help='Operator to compare with, one of: ' + str(
@@ -586,7 +809,7 @@ def do_alarm_threshold_update(cc, args={}):
help=('True if actions should be repeatedly notified '
'while alarm remains in target state.'))
def do_alarm_combination_update(cc, args={}):
'''Update an existing alarm based on state of other alarms.'''
"""Update an existing alarm based on state of other alarms."""
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields = utils.args_array_to_list_of_dicts(fields, 'time_constraints')
fields = utils.key_with_slash_to_nested_dict(fields)
@@ -599,23 +822,29 @@ def do_alarm_combination_update(cc, args={}):
_display_alarm(alarm)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True,
help='ID of the alarm to delete.')
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS,
dest='alarm_id_deprecated')
@utils.arg('alarm_id', metavar='<ALARM_ID>', nargs='?',
action=NotEmptyAction, help='ID of the alarm to delete.')
def do_alarm_delete(cc, args={}):
'''Delete an alarm.'''
"""Delete an alarm."""
try:
cc.alarms.delete(args.alarm_id)
except exc.HTTPNotFound:
raise exc.CommandError('Alarm not found: %s' % args.alarm_id)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True,
help='ID of the alarm state to set.')
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS,
dest='alarm_id_deprecated')
@utils.arg('alarm_id', metavar='<ALARM_ID>', nargs='?',
action=NotEmptyAction, help='ID of the alarm state to set.')
@utils.arg('--state', metavar='<STATE>', required=True,
help='State of the alarm, one of: ' + str(ALARM_STATES) +
'.')
def do_alarm_state_set(cc, args={}):
'''Set the state of an alarm.'''
"""Set the state of an alarm."""
try:
state = cc.alarms.set_state(args.alarm_id, args.state)
except exc.HTTPNotFound:
@@ -623,10 +852,13 @@ def do_alarm_state_set(cc, args={}):
utils.print_dict({'state': state}, wrap=72)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True,
help='ID of the alarm state to show.')
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS,
dest='alarm_id_deprecated')
@utils.arg('alarm_id', metavar='<ALARM_ID>', nargs='?',
action=NotEmptyAction, help='ID of the alarm state to show.')
def do_alarm_state_get(cc, args={}):
'''Get the state of an alarm.'''
"""Get the state of an alarm."""
try:
state = cc.alarms.get_state(args.alarm_id)
except exc.HTTPNotFound:
@@ -634,13 +866,16 @@ def do_alarm_state_get(cc, args={}):
utils.print_dict({'state': state}, wrap=72)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>', required=True,
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
action=obsoleted_by('alarm_id'), help=argparse.SUPPRESS,
dest='alarm_id_deprecated')
@utils.arg('alarm_id', metavar='<ALARM_ID>', nargs='?', action=NotEmptyAction,
help='ID of the alarm for which history is shown.')
@utils.arg('-q', '--query', metavar='<QUERY>',
help='key[op]data_type::value; list. data_type is optional, '
'but if supplied must be string, integer, float, or boolean.')
def do_alarm_history(cc, args={}):
'''Display the change history of an alarm.'''
"""Display the change history of an alarm."""
kwargs = dict(alarm_id=args.alarm_id,
q=options.cli_to_array(args.query))
try:
@@ -649,16 +884,20 @@ def do_alarm_history(cc, args={}):
raise exc.CommandError('Alarm not found: %s' % args.alarm_id)
field_labels = ['Type', 'Timestamp', 'Detail']
fields = ['type', 'timestamp', 'detail']
# We're using sortby=None as the alarm history returned from the Ceilometer
# is already sorted in the "the newer state is the earlier one in the
# list". If we'll pass any field as a sortby param, it'll be sorted in the
# ASC way by the PrettyTable
utils.print_list(history, fields, field_labels,
formatters={'detail': alarm_change_detail_formatter},
sortby=1)
sortby=None)
@utils.arg('-q', '--query', metavar='<QUERY>',
help='key[op]data_type::value; list. data_type is optional, '
'but if supplied must be string, integer, float, or boolean.')
def do_resource_list(cc, args={}):
'''List the resources.'''
"""List the resources."""
resources = cc.resources.list(q=options.cli_to_array(args.query))
field_labels = ['Resource ID', 'Source', 'User ID', 'Project ID']
@@ -667,10 +906,10 @@ def do_resource_list(cc, args={}):
sortby=1)
@utils.arg('-r', '--resource_id', metavar='<RESOURCE_ID>', required=True,
help='ID of the resource to show.')
@utils.arg('resource_id', metavar='<RESOURCE_ID>',
action=NotEmptyAction, help='ID of the resource to show.')
def do_resource_show(cc, args={}):
'''Show the resource.'''
"""Show the resource."""
try:
resource = cc.resources.get(args.resource_id)
except exc.HTTPNotFound:
@@ -686,24 +925,28 @@ def do_resource_show(cc, args={}):
help='key[op]data_type::value; list. data_type is optional, '
'but if supplied must be string, integer, float'
'or datetime.')
@utils.arg('--no-traits', dest='no_traits', action='store_true',
help='If specified, traits will not be printed.')
def do_event_list(cc, args={}):
'''List events.'''
"""List events."""
events = cc.events.list(q=options.cli_to_array(args.query))
field_labels = ['Message ID', 'Event Type', 'Generated', 'Traits']
fields = ['message_id', 'event_type', 'generated', 'traits']
if args.no_traits:
field_labels.pop()
fields.pop()
utils.print_list(events, fields, field_labels,
formatters={
'traits': utils.nested_list_of_dict_formatter('traits',
['name',
'type',
'value'])})
'traits': utils.nested_list_of_dict_formatter(
'traits', ['name', 'type', 'value']
)},
sortby=None)
@utils.arg('-m', '--message_id', metavar='<message_id>',
help='The id of the event. Should be a UUID',
required=True)
@utils.arg('message_id', metavar='<message_id>', action=NotEmptyAction,
help='The ID of the event. Should be a UUID.')
def do_event_show(cc, args={}):
'''Show a particular event.'''
"""Show a particular event."""
event = cc.events.get(args.message_id)
fields = ['event_type', 'generated', 'traits']
data = dict([(f, getattr(event, f, '')) for f in fields])
@@ -711,16 +954,16 @@ def do_event_show(cc, args={}):
def do_event_type_list(cc, args={}):
'''List event types.'''
"""List event types."""
event_types = cc.event_types.list()
utils.print_list(event_types, ['event_type'], ['Event Type'])
@utils.arg('-e', '--event_type', metavar='<EVENT_TYPE>',
help='Type of the event for which traits will be shown.',
required=True)
required=True, action=NotEmptyAction)
def do_trait_description_list(cc, args={}):
'''List trait info for an event type.'''
"""List trait info for an event type."""
trait_descriptions = cc.trait_descriptions.list(args.event_type)
field_labels = ['Trait Name', 'Data Type']
fields = ['name', 'type']
@@ -729,14 +972,12 @@ def do_trait_description_list(cc, args={}):
@utils.arg('-e', '--event_type', metavar='<EVENT_TYPE>',
help='Type of the event for which traits will listed.',
required=True)
required=True, action=NotEmptyAction)
@utils.arg('-t', '--trait_name', metavar='<TRAIT_NAME>',
help='The name of the trait to list.',
required=True)
required=True, action=NotEmptyAction)
def do_trait_list(cc, args={}):
'''List trait all traits with name <trait_name> for Event Type
<event_type>.
'''
"""List all traits with name <trait_name> for Event Type <event_type>."""
traits = cc.traits.list(args.event_type, args.trait_name)
field_labels = ['Trait Name', 'Value', 'Data Type']
fields = ['name', 'value', 'type']
@@ -753,7 +994,7 @@ def do_trait_list(cc, args={}):
@utils.arg('-l', '--limit', metavar='<LIMIT>',
help='Maximum number of samples to return.')
def do_query_samples(cc, args):
'''Query samples.'''
"""Query samples."""
fields = {'filter': args.filter,
'orderby': args.orderby,
'limit': args.limit}
@@ -780,7 +1021,7 @@ def do_query_samples(cc, args):
@utils.arg('-l', '--limit', metavar='<LIMIT>',
help='Maximum number of alarms to return.')
def do_query_alarms(cc, args):
'''Query Alarms.'''
"""Query Alarms."""
fields = {'filter': args.filter,
'orderby': args.orderby,
'limit': args.limit}
@@ -789,13 +1030,7 @@ def do_query_alarms(cc, args):
except exc.HTTPNotFound:
raise exc.CommandError('Alarms not found')
else:
field_labels = ['Alarm ID', 'Name', 'State', 'Enabled', 'Continuous',
'Alarm condition']
fields = ['alarm_id', 'name', 'state', 'enabled', 'repeat_actions',
'rule']
utils.print_list(alarms, fields, field_labels,
formatters={'rule': alarm_rule_formatter},
sortby=None)
_display_alarm_list(alarms, sortby=None)
@utils.arg('-f', '--filter', metavar='<FILTER>',
@@ -808,7 +1043,7 @@ def do_query_alarms(cc, args):
@utils.arg('-l', '--limit', metavar='<LIMIT>',
help='Maximum number of alarm history items to return.')
def do_query_alarm_history(cc, args):
'''Query Alarm History.'''
"""Query Alarm History."""
fields = {'filter': args.filter,
'orderby': args.orderby,
'limit': args.limit}

View File

@@ -11,6 +11,8 @@
# License for the specific language governing permissions and limitations
# under the License.
import six
from ceilometerclient.common import base
from ceilometerclient.v2 import options
@@ -23,7 +25,8 @@ class Statistics(base.Resource):
class StatisticsManager(base.Manager):
resource_class = Statistics
def _build_aggregates(self, aggregates):
@staticmethod
def _build_aggregates(aggregates):
url_aggregates = []
for aggregate in aggregates:
if 'param' in aggregate:
@@ -43,6 +46,8 @@ class StatisticsManager(base.Manager):
def list(self, meter_name, q=None, period=None, groupby=[], aggregates=[]):
p = ['period=%s' % period] if period else []
if isinstance(groupby, six.string_types):
groupby = [groupby]
p.extend(['groupby=%s' % g for g in groupby] if groupby else [])
p.extend(self._build_aggregates(aggregates))
return self._list(options.build_url(

View File

@@ -1,4 +1,3 @@
# -*- encoding: utf-8 -*-
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may

View File

@@ -1,4 +1,3 @@
# -*- encoding: utf-8 -*-
# Copyright 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may

59
doc/ext/gen_ref.py Normal file
View File

@@ -0,0 +1,59 @@
# -*- coding: utf-8 -*-
# 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 os
import sys
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.abspath(os.path.join(BASE_DIR, "..", "..", ".."))
sys.path.insert(0, ROOT)
sys.path.insert(0, BASE_DIR)
def gen_ref(ver, title, names):
refdir = os.path.join(BASE_DIR, "ref")
pkg = "ceilometerclient"
if ver:
pkg = "%s.%s" % (pkg, ver)
refdir = os.path.join(refdir, ver)
if not os.path.exists(refdir):
os.makedirs(refdir)
idxpath = os.path.join(refdir, "index.rst")
with open(idxpath, "w") as idx:
idx.write(("%(title)s\n"
"%(signs)s\n"
"\n"
".. toctree::\n"
" :maxdepth: 1\n"
"\n") % {"title": title, "signs": "=" * len(title)})
for name in names:
idx.write(" %s\n" % name)
rstpath = os.path.join(refdir, "%s.rst" % name)
with open(rstpath, "w") as rst:
rst.write(("%(title)s\n"
"%(signs)s\n"
"\n"
".. automodule:: %(pkg)s.%(name)s\n"
" :members:\n"
" :undoc-members:\n"
" :show-inheritance:\n"
" :noindex:\n")
% {"title": name.capitalize(),
"signs": "=" * len(name),
"pkg": pkg, "name": name})
gen_ref("", "Client Reference", ["client", "exc"])
gen_ref("v1", "Version 1 API Reference",
["stacks", "resources", "events", "actions",
"software_configs", "software_deployments"])

48
doc/source/api.rst Normal file
View File

@@ -0,0 +1,48 @@
The :mod:`ceilometerclient` Python API
======================================
.. module:: ceilometerclient
:synopsis: A client for the OpenStack Ceilometer API.
.. currentmodule:: ceilometerclient
Usage
-----
First create a client instance with your credentials::
>>> import ceilometerclient.client
>>> cclient = ceilometerclient.client.get_client(VERSION, os_username=USERNAME, os_password=PASSWORD, os_tenant_name=PROJECT_NAME, os_auth_url=AUTH_URL)
Here ``VERSION`` can be: ``1`` and ``2``.
Then call methods on its managers::
>>> cclient.meters.list()
[<Meter ...>, ...]
>>> cclient.samples.list()
[<Sample ...>, ...]
V2 client tips
++++++++++++++
Use queries to narrow your search (more info at `Ceilometer V2 API reference`__)::
>>> query = [dict(field='resource_id', op='eq', value='5a301761-f78b-46e2-8900-8b4f6fe6675a')]
>>> ceilometer.samples.list(meter_name='cpu_util', limit=10, q=query)
[<Sample ...>, ...]
__ http://docs.openstack.org/developer/ceilometer/webapi/v2.html#Query
Reference
---------
For more information, see the reference:
.. toctree::
:maxdepth: 2
ref/index
ref/v1/index
ref/v2/index

View File

@@ -1,13 +1,32 @@
# -*- coding: utf-8 -*-
# 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 os
execfile(os.path.join("..", "ext", "gen_ref.py"))
project = 'python-ceilometerclient'
gen_ref("", "Client Reference", ["client", "exc"])
gen_ref("v1", "Version 1 API Reference",
["meters"])
gen_ref("v2", "Version 2 API Reference",
["meters", "samples", "statistics", "resources", "query", "alarms",
"events", "event_types", "traits", "trait_descriptions"])
# -- General configuration ----------------------------------------------------
# Add any Sphinx extension module names here, as strings. They can be
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx']
extensions = ['sphinx.ext.autodoc', 'oslosphinx' ]
# autodoc generation is a bit aggressive and a nuisance when doing heavy
# text edit cycles.
@@ -39,7 +58,7 @@ pygments_style = 'sphinx'
# The theme to use for HTML and HTML Help pages. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
html_theme = 'nature'
#html_theme = 'nature'
# Output file base name for HTML help builder.
htmlhelp_basename = '%sdoc' % project
@@ -57,6 +76,3 @@ latex_documents = [
'manual'
),
]
# Example configuration for intersphinx: refer to the Python standard library.
intersphinx_mapping = {'http://docs.python.org/': None}

View File

@@ -1,32 +1,47 @@
Python API
==========
In order to use the python api directly, you must first obtain an auth token and identify which endpoint you wish to speak to. Once you have done so, you can use the API like so::
Python bindings to the OpenStack Ceilometer API
==================================================
>>> from ceilometerclient import Client
>>> ceilometer = Client('1', endpoint=OS_IMAGE_ENDPOINT, token=OS_AUTH_TOKEN)
...
This is a client for OpenStack Ceilometer API. There's :doc:`a Python API
<api>` (the :mod:`ceilometerclient` module), and a :doc:`command-line script
<shell>` (installed as :program:`ceilometer`). Each implements the entire
OpenStack Ceilometer API.
.. seealso::
Command-line Tool
=================
In order to use the CLI, you must provide your OpenStack username, password, tenant, and auth endpoint. Use the corresponding configuration options (``--os-username``, ``--os-password``, ``--os-tenant-id``, and ``--os-auth-url``) or set them in environment variables::
You may want to read the `OpenStack Ceilometer Developer Guide`__ -- the overview, at
least -- to get an idea of the concepts. By understanding the concepts
this library should make more sense.
export OS_USERNAME=user
export OS_PASSWORD=pass
export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b
export OS_AUTH_URL=http://auth.example.com:5000/v2.0
__ http://docs.openstack.org/developer/ceilometer/
The command line tool will attempt to reauthenticate using your provided credentials for every request. You can override this behavior by manually supplying an auth token using ``--os-image-url`` and ``--os-auth-token``. You can alternatively set these environment variables::
Contents:
export OS_IMAGE_URL=http://ceilometer.example.org:8004/
export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155
.. toctree::
:maxdepth: 2
Once you've configured your authentication parameters, you can run ``ceilometer help`` to see a complete listing of available commands.
shell
api
ref/index
ref/v1/index
ref/v2/index
releases
Contributing
============
Release Notes
=============
Code is hosted at `git.openstack.org`_. Submit bugs to the Ceilometer project on
`Launchpad`_. Submit code to the openstack/python-ceilometerclient project using
`Gerrit`_.
0.1.0
-----
* Initial release
.. _git.openstack.org: https://git.openstack.org/cgit/openstack/python-ceilometerclient
.. _Launchpad: https://launchpad.net/ceilometer
.. _Gerrit: http://docs.openstack.org/infra/manual/developers.html#development-workflow
Run tests with ``python setup.py test``.
Indices and tables
==================
* :ref:`genindex`
* :ref:`modindex`
* :ref:`search`

61
doc/source/shell.rst Normal file
View File

@@ -0,0 +1,61 @@
The :program:`ceilometer` shell utility
=========================================
.. program:: ceilometer
.. highlight:: bash
The :program:`ceilometer` shell utility interacts with OpenStack Ceilometer API
from the command line. It supports the entirety of the OpenStack Ceilometer API.
You'll need to provide :program:`ceilometer` with your OpenStack credentials.
You can do this with the :option:`--os-username`, :option:`--os-password`,
:option:`--os-tenant-id` and :option:`--os-auth-url` options, but it's easier to
just set them as environment variables:
.. envvar:: OS_USERNAME
Your OpenStack username.
.. envvar:: OS_PASSWORD
Your password.
.. envvar:: OS_TENANT_NAME
Project to work on.
.. envvar:: OS_AUTH_URL
The OpenStack auth server URL (keystone).
For example, in Bash you would use::
export OS_USERNAME=user
export OS_PASSWORD=pass
export OS_TENANT_NAME=myproject
export OS_AUTH_URL=http://auth.example.com:5000/v2.0
The command line tool will attempt to reauthenticate using your provided credentials
for every request. You can override this behavior by manually supplying an auth
token using :option:`--os-ceilometer-url` and :option:`--os-auth-token`. You can alternatively
set these environment variables::
export OS_CEILOMETER_URL=http://ceilometer.example.org:8777
export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155
From there, all shell commands take the form::
ceilometer <command> [arguments...]
Run :program:`ceilometer help` to get a full list of all possible commands,
and run :program:`ceilometer help <command>` to get detailed help for that
command.
V2 client tips
++++++++++++++
Use queries to narrow your search (more info at `Ceilometer V2 API reference`__)::
ceilometer sample-list --meter cpu_util --query 'resource_id=5a301761-f78b-46e2-8900-8b4f6fe6675a' --limit 10
__ http://docs.openstack.org/developer/ceilometer/webapi/v2.html#Query

View File

@@ -1,8 +1,8 @@
[DEFAULT]
# The list of modules to copy from openstack-common
module=apiclient
module=cliutils
module=importutils
module=install_venv_common
# The base module to hold the copy of openstack.common

View File

@@ -1,6 +1,14 @@
pbr>=0.6,<1.0
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
pbr>=0.6,!=0.7,<1.0
argparse
iso8601>=0.1.9
oslo.i18n>=1.3.0 # Apache-2.0
oslo.serialization>=1.2.0 # Apache-2.0
oslo.utils>=1.2.0 # Apache-2.0
PrettyTable>=0.7,<0.8
python-keystoneclient>=0.6.0
six>=1.5.2
python-keystoneclient>=1.1.0
requests>=2.2.0,!=2.4.0
six>=1.7.0
stevedore>=1.1.0 # Apache-2.0

View File

@@ -17,6 +17,14 @@
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
# In python < 2.7.4, a lazy loading of package `pbr` will break
# setuptools if some other modules registered functions in `atexit`.
# solution from: http://bugs.python.org/issue15881#msg170215
try:
import multiprocessing # noqa
except ImportError:
pass
setuptools.setup(
setup_requires=['pbr'],
pbr=True)

View File

@@ -1,10 +1,14 @@
# The order of packages is significant, because pip processes them in the order
# of appearance. Changing the order has an impact on the overall integration
# process, which may cause wedges in the gate later.
# Hacking already pins down pep8, pyflakes and flake8
hacking>=0.8.0,<0.9
hacking>=0.10.0,<0.11
coverage>=3.6
discover
fixtures>=0.3.14
mock>=1.0
oslosphinx>=2.2.0 # Apache-2.0
python-subunit>=0.0.18
sphinx>=1.1.2,<1.2
sphinx>=1.1.2,!=1.2.0,!=1.3b1,<1.3
testrepository>=0.0.18
testtools>=0.9.34
testtools>=0.9.36,!=1.2.0

View File

@@ -1,5 +1,3 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# All Rights Reserved.
@@ -67,7 +65,6 @@ def main(argv):
install.check_dependencies()
install.create_virtualenv(no_site_packages=options.no_site_packages)
install.install_dependencies()
install.post_process()
print_help(venv, root)
if __name__ == '__main__':

View File

@@ -125,7 +125,7 @@ class InstallVenv(object):
parser.add_option('-n', '--no-site-packages',
action='store_true',
help="Do not inherit packages from global Python "
"install")
"install.")
return parser.parse_args(argv[1:])[0]

12
tox.ini
View File

@@ -10,16 +10,11 @@ setenv = VIRTUAL_ENV={envdir}
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands =
python setup.py testr --testr-args='{posargs}'
python setup.py testr --slowest --testr-args='{posargs}'
[tox:jenkins]
downloadcache = ~/cache/pip
[testenv:pypy]
deps = setuptools<3.2
-r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
[testenv:pep8]
commands = flake8
@@ -29,7 +24,10 @@ commands = python setup.py testr --coverage --testr-args='{posargs}'
[testenv:venv]
commands = {posargs}
[testenv:docs]
commands=
python setup.py build_sphinx
[flake8]
ignore = None
show-source = True
exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools