Merge tag '1.0.1' into debian/unstable

python-ceilometerclient 1.0.1 release
This commit is contained in:
Thomas Goirand
2013-07-26 11:20:57 +08:00
49 changed files with 1397 additions and 856 deletions

2
.gitignore vendored
View File

@@ -1,5 +1,7 @@
.coverage
.venv
.testrepository
subunit.log
*,cover
cover
*.pyc

4
.testr.conf Normal file
View File

@@ -0,0 +1,4 @@
[DEFAULT]
test_command=OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 ${PYTHON:-python} -m subunit.run discover -t ./ ./tests $LISTOPT $IDOPTION
test_id_option=--load-list $IDFILE
test_list_option=--list

View File

@@ -1,10 +1,8 @@
include AUTHORS
include HACKING
include LICENSE
include README.rst
include README.md
include ChangeLog
include tox.ini
include ceilometerclient/versioninfo
recursive-include doc *
recursive-include tests *
recursive-include tools *

View File

@@ -9,23 +9,13 @@
# 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 inspect
import os
__all__ = ['__version__']
def _get_ceilometerclient_version():
"""Read version from versioninfo file."""
mod_abspath = inspect.getabsfile(inspect.currentframe())
ceilometerclient_path = os.path.dirname(mod_abspath)
version_path = os.path.join(ceilometerclient_path, 'versioninfo')
import pbr.version
if os.path.exists(version_path):
version = open(version_path).read().strip()
else:
version = "Unknown, couldn't find versioninfo file at %s"\
% version_path
return version
__version__ = _get_ceilometerclient_version()
version_info = pbr.version.VersionInfo('python-ceilometerclient')
try:
__version__ = version_info.version_string()
except AttributeError:
__version__ = None

View File

@@ -11,6 +11,83 @@
# under the License.
from ceilometerclient.common import utils
from keystoneclient.v2_0 import client as ksclient
def _get_ksclient(**kwargs):
"""Get an endpoint and auth token from Keystone.
:param kwargs: keyword args containing credentials:
* username: name of user
* password: user's password
* auth_url: endpoint to authenticate against
* insecure: allow insecure SSL (no cert verification)
* tenant_{name|id}: name or ID of tenant
"""
return ksclient.Client(username=kwargs.get('username'),
password=kwargs.get('password'),
tenant_id=kwargs.get('tenant_id'),
tenant_name=kwargs.get('tenant_name'),
auth_url=kwargs.get('auth_url'),
insecure=kwargs.get('insecure'))
def _get_endpoint(client, **kwargs):
"""Get an endpoint using the provided keystone client."""
return client.service_catalog.url_for(
service_type=kwargs.get('service_type') or 'metering',
endpoint_type=kwargs.get('endpoint_type') or 'publicURL')
def get_client(api_version, **kwargs):
"""Get an authtenticated client, based on the credentials
in the keyword args.
:param api_version: the API version to use ('1' or '2')
:param kwargs: keyword args containing credentials, either:
* os_auth_token: pre-existing token to re-use
* ceilometer_url: ceilometer API endpoint
or:
* os_username: name of user
* os_password: user's password
* os_auth_url: endpoint to authenticate against
* insecure: allow insecure SSL (no cert verification)
* os_tenant_{name|id}: name or ID of tenant
"""
if kwargs.get('os_auth_token') and kwargs.get('ceilometer_url'):
token = kwargs.get('os_auth_token')
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 = {
'username': kwargs.get('os_username'),
'password': kwargs.get('os_password'),
'tenant_id': kwargs.get('os_tenant_id'),
'tenant_name': kwargs.get('os_tenant_name'),
'auth_url': kwargs.get('os_auth_url'),
'service_type': kwargs.get('os_service_type'),
'endpoint_type': kwargs.get('os_endpoint_type'),
'insecure': kwargs.get('insecure'),
}
_ksclient = _get_ksclient(**ks_kwargs)
token = kwargs.get('os_auth_token') or _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'),
'ca_file': kwargs.get('ca_file'),
'cert_file': kwargs.get('cert_file'),
'key_file': kwargs.get('key_file'),
}
return Client(api_version, endpoint, **cli_kwargs)
def Client(version, *args, **kwargs):

View File

@@ -29,9 +29,8 @@ except NameError:
def getid(obj):
"""
Abstracts the common pattern of allowing both an object or an object's ID
(UUID) as a parameter when dealing with relationships.
"""Abstracts the common pattern of allowing both an object or an
object's ID (UUID) as a parameter when dealing with relationships.
"""
try:
return obj.id
@@ -40,16 +39,21 @@ def getid(obj):
class Manager(object):
"""
Managers interact with a particular type of API (servers, flavors, images,
etc.) and provide CRUD operations for them.
"""Managers interact with a particular type of API
(samples, meters, alarms, etc.) and provide CRUD operations for them.
"""
resource_class = None
def __init__(self, api):
self.api = api
def _list(self, url, response_key=None, obj_class=None, body=None):
def _create(self, url, body):
resp, body = self.api.json_request('POST', url, body=body)
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)
if obj_class is None:
@@ -62,21 +66,22 @@ class Manager(object):
return []
else:
data = body
if expect_single:
data = [data]
return [obj_class(self, res, loaded=True) for res in data if res]
def _delete(self, url):
self.api.raw_request('DELETE', url)
def _update(self, url, body, response_key=None):
resp, body = self.api.json_request('PUT', url, body=body)
# PUT requests may not return a body
if body:
return self.resource_class(self, body[response_key])
return self.resource_class(self, body)
def _delete(self, url):
self.api.raw_request('DELETE', url)
class Resource(object):
"""
A resource represents a particular instance of an object (tenant, user,
"""A resource represents a particular instance of an object (tenant, user,
etc). This is pretty much just a bag for attributes.
:param manager: Manager object

View File

