Initial Climate client implementation

Partially implements: blueprint python-client

Change-Id: I7ac8aedf5b7504c63d12119e1ba0842e3ce01e63
This commit is contained in:
Dina Belova 2013-09-22 22:22:35 +04:00
parent 6e1b360798
commit aaf7c93ae6
26 changed files with 2107 additions and 0 deletions

2
MANIFEST.in Normal file
View File

@ -0,0 +1,2 @@
include tox.ini
include README.rst

5
README.rst Normal file
View File

@ -0,0 +1,5 @@
==============
Climate client
==============
**Climate client** provides possibility to use *Climate* API(s).

View File

130
climateclient/base.py Normal file
View File

@ -0,0 +1,130 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import json
import requests
from climateclient import exception
class BaseClientManager(object):
"""Base manager to interact with a particular type of API.
There are environments, nodes and jobs types of API requests.
Manager provides CRUD operations for them.
"""
def __init__(self, climate_url, auth_token):
self.climate_url = climate_url
self.auth_token = auth_token
USER_AGENT = 'python-climateclient'
def _get(self, url, response_key):
"""Sends get request to Climate.
:param url: URL to the wanted Climate resource.
:type url: str
:param response_key: Type of resource (environment, node, job).
:type response_key: str
:returns: Resource entity (entities) that was (were) asked.
:rtype: dict | list
"""
resp, body = self.request(url, 'GET')
return body[response_key]
def _create(self, url, body, response_key):
"""Sends create request to Climate.
:param url: URL to the wanted Climate resource.
:type url: str
:param body: Values resource to be created from.
:type body: dict
:param response_key: Type of resource (environment, node, job).
:type response_key: str
:returns: Resource entity that was created.
:rtype: dict
"""
resp, body = self.request(url, 'POST', body=body)
return body[response_key]
def _delete(self, url):
"""Sends delete request to Climate.
:param url: URL to the wanted Climate resource.
:type url: str
"""
resp, body = self.request(url, 'DELETE')
def _update(self, url, body, response_key=None):
"""Sends update request to Climate.
:param url: URL to the wanted Climate resource.
:type url: str
:param body: Values resource to be updated from.
:type body: dict
:param response_key: Type of resource (environment, node, job).
:type response_key: str
:returns: Resource entity that was updated.
:rtype: dict
"""
resp, body = self.request(url, 'PUT', body=body)
return body[response_key]
def request(self, url, method, **kwargs):
"""Base request method.
Adds specific headers and URL prefix to the request.
:param url: Resource URL.
:type url: str
:param method: Method to be called (GET, POST, PUT, DELETE).
:type method: str
:returns: Response and body.
:rtype: tuple
"""
kwargs.setdefault('headers', kwargs.get('headers', {}))
kwargs['headers']['User-Agent'] = self.USER_AGENT
kwargs['headers']['Accept'] = 'application/json'
kwargs['headers']['x-auth-token'] = self.auth_token
if 'body' in kwargs:
kwargs['headers']['Content-Type'] = 'application/json'
kwargs['data'] = json.dumps(kwargs['body'])
del kwargs['body']
resp = requests.request(method, self.climate_url + url, **kwargs)
try:
body = json.loads(resp.text)
except ValueError:
body = None
if resp.status_code >= 400:
raise exception.ClimateClientException(resp.body,
code=resp.status_code)
return resp, body

36
climateclient/client.py Normal file
View File

@ -0,0 +1,36 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from climateclient import exception
from climateclient.openstack.common.gettextutils import _ # noqa
from climateclient.openstack.common import importutils
def Client(version=1, *args, **kwargs):
version_map = {
'1': 'climateclient.v1.client.Client',
'1a0': 'climateclient.v1.client.Client',
}
try:
client_path = version_map[str(version)]
except (KeyError, ValueError):
msg = _("Invalid client version '%(version)s'. "
"Must be one of: %(available_version)s") % ({
'version': version,
'available_version': ', '.join(version_map.keys())
})
raise exception.UnsupportedVersion(msg)
return importutils.import_object(client_path, *args, **kwargs)

263
climateclient/command.py Normal file
View File

