Initial Climate client implementation
Partially implements: blueprint python-client Change-Id: I7ac8aedf5b7504c63d12119e1ba0842e3ce01e63
This commit is contained in:
parent
6e1b360798
commit
aaf7c93ae6
2
MANIFEST.in
Normal file
2
MANIFEST.in
Normal file
@ -0,0 +1,2 @@
|
||||
include tox.ini
|
||||
include README.rst
|
5
README.rst
Normal file
5
README.rst
Normal file
@ -0,0 +1,5 @@
|
||||
==============
|
||||
Climate client
|
||||
==============
|
||||
|
||||
**Climate client** provides possibility to use *Climate* API(s).
|
0
climateclient/__init__.py
Normal file
0
climateclient/__init__.py
Normal file
130
climateclient/base.py
Normal file
130
climateclient/base.py
Normal 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
36
climateclient/client.py
Normal 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
263
climateclient/command.py
Normal 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)))
|
72
climateclient/exception.py
Normal file
72
climateclient/exception.py
Normal 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
|
0
climateclient/openstack/__init__.py
Normal file
0
climateclient/openstack/__init__.py
Normal file
0
climateclient/openstack/common/__init__.py
Normal file
0
climateclient/openstack/common/__init__.py
Normal file
412
climateclient/openstack/common/gettextutils.py
Normal file
412
climateclient/openstack/common/gettextutils.py
Normal 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)
|
66
climateclient/openstack/common/importutils.py
Normal file
66
climateclient/openstack/common/importutils.py
Normal 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
|
222
climateclient/openstack/common/strutils.py
Normal file
222
climateclient/openstack/common/strutils.py
Normal 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
463
climateclient/shell.py
Normal 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
137
climateclient/utils.py
Normal 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)
|
0
climateclient/v1/__init__.py
Normal file
0
climateclient/v1/__init__.py
Normal file
38
climateclient/v1/client.py
Normal file
38
climateclient/v1/client.py
Normal 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)
|
77
climateclient/v1/leases.py
Normal file
77
climateclient/v1/leases.py
Normal 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')
|
0
climateclient/v1/shell_commands/__init__.py
Normal file
0
climateclient/v1/shell_commands/__init__.py
Normal file
81
climateclient/v1/shell_commands/leases.py
Normal file
81
climateclient/v1/shell_commands/leases.py
Normal 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
19
climateclient/version.py
Normal 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
9
openstack-common.conf
Normal 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
6
requirements.txt
Normal 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
21
setup.cfg
Normal 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
22
setup.py
Normal 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
4
test-requirements.txt
Normal 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
22
tox.ini
Normal 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}
|
Loading…
Reference in New Issue
Block a user