@@ -58,7 +58,8 @@ class HTTPClient(object):
parts = urlparse.urlparse(endpoint)
_args = (parts.hostname, parts.port, parts.path)
_kwargs = {'timeout': float(kwargs.get('timeout', 600))}
_kwargs = {'timeout': (float(kwargs.get('timeout'))
if kwargs.get('timeout') else 600)}
if parts.scheme == 'https':
_class = VerifiedHTTPSConnection
@@ -77,7 +78,7 @@ class HTTPClient(object):
def get_connection(self):
_class = self.connection_params[0]
try:
return _class(*self.connection_params[1],
return _class(*self.connection_params[1][0:2],
**self.connection_params[2])
except httplib.InvalidURL:
raise exc.InvalidEndpoint()
@@ -124,7 +125,7 @@ class HTTPClient(object):
return '%s/%s' % (base_url.rstrip('/'), url.lstrip('/'))
def _http_request(self, url, method, **kwargs):
""" Send an http request with the specified characteristics.
"""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.
@@ -180,8 +181,12 @@ class HTTPClient(object):
kwargs['body'] = json.dumps(kwargs['body'])
resp, body_iter = self._http_request(url, method, **kwargs)
content_type = resp.getheader('content-type', None)
if 'application/json' in 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)
@@ -220,8 +225,7 @@ class VerifiedHTTPSConnection(httplib.HTTPSConnection):
self.insecure = insecure
def connect(self):
"""
Connect to a host on a given (SSL) port.
"""Connect to a host on a given (SSL) port.
If ca_file is pointing somewhere, use it to check Server Certificate.
Redefined/copied and extended from httplib.py:1105 (Python 2.6.x).
@@ -249,7 +253,7 @@ class VerifiedHTTPSConnection(httplib.HTTPSConnection):
@staticmethod
def get_system_ca_file():
""""Return path to system default 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',

View File

@@ -13,10 +13,9 @@
# License for the specific language governing permissions and limitations
# under the License.
import errno
import hashlib
import os
import sys
import textwrap
import uuid
import prettytable
@@ -40,7 +39,8 @@ def pretty_choice_list(l):
def print_list(objs, fields, field_labels, formatters={}, sortby=0):
pt = prettytable.PrettyTable([f for f in field_labels], caching=False)
pt = prettytable.PrettyTable([f for f in field_labels],
caching=False, print_empty=False)
pt.align = 'l'
for o in objs:
@@ -49,22 +49,33 @@ def print_list(objs, fields, field_labels, formatters={}, sortby=0):
if field in formatters:
row.append(formatters[field](o))
else:
data = getattr(o, field, None) or ''
data = getattr(o, field, '')
row.append(data)
pt.add_row(row)
print pt.get_string(sortby=field_labels[sortby])
def print_dict(d, formatters={}):
pt = prettytable.PrettyTable(['Property', 'Value'], caching=False)
def print_dict(d, dict_property="Property", wrap=0):
pt = prettytable.PrettyTable([dict_property, 'Value'],
caching=False, print_empty=False)
pt.align = 'l'
for field in d.keys():
if field in formatters:
pt.add_row([field, formatters[field](d[field])])
for k, v in d.iteritems():
# convert dict to str to check length
if isinstance(v, dict):
v = str(v)
if wrap > 0:
v = textwrap.fill(str(v), wrap)
# if value has a newline, add in multiple rows
# e.g. fault with stacktrace
if v and isinstance(v, basestring) and r'\n' in v:
lines = v.strip().split(r'\n')
col1 = k
for line in lines:
pt.add_row([col1, line])
col1 = ''
else:
pt.add_row([field, d[field]])
print pt.get_string(sortby='Property')
pt.add_row([k, v])
print pt.get_string()
def find_resource(manager, name_or_id):

View File

@@ -23,7 +23,7 @@ class BaseException(Exception):
class CommandError(BaseException):
"""Invalid usage of CLI"""
"""Invalid usage of CLI."""
class InvalidEndpoint(BaseException):
@@ -35,11 +35,11 @@ class CommunicationError(BaseException):
class ClientException(Exception):
"""DEPRECATED"""
"""DEPRECATED."""
class HTTPException(ClientException):
"""Base exception for all HTTP-derived exceptions"""
"""Base exception for all HTTP-derived exceptions."""
code = 'N/A'
def __init__(self, details=None):
@@ -60,7 +60,7 @@ class HTTPMultipleChoices(HTTPException):
class BadRequest(HTTPException):
"""DEPRECATED"""
"""DEPRECATED."""
code = 400
@@ -69,7 +69,7 @@ class HTTPBadRequest(BadRequest):
class Unauthorized(HTTPException):
"""DEPRECATED"""
"""DEPRECATED."""
code = 401
@@ -78,7 +78,7 @@ class HTTPUnauthorized(Unauthorized):
class Forbidden(HTTPException):
"""DEPRECATED"""
"""DEPRECATED."""
code = 403
@@ -87,7 +87,7 @@ class HTTPForbidden(Forbidden):
class NotFound(HTTPException):
"""DEPRECATED"""
"""DEPRECATED."""
code = 404
@@ -100,7 +100,7 @@ class HTTPMethodNotAllowed(HTTPException):
class Conflict(HTTPException):
"""DEPRECATED"""
"""DEPRECATED."""
code = 409
@@ -109,7 +109,7 @@ class HTTPConflict(Conflict):
class OverLimit(HTTPException):
"""DEPRECATED"""
"""DEPRECATED."""
code = 413
@@ -130,7 +130,7 @@ class HTTPBadGateway(HTTPException):
class ServiceUnavailable(HTTPException):
"""DEPRECATED"""
"""DEPRECATED."""
code = 503
@@ -154,10 +154,10 @@ def from_response(response):
class NoTokenLookupException(Exception):
"""DEPRECATED"""
"""DEPRECATED."""
pass
class EndpointNotFound(Exception):
"""DEPRECATED"""
"""DEPRECATED."""
pass

View File

@@ -1,6 +1,6 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# Copyright 2011 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may

View File

@@ -1,335 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2011 OpenStack LLC.
# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
# 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.
"""
Utilities with minimum-depends for use in setup.py
"""
import email
import os
import re
import subprocess
import sys
from setuptools.command import sdist
def parse_mailmap(mailmap='.mailmap'):
mapping = {}
if os.path.exists(mailmap):
with open(mailmap, 'r') as fp:
for l in fp:
try:
canonical_email, alias = re.match(
r'[^#]*?(<.+>).*(<.+>).*', l).groups()
except AttributeError:
continue
mapping[alias] = canonical_email
return mapping
def canonicalize_emails(changelog, mapping):
"""Takes in a string and an email alias mapping and replaces all
instances of the aliases in the string with their real email.
"""
for alias, email_address in mapping.iteritems():
changelog = changelog.replace(alias, email_address)
return changelog
# Get requirements from the first file that exists
def get_reqs_from_files(requirements_files):
for requirements_file in requirements_files:
if os.path.exists(requirements_file):
with open(requirements_file, 'r') as fil:
return fil.read().split('\n')
return []
def parse_requirements(requirements_files=['requirements.txt',
'tools/pip-requires']):
requirements = []
for line in get_reqs_from_files(requirements_files):
# For the requirements list, we need to inject only the portion
# after egg= so that distutils knows the package it's looking for
# such as:
# -e git://github.com/openstack/nova/master#egg=nova
if re.match(r'\s*-e\s+', line):
requirements.append(re.sub(r'\s*-e\s+.*#egg=(.*)$', r'\1',
line))
# such as:
# http://github.com/openstack/nova/zipball/master#egg=nova
elif re.match(r'\s*https?:', line):
requirements.append(re.sub(r'\s*https?:.*#egg=(.*)$', r'\1',
line))
# -f lines are for index locations, and don't get used here
elif re.match(r'\s*-f\s+', line):
pass
# argparse is part of the standard library starting with 2.7
# adding it to the requirements list screws distro installs
elif line == 'argparse' and sys.version_info >= (2, 7):
pass
else:
requirements.append(line)
return requirements
def parse_dependency_links(requirements_files=['requirements.txt',
'tools/pip-requires']):
dependency_links = []
# dependency_links inject alternate locations to find packages listed
# in requirements
for line in get_reqs_from_files(requirements_files):
# skip comments and blank lines
if re.match(r'(\s*#)|(\s*$)', line):
continue
# lines with -e or -f need the whole line, minus the flag
if re.match(r'\s*-[ef]\s+', line):
dependency_links.append(re.sub(r'\s*-[ef]\s+', '', line))
# lines that are only urls can go in unmolested
elif re.match(r'\s*https?:', line):
dependency_links.append(line)
return dependency_links
def _run_shell_command(cmd, throw_on_error=False):
if os.name == 'nt':
output = subprocess.Popen(["cmd.exe", "/C", cmd],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
else:
output = subprocess.Popen(["/bin/sh", "-c", cmd],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
if output.returncode and throw_on_error:
raise Exception("%s returned %d" % cmd, output.returncode)
out = output.communicate()
if len(out) == 0:
return None
if len(out[0].strip()) == 0:
return None
return out[0].strip()
def write_git_changelog():
"""Write a changelog based on the git changelog."""
new_changelog = 'ChangeLog'
if not os.getenv('SKIP_WRITE_GIT_CHANGELOG'):
if os.path.isdir('.git'):
git_log_cmd = 'git log --stat'
changelog = _run_shell_command(git_log_cmd)
mailmap = parse_mailmap()
with open(new_changelog, "w") as changelog_file:
changelog_file.write(canonicalize_emails(changelog, mailmap))
else:
open(new_changelog, 'w').close()
def generate_authors():
"""Create AUTHORS file using git commits."""
jenkins_email = 'jenkins@review.(openstack|stackforge).org'
old_authors = 'AUTHORS.in'
new_authors = 'AUTHORS'
if not os.getenv('SKIP_GENERATE_AUTHORS'):
if os.path.isdir('.git'):
# don't include jenkins email address in AUTHORS file
git_log_cmd = ("git log --format='%aN <%aE>' | sort -u | "
"egrep -v '" + jenkins_email + "'")
changelog = _run_shell_command(git_log_cmd)
mailmap = parse_mailmap()
with open(new_authors, 'w') as new_authors_fh:
new_authors_fh.write(canonicalize_emails(changelog, mailmap))
if os.path.exists(old_authors):
with open(old_authors, "r") as old_authors_fh:
new_authors_fh.write('\n' + old_authors_fh.read())
else:
open(new_authors, 'w').close()
_rst_template = """%(heading)s
%(underline)s
.. automodule:: %(module)s
:members:
:undoc-members:
:show-inheritance:
"""
def get_cmdclass():
"""Return dict of commands to run from setup.py."""
cmdclass = dict()
def _find_modules(arg, dirname, files):
for filename in files:
if filename.endswith('.py') and filename != '__init__.py':
arg["%s.%s" % (dirname.replace('/', '.'),
filename[:-3])] = True
class LocalSDist(sdist.sdist):
"""Builds the ChangeLog and Authors files from VC first."""
def run(self):
write_git_changelog()
generate_authors()
# sdist.sdist is an old style class, can't use super()
sdist.sdist.run(self)
cmdclass['sdist'] = LocalSDist
# If Sphinx is installed on the box running setup.py,
# enable setup.py to build the documentation, otherwise,
# just ignore it
try:
from sphinx.setup_command import BuildDoc
class LocalBuildDoc(BuildDoc):
builders = ['html', 'man']
def generate_autoindex(self):
print "**Autodocumenting from %s" % os.path.abspath(os.curdir)
modules = {}
option_dict = self.distribution.get_option_dict('build_sphinx')
source_dir = os.path.join(option_dict['source_dir'][1], 'api')
if not os.path.exists(source_dir):
os.makedirs(source_dir)
for pkg in self.distribution.packages:
if '.' not in pkg:
os.path.walk(pkg, _find_modules, modules)
module_list = modules.keys()
module_list.sort()
autoindex_filename = os.path.join(source_dir, 'autoindex.rst')
with open(autoindex_filename, 'w') as autoindex:
autoindex.write(""".. toctree::
:maxdepth: 1
""")
for module in module_list:
output_filename = os.path.join(source_dir,
"%s.rst" % module)
heading = "The :mod:`%s` Module" % module
underline = "=" * len(heading)
values = dict(module=module, heading=heading,
underline=underline)
print "Generating %s" % output_filename
with open(output_filename, 'w') as output_file:
output_file.write(_rst_template % values)
autoindex.write(" %s.rst\n" % module)
def run(self):
if not os.getenv('SPHINX_DEBUG'):
self.generate_autoindex()
for builder in self.builders:
self.builder = builder
self.finalize_options()
self.project = self.distribution.get_name()
self.version = self.distribution.get_version()
self.release = self.distribution.get_version()
BuildDoc.run(self)
class LocalBuildLatex(LocalBuildDoc):
builders = ['latex']
cmdclass['build_sphinx'] = LocalBuildDoc
cmdclass['build_sphinx_latex'] = LocalBuildLatex
except ImportError:
pass
return cmdclass
def _get_revno():
"""Return the number of commits since the most recent tag.
We use git-describe to find this out, but if there are no
tags then we fall back to counting commits since the beginning
of time.
"""
describe = _run_shell_command("git describe --always")
if "-" in describe:
return describe.rsplit("-", 2)[-2]
# no tags found
revlist = _run_shell_command("git rev-list --abbrev-commit HEAD")
return len(revlist.splitlines())
def get_version_from_git(pre_version):
"""Return a version which is equal to the tag that's on the current
revision if there is one, or tag plus number of additional revisions
if the current revision has no tag."""
if os.path.isdir('.git'):
if pre_version:
try:
return _run_shell_command(
"git describe --exact-match",
throw_on_error=True).replace('-', '.')
except Exception:
sha = _run_shell_command("git log -n1 --pretty=format:%h")
return "%s.a%s.g%s" % (pre_version, _get_revno(), sha)
else:
return _run_shell_command(
"git describe --always").replace('-', '.')
return None
def get_version_from_pkg_info(package_name):
"""Get the version from PKG-INFO file if we can."""
try:
pkg_info_file = open('PKG-INFO', 'r')
except (IOError, OSError):
return None
try:
pkg_info = email.message_from_file(pkg_info_file)
except email.MessageError:
return None
# Check to make sure we're in our own dir
if pkg_info.get('Name', None) != package_name:
return None
return pkg_info.get('Version', None)
def get_version(package_name, pre_version=None):
"""Get the version of the project. First, try getting it from PKG-INFO, if
it exists. If it does, that means we're in a distribution tarball or that
install has happened. Otherwise, if there is no PKG-INFO file, pull the
version from git.
We do not support setup.py version sanity in git archive tarballs, nor do
we support packagers directly sucking our git repo into theirs. We expect
that a source tarball be made from our git repo - or that if someone wants
to make a source tarball from a fork of our repo with additional tags in it
that they understand and desire the results of doing that.
"""
version = os.environ.get("OSLO_PACKAGE_VERSION", None)
if version:
return version
version = get_version_from_pkg_info(package_name)
if version:
return version
version = get_version_from_git(pre_version)
if version:
return version
raise Exception("Versioning for this project requires either an sdist"
" tarball, or access to an upstream git repository.")

View File

@@ -1,94 +0,0 @@
# Copyright 2012 OpenStack LLC
# Copyright 2012-2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Utilities for consuming the version from pkg_resources.
"""
import pkg_resources
class VersionInfo(object):
def __init__(self, package):
"""Object that understands versioning for a package
:param package: name of the python package, such as glance, or
python-glanceclient
"""
self.package = package
self.release = None
self.version = None
self._cached_version = None
def __str__(self):
"""Make the VersionInfo object behave like a string."""
return self.version_string()
def __repr__(self):
"""Include the name."""
return "VersionInfo(%s:%s)" % (self.package, self.version_string())
def _get_version_from_pkg_resources(self):
"""Get the version of the package from the pkg_resources record
associated with the package."""
try:
requirement = pkg_resources.Requirement.parse(self.package)
provider = pkg_resources.get_provider(requirement)
return provider.version
except pkg_resources.DistributionNotFound:
# The most likely cause for this is running tests in a tree with
# produced from a tarball where the package itself has not been
# installed into anything. Check for a PKG-INFO file.
from ceilometerclient.openstack.common import setup
return setup.get_version_from_pkg_info(self.package)
def release_string(self):
"""Return the full version of the package including suffixes indicating
VCS status.
"""
if self.release is None:
self.release = self._get_version_from_pkg_resources()
return self.release
def version_string(self):
"""Return the short version minus any alpha/beta tags."""
if self.version is None:
parts = []
for part in self.release_string().split('.'):
if part[0].isdigit():
parts.append(part)
else:
break
self.version = ".".join(parts)
return self.version
# Compatibility functions
canonical_version_string = version_string
version_string_with_vcs = release_string
def cached_version_string(self, prefix=""):
"""Generate an object which will expand in a string context to
the results of version_string(). We do this so that don't
call into pkg_resources every time we start up a program when
passing version information into the CONF constructor, but
rather only do the calculation when and if a version is requested
"""
if not self._cached_version:
self._cached_version = "%s%s" % (prefix,
self.version_string())
return self._cached_version