@ -0,0 +1,263 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import print_function
import logging
import six
from cliff import command
from cliff.formatters import table
from cliff import lister
from cliff import show
from climateclient import utils
class OpenStackCommand(command.Command):
"""Base class for OpenStack commands."""
api = None
def run(self, parsed_args):
if not self.api:
return
else:
return super(OpenStackCommand, self).run(parsed_args)
def get_data(self, parsed_args):
pass
def take_action(self, parsed_args):
return self.get_data(parsed_args)
class TableFormatter(table.TableFormatter):
"""This class is used to keep consistency with prettytable 0.6."""
def emit_list(self, column_names, data, stdout, parsed_args):
if column_names:
super(TableFormatter, self).emit_list(column_names, data, stdout,
parsed_args)
else:
stdout.write('\n')
class ClimateCommand(OpenStackCommand):
"""Base Climate CLI command."""
api = 'reservation'
log = logging.getLogger(__name__ + '.ClimateCommand')
values_specs = []
json_indent = None
resource = None
def __init__(self, app, app_args):
super(ClimateCommand, self).__init__(app, app_args)
# NOTE(dbelova): This is no longer supported in cliff version 1.5.2
# the same moment occurred in Neutron:
# see https://bugs.launchpad.net/python-neutronclient/+bug/1265926
# if hasattr(self, 'formatters'):
# self.formatters['table'] = TableFormatter()
def get_client(self):
return self.app.client
def get_parser(self, prog_name):
parser = super(ClimateCommand, self).get_parser(prog_name)
return parser
def format_output_data(self, data):
for k, v in six.iteritems(data):
if isinstance(v, list):
value = '\n'.join(utils.dumps(
i, indent=self.json_indent) if isinstance(i, dict)
else str(i) for i in v)
data[k] = value
elif isinstance(v, dict):
value = utils.dumps(v, indent=self.json_indent)
data[k] = value
elif v is None:
data[k] = ''
def add_known_arguments(self, parser):
pass
def args2body(self, parsed_args):
return {}
class CreateCommand(ClimateCommand, show.ShowOne):
"""Create resource with passed args."""
api = 'reservation'
resource = None
log = None
def get_data(self, parsed_args):
self.log.debug('get_data(%s)' % parsed_args)
climate_client = self.get_client()
body = self.args2body(parsed_args)
resource_manager = getattr(climate_client, self.resource)
data = resource_manager.create(**body)
self.format_output_data(data)
if data:
print(self.app.stdout, 'Created a new %s:' % self.resource)
else:
data = {'': ''}
return zip(*sorted(six.iteritems(data)))
class UpdateCommand(ClimateCommand):
"""Update resource's information."""
api = 'reservation'
resource = None
log = None
def get_parser(self, prog_name):
parser = super(UpdateCommand, self).get_parser(prog_name)
parser.add_argument(
'id', metavar=self.resource.upper(),
help='ID or name of %s to update' % self.resource
)
self.add_known_arguments(parser)
return parser
def run(self, parsed_args):
self.log.debug('run(%s)' % parsed_args)
climate_client = self.get_client()
body = self.args2body(parsed_args)
res_id = utils.find_resource_id_by_name_or_id(climate_client,
self.resource,
parsed_args.id)
resource_manager = getattr(climate_client, self.resource)
resource_manager.update(res_id, **body)
print(self.app.stdout, 'Updated %s: %s' % (self.resource,
parsed_args.id))
return
class DeleteCommand(ClimateCommand):
"""Delete a given resource."""
api = 'reservation'
resource = None
log = None
allow_names = True
def get_parser(self, prog_name):
parser = super(DeleteCommand, self).get_parser(prog_name)
if self.allow_names:
help_str = 'ID or name of %s to delete'
else:
help_str = 'ID of %s to delete'
parser.add_argument(
'id', metavar=self.resource.upper(),
help=help_str % self.resource)
return parser
def run(self, parsed_args):
self.log.debug('run(%s)' % parsed_args)
climate_client = self.get_client()
resource_manager = getattr(climate_client, self.resource)
if self.allow_names:
res_id = utils.find_resource_id_by_name_or_id(climate_client,
self.resource,
parsed_args.id)
else:
res_id = parsed_args.id
resource_manager.delete(res_id)
print(self.app.stdout, 'Deleted %s: %s' % (self.resource,
parsed_args.id))
return
class ListCommand(ClimateCommand, lister.Lister):
"""List resources that belong to a given tenant."""
api = 'reservation'
resource = None
log = None
_formatters = {}
list_columns = []
unknown_parts_flag = True
def get_parser(self, prog_name):
parser = super(ListCommand, self).get_parser(prog_name)
return parser
def retrieve_list(self, parsed_args):
"""Retrieve a list of resources from Climate server"""
climate_client = self.get_client()
resource_manager = getattr(climate_client, self.resource)
data = resource_manager.list()
return data
def setup_columns(self, info, parsed_args):
columns = len(info) > 0 and sorted(info[0].keys()) or []
if not columns:
parsed_args.columns = []
elif parsed_args.columns:
columns = [col for col in parsed_args.columns if col in columns]
elif self.list_columns:
columns = [col for col in self.list_columns if col in columns]
return (
columns,
(utils.get_item_properties(s, columns, formatters=self._formatters)
for s in info)
)
def get_data(self, parsed_args):
self.log.debug('get_data(%s)' % parsed_args)
data = self.retrieve_list(parsed_args)
return self.setup_columns(data, parsed_args)
class ShowCommand(ClimateCommand, show.ShowOne):
"""Show information of a given resource."""
api = 'reservation'
resource = None
log = None
allow_names = True
def get_parser(self, prog_name):
parser = super(ShowCommand, self).get_parser(prog_name)
if self.allow_names:
help_str = 'ID or name of %s to look up'
else:
help_str = 'ID of %s to look up'
parser.add_argument('id', metavar=self.resource.upper(),
help=help_str % self.resource)
return parser
def get_data(self, parsed_args):
self.log.debug('get_data(%s)' % parsed_args)
climate_client = self.get_client()
if self.allow_names:
res_id = utils.find_resource_id_by_name_or_id(climate_client,
self.resource,
parsed_args.id)
else:
res_id = parsed_args.id
resource_manager = getattr(climate_client, self.resource)
data = resource_manager.get(res_id)
self.format_output_data(data)
return zip(*sorted(six.iteritems(data)))

View File

@ -0,0 +1,72 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from climateclient.openstack.common.gettextutils import _ # noqa
class ClimateClientException(Exception):
"""Base exception class."""
message = _("An unknown exception occurred %s.")
code = 500
def __init__(self, message=None, **kwargs):
self.kwargs = kwargs
if 'code' not in self.kwargs:
try:
self.kwargs['code'] = self.code
except AttributeError:
pass
if not message:
message = self.message % kwargs
super(ClimateClientException, self).__init__(message)
class CommandError(ClimateClientException):
"""Occurs if not all authentication vital options are set."""
message = _("You have to provide all options like user name or tenant "
"id to make authentication possible.")
code = 401
class NotAuthorized(ClimateClientException):
"""HTTP 401 - Not authorized.
User have no enough rights to perform action.
"""
code = 401
message = _("Not authorized request.")
class NoClimateEndpoint(ClimateClientException):
"""Occurs if no endpoint for Climate set in the Keystone."""
message = _("No publicURL endpoint for Climate found. Set endpoint "
"for Climate in the Keystone.")
code = 404
class NoUniqueMatch(ClimateClientException):
"""Occurs if there are more than one appropriate resources."""
message = _("There is no unique requested resource.")
code = 409
class UnsupportedVersion(ClimateClientException):
"""Occurs if unsupported client version was requested."""
message = _("Unsupported client version requested.")
code = 406

View File

View File

