Add basic functionality
Signed-off-by: Angus Salkeld <asalkeld@redhat.com>
This commit is contained in:
31
ceilometerclient/__init__.py
Normal file
31
ceilometerclient/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 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 inspect
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
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()
|
19
ceilometerclient/client.py
Normal file
19
ceilometerclient/client.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# 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 utils
|
||||||
|
|
||||||
|
|
||||||
|
def Client(version, *args, **kwargs):
|
||||||
|
module = utils.import_versioned_module(version, 'client')
|
||||||
|
client_class = getattr(module, 'Client')
|
||||||
|
return client_class(*args, **kwargs)
|
0
ceilometerclient/common/__init__.py
Normal file
0
ceilometerclient/common/__init__.py
Normal file
131
ceilometerclient/common/base.py
Normal file
131
ceilometerclient/common/base.py
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Base utilities to build API operation managers and objects on top of.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import copy
|
||||||
|
|
||||||
|
|
||||||
|
# Python 2.4 compat
|
||||||
|
try:
|
||||||
|
all
|
||||||
|
except NameError:
|
||||||
|
def all(iterable):
|
||||||
|
return True not in (not x for x in iterable)
|
||||||
|
|
||||||
|
|
||||||
|
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.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return obj.id
|
||||||
|
except AttributeError:
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class Manager(object):
|
||||||
|
"""
|
||||||
|
Managers interact with a particular type of API (servers, flavors, images,
|
||||||
|
etc.) and provide CRUD operations for them.
|
||||||
|
"""
|
||||||
|
resource_class = None
|
||||||
|
|
||||||
|
def __init__(self, api):
|
||||||
|
self.api = api
|
||||||
|
|
||||||
|
def _list(self, url, response_key, obj_class=None, body=None):
|
||||||
|
resp, body = self.api.json_request('GET', url)
|
||||||
|
|
||||||
|
if obj_class is None:
|
||||||
|
obj_class = self.resource_class
|
||||||
|
|
||||||
|
data = body[response_key]
|
||||||
|
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])
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(object):
|
||||||
|
"""
|
||||||
|
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
|
||||||
|
:param info: dictionary representing resource attributes
|
||||||
|
:param loaded: prevent lazy-loading if set to True
|
||||||
|
"""
|
||||||
|
def __init__(self, manager, info, loaded=False):
|
||||||
|
self.manager = manager
|
||||||
|
self._info = info
|
||||||
|
self._add_details(info)
|
||||||
|
self._loaded = loaded
|
||||||
|
|
||||||
|
def _add_details(self, info):
|
||||||
|
for (k, v) in info.iteritems():
|
||||||
|
setattr(self, k, v)
|
||||||
|
|
||||||
|
def __getattr__(self, k):
|
||||||
|
if k not in self.__dict__:
|
||||||
|
#NOTE(bcwaldon): disallow lazy-loading if already loaded once
|
||||||
|
if not self.is_loaded():
|
||||||
|
self.get()
|
||||||
|
return self.__getattr__(k)
|
||||||
|
|
||||||
|
raise AttributeError(k)
|
||||||
|
else:
|
||||||
|
return self.__dict__[k]
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and
|
||||||
|
k != 'manager')
|
||||||
|
info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
|
||||||
|
return "<%s %s>" % (self.__class__.__name__, info)
|
||||||
|
|
||||||
|
def get(self):
|
||||||
|
# set_loaded() first ... so if we have to bail, we know we tried.
|
||||||
|
self.set_loaded(True)
|
||||||
|
if not hasattr(self.manager, 'get'):
|
||||||
|
return
|
||||||
|
|
||||||
|
new = self.manager.get(self.id)
|
||||||
|
if new:
|
||||||
|
self._add_details(new._info)
|
||||||
|
|
||||||
|
def __eq__(self, other):
|
||||||
|
if not isinstance(other, self.__class__):
|
||||||
|
return False
|
||||||
|
if hasattr(self, 'id') and hasattr(other, 'id'):
|
||||||
|
return self.id == other.id
|
||||||
|
return self._info == other._info
|
||||||
|
|
||||||
|
def is_loaded(self):
|
||||||
|
return self._loaded
|
||||||
|
|
||||||
|
def set_loaded(self, val):
|
||||||
|
self._loaded = val
|
||||||
|
|
||||||
|
def to_dict(self):
|
||||||
|
return copy.deepcopy(self._info)
|
276
ceilometerclient/common/http.py
Normal file
276
ceilometerclient/common/http.py
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
# 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 copy
|
||||||
|
import httplib
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import socket
|
||||||
|
import StringIO
|
||||||
|
import urlparse
|
||||||
|
|
||||||
|
try:
|
||||||
|
import ssl
|
||||||
|
except ImportError:
|
||||||
|
#TODO(bcwaldon): Handle this failure more gracefully
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
import simplejson as json
|
||||||
|
|
||||||
|
# Python 2.5 compat fix
|
||||||
|
if not hasattr(urlparse, 'parse_qsl'):
|
||||||
|
import cgi
|
||||||
|
urlparse.parse_qsl = cgi.parse_qsl
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_connection_params(endpoint, **kwargs):
|
||||||
|
parts = urlparse.urlparse(endpoint)
|
||||||
|
|
||||||
|
_args = (parts.hostname, parts.port, parts.path)
|
||||||
|
_kwargs = {'timeout': float(kwargs.get('timeout', 600))}
|
||||||
|
|
||||||
|
if parts.scheme == 'https':
|
||||||
|
_class = VerifiedHTTPSConnection
|
||||||
|
_kwargs['ca_file'] = kwargs.get('ca_file', 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:
|
||||||
|
return _class(*self.connection_params[1],
|
||||||
|
**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'),
|
||||||
|
('ca_file', '--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, url))
|
||||||
|
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 _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)
|
||||||
|
if self.auth_token:
|
||||||
|
kwargs['headers'].setdefault('X-Auth-Token', self.auth_token)
|
||||||
|
|
||||||
|
self.log_curl_request(method, url, kwargs)
|
||||||
|
conn = self.get_connection()
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn_params = self.connection_params[1][2]
|
||||||
|
conn_url = os.path.normpath('%s/%s' % (conn_params, url))
|
||||||
|
conn.request(method, conn_url, **kwargs)
|
||||||
|
resp = conn.getresponse()
|
||||||
|
except socket.gaierror as e:
|
||||||
|
message = "Error finding address for %(url)s: %(e)s" % locals()
|
||||||
|
raise exc.InvalidEndpoint(message=message)
|
||||||
|
except (socket.error, socket.timeout) as e:
|
||||||
|
endpoint = self.endpoint
|
||||||
|
message = "Error communicating with %(endpoint)s %(e)s" % locals()
|
||||||
|
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 = StringIO.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)
|
||||||
|
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)
|
||||||
|
|
||||||
|
if 'application/json' in resp.getheader('content-type', None):
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
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,
|
||||||
|
ca_file=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 ca_file is not None:
|
||||||
|
self.ca_file = ca_file
|
||||||
|
else:
|
||||||
|
self.ca_file = self.get_system_ca_file()
|
||||||
|
self.timeout = timeout
|
||||||
|
self.insecure = insecure
|
||||||
|
|
||||||
|
def connect(self):
|
||||||
|
"""
|
||||||
|
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).
|
||||||
|
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.ca_file}
|
||||||
|
|
||||||
|
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()
|
122
ceilometerclient/common/utils.py
Normal file
122
ceilometerclient/common/utils.py
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# 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 errno
|
||||||
|
import hashlib
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
import prettytable
|
||||||
|
|
||||||
|
from ceilometerclient import exc
|
||||||
|
from ceilometerclient.openstack.common import importutils
|
||||||
|
|
||||||
|
|
||||||
|
# Decorator for cli-args
|
||||||
|
def arg(*args, **kwargs):
|
||||||
|
def _decorator(func):
|
||||||
|
# Because of the sematics of decorator composition if we just append
|
||||||
|
# to the options list positional options will appear to be backwards.
|
||||||
|
func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs))
|
||||||
|
return func
|
||||||
|
return _decorator
|
||||||
|
|
||||||
|
|
||||||
|
def pretty_choice_list(l):
|
||||||
|
return ', '.join("'%s'" % i for i in l)
|
||||||
|
|
||||||
|
|
||||||
|
def print_list(objs, fields, field_labels, formatters={}, sortby=0):
|
||||||
|
pt = prettytable.PrettyTable([f for f in field_labels], caching=False)
|
||||||
|
pt.align = 'l'
|
||||||
|
|
||||||
|
for o in objs:
|
||||||
|
row = []
|
||||||
|
for field in fields:
|
||||||
|
if field in formatters:
|
||||||
|
row.append(formatters[field](o))
|
||||||
|
else:
|
||||||
|
data = getattr(o, field, None) or ''
|
||||||
|
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)
|
||||||
|
pt.align = 'l'
|
||||||
|
|
||||||
|
for field in d.keys():
|
||||||
|
if field in formatters:
|
||||||
|
pt.add_row([field, formatters[field](d[field])])
|
||||||
|
else:
|
||||||
|
pt.add_row([field, d[field]])
|
||||||
|
print pt.get_string(sortby='Property')
|
||||||
|
|
||||||
|
|
||||||
|
def find_resource(manager, name_or_id):
|
||||||
|
"""Helper for the _find_* methods."""
|
||||||
|
# first try to get entity as integer id
|
||||||
|
try:
|
||||||
|
if isinstance(name_or_id, int) or name_or_id.isdigit():
|
||||||
|
return manager.get(int(name_or_id))
|
||||||
|
except exc.NotFound:
|
||||||
|
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):
|
||||||
|
pass
|
||||||
|
|
||||||
|
# finally try to find entity by name
|
||||||
|
try:
|
||||||
|
return manager.find(name=name_or_id)
|
||||||
|
except exc.NotFound:
|
||||||
|
msg = "No %s with a name or ID of '%s' exists." % \
|
||||||
|
(manager.resource_class.__name__.lower(), name_or_id)
|
||||||
|
raise exc.CommandError(msg)
|
||||||
|
|
||||||
|
|
||||||
|
def string_to_bool(arg):
|
||||||
|
return arg.strip().lower() in ('t', 'true', 'yes', '1')
|
||||||
|
|
||||||
|
|
||||||
|
def env(*vars, **kwargs):
|
||||||
|
"""Search for the first defined of possibly many env vars
|
||||||
|
|
||||||
|
Returns the first environment variable defined in vars, or
|
||||||
|
returns the default defined in kwargs.
|
||||||
|
"""
|
||||||
|
for v in vars:
|
||||||
|
value = os.environ.get(v, None)
|
||||||
|
if value:
|
||||||
|
return value
|
||||||
|
return kwargs.get('default', '')
|
||||||
|
|
||||||
|
|
||||||
|
def import_versioned_module(version, submodule=None):
|
||||||
|
module = 'ceilometerclient.v%s' % version
|
||||||
|
if submodule:
|
||||||
|
module = '.'.join((module, submodule))
|
||||||
|
return importutils.import_module(module)
|
||||||
|
|
||||||
|
|
||||||
|
def exit(msg=''):
|
||||||
|
if msg:
|
||||||
|
print >> sys.stderr, msg
|
||||||
|
sys.exit(1)
|
163
ceilometerclient/exc.py
Normal file
163
ceilometerclient/exc.py
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
||||||
|
# not use this file except in compliance with the License. You may obtain
|
||||||
|
# a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing, software
|
||||||
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
||||||
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
||||||
|
# License for the specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
class BaseException(Exception):
|
||||||
|
"""An error occurred."""
|
||||||
|
def __init__(self, message=None):
|
||||||
|
self.message = message
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.message or self.__class__.__doc__
|
||||||
|
|
||||||
|
|
||||||
|
class CommandError(BaseException):
|
||||||
|
"""Invalid usage of CLI"""
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidEndpoint(BaseException):
|
||||||
|
"""The provided endpoint is invalid."""
|
||||||
|
|
||||||
|
|
||||||
|
class CommunicationError(BaseException):
|
||||||
|
"""Unable to communicate with server."""
|
||||||
|
|
||||||
|
|
||||||
|
class ClientException(Exception):
|
||||||
|
"""DEPRECATED"""
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPException(ClientException):
|
||||||
|
"""Base exception for all HTTP-derived exceptions"""
|
||||||
|
code = 'N/A'
|
||||||
|
|
||||||
|
def __init__(self, details=None):
|
||||||
|
self.details = details
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s (HTTP %s)" % (self.__class__.__name__, self.code)
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPMultipleChoices(HTTPException):
|
||||||
|
code = 300
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
self.details = ("Requested version of OpenStack Images API is not"
|
||||||
|
"available.")
|
||||||
|
return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code,
|
||||||
|
self.details)
|
||||||
|
|
||||||
|
|
||||||
|
class BadRequest(HTTPException):
|
||||||
|
"""DEPRECATED"""
|
||||||
|
code = 400
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPBadRequest(BadRequest):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Unauthorized(HTTPException):
|
||||||
|
"""DEPRECATED"""
|
||||||
|
code = 401
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPUnauthorized(Unauthorized):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class Forbidden(HTTPException):
|
||||||
|
"""DEPRECATED"""
|
||||||
|
code = 403
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPForbidden(Forbidden):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class NotFound(HTTPException):
|
||||||
|
"""DEPRECATED"""
|
||||||
|
code = 404
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPNotFound(NotFound):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPMethodNotAllowed(HTTPException):
|
||||||
|
code = 405
|
||||||
|
|
||||||
|
|
||||||
|
class Conflict(HTTPException):
|
||||||
|
"""DEPRECATED"""
|
||||||
|
code = 409
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPConflict(Conflict):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class OverLimit(HTTPException):
|
||||||
|
"""DEPRECATED"""
|
||||||
|
code = 413
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPOverLimit(OverLimit):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPInternalServerError(HTTPException):
|
||||||
|
code = 500
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPNotImplemented(HTTPException):
|
||||||
|
code = 501
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPBadGateway(HTTPException):
|
||||||
|
code = 502
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceUnavailable(HTTPException):
|
||||||
|
"""DEPRECATED"""
|
||||||
|
code = 503
|
||||||
|
|
||||||
|
|
||||||
|
class HTTPServiceUnavailable(ServiceUnavailable):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
#NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception
|
||||||
|
# classes
|
||||||
|
_code_map = {}
|
||||||
|
for obj_name in dir(sys.modules[__name__]):
|
||||||
|
if obj_name.startswith('HTTP'):
|
||||||
|
obj = getattr(sys.modules[__name__], obj_name)
|
||||||
|
_code_map[obj.code] = obj
|
||||||
|
|
||||||
|
|
||||||
|
def from_response(response):
|
||||||
|
"""Return an instance of an HTTPException based on httplib response."""
|
||||||
|
cls = _code_map.get(response.status, HTTPException)
|
||||||
|
return cls()
|
||||||
|
|
||||||
|
|
||||||
|
class NoTokenLookupException(Exception):
|
||||||
|
"""DEPRECATED"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class EndpointNotFound(Exception):
|
||||||
|
"""DEPRECATED"""
|
||||||
|
pass
|
0
ceilometerclient/openstack/__init__.py
Normal file
0
ceilometerclient/openstack/__init__.py
Normal file
0
ceilometerclient/openstack/common/__init__.py
Normal file
0
ceilometerclient/openstack/common/__init__.py
Normal file
59
ceilometerclient/openstack/common/importutils.py
Normal file
59
ceilometerclient/openstack/common/importutils.py
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 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 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), exc:
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Import 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]
|
366
ceilometerclient/openstack/common/setup.py
Normal file
366
ceilometerclient/openstack/common/setup.py
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2011 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Utilities with minimum-depends for use in setup.py
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
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:
|
||||||
|
l = l.strip()
|
||||||
|
if not l.startswith('#') and ' ' in l:
|
||||||
|
canonical_email, alias = [x for x in l.split(' ')
|
||||||
|
if x.startswith('<')]
|
||||||
|
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 in mapping.iteritems():
|
||||||
|
changelog = changelog.replace(alias, email)
|
||||||
|
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 write_requirements():
|
||||||
|
venv = os.environ.get('VIRTUAL_ENV', None)
|
||||||
|
if venv is not None:
|
||||||
|
with open("requirements.txt", "w") as req_file:
|
||||||
|
output = subprocess.Popen(["pip", "-E", venv, "freeze", "-l"],
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
requirements = output.communicate()[0].strip()
|
||||||
|
req_file.write(requirements)
|
||||||
|
|
||||||
|
|
||||||
|
def _run_shell_command(cmd):
|
||||||
|
if os.name == 'nt':
|
||||||
|
output = subprocess.Popen(["cmd.exe", "/C", cmd],
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
else:
|
||||||
|
output = subprocess.Popen(["/bin/sh", "-c", cmd],
|
||||||
|
stdout=subprocess.PIPE)
|
||||||
|
out = output.communicate()
|
||||||
|
if len(out) == 0:
|
||||||
|
return None
|
||||||
|
if len(out[0].strip()) == 0:
|
||||||
|
return None
|
||||||
|
return out[0].strip()
|
||||||
|
|
||||||
|
|
||||||
|
def _get_git_next_version_suffix(branch_name):
|
||||||
|
datestamp = datetime.datetime.now().strftime('%Y%m%d')
|
||||||
|
if branch_name == 'milestone-proposed':
|
||||||
|
revno_prefix = "r"
|
||||||
|
else:
|
||||||
|
revno_prefix = ""
|
||||||
|
_run_shell_command("git fetch origin +refs/meta/*:refs/remotes/meta/*")
|
||||||
|
milestone_cmd = "git show meta/openstack/release:%s" % branch_name
|
||||||
|
milestonever = _run_shell_command(milestone_cmd)
|
||||||
|
if milestonever:
|
||||||
|
first_half = "%s~%s" % (milestonever, datestamp)
|
||||||
|
else:
|
||||||
|
first_half = datestamp
|
||||||
|
|
||||||
|
post_version = _get_git_post_version()
|
||||||
|
# post version should look like:
|
||||||
|
# 0.1.1.4.gcc9e28a
|
||||||
|
# where the bit after the last . is the short sha, and the bit between
|
||||||
|
# the last and second to last is the revno count
|
||||||
|
(revno, sha) = post_version.split(".")[-2:]
|
||||||
|
second_half = "%s%s.%s" % (revno_prefix, revno, sha)
|
||||||
|
return ".".join((first_half, second_half))
|
||||||
|
|
||||||
|
|
||||||
|
def _get_git_current_tag():
|
||||||
|
return _run_shell_command("git tag --contains HEAD")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_git_tag_info():
|
||||||
|
return _run_shell_command("git describe --tags")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_git_post_version():
|
||||||
|
current_tag = _get_git_current_tag()
|
||||||
|
if current_tag is not None:
|
||||||
|
return current_tag
|
||||||
|
else:
|
||||||
|
tag_info = _get_git_tag_info()
|
||||||
|
if tag_info is None:
|
||||||
|
base_version = "0.0"
|
||||||
|
cmd = "git --no-pager log --oneline"
|
||||||
|
out = _run_shell_command(cmd)
|
||||||
|
revno = len(out.split("\n"))
|
||||||
|
sha = _run_shell_command("git describe --always")
|
||||||
|
else:
|
||||||
|
tag_infos = tag_info.split("-")
|
||||||
|
base_version = "-".join(tag_infos[:-2])
|
||||||
|
(revno, sha) = tag_infos[-2:]
|
||||||
|
return "%s.%s.%s" % (base_version, revno, sha)
|
||||||
|
|
||||||
|
|
||||||
|
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 read_versioninfo(project):
|
||||||
|
"""Read the versioninfo file. If it doesn't exist, we're in a github
|
||||||
|
zipball, and there's really no way to know what version we really
|
||||||
|
are, but that should be ok, because the utility of that should be
|
||||||
|
just about nil if this code path is in use in the first place."""
|
||||||
|
versioninfo_path = os.path.join(project, 'versioninfo')
|
||||||
|
if os.path.exists(versioninfo_path):
|
||||||
|
with open(versioninfo_path, 'r') as vinfo:
|
||||||
|
version = vinfo.read().strip()
|
||||||
|
else:
|
||||||
|
version = "0.0.0"
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def write_versioninfo(project, version):
|
||||||
|
"""Write a simple file containing the version of the package."""
|
||||||
|
with open(os.path.join(project, 'versioninfo'), 'w') as fil:
|
||||||
|
fil.write("%s\n" % version)
|
||||||
|
|
||||||
|
|
||||||
|
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):
|
||||||
|
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 ['html', 'man']:
|
||||||
|
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)
|
||||||
|
cmdclass['build_sphinx'] = LocalBuildDoc
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return cmdclass
|
||||||
|
|
||||||
|
|
||||||
|
def get_git_branchname():
|
||||||
|
for branch in _run_shell_command("git branch --color=never").split("\n"):
|
||||||
|
if branch.startswith('*'):
|
||||||
|
_branch_name = branch.split()[1].strip()
|
||||||
|
if _branch_name == "(no":
|
||||||
|
_branch_name = "no-branch"
|
||||||
|
return _branch_name
|
||||||
|
|
||||||
|
|
||||||
|
def get_pre_version(projectname, base_version):
|
||||||
|
"""Return a version which is leading up to a version that will
|
||||||
|
be released in the future."""
|
||||||
|
if os.path.isdir('.git'):
|
||||||
|
current_tag = _get_git_current_tag()
|
||||||
|
if current_tag is not None:
|
||||||
|
version = current_tag
|
||||||
|
else:
|
||||||
|
branch_name = os.getenv('BRANCHNAME',
|
||||||
|
os.getenv('GERRIT_REFNAME',
|
||||||
|
get_git_branchname()))
|
||||||
|
version_suffix = _get_git_next_version_suffix(branch_name)
|
||||||
|
version = "%s~%s" % (base_version, version_suffix)
|
||||||
|
write_versioninfo(projectname, version)
|
||||||
|
return version
|
||||||
|
else:
|
||||||
|
version = read_versioninfo(projectname)
|
||||||
|
return version
|
||||||
|
|
||||||
|
|
||||||
|
def get_post_version(projectname):
|
||||||
|
"""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'):
|
||||||
|
version = _get_git_post_version()
|
||||||
|
write_versioninfo(projectname, version)
|
||||||
|
return version
|
||||||
|
return read_versioninfo(projectname)
|
148
ceilometerclient/openstack/common/version.py
Normal file
148
ceilometerclient/openstack/common/version.py
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# Copyright 2012 OpenStack LLC
|
||||||
|
#
|
||||||
|
# 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 auto-generated versioninfo files.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import pkg_resources
|
||||||
|
|
||||||
|
import setup
|
||||||
|
|
||||||
|
|
||||||
|
class _deferred_version_string(object):
|
||||||
|
"""Internal helper class which provides delayed version calculation."""
|
||||||
|
def __init__(self, version_info, prefix):
|
||||||
|
self.version_info = version_info
|
||||||
|
self.prefix = prefix
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return "%s%s" % (self.prefix, self.version_info.version_string())
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "%s%s" % (self.prefix, self.version_info.version_string())
|
||||||
|
|
||||||
|
|
||||||
|
class VersionInfo(object):
|
||||||
|
|
||||||
|
def __init__(self, package, python_package=None, pre_version=None):
|
||||||
|
"""Object that understands versioning for a package
|
||||||
|
:param package: name of the top level python namespace. For glance,
|
||||||
|
this would be "glance" for python-glanceclient, it
|
||||||
|
would be "glanceclient"
|
||||||
|
:param python_package: optional name of the project name. For
|
||||||
|
glance this can be left unset. For
|
||||||
|
python-glanceclient, this would be
|
||||||
|
"python-glanceclient"
|
||||||
|
:param pre_version: optional version that the project is working to
|
||||||
|
"""
|
||||||
|
self.package = package
|
||||||
|
if python_package is None:
|
||||||
|
self.python_package = package
|
||||||
|
else:
|
||||||
|
self.python_package = python_package
|
||||||
|
self.pre_version = pre_version
|
||||||
|
self.version = None
|
||||||
|
|
||||||
|
def _generate_version(self):
|
||||||
|
"""Defer to the openstack.common.setup routines for making a
|
||||||
|
version from git."""
|
||||||
|
if self.pre_version is None:
|
||||||
|
return setup.get_post_version(self.python_package)
|
||||||
|
else:
|
||||||
|
return setup.get_pre_version(self.python_package, self.pre_version)
|
||||||
|
|
||||||
|
def _newer_version(self, pending_version):
|
||||||
|
"""Check to see if we're working with a stale version or not.
|
||||||
|
We expect a version string that either looks like:
|
||||||
|
2012.2~f3~20120708.10.4426392
|
||||||
|
which is an unreleased version of a pre-version, or:
|
||||||
|
0.1.1.4.gcc9e28a
|
||||||
|
which is an unreleased version of a post-version, or:
|
||||||
|
0.1.1
|
||||||
|
Which is a release and which should match tag.
|
||||||
|
For now, if we have a date-embedded version, check to see if it's
|
||||||
|
old, and if so re-generate. Otherwise, just deal with it.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
version_date = int(self.version.split("~")[-1].split('.')[0])
|
||||||
|
if version_date < int(datetime.date.today().strftime('%Y%m%d')):
|
||||||
|
return self._generate_version()
|
||||||
|
else:
|
||||||
|
return pending_version
|
||||||
|
except Exception:
|
||||||
|
return pending_version
|
||||||
|
|
||||||
|
def version_string_with_vcs(self, always=False):
|
||||||
|
"""Return the full version of the package including suffixes indicating
|
||||||
|
VCS status.
|
||||||
|
|
||||||
|
For instance, if we are working towards the 2012.2 release,
|
||||||
|
canonical_version_string should return 2012.2 if this is a final
|
||||||
|
release, or else something like 2012.2~f1~20120705.20 if it's not.
|
||||||
|
|
||||||
|
:param always: if true, skip all version caching
|
||||||
|
"""
|
||||||
|
if always:
|
||||||
|
self.version = self._generate_version()
|
||||||
|
|
||||||
|
if self.version is None:
|
||||||
|
|
||||||
|
requirement = pkg_resources.Requirement.parse(self.python_package)
|
||||||
|
versioninfo = "%s/versioninfo" % self.package
|
||||||
|
try:
|
||||||
|
raw_version = pkg_resources.resource_string(requirement,
|
||||||
|
versioninfo)
|
||||||
|
self.version = self._newer_version(raw_version.strip())
|
||||||
|
except (IOError, pkg_resources.DistributionNotFound):
|
||||||
|
self.version = self._generate_version()
|
||||||
|
|
||||||
|
return self.version
|
||||||
|
|
||||||
|
def canonical_version_string(self, always=False):
|
||||||
|
"""Return the simple version of the package excluding any suffixes.
|
||||||
|
|
||||||
|
For instance, if we are working towards the 2012.2 release,
|
||||||
|
canonical_version_string should return 2012.2 in all cases.
|
||||||
|
|
||||||
|
:param always: if true, skip all version caching
|
||||||
|
"""
|
||||||
|
return self.version_string_with_vcs(always).split('~')[0]
|
||||||
|
|
||||||
|
def version_string(self, always=False):
|
||||||
|
"""Return the base version of the package.
|
||||||
|
|
||||||
|
For instance, if we are working towards the 2012.2 release,
|
||||||
|
version_string should return 2012.2 if this is a final release, or
|
||||||
|
2012.2-dev if it is not.
|
||||||
|
|
||||||
|
:param always: if true, skip all version caching
|
||||||
|
"""
|
||||||
|
version_parts = self.version_string_with_vcs(always).split('~')
|
||||||
|
if len(version_parts) == 1:
|
||||||
|
return version_parts[0]
|
||||||
|
else:
|
||||||
|
return '%s-dev' % (version_parts[0],)
|
||||||
|
|
||||||
|
def deferred_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
|
||||||
|
"""
|
||||||
|
return _deferred_version_string(self, prefix)
|
335
ceilometerclient/shell.py
Normal file
335
ceilometerclient/shell.py
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
Command-line interface to the OpenStack Metrices API.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import httplib2
|
||||||
|
import logging
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from keystoneclient.v2_0 import client as ksclient
|
||||||
|
|
||||||
|
from ceilometerclient import exc
|
||||||
|
from ceilometerclient import client as ceilometerclient
|
||||||
|
from ceilometerclient.common import utils
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class CeilometerShell(object):
|
||||||
|
|
||||||
|
def get_base_parser(self):
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
prog='ceilometer',
|
||||||
|
description=__doc__.strip(),
|
||||||
|
epilog='See "ceilometer help COMMAND" '
|
||||||
|
'for help on a specific command.',
|
||||||
|
add_help=False,
|
||||||
|
formatter_class=HelpFormatter,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Global arguments
|
||||||
|
parser.add_argument('-h', '--help',
|
||||||
|
action='store_true',
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
|
||||||
|
parser.add_argument('-d', '--debug',
|
||||||
|
default=bool(utils.env('CEILOMETERCLIENT_DEBUG')),
|
||||||
|
action='store_true',
|
||||||
|
help='Defaults to env[CEILOMETERCLIENT_DEBUG]')
|
||||||
|
|
||||||
|
parser.add_argument('-v', '--verbose',
|
||||||
|
default=False, action="store_true",
|
||||||
|
help="Print more verbose output")
|
||||||
|
|
||||||
|
parser.add_argument('-k', '--insecure',
|
||||||
|
default=False,
|
||||||
|
action='store_true',
|
||||||
|
help="Explicitly allow glanceclient 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('--ca-file',
|
||||||
|
help='Path of CA SSL certificate(s) used to verify the remote '
|
||||||
|
'server\'s certificate. Without this option glance looks '
|
||||||
|
'for the default system CA certificates.')
|
||||||
|
|
||||||
|
parser.add_argument('--timeout',
|
||||||
|
default=600,
|
||||||
|
help='Number of seconds to wait for a response')
|
||||||
|
|
||||||
|
parser.add_argument('--os-username',
|
||||||
|
default=utils.env('OS_USERNAME'),
|
||||||
|
help='Defaults to env[OS_USERNAME]')
|
||||||
|
|
||||||
|
parser.add_argument('--os_username',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--os-password',
|
||||||
|
default=utils.env('OS_PASSWORD'),
|
||||||
|
help='Defaults to env[OS_PASSWORD]')
|
||||||
|
|
||||||
|
parser.add_argument('--os_password',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--os-tenant-id',
|
||||||
|
default=utils.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=utils.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=utils.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=utils.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=utils.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',
|
||||||
|
default=utils.env('CEILOMETER_URL'),
|
||||||
|
help='Defaults to env[CEILOMETER_URL]')
|
||||||
|
|
||||||
|
parser.add_argument('--ceilometer_url',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--ceilometer-api-version',
|
||||||
|
default=utils.env('CEILOMETER_API_VERSION', default='1'),
|
||||||
|
help='Defaults to env[CEILOMETER_API_VERSION] or 1')
|
||||||
|
|
||||||
|
parser.add_argument('--ceilometer_api_version',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
parser.add_argument('--os-service-type',
|
||||||
|
default=utils.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=utils.env('OS_ENDPOINT_TYPE'),
|
||||||
|
help='Defaults to env[OS_ENDPOINT_TYPE]')
|
||||||
|
|
||||||
|
parser.add_argument('--os_endpoint_type',
|
||||||
|
help=argparse.SUPPRESS)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def get_subcommand_parser(self, version):
|
||||||
|
parser = self.get_base_parser()
|
||||||
|
|
||||||
|
self.subcommands = {}
|
||||||
|
subparsers = parser.add_subparsers(metavar='<subcommand>')
|
||||||
|
submodule = utils.import_versioned_module(version, 'shell')
|
||||||
|
self._find_actions(subparsers, submodule)
|
||||||
|
self._find_actions(subparsers, self)
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
def _find_actions(self, subparsers, actions_module):
|
||||||
|
for attr in (a for a in dir(actions_module) if a.startswith('do_')):
|
||||||
|
# I prefer to be hypen-separated instead of underscores.
|
||||||
|
command = attr[3:].replace('_', '-')
|
||||||
|
callback = getattr(actions_module, attr)
|
||||||
|
desc = callback.__doc__ or ''
|
||||||
|
help = desc.strip().split('\n')[0]
|
||||||
|
arguments = getattr(callback, 'arguments', [])
|
||||||
|
|
||||||
|
subparser = subparsers.add_parser(command,
|
||||||
|
help=help,
|
||||||
|
description=desc,
|
||||||
|
add_help=False,
|
||||||
|
formatter_class=HelpFormatter
|
||||||
|
)
|
||||||
|
subparser.add_argument('-h', '--help',
|
||||||
|
action='help',
|
||||||
|
help=argparse.SUPPRESS,
|
||||||
|
)
|
||||||
|
self.subcommands[command] = subparser
|
||||||
|
for (args, kwargs) in arguments:
|
||||||
|
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 'orchestration',
|
||||||
|
endpoint_type=kwargs.get('endpoint_type') or 'publicURL')
|
||||||
|
|
||||||
|
def _setup_debugging(self, debug):
|
||||||
|
if debug:
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(levelname)s (%(module)s:%(lineno)d) %(message)s",
|
||||||
|
level=logging.DEBUG)
|
||||||
|
|
||||||
|
httplib2.debuglevel = 1
|
||||||
|
else:
|
||||||
|
logging.basicConfig(
|
||||||
|
format="%(levelname)s %(message)s",
|
||||||
|
level=logging.INFO)
|
||||||
|
|
||||||
|
def main(self, argv):
|
||||||
|
# Parse args once to find version
|
||||||
|
parser = self.get_base_parser()
|
||||||
|
(options, args) = parser.parse_known_args(argv)
|
||||||
|
self._setup_debugging(options.debug)
|
||||||
|
|
||||||
|
# build available subcommands based on version
|
||||||
|
api_version = options.ceilometer_api_version
|
||||||
|
subcommand_parser = self.get_subcommand_parser(api_version)
|
||||||
|
self.parser = subcommand_parser
|
||||||
|
|
||||||
|
# Handle top-level --help/-h before attempting to parse
|
||||||
|
# a command off the command line
|
||||||
|
if options.help or not argv:
|
||||||
|
self.do_help(options)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# Parse args again and call whatever callback was selected
|
||||||
|
args = subcommand_parser.parse_args(argv)
|
||||||
|
|
||||||
|
# Short-circuit and deal with help command right away.
|
||||||
|
if args.func == self.do_help:
|
||||||
|
self.do_help(args)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if not args.os_username:
|
||||||
|
raise exc.CommandError("You must provide a username via"
|
||||||
|
" either --os-username or env[OS_USERNAME]")
|
||||||
|
|
||||||
|
if not args.os_password:
|
||||||
|
raise exc.CommandError("You must provide a password via"
|
||||||
|
" either --os-password or env[OS_PASSWORD]")
|
||||||
|
|
||||||
|
if not (args.os_tenant_id or args.os_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:
|
||||||
|
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)
|
||||||
|
|
||||||
|
try:
|
||||||
|
args.func(client, args)
|
||||||
|
except exc.Unauthorized:
|
||||||
|
raise exc.CommandError("Invalid OpenStack Identity credentials.")
|
||||||
|
|
||||||
|
@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.
|
||||||
|
"""
|
||||||
|
if getattr(args, 'command', None):
|
||||||
|
if args.command in self.subcommands:
|
||||||
|
self.subcommands[args.command].print_help()
|
||||||
|
else:
|
||||||
|
raise exc.CommandError("'%s' is not a valid subcommand" %
|
||||||
|
args.command)
|
||||||
|
else:
|
||||||
|
self.parser.print_help()
|
||||||
|
|
||||||
|
|
||||||
|
class HelpFormatter(argparse.HelpFormatter):
|
||||||
|
def start_section(self, heading):
|
||||||
|
# Title-case the headings
|
||||||
|
heading = '%s%s' % (heading[0].upper(), heading[1:])
|
||||||
|
super(HelpFormatter, self).start_section(heading)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
try:
|
||||||
|
CeilometerShell().main(sys.argv[1:])
|
||||||
|
|
||||||
|
except Exception, e:
|
||||||
|
print >> sys.stderr, e
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
16
ceilometerclient/v1/__init__.py
Normal file
16
ceilometerclient/v1/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from ceilometerclient.v1.client import Client
|
37
ceilometerclient/v1/client.py
Normal file
37
ceilometerclient/v1/client.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
# 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.
|
||||||
|
|
||||||
|
from ceilometerclient.common import http
|
||||||
|
from ceilometerclient.v1 import meters
|
||||||
|
|
||||||
|
|
||||||
|
class Client(http.HTTPClient):
|
||||||
|
"""Client for the Ceilometer v1 API.
|
||||||
|
|
||||||
|
:param string endpoint: A user-supplied endpoint URL for the ceilometer
|
||||||
|
service.
|
||||||
|
:param string token: Token for authentication.
|
||||||
|
:param integer timeout: Allows customization of the timeout for client
|
||||||
|
http requests. (optional)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
""" Initialize a new client for the Ceilometer v1 API. """
|
||||||
|
super(Client, self).__init__(*args, **kwargs)
|
||||||
|
self.meters = meters.MeterManager(self)
|
||||||
|
self.events = meters.EventManager(self)
|
||||||
|
self.users = meters.UserManager(self)
|
||||||
|
self.resources = meters.ResourceManager(self)
|
||||||
|
self.projects = meters.ProjectManager(self)
|
143
ceilometerclient/v1/meters.py
Normal file
143
ceilometerclient/v1/meters.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
# Copyright 2012 OpenMeter 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.
|
||||||
|
|
||||||
|
from ceilometerclient.common import base
|
||||||
|
|
||||||
|
class User(base.Resource):
|
||||||
|
def __init__(self, manager, info, loaded=False):
|
||||||
|
print 'user %s' % str(info)
|
||||||
|
_d = {unicode('user_id'): info}
|
||||||
|
super(User, self).__init__(manager, _d, loaded)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<User %s>" % self._info
|
||||||
|
|
||||||
|
def data(self, **kwargs):
|
||||||
|
return self.manager.data(self, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class UserManager(base.Manager):
|
||||||
|
resource_class = User
|
||||||
|
|
||||||
|
def list(self, **kwargs):
|
||||||
|
return self._list('/users', 'users')
|
||||||
|
|
||||||
|
|
||||||
|
class Project(base.Resource):
|
||||||
|
def __init__(self, manager, info, loaded=False):
|
||||||
|
_d = {unicode('project_id'): info}
|
||||||
|
super(Project, self).__init__(manager, _d, loaded)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Project %s>" % self._info
|
||||||
|
|
||||||
|
def data(self, **kwargs):
|
||||||
|
return self.manager.data(self, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ProjectManager(base.Manager):
|
||||||
|
resource_class = Project
|
||||||
|
|
||||||
|
def list(self, **kwargs):
|
||||||
|
s = kwargs.get('source')
|
||||||
|
if s:
|
||||||
|
path = '/sources/%s/projects' % (kwargs['source'])
|
||||||
|
else:
|
||||||
|
path = '/projects'
|
||||||
|
|
||||||
|
return self._list(path, 'projects')
|
||||||
|
|
||||||
|
|
||||||
|
class Resource(base.Resource):
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Resource %s>" % self._info
|
||||||
|
|
||||||
|
def data(self, **kwargs):
|
||||||
|
return self.manager.data(self, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class ResourceManager(base.Manager):
|
||||||
|
resource_class = Resource
|
||||||
|
|
||||||
|
def list(self, **kwargs):
|
||||||
|
u = kwargs.get('user_id')
|
||||||
|
s = kwargs.get('source')
|
||||||
|
if u:
|
||||||
|
path = '/users/%s/resources' % (u)
|
||||||
|
elif s:
|
||||||
|
path = '/sources/%s/resources' % (s)
|
||||||
|
else:
|
||||||
|
path = '/resources'
|
||||||
|
return self._list(path, 'resources')
|
||||||
|
|
||||||
|
|
||||||
|
class Event(base.Resource):
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Event %s>" % self._info
|
||||||
|
|
||||||
|
def data(self, **kwargs):
|
||||||
|
return self.manager.data(self, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class EventManager(base.Manager):
|
||||||
|
resource_class = Event
|
||||||
|
|
||||||
|
def list(self, **kwargs):
|
||||||
|
c = kwargs['counter_name']
|
||||||
|
r = kwargs.get('resource_id')
|
||||||
|
u = kwargs.get('user_id')
|
||||||
|
p = kwargs.get('project_id')
|
||||||
|
s = kwargs.get('source')
|
||||||
|
if r:
|
||||||
|
path = '/resources/%s/events/%s' % (r, c)
|
||||||
|
elif u:
|
||||||
|
path = '/users/%s/events/%s' % (u, c)
|
||||||
|
elif p:
|
||||||
|
path = '/projects/%s/events/%s' % (p, c)
|
||||||
|
elif s:
|
||||||
|
path = '/sources/%s/events/%s' % (s, c)
|
||||||
|
else:
|
||||||
|
path = '/events'
|
||||||
|
|
||||||
|
self._list(path, 'events')
|
||||||
|
|
||||||
|
|
||||||
|
class Meter(base.Resource):
|
||||||
|
def __repr__(self):
|
||||||
|
return "<Meter %s>" % self._info
|
||||||
|
|
||||||
|
def data(self, **kwargs):
|
||||||
|
return self.manager.data(self, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class MeterManager(base.Manager):
|
||||||
|
resource_class = Meter
|
||||||
|
|
||||||
|
def list(self, **kwargs):
|
||||||
|
r = kwargs.get('resource_id')
|
||||||
|
u = kwargs.get('user_id')
|
||||||
|
p = kwargs.get('project_id')
|
||||||
|
s = kwargs.get('source')
|
||||||
|
if u:
|
||||||
|
path = '/users/%s/meters' % u
|
||||||
|
elif r:
|
||||||
|
path = '/resources/%s/meters' % r
|
||||||
|
elif p:
|
||||||
|
path = '/projects/%s/meters' % p
|
||||||
|
elif s:
|
||||||
|
path = '/sources/%s/meters' % s
|
||||||
|
else:
|
||||||
|
path = '/meters'
|
||||||
|
return self._list(path, 'meters')
|
109
ceilometerclient/v1/shell.py
Normal file
109
ceilometerclient/v1/shell.py
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
# 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 json
|
||||||
|
|
||||||
|
from ceilometerclient.common import utils
|
||||||
|
import ceilometerclient.exc as exc
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('-s', '--source', metavar='<SOURCE>',
|
||||||
|
help='ID of the resource to show events for.')
|
||||||
|
@utils.arg('-r', '--resource_id', metavar='<RESOURCE_ID>',
|
||||||
|
help='ID of the resource to show events for.')
|
||||||
|
@utils.arg('-u', '--user_id', metavar='<USER_ID>',
|
||||||
|
help='ID of the user to show events for.')
|
||||||
|
@utils.arg('-p', '--project_id', metavar='<PROJECT_ID>',
|
||||||
|
help='ID of the project to show events for.')
|
||||||
|
@utils.arg('-c', '--counter_name', metavar='<NAME>',
|
||||||
|
help='Name of meter to show events for.')
|
||||||
|
def do_event_list(cc, args):
|
||||||
|
'''List the events for this meters'''
|
||||||
|
fields = {'counter_name': args.counter_name,
|
||||||
|
'resource_id': args.resource_id,
|
||||||
|
'user_id': args.user_id,
|
||||||
|
'project_id': args.project_id,
|
||||||
|
'source': args.source}
|
||||||
|
try:
|
||||||
|
events = cc.events.list(**fields)
|
||||||
|
except exc.HTTPNotFound:
|
||||||
|
raise exc.CommandError('Events not found: %s' % args.counter_name)
|
||||||
|
else:
|
||||||
|
json_format = lambda js: json.dumps(js, indent=2)
|
||||||
|
formatters = {
|
||||||
|
'metadata': json_format,
|
||||||
|
}
|
||||||
|
for e in events:
|
||||||
|
utils.print_dict(e.to_dict(), formatters=formatters)
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('-s', '--source', metavar='<SOURCE>',
|
||||||
|
help='ID of the resource to show events for.')
|
||||||
|
@utils.arg('-r', '--resource_id', metavar='<RESOURCE_ID>',
|
||||||
|
help='ID of the resource to show events for.')
|
||||||
|
@utils.arg('-u', '--user_id', metavar='<USER_ID>',
|
||||||
|
help='ID of the user to show events for.')
|
||||||
|
@utils.arg('-p', '--project_id', metavar='<PROJECT_ID>',
|
||||||
|
help='ID of the project to show events for.')
|
||||||
|
def do_metric_list(cc, args={}):
|
||||||
|
'''List the user's metrices'''
|
||||||
|
fields = {'resource_id': args.resource_id,
|
||||||
|
'user_id': args.user_id,
|
||||||
|
'project_id': args.project_id,
|
||||||
|
'source': args.source}
|
||||||
|
metrics = cc.meters.list(**fields)
|
||||||
|
field_labels = ['Name', 'Resource ID', 'User ID']
|
||||||
|
fields = ['counter_name', 'resource_id', 'user_id']
|
||||||
|
utils.print_list(metrics, fields, field_labels,
|
||||||
|
sortby=0)
|
||||||
|
|
||||||
|
|
||||||
|
def do_user_list(cc, args={}):
|
||||||
|
'''List the users'''
|
||||||
|
kwargs = {}
|
||||||
|
users = cc.users.list(**kwargs)
|
||||||
|
field_labels = ['User ID']
|
||||||
|
fields = ['user_id']
|
||||||
|
utils.print_list(users, fields, field_labels,
|
||||||
|
sortby=0)
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('-s', '--source', metavar='<SOURCE>',
|
||||||
|
help='ID of the resource to show events for.')
|
||||||
|
@utils.arg('-u', '--user_id', metavar='<USER_ID>',
|
||||||
|
help='ID of the user to show events for.')
|
||||||
|
def do_resource_list(cc, args={}):
|
||||||
|
'''List the users'''
|
||||||
|
kwargs = {'source': args.source,
|
||||||
|
'user_id': args.user_id}
|
||||||
|
resources = cc.resources.list(**kwargs)
|
||||||
|
|
||||||
|
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('-s', '--source', metavar='<SOURCE>',
|
||||||
|
help='ID of the resource to show events for.')
|
||||||
|
def do_project_list(cc, args={}):
|
||||||
|
'''List the projects'''
|
||||||
|
kwargs = {'source': args.source}
|
||||||
|
projects = cc.projects.list(**kwargs)
|
||||||
|
|
||||||
|
field_labels = ['Project ID']
|
||||||
|
fields = ['project_id']
|
||||||
|
utils.print_list(projects, fields, field_labels,
|
||||||
|
sortby=0)
|
19
ceilometerclient/version.py
Normal file
19
ceilometerclient/version.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# vim: tabstop=4 shiftwidth=4 softtabstop=4
|
||||||
|
|
||||||
|
# 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.openstack.common import version as common_version
|
||||||
|
|
||||||
|
version_info = common_version.VersionInfo('ceilometerclient',
|
||||||
|
python_package='python-ceilometerclient')
|
2
setup.py
2
setup.py
@@ -43,7 +43,7 @@ setuptools.setup(
|
|||||||
'Programming Language :: Python',
|
'Programming Language :: Python',
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': ['ceilometerclient = ceilometerclient.shell:main']
|
'console_scripts': ['ceilometer = ceilometerclient.shell:main']
|
||||||
},
|
},
|
||||||
dependency_links=setup.parse_dependency_links(),
|
dependency_links=setup.parse_dependency_links(),
|
||||||
tests_require=setup.parse_requirements(['tools/test-requires']),
|
tests_require=setup.parse_requirements(['tools/test-requires']),
|
||||||
|
0
tests/__init__.py
Normal file
0
tests/__init__.py
Normal file
80
tests/fakes.py
Normal file
80
tests/fakes.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
|
||||||
|
import httplib
|
||||||
|
import json
|
||||||
|
import mox
|
||||||
|
|
||||||
|
from keystoneclient.v2_0 import client as ksclient
|
||||||
|
from ceilometerclient.v1 import client as v1client
|
||||||
|
|
||||||
|
|
||||||
|
def script_keystone_client():
|
||||||
|
ksclient.Client(auth_url='http://no.where',
|
||||||
|
insecure=False,
|
||||||
|
password='password',
|
||||||
|
tenant_id='',
|
||||||
|
tenant_name='tenant_name',
|
||||||
|
username='username').AndReturn(
|
||||||
|
FakeKeystone('abcd1234'))
|
||||||
|
|
||||||
|
|
||||||
|
def script_heat_list():
|
||||||
|
resp_dict = {"stacks": [{
|
||||||
|
"id": "1",
|
||||||
|
"stack_name": "teststack",
|
||||||
|
"stack_status": 'CREATE_COMPLETE',
|
||||||
|
"creation_time": "2012-10-25T01:58:47Z"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "2",
|
||||||
|
"stack_name": "teststack2",
|
||||||
|
"stack_status": 'IN_PROGRESS',
|
||||||
|
"creation_time": "2012-10-25T01:58:47Z"
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
resp = FakeHTTPResponse(200,
|
||||||
|
'success, yo',
|
||||||
|
{'content-type': 'application/json'},
|
||||||
|
json.dumps(resp_dict))
|
||||||
|
v1client.Client.json_request('GET',
|
||||||
|
'/stacks?limit=20').AndReturn((resp, resp_dict))
|
||||||
|
|
||||||
|
|
||||||
|
def fake_headers():
|
||||||
|
return {'X-Auth-Token': 'abcd1234',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'User-Agent': 'python-ceilometerclient'}
|
||||||
|
|
||||||
|
|
||||||
|
class FakeServiceCatalog():
|
||||||
|
def url_for(self, endpoint_type, service_type):
|
||||||
|
return 'http://192.168.1.5:8004/v1/f14b41234'
|
||||||
|
|
||||||
|
|
||||||
|
class FakeKeystone():
|
||||||
|
service_catalog = FakeServiceCatalog()
|
||||||
|
|
||||||
|
def __init__(self, auth_token):
|
||||||
|
self.auth_token = auth_token
|
||||||
|
|
||||||
|
|
||||||
|
class FakeHTTPResponse():
|
||||||
|
|
||||||
|
version = 1.1
|
||||||
|
|
||||||
|
def __init__(self, status, reason, headers, body):
|
||||||
|
self.headers = headers
|
||||||
|
self.body = body
|
||||||
|
self.status = status
|
||||||
|
self.reason = reason
|
||||||
|
|
||||||
|
def getheader(self, name, default=None):
|
||||||
|
return self.headers.get(name, default)
|
||||||
|
|
||||||
|
def getheaders(self):
|
||||||
|
return self.headers.items()
|
||||||
|
|
||||||
|
def read(self, amt=None):
|
||||||
|
b = self.body
|
||||||
|
self.body = None
|
||||||
|
return b
|
390
tests/test_shell.py
Normal file
390
tests/test_shell.py
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
import cStringIO
|
||||||
|
import os
|
||||||
|
import httplib2
|
||||||
|
import sys
|
||||||
|
|
||||||
|
import mox
|
||||||
|
import unittest
|
||||||
|
try:
|
||||||
|
import json
|
||||||
|
except ImportError:
|
||||||
|
import simplejson as json
|
||||||
|
from keystoneclient.v2_0 import client as ksclient
|
||||||
|
|
||||||
|
from ceilometerclient import exc
|
||||||
|
from ceilometerclient.v1 import client as v1client
|
||||||
|
import ceilometerclient.shell
|
||||||
|
import fakes
|
||||||
|
|
||||||
|
TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__),
|
||||||
|
'var'))
|
||||||
|
|
||||||
|
|
||||||
|
class ShellValidationTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_missing_auth(self):
|
||||||
|
_old_env, os.environ = os.environ, {
|
||||||
|
'OS_PASSWORD': 'password',
|
||||||
|
'OS_TENANT_NAME': 'tenant_name',
|
||||||
|
'OS_AUTH_URL': 'http://no.where',
|
||||||
|
}
|
||||||
|
self.shell_error('list', 'You must provide a username')
|
||||||
|
|
||||||
|
os.environ = _old_env
|
||||||
|
|
||||||
|
_old_env, os.environ = os.environ, {
|
||||||
|
'OS_USERNAME': 'username',
|
||||||
|
'OS_TENANT_NAME': 'tenant_name',
|
||||||
|
'OS_AUTH_URL': 'http://no.where',
|
||||||
|
}
|
||||||
|
self.shell_error('list', 'You must provide a password')
|
||||||
|
|
||||||
|
os.environ = _old_env
|
||||||
|
|
||||||
|
_old_env, os.environ = os.environ, {
|
||||||
|
'OS_USERNAME': 'username',
|
||||||
|
'OS_PASSWORD': 'password',
|
||||||
|
'OS_AUTH_URL': 'http://no.where',
|
||||||
|
}
|
||||||
|
self.shell_error('list', 'You must provide a tenant_id')
|
||||||
|
|
||||||
|
os.environ = _old_env
|
||||||
|
|
||||||
|
_old_env, os.environ = os.environ, {
|
||||||
|
'OS_USERNAME': 'username',
|
||||||
|
'OS_PASSWORD': 'password',
|
||||||
|
'OS_TENANT_NAME': 'tenant_name',
|
||||||
|
}
|
||||||
|
self.shell_error('list', 'You must provide an auth url')
|
||||||
|
|
||||||
|
os.environ = _old_env
|
||||||
|
|
||||||
|
def test_failed_auth(self):
|
||||||
|
m = mox.Mox()
|
||||||
|
m.StubOutWithMock(ksclient, 'Client')
|
||||||
|
m.StubOutWithMock(v1client.Client, 'json_request')
|
||||||
|
fakes.script_keystone_client()
|
||||||
|
v1client.Client.json_request('GET',
|
||||||
|
'/stacks?limit=20').AndRaise(exc.Unauthorized)
|
||||||
|
|
||||||
|
m.ReplayAll()
|
||||||
|
_old_env, os.environ = os.environ, {
|
||||||
|
'OS_USERNAME': 'username',
|
||||||
|
'OS_PASSWORD': 'password',
|
||||||
|
'OS_TENANT_NAME': 'tenant_name',
|
||||||
|
'OS_AUTH_URL': 'http://no.where',
|
||||||
|
}
|
||||||
|
self.shell_error('list', 'Invalid OpenStack Identity credentials.')
|
||||||
|
|
||||||
|
m.VerifyAll()
|
||||||
|
|
||||||
|
os.environ = _old_env
|
||||||
|
m.UnsetStubs()
|
||||||
|
|
||||||
|
def test_create_validation(self):
|
||||||
|
m = mox.Mox()
|
||||||
|
m.StubOutWithMock(ksclient, 'Client')
|
||||||
|
m.StubOutWithMock(v1client.Client, 'json_request')
|
||||||
|
fakes.script_keystone_client()
|
||||||
|
|
||||||
|
m.ReplayAll()
|
||||||
|
_old_env, os.environ = os.environ, {
|
||||||
|
'OS_USERNAME': 'username',
|
||||||
|
'OS_PASSWORD': 'password',
|
||||||
|
'OS_TENANT_NAME': 'tenant_name',
|
||||||
|
'OS_AUTH_URL': 'http://no.where',
|
||||||
|
}
|
||||||
|
self.shell_error('create teststack '
|
||||||
|
'--parameters="InstanceType=m1.large;DBUsername=wp;'
|
||||||
|
'DBPassword=verybadpassword;KeyName=heat_key;'
|
||||||
|
'LinuxDistribution=F17"',
|
||||||
|
'Need to specify exactly one of')
|
||||||
|
|
||||||
|
m.VerifyAll()
|
||||||
|
|
||||||
|
os.environ = _old_env
|
||||||
|
m.UnsetStubs()
|
||||||
|
|
||||||
|
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(unittest.TestCase):
|
||||||
|
|
||||||
|
# Patch os.environ to avoid required auth info.
|
||||||
|
def setUp(self):
|
||||||
|
self.m = mox.Mox()
|
||||||
|
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.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
|
||||||
|
|
||||||
|
def test_help_unknown_command(self):
|
||||||
|
self.assertRaises(exc.CommandError, self.shell, 'help foofoo')
|
||||||
|
|
||||||
|
def test_debug(self):
|
||||||
|
httplib2.debuglevel = 0
|
||||||
|
self.shell('--debug help')
|
||||||
|
self.assertEqual(httplib2.debuglevel, 1)
|
||||||
|
|
||||||
|
def test_help(self):
|
||||||
|
required = [
|
||||||
|
'^usage: heat',
|
||||||
|
'(?m)^See "heat 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)
|
||||||
|
|
||||||
|
def test_help_on_subcommand(self):
|
||||||
|
required = [
|
||||||
|
'^usage: heat list',
|
||||||
|
"(?m)^List the user's stacks",
|
||||||
|
]
|
||||||
|
argstrings = [
|
||||||
|
'help list',
|
||||||
|
]
|
||||||
|
for argstr in argstrings:
|
||||||
|
help_text = self.shell(argstr)
|
||||||
|
for r in required:
|
||||||
|
self.assertRegexpMatches(help_text, r)
|
||||||
|
|
||||||
|
def test_list(self):
|
||||||
|
fakes.script_keystone_client()
|
||||||
|
fakes.script_heat_list()
|
||||||
|
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
list_text = self.shell('list')
|
||||||
|
|
||||||
|
required = [
|
||||||
|
'ID',
|
||||||
|
'Status',
|
||||||
|
'Created',
|
||||||
|
'teststack/1',
|
||||||
|
'CREATE_COMPLETE',
|
||||||
|
'IN_PROGRESS',
|
||||||
|
]
|
||||||
|
for r in required:
|
||||||
|
self.assertRegexpMatches(list_text, r)
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def test_describe(self):
|
||||||
|
fakes.script_keystone_client()
|
||||||
|
resp_dict = {"stack": {
|
||||||
|
"id": "1",
|
||||||
|
"stack_name": "teststack",
|
||||||
|
"stack_status": 'CREATE_COMPLETE',
|
||||||
|
"creation_time": "2012-10-25T01:58:47Z"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
resp = fakes.FakeHTTPResponse(200,
|
||||||
|
'OK',
|
||||||
|
{'content-type': 'application/json'},
|
||||||
|
json.dumps(resp_dict))
|
||||||
|
v1client.Client.json_request('GET',
|
||||||
|
'/stacks/teststack/1').AndReturn((resp, resp_dict))
|
||||||
|
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
list_text = self.shell('describe teststack/1')
|
||||||
|
|
||||||
|
required = [
|
||||||
|
'id',
|
||||||
|
'stack_name',
|
||||||
|
'stack_status',
|
||||||
|
'creation_time',
|
||||||
|
'teststack',
|
||||||
|
'CREATE_COMPLETE',
|
||||||
|
'2012-10-25T01:58:47Z'
|
||||||
|
]
|
||||||
|
for r in required:
|
||||||
|
self.assertRegexpMatches(list_text, r)
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
fakes.script_keystone_client()
|
||||||
|
resp = fakes.FakeHTTPResponse(201,
|
||||||
|
'Created',
|
||||||
|
{'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2'},
|
||||||
|
None)
|
||||||
|
v1client.Client.json_request('POST', '/stacks',
|
||||||
|
body=mox.IgnoreArg()).AndReturn((resp, None))
|
||||||
|
fakes.script_heat_list()
|
||||||
|
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
template_file = os.path.join(TEST_VAR_DIR, 'minimal.template')
|
||||||
|
create_text = self.shell('create teststack '
|
||||||
|
'--template-file=%s '
|
||||||
|
'--parameters="InstanceType=m1.large;DBUsername=wp;'
|
||||||
|
'DBPassword=verybadpassword;KeyName=heat_key;'
|
||||||
|
'LinuxDistribution=F17"' % template_file)
|
||||||
|
|
||||||
|
required = [
|
||||||
|
'Name/ID',
|
||||||
|
'teststack/1'
|
||||||
|
]
|
||||||
|
for r in required:
|
||||||
|
self.assertRegexpMatches(create_text, r)
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def test_create_url(self):
|
||||||
|
|
||||||
|
fakes.script_keystone_client()
|
||||||
|
resp = fakes.FakeHTTPResponse(201,
|
||||||
|
'Created',
|
||||||
|
{'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2'},
|
||||||
|
None)
|
||||||
|
v1client.Client.json_request('POST', '/stacks',
|
||||||
|
body=mox.IgnoreArg()).AndReturn((resp, None))
|
||||||
|
fakes.script_heat_list()
|
||||||
|
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
create_text = self.shell('create teststack '
|
||||||
|
'--template-url=http://no.where/minimal.template '
|
||||||
|
'--parameters="InstanceType=m1.large;DBUsername=wp;'
|
||||||
|
'DBPassword=verybadpassword;KeyName=heat_key;'
|
||||||
|
'LinuxDistribution=F17"')
|
||||||
|
|
||||||
|
required = [
|
||||||
|
'Name/ID',
|
||||||
|
'teststack2/2'
|
||||||
|
]
|
||||||
|
for r in required:
|
||||||
|
self.assertRegexpMatches(create_text, r)
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def test_create_object(self):
|
||||||
|
|
||||||
|
fakes.script_keystone_client()
|
||||||
|
template_file = os.path.join(TEST_VAR_DIR, 'minimal.template')
|
||||||
|
template_data = open(template_file).read()
|
||||||
|
v1client.Client.raw_request('GET',
|
||||||
|
'http://no.where/container/minimal.template',
|
||||||
|
).AndReturn(template_data)
|
||||||
|
|
||||||
|
resp = fakes.FakeHTTPResponse(201,
|
||||||
|
'Created',
|
||||||
|
{'location': 'http://no.where/v1/tenant_id/stacks/teststack2/2'},
|
||||||
|
None)
|
||||||
|
v1client.Client.json_request('POST', '/stacks',
|
||||||
|
body=mox.IgnoreArg()).AndReturn((resp, None))
|
||||||
|
|
||||||
|
fakes.script_heat_list()
|
||||||
|
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
create_text = self.shell('create teststack2 '
|
||||||
|
'--template-object=http://no.where/container/minimal.template '
|
||||||
|
'--parameters="InstanceType=m1.large;DBUsername=wp;'
|
||||||
|
'DBPassword=verybadpassword;KeyName=heat_key;'
|
||||||
|
'LinuxDistribution=F17"')
|
||||||
|
|
||||||
|
required = [
|
||||||
|
'Name/ID',
|
||||||
|
'teststack2/2'
|
||||||
|
]
|
||||||
|
for r in required:
|
||||||
|
self.assertRegexpMatches(create_text, r)
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def test_update(self):
|
||||||
|
fakes.script_keystone_client()
|
||||||
|
resp = fakes.FakeHTTPResponse(202,
|
||||||
|
'Accepted',
|
||||||
|
{},
|
||||||
|
'The request is accepted for processing.')
|
||||||
|
v1client.Client.json_request('PUT', '/stacks/teststack2/2',
|
||||||
|
body=mox.IgnoreArg()).AndReturn((resp, None))
|
||||||
|
fakes.script_heat_list()
|
||||||
|
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
template_file = os.path.join(TEST_VAR_DIR, 'minimal.template')
|
||||||
|
create_text = self.shell('update teststack2/2 '
|
||||||
|
'--template-file=%s '
|
||||||
|
'--parameters="InstanceType=m1.large;DBUsername=wp;'
|
||||||
|
'DBPassword=verybadpassword;KeyName=heat_key;'
|
||||||
|
'LinuxDistribution=F17"' % template_file)
|
||||||
|
|
||||||
|
required = [
|
||||||
|
'Name/ID',
|
||||||
|
'teststack/1'
|
||||||
|
]
|
||||||
|
for r in required:
|
||||||
|
self.assertRegexpMatches(create_text, r)
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
||||||
|
|
||||||
|
def test_delete(self):
|
||||||
|
fakes.script_keystone_client()
|
||||||
|
resp = fakes.FakeHTTPResponse(204,
|
||||||
|
'No Content',
|
||||||
|
{},
|
||||||
|
None)
|
||||||
|
v1client.Client.raw_request('DELETE', '/stacks/teststack2/2',
|
||||||
|
).AndReturn((resp, None))
|
||||||
|
fakes.script_heat_list()
|
||||||
|
|
||||||
|
self.m.ReplayAll()
|
||||||
|
|
||||||
|
create_text = self.shell('delete teststack2/2')
|
||||||
|
|
||||||
|
required = [
|
||||||
|
'Name/ID',
|
||||||
|
'teststack/1'
|
||||||
|
]
|
||||||
|
for r in required:
|
||||||
|
self.assertRegexpMatches(create_text, r)
|
||||||
|
|
||||||
|
self.m.VerifyAll()
|
0
tests/v1/__init__.py
Normal file
0
tests/v1/__init__.py
Normal file
31
tests/v1/test_shell.py
Normal file
31
tests/v1/test_shell.py
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# 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 unittest
|
||||||
|
import ceilometerclient.v1.shell as shell
|
||||||
|
|
||||||
|
|
||||||
|
class shellTest(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_format_parameters(self):
|
||||||
|
p = shell.format_parameters('InstanceType=m1.large;DBUsername=wp;'
|
||||||
|
'DBPassword=verybadpassword;KeyName=heat_key;'
|
||||||
|
'LinuxDistribution=F17')
|
||||||
|
self.assertEqual({'InstanceType': 'm1.large',
|
||||||
|
'DBUsername': 'wp',
|
||||||
|
'DBPassword': 'verybadpassword',
|
||||||
|
'KeyName': 'heat_key',
|
||||||
|
'LinuxDistribution': 'F17'
|
||||||
|
}, p)
|
||||||
|
self.assertEqual({}, shell.format_parameters(None))
|
Reference in New Issue
Block a user