View File

@@ -19,13 +19,10 @@ import httplib2
import logging
import sys
from keystoneclient.v2_0 import client as ksclient
from ceilometerclient import exc
from ceilometerclient import client as ceilometerclient
import ceilometerclient
from ceilometerclient import client as ceiloclient
from ceilometerclient.common import utils
logger = logging.getLogger(__name__)
from ceilometerclient import exc
class CeilometerShell(object):
@@ -46,6 +43,10 @@ class CeilometerShell(object):
help=argparse.SUPPRESS,
)
parser.add_argument('--version',
action='version',
version=ceilometerclient.__version__)
parser.add_argument('-d', '--debug',
default=bool(utils.env('CEILOMETERCLIENT_DEBUG')),
action='store_true',
@@ -143,9 +144,9 @@ class CeilometerShell(object):
parser.add_argument('--ceilometer-api-version',
default=utils.env(
'CEILOMETER_API_VERSION', default='1'),
'CEILOMETER_API_VERSION', default='2'),
help='Defaults to env[CEILOMETER_API_VERSION] '
'or 1')
'or 2')
parser.add_argument('--ceilometer_api_version',
help=argparse.SUPPRESS)
@@ -197,28 +198,6 @@ class CeilometerShell(object):
subparser.add_argument(*args, **kwargs)
subparser.set_defaults(func=callback)
def _get_ksclient(self, **kwargs):
"""Get an endpoint and auth token from Keystone.
:param username: name of user
:param password: user's password
:param tenant_id: unique identifier of tenant
:param tenant_name: name of tenant
:param auth_url: endpoint to authenticate against
"""
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'),
insecure=kwargs.get('insecure'))
def _get_endpoint(self, 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 _setup_debugging(self, debug):
if debug:
logging.basicConfig(
@@ -252,10 +231,7 @@ class CeilometerShell(object):
self.do_help(args)
return 0
if args.os_auth_token and args.ceilometer_url:
token = args.os_auth_token
endpoint = args.ceilometer_url
else:
if not (args.os_auth_token and args.ceilometer_url):
if not args.os_username:
raise exc.CommandError("You must provide a username via "
"either --os-username or via "
@@ -275,32 +251,8 @@ class CeilometerShell(object):
raise exc.CommandError("You must provide an auth url via "
"either --os-auth-url or via "
"env[OS_AUTH_URL]")
kwargs = {
'username': args.os_username,
'password': args.os_password,
'tenant_id': args.os_tenant_id,
'tenant_name': args.os_tenant_name,
'auth_url': args.os_auth_url,
'service_type': args.os_service_type,
'endpoint_type': args.os_endpoint_type,
'insecure': args.insecure
}
_ksclient = self._get_ksclient(**kwargs)
token = args.os_auth_token or _ksclient.auth_token
endpoint = args.ceilometer_url or \
self._get_endpoint(_ksclient, **kwargs)
kwargs = {
'token': token,
'insecure': args.insecure,
'timeout': args.timeout,
'ca_file': args.ca_file,
'cert_file': args.cert_file,
'key_file': args.key_file,
}
client = ceilometerclient.Client(api_version, endpoint, **kwargs)
client = ceiloclient.get_client(api_version, **(args.__dict__))
try:
args.func(client, args)
@@ -310,9 +262,7 @@ class CeilometerShell(object):
@utils.arg('command', metavar='<subcommand>', nargs='?',
help='Display help for <subcommand>')
def do_help(self, args):
"""
Display help about this program or one of its subcommands.
"""
"""Display help about this program or one of its subcommands."""
if getattr(args, 'command', None):
if args.command in self.subcommands:
self.subcommands[args.command].print_help()
@@ -334,7 +284,7 @@ def main():
try:
CeilometerShell().main(sys.argv[1:])
except Exception, e:
except Exception as e:
print >> sys.stderr, e
sys.exit(1)

View File

@@ -13,4 +13,4 @@
# License for the specific language governing permissions and limitations
# under the License.
from ceilometerclient.v1.client import Client
from ceilometerclient.v1.client import Client # noqa

View File

@@ -28,7 +28,7 @@ class Client(http.HTTPClient):
"""
def __init__(self, *args, **kwargs):
""" Initialize a new client for the Ceilometer v1 API. """
"""Initialize a new client for the Ceilometer v1 API."""
super(Client, self).__init__(*args, **kwargs)
self.meters = meters.MeterManager(self)
self.samples = meters.SampleManager(self)

View File

@@ -36,7 +36,7 @@ import ceilometerclient.exc as exc
help='ISO date in UTC which limits events by '
'timestamp <= this value')
def do_sample_list(cc, args):
'''List the samples for this meters'''
'''List the samples for this meters.'''
fields = {'counter_name': args.counter_name,
'resource_id': args.resource_id,
'user_id': args.user_id,
@@ -84,7 +84,7 @@ def do_meter_list(cc, args={}):
@utils.arg('-s', '--source', metavar='<SOURCE>',
help='ID of the resource to show projects for.')
def do_user_list(cc, args={}):
'''List the users'''
'''List the users.'''
kwargs = {'source': args.source}
users = cc.users.list(**kwargs)
field_labels = ['User 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

@@ -13,4 +13,4 @@
# License for the specific language governing permissions and limitations
# under the License.
from ceilometerclient.v2.client import Client
from ceilometerclient.v2.client import Client # noqa

View File

@@ -0,0 +1,72 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 Red Hat, Inc
#
# Author: Eoghan Glynn <eglynn@redhat.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
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from ceilometerclient.common import base
from ceilometerclient.v2 import options
UPDATABLE_ATTRIBUTES = [
'description',
'period',
'evaluation_periods',
'state',
'enabled',
'counter_name',
'statistic',
'comparison_operator',
'threshold',
'alarm_actions',
'ok_actions',
'insufficient_data_actions',
]
CREATION_ATTRIBUTES = UPDATABLE_ATTRIBUTES + ['name', 'project_id', 'user_id']
class Alarm(base.Resource):
def __repr__(self):
return "<Alarm %s>" % self._info
class AlarmManager(base.Manager):
resource_class = Alarm
@staticmethod
def _path(id=None):
return '/v2/alarms/%s' % id if id else '/v2/alarms'
def list(self, q=None):
return self._list(options.build_url(self._path(), q))
def get(self, alarm_id):
try:
return self._list(self._path(alarm_id), expect_single=True)[0]
except IndexError:
return None
def create(self, **kwargs):
new = dict((key, value) for (key, value) in kwargs.items()
if key in CREATION_ATTRIBUTES)
return self._create(self._path(), new)
def update(self, alarm_id, **kwargs):
updated = dict((key, value) for (key, value) in kwargs.items()
if key in UPDATABLE_ATTRIBUTES)
return self._update(self._path(alarm_id), updated)
def delete(self, alarm_id):
return self._delete(self._path(alarm_id))

View File

@@ -14,6 +14,7 @@
# under the License.
from ceilometerclient.common import http
from ceilometerclient.v2 import alarms
from ceilometerclient.v2 import meters
from ceilometerclient.v2 import resources
from ceilometerclient.v2 import samples
@@ -31,9 +32,10 @@ class Client(http.HTTPClient):
"""
def __init__(self, *args, **kwargs):
""" Initialize a new client for the Ceilometer v1 API. """
"""Initialize a new client for the Ceilometer v1 API."""
super(Client, self).__init__(*args, **kwargs)
self.meters = meters.MeterManager(self)
self.samples = samples.SampleManager(self)
self.statistics = statistics.StatisticsManager(self)
self.resources = resources.ResourceManager(self)
self.alarms = alarms.AlarmManager(self)

View File

@@ -15,14 +15,15 @@ import re
import urllib
def build_url(path, q):
'''
This converts from a list of dict's to what the rest api needs
so from:
"[{field=this,op=le,value=34},{field=that,op=eq,value=foo}]"
def build_url(path, q, params=None):
'''This converts from a list of dicts and a list of params to
what the rest api needs, so from:
"[{field=this,op=le,value=34},{field=that,op=eq,value=foo}],
['foo=bar','sna=fu']"
to:
"?q.field=this&q.op=le&q.value=34&
q.field=that&q.op=eq&q.value=foo"
q.field=that&q.op=eq&q.value=foo&
foo=bar&sna=fu"
'''
if q:
query_params = {'q.field': [],
@@ -35,12 +36,15 @@ def build_url(path, q):
path += "?" + urllib.urlencode(query_params, doseq=True)
if params:
for p in params:
path += '&%s' % p
return path
def cli_to_array(cli_query):
'''
This converts from the cli list of queries to what is required
'''This converts from the cli list of queries to what is required
by the python api.
so from:
"this<=34;that=foo"

View File

@@ -29,3 +29,10 @@ class ResourceManager(base.Manager):
def list(self, q=None):
path = '/v2/resources'
return self._list(options.build_url(path, q))
def get(self, resource_id):
path = '/v2/resources/%s' % resource_id
try:
return self._list(path, expect_single=True)[0]
except IndexError:
return None

View File

@@ -17,18 +17,26 @@
# under the License.
from ceilometerclient.common import utils
from ceilometerclient import exc
from ceilometerclient.v2 import options
import ceilometerclient.exc as exc
ALARM_STATES = ['ok', 'alarm', 'insufficient_data']
ALARM_OPERATORS = ['lt', 'le', 'eq', 'ne', 'ge', 'gt']
STATISTICS = ['max', 'min', 'avg', 'sum', 'count']
@utils.arg('-q', '--query', metavar='<QUERY>',
help='key[op]value; list.')
@utils.arg('-m', '--meter', metavar='<NAME>',
help='Name of meter to show samples for.')
@utils.arg('-p', '--period', metavar='<PERIOD>',
help='Period in seconds over which to group samples.')
def do_statistics(cc, args):
'''List the statistics for this meters'''
'''List the statistics for this meter.'''
fields = {'meter_name': args.meter,
'q': options.cli_to_array(args.query)}
'q': options.cli_to_array(args.query),
'period': args.period}
if args.meter is None:
raise exc.CommandError('Meter name not provided (-m <meter name>)')
try:
@@ -50,7 +58,7 @@ def do_statistics(cc, args):
@utils.arg('-m', '--meter', metavar='<NAME>',
help='Name of meter to show samples for.')
def do_sample_list(cc, args):
'''List the samples for this meters'''
'''List the samples for this meters.'''
fields = {'meter_name': args.meter,
'q': options.cli_to_array(args.query)}
if args.meter is None:
@@ -71,7 +79,7 @@ def do_sample_list(cc, args):
@utils.arg('-q', '--query', metavar='<QUERY>',
help='key[op]value; list.')
def do_meter_list(cc, args={}):
'''List the user's meter'''
'''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']
@@ -81,13 +89,167 @@ def do_meter_list(cc, args={}):
sortby=0)
@utils.arg('-q', '--query', metavar='<QUERY>',
help='key[op]value; list.')
def do_alarm_list(cc, args={}):
'''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 = ['Name', 'Description', 'Metric', 'Period', 'Count',
'Threshold', 'Comparison', 'State', 'Enabled', 'Alarm ID',
'User ID', 'Project ID']
fields = ['name', 'description', 'counter_name', 'period',
'evaluation_periods', 'threshold', 'comparison_operator',
'state', 'enabled', 'alarm_id', 'user_id', 'project_id']
utils.print_list(alarms, fields, field_labels,
sortby=0)
def _display_alarm(alarm):
fields = ['name', 'description', 'counter_name', 'period',
'evaluation_periods', 'threshold', 'comparison_operator',
'state', 'enabled', 'alarm_id', 'user_id', 'project_id',
'alarm_actions', 'ok_actions', 'insufficient_data_actions']
data = dict([(f, getattr(alarm, f, '')) for f in fields])
utils.print_dict(data, wrap=72)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
help='ID of the alarm to show.')
def do_alarm_show(cc, args={}):
'''Show an alarm.'''
if args.alarm_id is None:
raise exc.CommandError('Alarm ID not provided (-a <alarm id>)')
try:
alarm = cc.alarms.get(args.alarm_id)
except exc.HTTPNotFound:
raise exc.CommandError('Alarm not found: %s' % args.alarm_id)
else:
_display_alarm(alarm)
@utils.arg('--name', metavar='<NAME>',
help='Name of the alarm (must be unique per tenant)')
@utils.arg('--project-id', metavar='<PROJECT_ID>',
help='Tenant to associate with alarm '
'(only settable by admin users)')
@utils.arg('--user-id', metavar='<USER_ID>',
help='User to associate with alarm '
'(only settable by admin users)')
@utils.arg('--description', metavar='<DESCRIPTION>',
help='Free text description of the alarm')
@utils.arg('--period', type=int, metavar='<PERIOD>',
help='Length of each period (seconds) to evaluate over')
@utils.arg('--evaluation-periods', type=int, metavar='<COUNT>',
help='Number of periods to evaluate over')
@utils.arg('--state', metavar='<STATE>',
help='State of the alarm, one of: ' + str(ALARM_STATES))
@utils.arg('--enabled', type=utils.string_to_bool, metavar='{True|False}',
help='True if alarm evaluation/actioning is enabled')
@utils.arg('--counter-name', metavar='<METRIC>',
help='Metric to evaluate against')
@utils.arg('--statistic', metavar='<STATISTIC>',
help='Statistic to evaluate, one of: ' + str(STATISTICS))
@utils.arg('--comparison-operator', metavar='<OPERATOR>',
help='Operator to compare with, one of: ' + str(ALARM_OPERATORS))
@utils.arg('--threshold', type=float, metavar='<THRESHOLD>',
help='Threshold to evaluate against')
@utils.arg('--alarm-action', dest='alarm_actions',
metavar='<Webhook URL>', action='append', default=None,
help=('URL to invoke when state transitions to alarm. '
'May be used multiple times.'))
@utils.arg('--ok-action', dest='ok_actions',
metavar='<Webhook URL>', action='append', default=None,
help=('URL to invoke when state transitions to OK. '
'May be used multiple times.'))
@utils.arg('--insufficient-data-action', dest='insufficient_data_actions',
metavar='<Webhook URL>', action='append', default=None,
help=('URL to invoke when state transitions to unkown. '
'May be used multiple times.'))
def do_alarm_create(cc, args={}):
'''Create a new alarm.'''
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
alarm = cc.alarms.create(**fields)
_display_alarm(alarm)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
help='ID of the alarm to update.')
@utils.arg('--description', metavar='<DESCRIPTION>',
help='Free text description of the alarm')
@utils.arg('--period', type=int, metavar='<PERIOD>',
help='Length of each period (seconds) to evaluate over')
@utils.arg('--evaluation-periods', type=int, metavar='<COUNT>',
help='Number of periods to evaluate over')
@utils.arg('--state', metavar='<STATE>',
help='State of the alarm, one of: ' + str(ALARM_STATES))
@utils.arg('--enabled', type=utils.string_to_bool, metavar='{True|False}',
help='True if alarm evaluation/actioning is enabled')
@utils.arg('--counter-name', metavar='<METRIC>',
help='Metric to evaluate against')
@utils.arg('--statistic', metavar='<STATISTIC>',
help='Statistic to evaluate, one of: ' + str(STATISTICS))
@utils.arg('--comparison-operator', metavar='<OPERATOR>',
help='Operator to compare with, one of: ' + str(ALARM_OPERATORS))
@utils.arg('--threshold', type=float, metavar='<THRESHOLD>',
help='Threshold to evaluate against')
@utils.arg('--alarm-action', dest='alarm_actions',
metavar='<Webhook URL>', action='append', default=None,
help=('URL to invoke when state transitions to alarm. '
'May be used multiple times.'))
@utils.arg('--ok-action', dest='ok_actions',
metavar='<Webhook URL>', action='append', default=None,
help=('URL to invoke when state transitions to OK. '
'May be used multiple times.'))
@utils.arg('--insufficient-data-action', dest='insufficient_data_actions',
metavar='<Webhook URL>', action='append', default=None,
help=('URL to invoke when state transitions to unkown. '
'May be used multiple times.'))
def do_alarm_update(cc, args={}):
'''Update an existing alarm.'''
fields = dict(filter(lambda x: not (x[1] is None), vars(args).items()))
fields.pop('alarm_id')
alarm = cc.alarms.update(args.alarm_id, **fields)
_display_alarm(alarm)
@utils.arg('-a', '--alarm_id', metavar='<ALARM_ID>',
help='ID of the alarm to show.')
def do_alarm_delete(cc, args={}):
'''Delete an alarm.'''
if args.alarm_id is None:
raise exc.CommandError('Alarm ID not provided (-a <alarm id>)')
try:
cc.alarms.delete(args.alarm_id)
except exc.HTTPNotFound:
raise exc.CommandError('Alarm not found: %s' % args.alarm_id)
@utils.arg('-q', '--query', metavar='<QUERY>',
help='key[op]value; list.')
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']
fields = ['resource_id', 'source', 'user_id', 'project_id']
utils.print_list(resources, fields, field_labels,
sortby=1)
@utils.arg('-r', '--resource_id', metavar='<RESOURCE_ID>',
help='ID of the resource to show.')
def do_resource_show(cc, args={}):
'''Show the resource.'''
if args.resource_id is None:
raise exc.CommandError('Resource id not provided (-r <resource id>)')
try:
resource = cc.resources.get(args.resource_id)
except exc.HTTPNotFound:
raise exc.CommandError('Resource not found: %s' % args.resource_id)
else:
fields = ['resource_id', 'source', 'user_id',
'project_id', 'metadata']
data = dict([(f, getattr(resource, f, '')) for f in fields])
utils.print_dict(data, wrap=72)

View File

@@ -23,7 +23,8 @@ class Statistics(base.Resource):
class StatisticsManager(base.Manager):
resource_class = Statistics
def list(self, meter_name, q=None):
def list(self, meter_name, q=None, period=None):
p = ['period=%s' % period] if period else None
return self._list(options.build_url(
'/v2/meters/' + meter_name + '/statistics',
q))
q, p))