@ -0,0 +1,412 @@
# Copyright 2012 Red Hat, Inc.
# Copyright 2013 IBM Corp.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
gettext for openstack-common modules.
Usual usage in an openstack.common module:
from climateclient.openstack.common.gettextutils import _
"""
import copy
import gettext
import locale
from logging import handlers
import os
import re
from babel import localedata
import six
_localedir = os.environ.get('climateclient'.upper() + '_LOCALEDIR')
_t = gettext.translation('climateclient', localedir=_localedir, fallback=True)
_AVAILABLE_LANGUAGES = {}
USE_LAZY = False
def enable_lazy():
"""Convenience function for configuring _() to use lazy gettext
Call this at the start of execution to enable the gettextutils._
function to use lazy gettext functionality. This is useful if
your project is importing _ directly instead of using the
gettextutils.install() way of importing the _ function.
"""
global USE_LAZY
USE_LAZY = True
def _(msg):
if USE_LAZY:
return Message(msg, domain='climateclient')
else:
if six.PY3:
return _t.gettext(msg)
return _t.ugettext(msg)
def install(domain, lazy=False):
"""Install a _() function using the given translation domain.
Given a translation domain, install a _() function using gettext's
install() function.
The main difference from gettext.install() is that we allow
overriding the default localedir (e.g. /usr/share/locale) using
a translation-domain-specific environment variable (e.g.
NOVA_LOCALEDIR).
:param domain: the translation domain
:param lazy: indicates whether or not to install the lazy _() function.
The lazy _() introduces a way to do deferred translation
of messages by installing a _ that builds Message objects,
instead of strings, which can then be lazily translated into
any available locale.
"""
if lazy:
# NOTE(mrodden): Lazy gettext functionality.
#
# The following introduces a deferred way to do translations on
# messages in OpenStack. We override the standard _() function
# and % (format string) operation to build Message objects that can
# later be translated when we have more information.
def _lazy_gettext(msg):
"""Create and return a Message object.
Lazy gettext function for a given domain, it is a factory method
for a project/module to get a lazy gettext function for its own
translation domain (i.e. nova, glance, cinder, etc.)
Message encapsulates a string so that we can translate
it later when needed.
"""
return Message(msg, domain=domain)
from six import moves
moves.builtins.__dict__['_'] = _lazy_gettext
else:
localedir = '%s_LOCALEDIR' % domain.upper()
if six.PY3:
gettext.install(domain,
localedir=os.environ.get(localedir))
else:
gettext.install(domain,
localedir=os.environ.get(localedir),
unicode=True)
class Message(six.text_type):
"""A Message object is a unicode object that can be translated.
Translation of Message is done explicitly using the translate() method.
For all non-translation intents and purposes, a Message is simply unicode,
and can be treated as such.
"""
def __new__(cls, msgid, msgtext=None, params=None, domain='climateclient',
*args):
"""Create a new Message object.
In order for translation to work gettext requires a message ID, this
msgid will be used as the base unicode text. It is also possible
for the msgid and the base unicode text to be different by passing
the msgtext parameter.
"""
# If the base msgtext is not given, we use the default translation
# of the msgid (which is in English) just in case the system locale is
# not English, so that the base text will be in that locale by default.
if not msgtext:
msgtext = Message._translate_msgid(msgid, domain)
# We want to initialize the parent unicode with the actual object that
# would have been plain unicode if 'Message' was not enabled.
msg = super(Message, cls).__new__(cls, msgtext)
msg.msgid = msgid
msg.domain = domain
msg.params = params
return msg
def translate(self, desired_locale=None):
"""Translate this message to the desired locale.
:param desired_locale: The desired locale to translate the message to,
if no locale is provided the message will be
translated to the system's default locale.
:returns: the translated message in unicode
"""
translated_message = Message._translate_msgid(self.msgid,
self.domain,
desired_locale)
if self.params is None:
# No need for more translation
return translated_message
# This Message object may have been formatted with one or more
# Message objects as substitution arguments, given either as a single
# argument, part of a tuple, or as one or more values in a dictionary.
# When translating this Message we need to translate those Messages too
translated_params = _translate_args(self.params, desired_locale)
translated_message = translated_message % translated_params
return translated_message
@staticmethod
def _translate_msgid(msgid, domain, desired_locale=None):
if not desired_locale:
system_locale = locale.getdefaultlocale()
# If the system locale is not available to the runtime use English
if not system_locale[0]:
desired_locale = 'en_US'
else:
desired_locale = system_locale[0]
locale_dir = os.environ.get(domain.upper() + '_LOCALEDIR')
lang = gettext.translation(domain,
localedir=locale_dir,
languages=[desired_locale],
fallback=True)
if six.PY3:
translator = lang.gettext
else:
translator = lang.ugettext
translated_message = translator(msgid)
return translated_message
def __mod__(self, other):
# When we mod a Message we want the actual operation to be performed
# by the parent class (i.e. unicode()), the only thing we do here is
# save the original msgid and the parameters in case of a translation
unicode_mod = super(Message, self).__mod__(other)
modded = Message(self.msgid,
msgtext=unicode_mod,
params=self._sanitize_mod_params(other),
domain=self.domain)
return modded
def _sanitize_mod_params(self, other):
"""Sanitize the object being modded with this Message.
- Add support for modding 'None' so translation supports it
- Trim the modded object, which can be a large dictionary, to only
those keys that would actually be used in a translation
- Snapshot the object being modded, in case the message is
translated, it will be used as it was when the Message was created
"""
if other is None:
params = (other,)
elif isinstance(other, dict):
params = self._trim_dictionary_parameters(other)
else:
params = self._copy_param(other)
return params
def _trim_dictionary_parameters(self, dict_param):
"""Return a dict that only has matching entries in the msgid."""
# NOTE(luisg): Here we trim down the dictionary passed as parameters
# to avoid carrying a lot of unnecessary weight around in the message
# object, for example if someone passes in Message() % locals() but
# only some params are used, and additionally we prevent errors for
# non-deepcopyable objects by unicoding() them.
# Look for %(param) keys in msgid;
# Skip %% and deal with the case where % is first character on the line
keys = re.findall('(?:[^%]|^)?%\((\w*)\)[a-z]', self.msgid)
# If we don't find any %(param) keys but have a %s
if not keys and re.findall('(?:[^%]|^)%[a-z]', self.msgid):
# Apparently the full dictionary is the parameter
params = self._copy_param(dict_param)
else:
params = {}
for key in keys:
params[key] = self._copy_param(dict_param[key])
return params
def _copy_param(self, param):
try:
return copy.deepcopy(param)
except TypeError:
# Fallback to casting to unicode this will handle the
# python code-like objects that can't be deep-copied
return six.text_type(param)
def __add__(self, other):
msg = _('Message objects do not support addition.')
raise TypeError(msg)
def __radd__(self, other):
return self.__add__(other)
def __str__(self):
# NOTE(luisg): Logging in python 2.6 tries to str() log records,
# and it expects specifically a UnicodeError in order to proceed.
msg = _('Message objects do not support str() because they may '
'contain non-ascii characters. '
'Please use unicode() or translate() instead.')
raise UnicodeError(msg)
def get_available_languages(domain):
"""Lists the available languages for the given translation domain.
:param domain: the domain to get languages for
"""
if domain in _AVAILABLE_LANGUAGES:
return copy.copy(_AVAILABLE_LANGUAGES[domain])
localedir = '%s_LOCALEDIR' % domain.upper()
find = lambda x: gettext.find(domain,
localedir=os.environ.get(localedir),
languages=[x])
# NOTE(mrodden): en_US should always be available (and first in case
# order matters) since our in-line message strings are en_US
language_list = ['en_US']
# NOTE(luisg): Babel <1.0 used a function called list(), which was
# renamed to locale_identifiers() in >=1.0, the requirements master list
# requires >=0.9.6, uncapped, so defensively work with both. We can remove
# this check when the master list updates to >=1.0, and update all projects
list_identifiers = (getattr(localedata, 'list', None) or
getattr(localedata, 'locale_identifiers'))
locale_identifiers = list_identifiers()
for i in locale_identifiers:
if find(i) is not None:
language_list.append(i)
_AVAILABLE_LANGUAGES[domain] = language_list
return copy.copy(language_list)
def translate(obj, desired_locale=None):
"""Gets the translated unicode representation of the given object.
If the object is not translatable it is returned as-is.
If the locale is None the object is translated to the system locale.
:param obj: the object to translate
:param desired_locale: the locale to translate the message to, if None the
default system locale will be used
:returns: the translated object in unicode, or the original object if
it could not be translated
"""
message = obj
if not isinstance(message, Message):
# If the object to translate is not already translatable,
# let's first get its unicode representation
message = six.text_type(obj)
if isinstance(message, Message):
# Even after unicoding() we still need to check if we are
# running with translatable unicode before translating
return message.translate(desired_locale)
return obj
def _translate_args(args, desired_locale=None):
"""Translates all the translatable elements of the given arguments object.
This method is used for translating the translatable values in method
arguments which include values of tuples or dictionaries.
If the object is not a tuple or a dictionary the object itself is
translated if it is translatable.
If the locale is None the object is translated to the system locale.
:param args: the args to translate
:param desired_locale: the locale to translate the args to, if None the
default system locale will be used
:returns: a new args object with the translated contents of the original
"""
if isinstance(args, tuple):
return tuple(translate(v, desired_locale) for v in args)
if isinstance(args, dict):
translated_dict = {}
for (k, v) in six.iteritems(args):
translated_v = translate(v, desired_locale)
translated_dict[k] = translated_v
return translated_dict
return translate(args, desired_locale)
class TranslationHandler(handlers.MemoryHandler):
"""Handler that translates records before logging them.
The TranslationHandler takes a locale and a target logging.Handler object
to forward LogRecord objects to after translating them. This handler
depends on Message objects being logged, instead of regular strings.
The handler can be configured declaratively in the logging.conf as follows:
[handlers]
keys = translatedlog, translator
[handler_translatedlog]
class = handlers.WatchedFileHandler
args = ('/var/log/api-localized.log',)
formatter = context
[handler_translator]
class = openstack.common.log.TranslationHandler
target = translatedlog
args = ('zh_CN',)
If the specified locale is not available in the system, the handler will
log in the default locale.
"""
def __init__(self, locale=None, target=None):
"""Initialize a TranslationHandler
:param locale: locale to use for translating messages
:param target: logging.Handler object to forward
LogRecord objects to after translation
"""
# NOTE(luisg): In order to allow this handler to be a wrapper for
# other handlers, such as a FileHandler, and still be able to
# configure it using logging.conf, this handler has to extend
# MemoryHandler because only the MemoryHandlers' logging.conf
# parsing is implemented such that it accepts a target handler.
handlers.MemoryHandler.__init__(self, capacity=0, target=target)
self.locale = locale
def setFormatter(self, fmt):
self.target.setFormatter(fmt)
def emit(self, record):
# We save the message from the original record to restore it
# after translation, so other handlers are not affected by this
original_msg = record.msg
original_args = record.args
try:
self._translate_and_log_record(record)
finally:
record.msg = original_msg
record.args = original_args
def _translate_and_log_record(self, record):
record.msg = translate(record.msg, self.locale)
# In addition to translating the message, we also need to translate
# arguments that were passed to the log method that were not part
# of the main message e.g., log.info(_('Some message %s'), this_one))
record.args = _translate_args(record.args, self.locale)
self.target.emit(record)

View File

@ -0,0 +1,66 @@
# Copyright 2011 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
Import related utilities and helper functions.
"""
import sys
import traceback
def import_class(import_str):
"""Returns a class from a string including module and class."""
mod_str, _sep, class_str = import_str.rpartition('.')
try:
__import__(mod_str)
return getattr(sys.modules[mod_str], class_str)
except (ValueError, AttributeError):
raise ImportError('Class %s cannot be found (%s)' %
(class_str,
traceback.format_exception(*sys.exc_info())))
def import_object(import_str, *args, **kwargs):
"""Import a class and return an instance of it."""
return import_class(import_str)(*args, **kwargs)
def import_object_ns(name_space, import_str, *args, **kwargs):
"""Tries to import object from default namespace.
Imports a class and return an instance of it, first by trying
to find the class in a default namespace, then failing back to
a full path if not found in the default namespace.
"""
import_value = "%s.%s" % (name_space, import_str)
try:
return import_class(import_value)(*args, **kwargs)
except ImportError:
return import_class(import_str)(*args, **kwargs)
def import_module(import_str):
"""Import a module."""
__import__(import_str)
return sys.modules[import_str]
def try_import(import_str, default=None):
"""Try to import a module and if it fails return default."""
try:
return import_module(import_str)
except ImportError:
return default

View File

@ -0,0 +1,222 @@
# Copyright 2011 OpenStack Foundation.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
"""
System-level utilities and helper functions.
"""
import re
import sys
import unicodedata
import six
from climateclient.openstack.common.gettextutils import _
# Used for looking up extensions of text
# to their 'multiplied' byte amount
BYTE_MULTIPLIERS = {
'': 1,
't': 1024 ** 4,
'g': 1024 ** 3,
'm': 1024 ** 2,
'k': 1024,
}
BYTE_REGEX = re.compile(r'(^-?\d+)(\D*)')
TRUE_STRINGS = ('1', 't', 'true', 'on', 'y', 'yes')
FALSE_STRINGS = ('0', 'f', 'false', 'off', 'n', 'no')
SLUGIFY_STRIP_RE = re.compile(r"[^\w\s-]")
SLUGIFY_HYPHENATE_RE = re.compile(r"[-\s]+")
def int_from_bool_as_string(subject):
"""Interpret a string as a boolean and return either 1 or 0.
Any string value in:
('True', 'true', 'On', 'on', '1')
is interpreted as a boolean True.
Useful for JSON-decoded stuff and config file parsing
"""
return bool_from_string(subject) and 1 or 0
def bool_from_string(subject, strict=False):
"""Interpret a string as a boolean.
A case-insensitive match is performed such that strings matching 't',
'true', 'on', 'y', 'yes', or '1' are considered True and, when
`strict=False`, anything else is considered False.
Useful for JSON-decoded stuff and config file parsing.
If `strict=True`, unrecognized values, including None, will raise a
ValueError which is useful when parsing values passed in from an API call.
Strings yielding False are 'f', 'false', 'off', 'n', 'no', or '0'.
"""
if not isinstance(subject, six.string_types):
subject = str(subject)
lowered = subject.strip().lower()
if lowered in TRUE_STRINGS:
return True
elif lowered in FALSE_STRINGS:
return False
elif strict:
acceptable = ', '.join(
"'%s'" % s for s in sorted(TRUE_STRINGS + FALSE_STRINGS))
msg = _("Unrecognized value '%(val)s', acceptable values are:"
" %(acceptable)s") % {'val': subject,
'acceptable': acceptable}
raise ValueError(msg)
else:
return False
def safe_decode(text, incoming=None, errors='strict'):
"""Decodes incoming str using `incoming` if they're not already unicode.
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a unicode `incoming` encoded
representation of it.
:raises TypeError: If text is not an instance of str
"""
if not isinstance(text, six.string_types):
raise TypeError("%s can't be decoded" % type(text))
if isinstance(text, six.text_type):
return text
if not incoming:
incoming = (sys.stdin.encoding or
sys.getdefaultencoding())
try:
return text.decode(incoming, errors)
except UnicodeDecodeError:
# Note(flaper87) If we get here, it means that
# sys.stdin.encoding / sys.getdefaultencoding
# didn't return a suitable encoding to decode
# text. This happens mostly when global LANG
# var is not set correctly and there's no
# default encoding. In this case, most likely
# python will use ASCII or ANSI encoders as
# default encodings but they won't be capable
# of decoding non-ASCII characters.
#
# Also, UTF-8 is being used since it's an ASCII
# extension.
return text.decode('utf-8', errors)
def safe_encode(text, incoming=None,
encoding='utf-8', errors='strict'):
"""Encodes incoming str/unicode using `encoding`.
If incoming is not specified, text is expected to be encoded with
current python's default encoding. (`sys.getdefaultencoding`)
:param incoming: Text's current encoding
:param encoding: Expected encoding for text (Default UTF-8)
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: text or a bytestring `encoding` encoded
representation of it.
:raises TypeError: If text is not an instance of str
"""
if not isinstance(text, six.string_types):
raise TypeError("%s can't be encoded" % type(text))
if not incoming:
incoming = (sys.stdin.encoding or
sys.getdefaultencoding())
if isinstance(text, six.text_type):
if six.PY3:
return text.encode(encoding, errors).decode(incoming)
else:
return text.encode(encoding, errors)
elif text and encoding != incoming:
# Decode text before encoding it with `encoding`
text = safe_decode(text, incoming, errors)
if six.PY3:
return text.encode(encoding, errors).decode(incoming)
else:
return text.encode(encoding, errors)
return text
def to_bytes(text, default=0):
"""Converts a string into an integer of bytes.
Looks at the last characters of the text to determine
what conversion is needed to turn the input text into a byte number.
Supports "B, K(B), M(B), G(B), and T(B)". (case insensitive)
:param text: String input for bytes size conversion.
:param default: Default return value when text is blank.
"""
match = BYTE_REGEX.search(text)
if match:
magnitude = int(match.group(1))
mult_key_org = match.group(2)
if not mult_key_org:
return magnitude
elif text:
msg = _('Invalid string format: %s') % text
raise TypeError(msg)
else:
return default
mult_key = mult_key_org.lower().replace('b', '', 1)
multiplier = BYTE_MULTIPLIERS.get(mult_key)
if multiplier is None:
msg = _('Unknown byte multiplier: %s') % mult_key_org
raise TypeError(msg)
return magnitude * multiplier
def to_slug(value, incoming=None, errors="strict"):
"""Normalize string.
Convert to lowercase, remove non-word characters, and convert spaces
to hyphens.
Inspired by Django's `slugify` filter.
:param value: Text to slugify
:param incoming: Text's current encoding
:param errors: Errors handling policy. See here for valid
values http://docs.python.org/2/library/codecs.html
:returns: slugified unicode representation of `value`
:raises TypeError: If text is not an instance of str
"""
value = safe_decode(value, incoming, errors)
# NOTE(aababilov): no need to use safe_(encode|decode) here:
# encodings are always "ascii", error handling is always "ignore"
# and types are always known (first: unicode; second: str)
value = unicodedata.normalize("NFKD", value).encode(
"ascii", "ignore").decode("ascii")
value = SLUGIFY_STRIP_RE.sub("", value).strip().lower()
return SLUGIFY_HYPHENATE_RE.sub("-", value)