View File

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

6
requirements.txt Normal file
View File

@@ -0,0 +1,6 @@
# This file is managed by openstack-depends
argparse
httplib2
iso8601>=0.1.4
prettytable>=0.6,<0.8
python-keystoneclient>=0.2,<0.3

View File

@@ -1,49 +1,164 @@
#!/bin/bash
set -eu
function usage {
echo "Usage: $0 [OPTION]..."
echo "Run python-ceilometerclient's test suite(s)"
echo "Run python-ceilometerclient test suite"
echo ""
echo " -V, --virtual-env Always use virtualenv. Install automatically if not present"
echo " -N, --no-virtual-env Don't use virtualenv. Run tests in local environment"
echo " -s, --no-site-packages Isolate the virtualenv from the global Python environment"
echo " -x, --stop Stop running tests after the first error or failure."
echo " -f, --force Force a clean re-build of the virtual environment. Useful when dependencies have been added."
echo " -p, --pep8 Just run pep8"
echo " -P, --no-pep8 Don't run pep8"
echo " -c, --coverage Generate coverage report"
echo " -h, --help Print this usage message"
echo " --hide-elapsed Don't print the elapsed time for each test along with slow test list"
echo ""
echo "This script is deprecated and currently retained for compatibility."
echo 'You can run the full test suite for multiple environments by running "tox".'
echo 'You can run tests for only python 2.7 by running "tox -e py27", or run only'
echo 'the pep8 tests with "tox -e pep8".'
echo "Note: with no options specified, the script will try to run the tests in a virtual environment,"
echo " If no virtualenv is found, the script will ask if you would like to create one. If you "
echo " prefer to run tests NOT in a virtual environment, simply pass the -N option."
exit
}
command -v tox > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo 'This script requires "tox" to run.'
echo 'You can install it with "pip install tox".'
exit 1;
fi
just_pep8=0
function process_option {
case "$1" in
-h|--help) usage;;
-p|--pep8) let just_pep8=1;;
-V|--virtual-env) always_venv=1; never_venv=0;;
-N|--no-virtual-env) always_venv=0; never_venv=1;;
-s|--no-site-packages) no_site_packages=1;;
-f|--force) force=1;;
-p|--pep8) just_pep8=1;;
-P|--no-pep8) no_pep8=1;;
-c|--coverage) coverage=1;;
-*) testropts="$testropts $1";;
*) testrargs="$testrargs $1"
esac
}
venv=.venv
with_venv=tools/with_venv.sh
always_venv=0
never_venv=0
force=0
no_site_packages=0
installvenvopts=
testrargs=
testropts=
wrapper=""
just_pep8=0
no_pep8=0
coverage=0
LANG=en_US.UTF-8
LANGUAGE=en_US:en
LC_ALL=C
for arg in "$@"; do
process_option $arg
done
if [ $no_site_packages -eq 1 ]; then
installvenvopts="--no-site-packages"
fi
function init_testr {
if [ ! -d .testrepository ]; then
${wrapper} testr init
fi
}
function run_tests {
# Cleanup *pyc
${wrapper} find . -type f -name "*.pyc" -delete
if [ $coverage -eq 1 ]; then
# Do not test test_coverage_ext when gathering coverage.
if [ "x$testrargs" = "x" ]; then
testrargs="^(?!.*test_coverage_ext).*$"
fi
export PYTHON="${wrapper} coverage run --source novaclient --parallel-mode"
fi
# Just run the test suites in current environment
set +e
TESTRTESTS="$TESTRTESTS $testrargs"
echo "Running \`${wrapper} $TESTRTESTS\`"
${wrapper} $TESTRTESTS
RESULT=$?
set -e
copy_subunit_log
return $RESULT
}
function copy_subunit_log {
LOGNAME=`cat .testrepository/next-stream`
LOGNAME=$(($LOGNAME - 1))
LOGNAME=".testrepository/${LOGNAME}"
cp $LOGNAME subunit.log
}
function run_pep8 {
echo "Running flake8 ..."
${wrapper} flake8
}
TESTRTESTS="testr run --parallel $testropts"
if [ $never_venv -eq 0 ]
then
# Remove the virtual environment if --force used
if [ $force -eq 1 ]; then
echo "Cleaning virtualenv..."
rm -rf ${venv}
fi
if [ -e ${venv} ]; then
wrapper="${with_venv}"
else
if [ $always_venv -eq 1 ]; then
# Automatically install the virtualenv
python tools/install_venv.py $installvenvopts
wrapper="${with_venv}"
else
echo -e "No virtual environment found...create one? (Y/n) \c"
read use_ve
if [ "x$use_ve" = "xY" -o "x$use_ve" = "x" -o "x$use_ve" = "xy" ]; then
# Install the virtualenv and run the test suite in it
python tools/install_venv.py $installvenvopts
wrapper=${with_venv}
fi
fi
fi
fi
# Delete old coverage data from previous runs
if [ $coverage -eq 1 ]; then
${wrapper} coverage erase
fi
if [ $just_pep8 -eq 1 ]; then
tox -e pep8
exit
run_pep8
exit
fi
tox -e py27 $toxargs 2>&1 | tee run_tests.err.log || exit
if [ ${PIPESTATUS[0]} -ne 0 ]; then
exit ${PIPESTATUS[0]}
init_testr
run_tests
# NOTE(sirp): we only want to run pep8 when we're running the full-test suite,
# not when we're running tests individually. To handle this, we need to
# distinguish between options (noseopts), which begin with a '-', and
# arguments (testrargs).
if [ -z "$testrargs" ]; then
if [ $no_pep8 -eq 0 ]; then
run_pep8
fi
fi
if [ -z "$toxargs" ]; then
tox -e pep8
if [ $coverage -eq 1 ]; then
echo "Generating coverage report in covhtml/"
${wrapper} coverage combine
${wrapper} coverage html --include='novaclient/*' --omit='novaclient/openstack/common/*' -d covhtml -i
fi

View File

@@ -1,10 +1,33 @@
[nosetests]
cover-package = ceilometerclient
cover-html = true
cover-erase = true
cover-inclusive = true
verbosity=2
detailed-errors=1
[metadata]
name = python-ceilometerclient
summary = OpenStack Metering API Client Library
description-file =
README.md
author = OpenStack
author-email = openstack-dev@lists.openstack.org
home-page = http://www.openstack.org/
classifier =
Environment :: OpenStack
Intended Audience :: Information Technology
Intended Audience :: System Administrators
License :: OSI Approved :: Apache Software License
Operating System :: POSIX :: Linux
Programming Language :: Python
Programming Language :: Python :: 2
Programming Language :: Python :: 2.7
Programming Language :: Python :: 2.6
[files]
packages =
ceilometerclient
[global]
setup-hooks =
pbr.hooks.setup_hook
[entry_points]
console_scripts =
ceilometer = ceilometerclient.shell:main
[build_sphinx]
source-dir = doc/source

View File

@@ -1,53 +1,21 @@
#!/usr/bin/env python
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
# 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.
# 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 setuptools
from ceilometerclient.openstack.common import setup
project = 'python-ceilometerclient'
def read(fname):
return open(os.path.join(os.path.dirname(__file__), fname)).read()
setuptools.setup(
name=project,
version=setup.get_version(project),
author='Ceilometer Developers',
author_email='openstack-dev@lists.openstack.org',
description="Client library for ceilometer",
long_description=read('README.md'),
license='Apache',
url='https://github.com/openstack/python-ceilometerclient',
packages=setuptools.find_packages(exclude=['tests', 'tests.*']),
include_package_data=True,
install_requires=setup.parse_requirements(),
test_suite="nose.collector",
cmdclass=setup.get_cmdclass(),
classifiers=[
'Development Status :: 4 - Beta',
'Environment :: Console',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'License :: OSI Approved :: Apache Software License',
'Operating System :: OS Independent',
'Programming Language :: Python',
],
entry_points={
'console_scripts': ['ceilometer = ceilometerclient.shell:main']
},
dependency_links=setup.parse_dependency_links(),
tests_require=setup.parse_requirements(['tools/test-requires']),
setup_requires=['setuptools-git>=0.4'],
)
setup_requires=['d2to1>=0.2.10,<0.3', 'pbr>=0.5,<0.6'],
d2to1=True)

14
test-requirements.txt Normal file
View File

@@ -0,0 +1,14 @@
# Install bounded pep8/pyflakes first, then let flake8 install
pep8==1.4.5
pyflakes==0.7.2
flake8==2.0
hacking>=0.5.3,<0.6
coverage>=3.6
discover
fixtures>=0.3.12
mox>=0.5.3
python-subunit
sphinx>=1.1.2
testrepository>=0.0.13
testtools>=0.9.29

View File

@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import unittest
from tests import utils
from ceilometerclient.common import http
@@ -23,7 +21,7 @@ from ceilometerclient.common import http
fixtures = {}
class HttpClientTest(unittest.TestCase):
class HttpClientTest(utils.BaseTestCase):
def test_url_generation_trailing_slash_in_base(self):
client = http.HTTPClient('http://localhost/')

View File