463
climateclient/shell.py Normal file
View File

@ -0,0 +1,463 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Command-line interface to the Climate APIs
"""
from __future__ import print_function
import argparse
import logging
import os
import sys
from cliff import app
from cliff import commandmanager
from keystoneclient import exceptions as keystone_exceptions
import keystoneclient.v2_0 as keystone_client
from climateclient import client as climate_client
from climateclient import exception
from climateclient.openstack.common import strutils
from climateclient import utils
from climateclient.v1.shell_commands import leases
from climateclient import version as base_version
COMMANDS_V1 = {
'lease-list': leases.ListLeases,
'lease-show': leases.ShowLease,
'lease-create': leases.CreateLease,
'lease-update': leases.UpdateLease,
'lease-delete': leases.DeleteLease
}
VERSION = 1
DEFAULT_API_VERSION = 1
COMMANDS = {'v1': COMMANDS_V1}
def run_command(cmd, cmd_parser, sub_argv):
_argv = sub_argv
index = -1
values_specs = []
if '--' in sub_argv:
index = sub_argv.index('--')
_argv = sub_argv[:index]
values_specs = sub_argv[index:]
known_args, _values_specs = cmd_parser.parse_known_args(_argv)
cmd.values_specs = (index == -1 and _values_specs or values_specs)
return cmd.run(known_args)
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', '')
class HelpAction(argparse.Action):
"""Provide a custom action so the -h and --help options
to the main app will print a list of the commands.
The commands are determined by checking the CommandManager
instance, passed in as the "default" value for the action.
"""
def __call__(self, parser, namespace, values, option_string=None):
outputs = []
max_len = 0
app = self.default
parser.print_help(app.stdout)
app.stdout.write('\nCommands for API v%s:\n' % app.api_version)
command_manager = app.command_manager
for name, ep in sorted(command_manager):
factory = ep.load()
cmd = factory(self, None)
one_liner = cmd.get_description().split('\n')[0]
outputs.append((name, one_liner))
max_len = max(len(name), max_len)
for (name, one_liner) in outputs:
app.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner))
sys.exit(0)
class ClimateShell(app.App):
"""Manager class for the Climate CLI."""
CONSOLE_MESSAGE_FORMAT = '%(message)s'
DEBUG_MESSAGE_FORMAT = '%(levelname)s: %(name)s %(message)s'
log = logging.getLogger(__name__)
def __init__(self):
super(ClimateShell, self).__init__(
description=__doc__.strip(),
version=VERSION,
command_manager=commandmanager.CommandManager('climate.cli'), )
self.commands = COMMANDS
def build_option_parser(self, description, version, argparse_kwargs=None):
"""Return an argparse option parser for this application.
Subclasses may override this method to extend
the parser with more global options.
"""
parser = argparse.ArgumentParser(
description=description,
add_help=False)
parser.add_argument(
'--version',
action='version',
version=base_version.__version__)
parser.add_argument(
'-v', '--verbose',
action='count',
dest='verbose_level',
default=self.DEFAULT_VERBOSE_LEVEL,
help='Increase verbosity of output. Can be repeated.')
parser.add_argument(
'-q', '--quiet',
action='store_const',
dest='verbose_level',
const=0,
help='suppress output except warnings and errors')
parser.add_argument(
'-h', '--help',
action=HelpAction,
nargs=0,
default=self,
help="show this help message and exit")
parser.add_argument(
'--debug',
default=False,
action='store_true',
help='show tracebacks on errors')
# Global arguments
parser.add_argument(
'--os-reservation-api-version',
default=env('OS_RESERVATION_API_VERSION',
default=DEFAULT_API_VERSION),
help='Accepts 1 now, defaults to 1.')
parser.add_argument(
'--os_reservation_api_version',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-auth-strategy', metavar='<auth-strategy>',
default=env('OS_AUTH_STRATEGY', default='keystone'),
help='Authentication strategy (Env: OS_AUTH_STRATEGY'
', default keystone). For now, any other value will'
' disable the authentication')
parser.add_argument(
'--os_auth_strategy',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-auth-url', metavar='<auth-url>',
default=env('OS_AUTH_URL'),
help='Authentication URL (Env: OS_AUTH_URL)')
parser.add_argument(
'--os_auth_url',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-tenant-name', metavar='<auth-tenant-name>',
default=env('OS_TENANT_NAME'),
help='Authentication tenant name (Env: OS_TENANT_NAME)')
parser.add_argument(
'--os_tenant_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-tenant-id', metavar='<auth-tenant-id>',
default=env('OS_TENANT_ID'),
help='Authentication tenant name (Env: OS_TENANT_ID)')
parser.add_argument(
'--os-username', metavar='<auth-username>',
default=utils.env('OS_USERNAME'),
help='Authentication username (Env: OS_USERNAME)')
parser.add_argument(
'--os_username',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-password', metavar='<auth-password>',
default=utils.env('OS_PASSWORD'),
help='Authentication password (Env: OS_PASSWORD)')
parser.add_argument(
'--os_password',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-region-name', metavar='<auth-region-name>',
default=env('OS_REGION_NAME'),
help='Authentication region name (Env: OS_REGION_NAME)')
parser.add_argument(
'--os_region_name',
help=argparse.SUPPRESS)
parser.add_argument(
'--os-token', metavar='<token>',
default=env('OS_TOKEN'),
help='Defaults to env[OS_TOKEN]')
parser.add_argument(
'--os_token',
help=argparse.SUPPRESS)
parser.add_argument(
'--endpoint-type', metavar='<endpoint-type>',
default=env('OS_ENDPOINT_TYPE', default='publicURL'),
help='Defaults to env[OS_ENDPOINT_TYPE] or publicURL.')
parser.add_argument(
'--os-cacert',
metavar='<ca-certificate>',
default=env('OS_CACERT', default=None),
help="Specify a CA bundle file to use in "
"verifying a TLS (https) server certificate. "
"Defaults to env[OS_CACERT]")
parser.add_argument(
'--insecure',
action='store_true',
default=env('CLIMATECLIENT_INSECURE', default=False),
help="Explicitly allow climateclient 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.")
return parser
def _bash_completion(self):
"""Prints all of the commands and options for bash-completion."""
commands = set()
options = set()
for option, _action in self.parser._option_string_actions.items():
options.add(option)
for command_name, command in self.command_manager:
commands.add(command_name)
cmd_factory = command.load()
cmd = cmd_factory(self, None)
cmd_parser = cmd.get_parser('')
for option, _action in cmd_parser._option_string_actions.items():
options.add(option)
print(' '.join(commands | options))
def run(self, argv):
"""Equivalent to the main program for the application.
:param argv: input arguments and options
:paramtype argv: list of str
"""
try:
self.options, remainder = self.parser.parse_known_args(argv)
self.api_version = 'v%s' % self.options.os_reservation_api_version
for k, v in self.commands[self.api_version].items():
self.command_manager.add_command(k, v)
index = 0
command_pos = -1
help_pos = -1
help_command_pos = -1
for arg in argv:
if arg == 'bash-completion':
self._bash_completion()
return 0
if arg in self.commands[self.api_version]:
if command_pos == -1:
command_pos = index
elif arg in ('-h', '--help'):
if help_pos == -1:
help_pos = index
elif arg == 'help':
if help_command_pos == -1:
help_command_pos = index
index += 1
if -1 < command_pos < help_pos:
argv = ['help', argv[command_pos]]
if help_command_pos > -1 and command_pos == -1:
argv[help_command_pos] = '--help'
self.configure_logging()
self.interactive_mode = not remainder
self.initialize_app(remainder)
except Exception as err:
if self.options.debug:
self.log.exception(unicode(err))
raise
else:
self.log.error(unicode(err))
return 1
if self.interactive_mode:
_argv = [sys.argv[0]]
sys.argv = _argv
result = self.interact()
else:
result = self.run_subcommand(remainder)
return result
def run_subcommand(self, argv):
subcommand = self.command_manager.find_command(argv)
cmd_factory, cmd_name, sub_argv = subcommand
cmd = cmd_factory(self, self.options)
result = 1
try:
self.prepare_to_run_command(cmd)
full_name = (cmd_name if self.interactive_mode else
' '.join([self.NAME, cmd_name]))
cmd_parser = cmd.get_parser(full_name)
return run_command(cmd, cmd_parser, sub_argv)
except Exception as err:
if self.options.debug:
self.log.exception(unicode(err))
else:
self.log.error(unicode(err))
try:
self.clean_up(cmd, result, err)
except Exception as err2:
if self.options.debug:
self.log.exception(unicode(err2))
else:
self.log.error('Could not clean up: %s', unicode(err2))
if self.options.debug:
raise
else:
try:
self.clean_up(cmd, result, None)
except Exception as err3:
if self.options.debug:
self.log.exception(unicode(err3))
else:
self.log.error('Could not clean up: %s', unicode(err3))
return result
def authenticate_user(self):
"""Make sure the user has provided all of the authentication
info we need.
"""
if not self.options.os_token:
if not self.options.os_username:
raise exception.CommandError(
"You must provide a username via"
" either --os-username or env[OS_USERNAME]")
if not self.options.os_password:
raise exception.CommandError(
"You must provide a password via"
" either --os-password or env[OS_PASSWORD]")
if (not self.options.os_tenant_name and
not self.options.os_tenant_id):
raise exception.CommandError(
"You must provide a tenant_name or tenant_id via"
" --os-tenant-name, env[OS_TENANT_NAME]"
" --os-tenant-id, or via env[OS_TENANT_ID]")
if not self.options.os_auth_url:
raise exception.CommandError(
"You must provide an auth url via"
" either --os-auth-url or via env[OS_AUTH_URL]")
keystone = keystone_client.Client(
token=self.options.os_token,
auth_url=self.options.os_auth_url,
tenant_id=self.options.os_tenant_id,
tenant_name=self.options.os_tenant_name,
password=self.options.os_password,
region_name=self.options.os_region_name,
username=self.options.os_username,
insecure=self.options.insecure,
cert=self.options.os_cacert
)
auth = keystone.authenticate()
if auth:
try:
climate_url = keystone.service_catalog.url_for(
service_type='reservation'
)
except keystone_exceptions.EndpointNotFound:
raise exception.NoClimateEndpoint()
else:
raise exception.NotAuthorized("User %s is not authorized." %
self.options.os_username)
client = climate_client.Client(self.options.os_reservation_api_version,
climate_url=climate_url,
auth_token=keystone.auth_token)
self.client = client
return
def initialize_app(self, argv):
"""Global app init bits:
* set up API versions
* validate authentication info
"""
super(ClimateShell, self).initialize_app(argv)
cmd_name = None
if argv:
cmd_info = self.command_manager.find_command(argv)
cmd_factory, cmd_name, sub_argv = cmd_info
if self.interactive_mode or cmd_name != 'help':
self.authenticate_user()
def clean_up(self, cmd, result, err):
self.log.debug('clean_up %s', cmd.__class__.__name__)
if err:
self.log.debug('got an error: %s', unicode(err))
def configure_logging(self):
"""Create logging handlers for any log output."""
root_logger = logging.getLogger('')
# Set up logging to a file
root_logger.setLevel(logging.DEBUG)
# Send higher-level messages to the console via stderr
console = logging.StreamHandler(self.stderr)
console_level = {0: logging.WARNING,
1: logging.INFO,
2: logging.DEBUG}.get(self.options.verbose_level,
logging.DEBUG)
console.setLevel(console_level)
if logging.DEBUG == console_level:
formatter = logging.Formatter(self.DEBUG_MESSAGE_FORMAT)
else:
formatter = logging.Formatter(self.CONSOLE_MESSAGE_FORMAT)
console.setFormatter(formatter)
root_logger.addHandler(console)
return
def main(argv=sys.argv[1:]):
try:
return ClimateShell().run(map(strutils.safe_decode, argv))
except exception.ClimateClientException:
return 1
except Exception as e:
print(unicode(e))
return 1
if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))

137
climateclient/utils.py Normal file
View File

@ -0,0 +1,137 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
import json
import os
import re
import six
from climateclient import exception
HEX_ELEM = '[0-9A-Fa-f]'
UUID_PATTERN = '-'.join([HEX_ELEM + '{8}', HEX_ELEM + '{4}',
HEX_ELEM + '{4}', HEX_ELEM + '{4}',
HEX_ELEM + '{12}'])
def env(*args, **kwargs):
"""Returns the first environment variable set.
if none are non-empty, defaults to '' or keyword arg default.
"""
for v in args:
value = os.environ.get(v)
if value:
return value
return kwargs.get('default', '')
def to_primitive(value):
if isinstance(value, list) or isinstance(value, tuple):
o = []
for v in value:
o.append(to_primitive(v))
return o
elif isinstance(value, dict):
o = {}
for k, v in six.iteritems(value):
o[k] = to_primitive(v)
return o
elif isinstance(value, datetime.datetime):
return str(value)
elif hasattr(value, 'iteritems'):
return to_primitive(dict(six.iteritems(value)))
elif hasattr(value, '__iter__'):
return to_primitive(list(value))
else:
return value
def dumps(value, indent=None):
try:
return json.dumps(value, indent=indent)
except TypeError:
pass
return json.dumps(to_primitive(value))
def get_item_properties(item, fields, mixed_case_fields=None, formatters=None):
"""Return a tuple containing the item properties.
:param item: a single item resource (e.g. Server, Tenant, etc)
:param fields: tuple of strings with the desired field names
:param mixed_case_fields: tuple of field names to preserve case
:param formatters: dictionary mapping field names to callables
to format the values
"""
row = []
if mixed_case_fields is None:
mixed_case_fields = []
if formatters is None:
formatters = {}
for field in fields:
if field in formatters:
row.append(formatters[field](item))
else:
if field in mixed_case_fields:
field_name = field.replace(' ', '_')
else:
field_name = field.lower().replace(' ', '_')
if not hasattr(item, field_name) and isinstance(item, dict):
data = item[field_name]
else:
data = getattr(item, field_name, '')
if data is None:
data = ''
row.append(data)
return tuple(row)
def find_resource_id_by_name_or_id(client, resource, name_or_id):
resource_manager = getattr(client, resource)
is_id = re.match(UUID_PATTERN, name_or_id)
if is_id:
resources = resource_manager.list()
for resource in resources:
if resource['id'] == name_or_id:
return name_or_id
raise exception.ClimateClientException('No %s found with ID %s' %
(resource, name_or_id))
return _find_resource_id_by_name(client, resource, name_or_id)
def _find_resource_id_by_name(client, resource, name):
resource_manager = getattr(client, resource)
resources = resource_manager.list()
named_resources = []
for resource in resources:
if resource['name'] == name:
named_resources.append(resource['id'])
if len(named_resources) > 1:
raise exception.NoUniqueMatch(message="There are more than one "
"appropriate resources for the "
"name '%s' and type '%s'" %
(name, resource))
elif named_resources:
return named_resources[0]
else:
message = "Unable to find %s with name '%s'" % (resource, name)
raise exception.ClimateClientException(message=message,
status_code=404)

View File

View File

@ -0,0 +1,38 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from climateclient.v1 import leases
class Client(object):
"""Top level object to communicate with Climate.
Contains managers to control requests that should be passed to each type of
resources - leases, events, etc.
**Examples**
client = Client()
client.lease.list()
client.event.list(<lease_id>)
...
"""
def __init__(self, climate_url, auth_token):
self.climate_url = climate_url
self.auth_token = auth_token
self.lease = leases.LeaseClientManager(self.climate_url,
self.auth_token)

View File

@ -0,0 +1,77 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import datetime
from climateclient import base
from climateclient import exception
from climateclient.openstack.common.gettextutils import _ # noqa
class LeaseClientManager(base.BaseClientManager):
"""Manager for the lease connected requests."""
def create(self, name, start, end, reservations, events):
"""Creates lease from values passed."""
values = {'name': name, 'start_date': start, 'end_date': end,
'reservations': reservations, 'events': events}
return self._create('/leases', values, 'lease')
def get(self, lease_id):
"""Describes lease specifications such as name, status and locked
condition.
"""
return self._get('/leases/%s' % lease_id, 'lease')
def update(self, lease_id, name=None, prolong_for=None):
"""Update attributes of the lease."""
values = {}
if name:
values['name'] = name
if prolong_for:
if prolong_for.endswith('s'):
coefficient = 1
elif prolong_for.endswith('m'):
coefficient = 60
elif prolong_for.endswith('h'):
coefficient = 60 * 60
elif prolong_for.endswith('d'):
coefficient = 24 * 60 * 60
else:
raise exception.ClimateClientException(_("Unsupportable date "
"format for lease "
"prolonging."))
lease = self.get(lease_id)
cur_end_date = datetime.datetime.strptime(lease['end_date'],
'%Y-%m-%dT%H:%M:%S.%f')
seconds = int(prolong_for[:-1]) * coefficient
delta_sec = datetime.timedelta(seconds=seconds)
new_end_date = cur_end_date + delta_sec
values['end_date'] = datetime.datetime.strftime(
new_end_date, '%Y-%m-%dT%H:%M:%S.%f'
)
if not values:
return _('No values to update passed.')
return self._update('/leases/%s' % lease_id, values,
response_key='lease')
def delete(self, lease_id):
"""Deletes lease with specified ID."""
self._delete('/leases/%s' % lease_id)
def list(self):
"""List all leases."""
return self._get('/leases', 'leases')

View File

@ -0,0 +1,81 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import logging
from climateclient import command
class ListLeases(command.ListCommand):
resource = 'lease'
log = logging.getLogger(__name__ + '.ListLeases')
list_columns = ['id', 'name', 'start_date', 'end_date']
class ShowLease(command.ShowCommand):
resource = 'lease'
json_indent = 4
log = logging.getLogger(__name__ + '.ShowLease')
class CreateLease(command.CreateCommand):
"""Comprehended only for physical reservations.
For physical reservations lease is created manually.
For virtual reservations we need id of the reserved resource to create
lease. When service creates reserved resource (Nova-VM, Cinder-volume,
etc.) it comes to Climate and creates lease via Python client.
"""
pass
class UpdateLease(command.UpdateCommand):
resource = 'lease'
log = logging.getLogger(__name__ + '.UpdateLease')
def get_parser(self, prog_name):
parser = super(UpdateLease, self).get_parser(prog_name)
parser.add_argument(
'--name',
help='New name for the lease',
default=None
)
parser.add_argument(
'--prolong-for',
help='Time to prolong lease for',
default=None
)
parser.add_argument(
'--prolong_for',
help=argparse.SUPPRESS,
default=None
)
return parser
def args2body(self, parsed_args):
params = {}
if parsed_args.name:
params['name'] = parsed_args.name
if parsed_args.prolong_for:
params['prolong_for'] = parsed_args.prolong_for
return params
class DeleteLease(command.DeleteCommand):
resource = 'lease'
log = logging.getLogger(__name__ + '.DeleteLease')

19
climateclient/version.py Normal file
View File

@ -0,0 +1,19 @@
# Copyright (c) 2013 Mirantis Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pbr.version
__version__ = pbr.version.VersionInfo('python-climateclient').version_string()

9
openstack-common.conf Normal file
View File

@ -0,0 +1,9 @@
[DEFAULT]
# The list of modules to copy from oslo-incubator
module=gettextutils
module=importutils
module=strutils
# The base module to hold the copy of openstack.common
base=climateclient

6
requirements.txt Normal file
View File

@ -0,0 +1,6 @@
cliff>=1.4.3
PrettyTable>=0.7,<0.8
python-keystoneclient>=0.4.1
requests>=1.1
six>=1.4.1
Babel>=1.3

21
setup.cfg Normal file
View File

@ -0,0 +1,21 @@
[metadata]
name = python-climateclient
version = 2014.1a0
summary = Client for OpenStack Reservation Service
description-file = README.rst
license = Apache Software License
author = OpenStack
author_email = climate@lists.launchpad.net
home-page = https://launchpad.net/climate
[global]
setup-hooks = pbr.hooks.setup_hook
[build_sphinx]
all_files = 1
build-dir = doc/build
source-dir = doc/source
[entry_points]
console_scripts =
climate = climateclient.shell:main

22
setup.py Normal file
View File

@ -0,0 +1,22 @@
#!/usr/bin/env python
# Copyright (c) 2013 Hewlett-Packard Development Company, L.P.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
# implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# THIS FILE IS MANAGED BY THE GLOBAL REQUIREMENTS REPO - DO NOT EDIT
import setuptools
setuptools.setup(
setup_requires=['pbr'],
pbr=True)

4
test-requirements.txt Normal file
View File

@ -0,0 +1,4 @@
pep8==1.4.5
pyflakes>=0.7.2,<0.7.4
flake8==2.0
hacking>=0.8.0,<0.9

22
tox.ini Normal file
View File

@ -0,0 +1,22 @@
[tox]
envlist = py27,pep8
[testenv]
deps =
-r{toxinidir}/requirements.txt
[tox:jenkins]
downloadcache = ~/cache/pip
[testenv:pep8]
deps =
-r{toxinidir}/test-requirements.txt
commands = flake8
[flake8]
show-source = true
builtins = _
exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg
[testenv:venv]
commands = {posargs}