@@ -1,69 +1,43 @@
import cStringIO
import os
import httplib2
import re
import sys
import mox
import unittest
import unittest2
try:
import json
except ImportError:
import simplejson as json
import fixtures
from testtools import matchers
from keystoneclient.v2_0 import client as ksclient
from ceilometerclient import exc
from ceilometerclient import shell as ceilometer_shell
from ceilometerclient.v1 import client as v1client
import ceilometerclient.shell
from tests import utils
FAKE_ENV = {'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': 'http://no.where'}
class ShellValidationTest(unittest.TestCase):
def shell_error(self, argstr, error_match):
orig = sys.stderr
try:
sys.stderr = cStringIO.StringIO()
_shell = ceilometerclient.shell.CeilometerShell()
_shell.main(argstr.split())
except exc.CommandError as e:
self.assertRegexpMatches(e.__str__(), error_match)
else:
self.fail('Expected error matching: %s' % error_match)
finally:
err = sys.stderr.getvalue()
sys.stderr.close()
sys.stderr = orig
return err
class ShellTest(unittest2.TestCase):
class ShellTest(utils.BaseTestCase):
re_options = re.DOTALL | re.MULTILINE
# 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)
self.useFixture(fixtures.MonkeyPatch('os.environ', env))
def setUp(self):
self.m = mox.Mox()
super(ShellTest, self).setUp()
self.m.StubOutWithMock(ksclient, 'Client')
self.m.StubOutWithMock(v1client.Client, 'json_request')
self.m.StubOutWithMock(v1client.Client, 'raw_request')
global _old_env
fake_env = {
'OS_USERNAME': 'username',
'OS_PASSWORD': 'password',
'OS_TENANT_NAME': 'tenant_name',
'OS_AUTH_URL': 'http://no.where',
}
_old_env, os.environ = os.environ, fake_env.copy()
def tearDown(self):
self.m.UnsetStubs()
global _old_env
os.environ = _old_env
def shell(self, argstr):
orig = sys.stdout
try:
sys.stdout = cStringIO.StringIO()
_shell = ceilometerclient.shell.CeilometerShell()
_shell = ceilometer_shell.CeilometerShell()
_shell.main(argstr.split())
except SystemExit:
exc_type, exc_value, exc_traceback = sys.exc_info()
@@ -85,18 +59,21 @@ class ShellTest(unittest2.TestCase):
def test_help(self):
required = [
'^usage: ceilometer',
'(?m)^See "ceilometer help COMMAND" for help on a specific command',
'.*?^usage: ceilometer',
'.*?^See "ceilometer help COMMAND" '
'for help on a specific command',
]
for argstr in ['--help', 'help']:
help_text = self.shell(argstr)
for r in required:
self.assertRegexpMatches(help_text, r)
self.assertThat(help_text,
matchers.MatchesRegex(r,
self.re_options))
def test_help_on_subcommand(self):
required = [
'^usage: ceilometer meter-list',
"(?m)^List the user's meter",
'.*?^usage: ceilometer meter-list',
".*?^List the user's meter",
]
argstrings = [
'help meter-list',
@@ -104,19 +81,9 @@ class ShellTest(unittest2.TestCase):
for argstr in argstrings:
help_text = self.shell(argstr)
for r in required:
self.assertRegexpMatches(help_text, r)
self.assertThat(help_text,
matchers.MatchesRegex(r, self.re_options))
def test_auth_param(self):
class TokenContext(object):
def __enter__(self):
fake_env = {
'OS_AUTH_TOKEN': 'token',
'CEILOMETER_URL': 'http://no.where'
}
self.old_env, os.environ = os.environ, fake_env.copy()
def __exit__(self, exc_type, exc_value, traceback):
os.environ = self.old_env
with TokenContext():
self.test_help()
self.make_env(exclude='OS_USERNAME')
self.test_help()

47
tests/test_utils.py Normal file
View File

@@ -0,0 +1,47 @@
# Copyright 2013 OpenStack LLC.
# 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 cStringIO
import sys
from ceilometerclient.common import utils
from tests import utils as test_utils
class UtilsTest(test_utils.BaseTestCase):
def test_prettytable(self):
class Struct:
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 = cStringIO.StringIO()
utils.print_dict({'K': 'k', 'Key': 'Value'})
finally:
sys.stdout = saved_stdout
self.assertEqual(output_dict.getvalue(), '''\
+----------+-------+
| Property | Value |
+----------+-------+
| K | k |
| Key | Value |
+----------+-------+
''')

View File

@@ -14,11 +14,23 @@
# under the License.
import copy
import fixtures
import mox
import StringIO
import testtools
from ceilometerclient.common import http
class BaseTestCase(testtools.TestCase):
def setUp(self):
super(BaseTestCase, self).setUp()
self.m = mox.Mox()
self.addCleanup(self.m.UnsetStubs)
self.useFixture(fixtures.FakeLogger())
class FakeAPI(object):
def __init__(self, fixtures):
self.fixtures = fixtures
@@ -41,8 +53,7 @@ class FakeAPI(object):
class FakeResponse(object):
def __init__(self, headers, body=None, version=None):
"""
:param headers: dict representing HTTP response headers
""":param headers: dict representing HTTP response headers
:param body: file-like object
"""
self.headers = headers
@@ -56,4 +67,3 @@ class FakeResponse(object):
def read(self, amt):
return self.body.read(amt)

View File

@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import unittest
import ceilometerclient.v1.meters
from tests import utils
@@ -107,9 +105,10 @@ fixtures = {
}
class MeterManagerTest(unittest.TestCase):
class MeterManagerTest(utils.BaseTestCase):
def setUp(self):
super(MeterManagerTest, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.mgr = ceilometerclient.v1.meters.MeterManager(self.api)

View File

@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import unittest
import ceilometerclient.v1.meters
from tests import utils
@@ -32,17 +30,16 @@ fixtures = {
'/v1/sources/source_b/projects': {
'GET': (
{},
{'projects': [
'b',
]},
{'projects': ['b']},
),
},
}
class ProjectManagerTest(unittest.TestCase):
class ProjectManagerTest(utils.BaseTestCase):
def setUp(self):
super(ProjectManagerTest, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.mgr = ceilometerclient.v1.meters.ProjectManager(self.api)

View File

@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import unittest
import ceilometerclient.v1.meters
from tests import utils
@@ -106,9 +104,10 @@ fixtures = {
}
class ResourceManagerTest(unittest.TestCase):
class ResourceManagerTest(utils.BaseTestCase):
def setUp(self):
super(ResourceManagerTest, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.mgr = ceilometerclient.v1.meters.ResourceManager(self.api)
@@ -150,9 +149,11 @@ class ResourceManagerTest(unittest.TestCase):
self.assertEqual(resources[0].resource_id, 'a')
def test_list_by_timestamp(self):
resources = list(self.mgr.list(start_timestamp='now', end_timestamp='now'))
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',
{}, None),
]
self.assertEqual(self.api.calls, expect)
self.assertEqual(len(resources), 1)

View File

@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import unittest
import ceilometerclient.v1.meters
from tests import utils
@@ -120,9 +118,10 @@ fixtures = {
}
class SampleManagerTest(unittest.TestCase):
class SampleManagerTest(utils.BaseTestCase):
def setUp(self):
super(SampleManagerTest, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.mgr = ceilometerclient.v1.meters.SampleManager(self.api)

View File

@@ -13,8 +13,6 @@
# License for the specific language governing permissions and limitations
# under the License.
import unittest
import ceilometerclient.v1.meters
from tests import utils
@@ -32,17 +30,16 @@ fixtures = {
'/v1/sources/source_b/users': {
'GET': (
{},
{'users': [
'b',
]},
{'users': ['b']},
),
},
}
class UserManagerTest(unittest.TestCase):
class UserManagerTest(utils.BaseTestCase):
def setUp(self):
super(UserManagerTest, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.mgr = ceilometerclient.v1.meters.UserManager(self.api)

163
tests/v2/test_alarms.py Normal file
View File

@@ -0,0 +1,163 @@
# -*- encoding: utf-8 -*-
#
# Copyright © 2013 Red Hat, Inc
#
# Author: Eoghan Glynn <eglynn@redhat.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
#
# 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 unittest
import ceilometerclient.v2.alarms
from tests import utils
AN_ALARM = {u'alarm_actions': [u'http://site:8000/alarm'],
u'ok_actions': [u'http://site:8000/ok'],
u'description': u'An alarm',
u'matching_metadata': {u'key_name': u'key_value'},
u'evaluation_periods': 2,
u'timestamp': u'2013-05-09T13:41:23.085000',
u'enabled': True,
u'counter_name': u'storage.objects',
u'period': 240.0,
u'alarm_id': u'alarm-id',
u'state': u'ok',
u'insufficient_data_actions': [u'http://site:8000/nodata'],
u'statistic': u'avg',
u'threshold': 200.0,
u'user_id': u'user-id',
u'project_id': u'project-id',
u'state_timestamp': u'2013-05-09T13:41:23.085000',
u'comparison_operator': 'gt',
u'name': 'SwiftObjectAlarm'}
CREATE_ALARM = copy.deepcopy(AN_ALARM)
del CREATE_ALARM['timestamp']
del CREATE_ALARM['state_timestamp']
del CREATE_ALARM['matching_metadata']
del CREATE_ALARM['alarm_id']
DELTA_ALARM = {u'alarm_actions': ['url1', 'url2'],
u'comparison_operator': u'lt',
u'threshold': 42.1}
UPDATED_ALARM = copy.deepcopy(AN_ALARM)
UPDATED_ALARM.update(DELTA_ALARM)
fixtures = {
'/v2/alarms':
{
'GET': (
{},
[AN_ALARM],
),
'POST': (
{},
CREATE_ALARM,
),
},
'/v2/alarms/alarm-id':
{
'GET': (
{},
AN_ALARM,
),
'PUT': (
{},
UPDATED_ALARM,
),
},
'/v2/alarms?q.op=&q.op=&q.value=project-id&q.value=SwiftObjectAlarm'
'&q.field=project_id&q.field=name':
{
'GET': (
{},
[AN_ALARM],
),
},
'/v2/alarms/victim-id':
{
'DELETE': (
{},
None,
),
},
}
class AlarmManagerTest(unittest.TestCase):
def setUp(self):
self.api = utils.FakeAPI(fixtures)
self.mgr = ceilometerclient.v2.alarms.AlarmManager(self.api)
def test_list_all(self):
alarms = list(self.mgr.list())
expect = [
('GET', '/v2/alarms', {}, None),
]
self.assertEqual(self.api.calls, expect)
self.assertEqual(len(alarms), 1)
self.assertEqual(alarms[0].alarm_id, 'alarm-id')
def test_list_with_query(self):
alarms = list(self.mgr.list(q=[
{"field": "project_id",
"value": "project-id"},
{"field": "name",
"value": "SwiftObjectAlarm"},
]))
expect = [
('GET',
'/v2/alarms?q.op=&q.op=&q.value=project-id&q.value='
'SwiftObjectAlarm&q.field=project_id&q.field=name',
{}, None),
]
self.assertEqual(self.api.calls, 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),
]
self.assertEqual(self.api.calls, expect)
self.assertTrue(alarm)
self.assertEqual(alarm.alarm_id, 'alarm-id')
def test_create(self):
alarm = self.mgr.create(**CREATE_ALARM)
expect = [
('POST', '/v2/alarms', {}, CREATE_ALARM),
]
self.assertEqual(self.api.calls, expect)
self.assertTrue(alarm)
def test_update(self):
alarm = self.mgr.update(alarm_id='alarm-id', **DELTA_ALARM)
expect = [
('PUT', '/v2/alarms/alarm-id', {}, DELTA_ALARM),
]
self.assertEqual(self.api.calls, expect)
self.assertTrue(alarm)
self.assertEqual(alarm.alarm_id, 'alarm-id')
for (key, value) in DELTA_ALARM.iteritems():
self.assertEqual(getattr(alarm, key), value)
def test_delete(self):
deleted = self.mgr.delete(alarm_id='victim-id')
expect = [
('DELETE', '/v2/alarms/victim-id', {}, None),
]
self.assertEqual(self.api.calls, expect)
self.assertTrue(deleted is None)

View File

@@ -11,12 +11,11 @@
# License for the specific language governing permissions and limitations
# under the License.
import unittest
from ceilometerclient.v2 import options
from tests import utils
class BuildUrlTest(unittest.TestCase):
class BuildUrlTest(utils.BaseTestCase):
def test_one(self):
url = options.build_url('/', [{'field': 'this',
@@ -42,31 +41,35 @@ class BuildUrlTest(unittest.TestCase):
self.assertEqual(url, '/?q.op=&q.value=43&q.field=this')
class CliTest(unittest.TestCase):
class CliTest(utils.BaseTestCase):
def test_one(self):
ar = options.cli_to_array('this<=34')
self.assertEqual(ar, [{'field': 'this','op': 'le','value': '34'}])
self.assertEqual(ar, [{'field': 'this', 'op': 'le', 'value': '34'}])
def test_two(self):
ar = options.cli_to_array('this<=34;that!=foo')
self.assertEqual(ar, [{'field': 'this','op': 'le','value': '34'},
{'field': 'that','op': 'ne','value': 'foo'}])
self.assertEqual(ar, [{'field': 'this', 'op': 'le', 'value': '34'},
{'field': 'that', 'op': 'ne', 'value': 'foo'}])
def test_negative(self):
ar = options.cli_to_array('this>=-783')
self.assertEqual(ar, [{'field': 'this','op': 'ge','value': '-783'}])
self.assertEqual(ar, [{'field': 'this', 'op': 'ge', 'value': '-783'}])
def test_float(self):
ar = options.cli_to_array('this<=283.347')
self.assertEqual(ar, [{'field': 'this','op': 'le','value': '283.347'}])
self.assertEqual(ar, [{'field': 'this',
'op': 'le', 'value': '283.347'}])
def test_invalid_seperator(self):
self.assertRaises(ValueError, options.cli_to_array, 'this=2.4,fooo=doof')
self.assertRaises(ValueError, options.cli_to_array,
'this=2.4,fooo=doof')
def test_invalid_operator(self):
self.assertRaises(ValueError, options.cli_to_array, 'this=2.4;fooo-doof')
self.assertRaises(ValueError, options.cli_to_array,
'this=2.4;fooo-doof')
def test_with_dot(self):
ar = options.cli_to_array('metadata.this<=34')
self.assertEqual(ar, [{'field': 'metadata.this','op': 'le','value': '34'}])
self.assertEqual(ar, [{'field': 'metadata.this',
'op': 'le', 'value': '34'}])

105
tests/v2/test_resources.py Normal file
View File

@@ -0,0 +1,105 @@
# Copyright 2012 OpenStack LLC.
# 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 ceilometerclient.v2.resources
from tests import utils
fixtures = {
'/v2/resources': {
'GET': (
{},
[
{
'resource_id': 'a',
'project_id': 'project_bla',
'user_id': 'freddy',
'metadata': {'zxc_id': 'bla'},
},
{
'resource_id': 'b',
'project_id': 'dig_the_ditch',
'user_id': 'joey',
'metadata': {'zxc_id': 'foo'},
},
]
),
},
'/v2/resources?q.op=&q.value=a&q.field=resource_id':
{
'GET': (
{},
[
{
'resource_id': 'a',
'project_id': 'project_bla',
'user_id': 'freddy',
'metadata': {'zxc_id': 'bla'},
},
]
),
},
'/v2/resources/a':
{
'GET': (
{},
{
'resource_id': 'a',
'project_id': 'project_bla',
'user_id': 'freddy',
'metadata': {'zxc_id': 'bla'},
},
),
},
}
class ResourceManagerTest(utils.BaseTestCase):
def setUp(self):
super(ResourceManagerTest, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.mgr = ceilometerclient.v2.resources.ResourceManager(self.api)
def test_list_all(self):
resources = list(self.mgr.list())
expect = [
('GET', '/v2/resources', {}, None),
]
self.assertEqual(self.api.calls, expect)
self.assertEqual(len(resources), 2)
self.assertEqual(resources[0].resource_id, 'a')
self.assertEqual(resources[1].resource_id, 'b')
def test_list_one(self):
resource = self.mgr.get(resource_id='a')
expect = [
('GET', '/v2/resources/a', {}, None),
]
self.assertEqual(self.api.calls, expect)
self.assertTrue(resource)
self.assertEqual(resource.resource_id, 'a')
def test_list_by_query(self):
resources = list(self.mgr.list(q=[{"field": "resource_id",
"value": "a"},
]))
expect = [
('GET', '/v2/resources?q.op=&q.value=a&q.field=resource_id',
{}, None),
]
self.assertEqual(self.api.calls, expect)
self.assertEqual(len(resources), 1)
self.assertEqual(resources[0].resource_id, 'a')

View File

@@ -13,14 +13,14 @@
# License for the specific language governing permissions and limitations
# under the License.
import unittest
import ceilometerclient.v2.samples
from tests import utils
base_url = '/v2/meters/instance'
args = 'q.op=&q.op=&q.value=foo&q.value=bar&q.field=resource_id&q.field=source'
fixtures = {
'/v2/meters/instance':
base_url:
{
'GET': (
{},
@@ -40,7 +40,7 @@ fixtures = {
]
),
},
'/v2/meters/instance?q.op=&q.op=&q.value=foo&q.value=bar&q.field=resource_id&q.field=source':
'%s?%s' % (base_url, args):
{
'GET': (
{},
@@ -50,9 +50,10 @@ fixtures = {
}
class SampleManagerTest(unittest.TestCase):
class SampleManagerTest(utils.BaseTestCase):
def setUp(self):
super(SampleManagerTest, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.mgr = ceilometerclient.v2.samples.SampleManager(self.api)
@@ -73,10 +74,6 @@ class SampleManagerTest(unittest.TestCase):
{"field": "source",
"value": "bar"},
]))
expect = [
('GET',
'/v2/meters/instance?q.op=&q.op=&q.value=foo&q.value=bar&q.field=resource_id&q.field=source',
{}, None),
]
expect = [('GET', '%s?%s' % (base_url, args), {}, None)]
self.assertEqual(self.api.calls, expect)
self.assertEqual(len(samples), 0)

View File

@@ -13,53 +13,52 @@
# License for the specific language governing permissions and limitations
# under the License.
import unittest
import ceilometerclient.v2.statistics
from tests import utils
base_url = '/v2/meters/instance/statistics'
qry = 'q.op=&q.op=&q.value=foo&q.value=bar&q.field=resource_id&q.field=source'
period = '&period=60'
samples = [{
u'count': 135,
u'duration_start': u'2013-02-04T10:51:42',
u'min': 1.0,
u'max': 1.0,
u'duration_end':
u'2013-02-05T15:46:09',
u'duration': 1734.0,
u'avg': 1.0,
u'sum': 135.0,
}]
fixtures = {
'/v2/meters/instance/statistics':
base_url:
{
'GET': (
{},
[{
u'count': 135,
u'duration_start': u'2013-02-04T10:51:42',
u'min': 1.0,
u'max': 1.0,
u'duration_end':
u'2013-02-05T15:46:09',
u'duration': 1734.0,
u'avg': 1.0,
u'sum': 135.0,
}]
samples
),
},
'/v2/meters/instance/statistics?q.op=&q.op=&q.value=foo&q.value=bar&q.field=resource_id&q.field=source':
'%s?%s' % (base_url, qry):
{
'GET': (
{},
[{
u'count': 135,
u'duration_start': u'2013-02-04T10:51:42',
u'min': 1.0,
u'max': 1.0,
u'duration_end':
u'2013-02-05T15:46:09',
u'duration': 1734.0,
u'avg': 1.0,
u'sum': 135.0,
}]
samples
),
}
},
'%s?%s%s' % (base_url, qry, period):
{
'GET': (
{},
samples
),
},
}
class StatisticsManagerTest(unittest.TestCase):
class StatisticsManagerTest(utils.BaseTestCase):
def setUp(self):
super(StatisticsManagerTest, self).setUp()
self.api = utils.FakeAPI(fixtures)
self.mgr = ceilometerclient.v2.statistics.StatisticsManager(self.api)
@@ -82,8 +81,24 @@ class StatisticsManagerTest(unittest.TestCase):
]))
expect = [
('GET',
'/v2/meters/instance/statistics?q.op=&q.op=&q.value=foo&q.value=bar&q.field=resource_id&q.field=source',
{}, None),
'%s?%s' % (base_url, qry), {}, None),
]
self.assertEqual(self.api.calls, expect)
self.assertEqual(len(stats), 1)
self.assertEqual(stats[0].count, 135)
def test_list_by_meter_name_with_period(self):
stats = list(self.mgr.list(meter_name='instance',
q=[
{"field": "resource_id",
"value": "foo"},
{"field": "source",
"value": "bar"},
],
period=60))
expect = [
('GET',
'%s?%s%s' % (base_url, qry, period), {}, None),
]
self.assertEqual(self.api.calls, expect)
self.assertEqual(len(stats), 1)

74
tools/install_venv.py Normal file
View File

@@ -0,0 +1,74 @@
# 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.
#
# Copyright 2010 OpenStack Foundation
# Copyright 2013 IBM Corp.
#
# 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
import install_venv_common as install_venv
def print_help(venv, root):
help = """
Ceilometerclient development environment setup is complete.
Ceilometerclient development uses virtualenv to track and manage Python
dependencies while in development and testing.
To activate the Ceilometerclient virtualenv for the extent of your current
shell session you can run:
$ source %s/bin/activate
Or, if you prefer, you can run commands in the virtualenv on a case by case
basis by running:
$ %s/tools/with_venv.sh <your command>
Also, make test will automatically use the virtualenv.
"""
print help % (venv, root)
def main(argv):
root = os.path.dirname(os.path.dirname(os.path.realpath(__file__)))
if os.environ.get('tools_path'):
root = os.environ['tools_path']
venv = os.path.join(root, '.venv')
if os.environ.get('venv'):
venv = os.environ['venv']
pip_requires = os.path.join(root, 'requirements.txt')
test_requires = os.path.join(root, 'test-requirements.txt')
py_version = "python%s.%s" % (sys.version_info[0], sys.version_info[1])
project = 'Ceilometerclient'
install = install_venv.InstallVenv(root, venv, pip_requires, test_requires,
py_version, project)
options = install.parse_args(argv)
install.check_python_version()
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__':
main(sys.argv)

View File

@@ -0,0 +1,221 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2013 OpenStack Foundation
# Copyright 2013 IBM Corp.
#
# 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.
"""Provides methods needed by installation script for OpenStack development
virtual environments.
Since this script is used to bootstrap a virtualenv from the system's Python
environment, it should be kept strictly compatible with Python 2.6.
Synced in from openstack-common
"""
from __future__ import print_function
import optparse
import os
import subprocess
import sys
class InstallVenv(object):
def __init__(self, root, venv, pip_requires, test_requires, py_version,
project):
self.root = root
self.venv = venv
self.pip_requires = pip_requires
self.test_requires = test_requires
self.py_version = py_version
self.project = project
def die(self, message, *args):
print(message % args, file=sys.stderr)
sys.exit(1)
def check_python_version(self):
if sys.version_info < (2, 6):
self.die("Need Python Version >= 2.6")
def run_command_with_code(self, cmd, redirect_output=True,
check_exit_code=True):
"""Runs a command in an out-of-process shell.
Returns the output of that command. Working directory is self.root.
"""
if redirect_output:
stdout = subprocess.PIPE
else:
stdout = None
proc = subprocess.Popen(cmd, cwd=self.root, stdout=stdout)
output = proc.communicate()[0]
if check_exit_code and proc.returncode != 0:
self.die('Command "%s" failed.\n%s', ' '.join(cmd), output)
return (output, proc.returncode)
def run_command(self, cmd, redirect_output=True, check_exit_code=True):
return self.run_command_with_code(cmd, redirect_output,
check_exit_code)[0]
def get_distro(self):
if (os.path.exists('/etc/fedora-release') or
os.path.exists('/etc/redhat-release')):
return Fedora(self.root, self.venv, self.pip_requires,
self.test_requires, self.py_version, self.project)
else:
return Distro(self.root, self.venv, self.pip_requires,
self.test_requires, self.py_version, self.project)
def check_dependencies(self):
self.get_distro().install_virtualenv()
def create_virtualenv(self, no_site_packages=True):
"""Creates the virtual environment and installs PIP.
Creates the virtual environment and installs PIP only into the
virtual environment.
"""
if not os.path.isdir(self.venv):
print('Creating venv...', end=' ')
if no_site_packages:
self.run_command(['virtualenv', '-q', '--no-site-packages',
self.venv])
else:
self.run_command(['virtualenv', '-q', self.venv])
print('done.')
print('Installing pip in venv...', end=' ')
if not self.run_command(['tools/with_venv.sh', 'easy_install',
'pip>1.0']).strip():
self.die("Failed to install pip.")
print('done.')
else:
print("venv already exists...")
pass
def pip_install(self, *args):
self.run_command(['tools/with_venv.sh',
'pip', 'install', '--upgrade'] + list(args),
redirect_output=False)
def install_dependencies(self):
print('Installing dependencies with pip (this can take a while)...')
# First things first, make sure our venv has the latest pip and
# distribute.
# NOTE: we keep pip at version 1.1 since the most recent version causes
# the .venv creation to fail. See:
# https://bugs.launchpad.net/nova/+bug/1047120
self.pip_install('pip==1.1')
self.pip_install('distribute')
# Install greenlet by hand - just listing it in the requires file does
# not
# get it installed in the right order
self.pip_install('greenlet')
self.pip_install('-r', self.pip_requires)
self.pip_install('-r', self.test_requires)
def post_process(self):
self.get_distro().post_process()
def parse_args(self, argv):
"""Parses command-line arguments."""
parser = optparse.OptionParser()
parser.add_option('-n', '--no-site-packages',
action='store_true',
help="Do not inherit packages from global Python "
"install")
return parser.parse_args(argv[1:])[0]
class Distro(InstallVenv):
def check_cmd(self, cmd):
return bool(self.run_command(['which', cmd],
check_exit_code=False).strip())
def install_virtualenv(self):
if self.check_cmd('virtualenv'):
return
if self.check_cmd('easy_install'):
print('Installing virtualenv via easy_install...', end=' ')
if self.run_command(['easy_install', 'virtualenv']):
print('Succeeded')
return
else:
print('Failed')
self.die('ERROR: virtualenv not found.\n\n%s development'
' requires virtualenv, please install it using your'
' favorite package management tool' % self.project)
def post_process(self):
"""Any distribution-specific post-processing gets done here.
In particular, this is useful for applying patches to code inside
the venv.
"""
pass
class Fedora(Distro):
"""This covers all Fedora-based distributions.
Includes: Fedora, RHEL, CentOS, Scientific Linux
"""
def check_pkg(self, pkg):
return self.run_command_with_code(['rpm', '-q', pkg],
check_exit_code=False)[1] == 0
def apply_patch(self, originalfile, patchfile):
self.run_command(['patch', '-N', originalfile, patchfile],
check_exit_code=False)
def install_virtualenv(self):
if self.check_cmd('virtualenv'):
return
if not self.check_pkg('python-virtualenv'):
self.die("Please install 'python-virtualenv'.")
super(Fedora, self).install_virtualenv()
def post_process(self):
"""Workaround for a bug in eventlet.
This currently affects RHEL6.1, but the fix can safely be
applied to all RHEL and Fedora distributions.
This can be removed when the fix is applied upstream.
Nova: https://bugs.launchpad.net/nova/+bug/884915
Upstream: https://bitbucket.org/which_linden/eventlet/issue/89
"""
# Install "patch" program if it's not there
if not self.check_pkg('patch'):
self.die("Please install 'patch'.")
# Apply the eventlet patch
self.apply_patch(os.path.join(self.venv, 'lib', self.py_version,
'site-packages',
'eventlet/green/subprocess.py'),
'contrib/redhat-eventlet.patch')

View File

@@ -1,5 +0,0 @@
argparse
prettytable>=0.6,<0.7
python-keystoneclient>=0.1.2
httplib2
iso8601>=0.1.4

View File

@@ -1,11 +0,0 @@
distribute>=0.6.24
mox
nose
nose-exclude
nosexcover
openstack.nose_plugin
nosehtmloutput
pep8==1.3.3
setuptools-git>=0.4
sphinx>=1.1.2
unittest2

View File

@@ -1,10 +1,4 @@
#!/bin/bash
command -v tox > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo 'This script requires "tox" to run.'
echo 'You can install it with "pip install tox".'
exit 1;
fi
tox -evenv -- $@
TOOLS=`dirname $0`
VENV=$TOOLS/../.venv
source $VENV/bin/activate && $@

51
tox.ini
View File

@@ -3,44 +3,27 @@ envlist = py26,py27,pep8
[testenv]
setenv = VIRTUAL_ENV={envdir}
NOSE_WITH_OPENSTACK=1
NOSE_OPENSTACK_COLOR=1
NOSE_OPENSTACK_RED=0.05
NOSE_OPENSTACK_YELLOW=0.025
NOSE_OPENSTACK_SHOW_ELAPSED=1
deps = -r{toxinidir}/tools/pip-requires
-r{toxinidir}/tools/test-requires
commands = nosetests
[testenv:pep8]
deps = pep8==1.3.3
commands = pep8 --repeat --show-source ceilometerclient setup.py
[testenv:venv]
commands = {posargs}
[testenv:cover]
commands = nosetests --cover-erase --cover-package=ceilometerclient --with-xcoverage
LANG=en_US.UTF-8
LANGUAGE=en_US:en
LC_ALL=C
deps = -r{toxinidir}/requirements.txt
-r{toxinidir}/test-requirements.txt
commands =
python setup.py testr --testr-args='{posargs}'
[tox:jenkins]
downloadcache = ~/cache/pip
[testenv:jenkins26]
basepython = python2.6
setenv = NOSE_WITH_XUNIT=1
deps = file://{toxinidir}/.cache.bundle
[testenv:pep8]
commands = flake8
[testenv:jenkins27]
basepython = python2.7
setenv = NOSE_WITH_XUNIT=1
deps = file://{toxinidir}/.cache.bundle
[testenv:cover]
commands = python setup.py testr --coverage --testr-args='{posargs}'
[testenv:jenkinscover]
deps = file://{toxinidir}/.cache.bundle
setenv = NOSE_WITH_XUNIT=1
commands = nosetests --cover-erase --cover-package=ceilometerclient --with-xcoverage
[testenv:jenkinsvenv]
deps = file://{toxinidir}/.cache.bundle
setenv = NOSE_WITH_XUNIT=1
[testenv:venv]
commands = {posargs}
[flake8]
ignore = E121,E122,E123,E128,E711,E721,E712,H302
show-source = True
exclude = .venv,.git,.tox,dist,doc,*openstack/common*,*lib/python*,*egg,build,tools