summaryrefslogtreecommitdiff
path: root/heatclient/common
diff options
context:
space:
mode:
authorrabi <ramishra@redhat.com>2016-11-04 13:33:12 +0530
committerrabi <ramishra@redhat.com>2016-11-12 19:15:40 +0530
commitb25baa9108bb37e0ece20895d464acf00ab2706c (patch)
treeb1699411a3c4f5933a364ed03390c1fe47df4158 /heatclient/common
parentea03fa2254a471edef0a655a63c42d7039428323 (diff)
downloadpython-heatclient-b25baa9108bb37e0ece20895d464acf00ab2706c.tar.gz
Move required modules from oslo-incubator
Move the required module to heatclient/common. - heatclient/openstack/common/apiclient/base.py It also moves some required functions and exceptions from heatclient/openstack/common/cliutils.py and heatclient/openstack/common/exceptions.py. Change-Id: I68704c7fab9417492d8390ad05a9797f78d46907
Diffstat (limited to 'heatclient/common')
-rw-r--r--heatclient/common/base.py523
-rw-r--r--heatclient/common/utils.py93
2 files changed, 611 insertions, 5 deletions
diff --git a/heatclient/common/base.py b/heatclient/common/base.py
new file mode 100644
index 0000000..902b86b
--- /dev/null
+++ b/heatclient/common/base.py
@@ -0,0 +1,523 @@
+# Copyright 2010 Jacob Kaplan-Moss
+# Copyright 2011 OpenStack Foundation
+# Copyright 2012 Grid Dynamics
+# Copyright 2013 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.
+
+"""
+Base utilities to build API operation managers and objects on top of.
+"""
+import abc
+import copy
+import logging
+
+from oslo_utils import reflection
+from oslo_utils import strutils
+import six
+from six.moves.urllib import parse
+
+from heatclient._i18n import _
+from heatclient._i18n import _LW
+from heatclient import exc as exceptions
+
+LOG = logging.getLogger(__name__)
+
+
+def getid(obj):
+ """Return id if argument is a Resource.
+
+ Abstracts the common pattern of allowing both an object or an object's ID
+ (UUID) as a parameter when dealing with relationships.
+ """
+ try:
+ if obj.uuid:
+ return obj.uuid
+ except AttributeError:
+ pass
+ try:
+ return obj.id
+ except AttributeError:
+ return obj
+
+
+# TODO(aababilov): call run_hooks() in HookableMixin's child classes
+class HookableMixin(object):
+ """Mixin so classes can register and run hooks."""
+ _hooks_map = {}
+
+ @classmethod
+ def add_hook(cls, hook_type, hook_func):
+ """Add a new hook of specified type.
+
+ :param cls: class that registers hooks
+ :param hook_type: hook type, e.g., '__pre_parse_args__'
+ :param hook_func: hook function
+ """
+ if hook_type not in cls._hooks_map:
+ cls._hooks_map[hook_type] = []
+
+ cls._hooks_map[hook_type].append(hook_func)
+
+ @classmethod
+ def run_hooks(cls, hook_type, *args, **kwargs):
+ """Run all hooks of specified type.
+
+ :param cls: class that registers hooks
+ :param hook_type: hook type, e.g., '__pre_parse_args__'
+ :param args: args to be passed to every hook function
+ :param kwargs: kwargs to be passed to every hook function
+ """
+ hook_funcs = cls._hooks_map.get(hook_type) or []
+ for hook_func in hook_funcs:
+ hook_func(*args, **kwargs)
+
+
+class BaseManager(HookableMixin):
+ """Basic manager type providing common operations.
+
+ Managers interact with a particular type of API (servers, flavors, images,
+ etc.) and provide CRUD operations for them.
+ """
+ resource_class = None
+
+ def __init__(self, client):
+ """Initializes BaseManager with `client`.
+
+ :param client: instance of BaseClient descendant for HTTP requests
+ """
+ super(BaseManager, self).__init__()
+ self.client = client
+
+ def _list(self, url, response_key=None, obj_class=None, json=None):
+ """List the collection.
+
+ :param url: a partial URL, e.g., '/servers'
+ :param response_key: the key to be looked up in response dictionary,
+ e.g., 'servers'. If response_key is None - all response body
+ will be used.
+ :param obj_class: class for constructing the returned objects
+ (self.resource_class will be used by default)
+ :param json: data that will be encoded as JSON and passed in POST
+ request (GET will be sent by default)
+ """
+ if json:
+ body = self.client.post(url, json=json).json()
+ else:
+ body = self.client.get(url).json()
+
+ if obj_class is None:
+ obj_class = self.resource_class
+
+ data = body[response_key] if response_key is not None else body
+ # NOTE(ja): keystone returns values as list as {'values': [ ... ]}
+ # unlike other services which just return the list...
+ try:
+ data = data['values']
+ except (KeyError, TypeError):
+ pass
+
+ return [obj_class(self, res, loaded=True) for res in data if res]
+
+ def _get(self, url, response_key=None):
+ """Get an object from collection.
+
+ :param url: a partial URL, e.g., '/servers'
+ :param response_key: the key to be looked up in response dictionary,
+ e.g., 'server'. If response_key is None - all response body
+ will be used.
+ """
+ body = self.client.get(url).json()
+ data = body[response_key] if response_key is not None else body
+ return self.resource_class(self, data, loaded=True)
+
+ def _head(self, url):
+ """Retrieve request headers for an object.
+
+ :param url: a partial URL, e.g., '/servers'
+ """
+ resp = self.client.head(url)
+ return resp.status_code == 204
+
+ def _post(self, url, json, response_key=None, return_raw=False):
+ """Create an object.
+
+ :param url: a partial URL, e.g., '/servers'
+ :param json: data that will be encoded as JSON and passed in POST
+ request (GET will be sent by default)
+ :param response_key: the key to be looked up in response dictionary,
+ e.g., 'server'. If response_key is None - all response body
+ will be used.
+ :param return_raw: flag to force returning raw JSON instead of
+ Python object of self.resource_class
+ """
+ body = self.client.post(url, json=json).json()
+ data = body[response_key] if response_key is not None else body
+ if return_raw:
+ return data
+ return self.resource_class(self, data)
+
+ def _put(self, url, json=None, response_key=None):
+ """Update an object with PUT method.
+
+ :param url: a partial URL, e.g., '/servers'
+ :param json: data that will be encoded as JSON and passed in POST
+ request (GET will be sent by default)
+ :param response_key: the key to be looked up in response dictionary,
+ e.g., 'servers'. If response_key is None - all response body
+ will be used.
+ """
+ resp = self.client.put(url, json=json)
+ # PUT requests may not return a body
+ if resp.content:
+ body = resp.json()
+ if response_key is not None:
+ return self.resource_class(self, body[response_key])
+ else:
+ return self.resource_class(self, body)
+
+ def _patch(self, url, json=None, response_key=None):
+ """Update an object with PATCH method.
+
+ :param url: a partial URL, e.g., '/servers'
+ :param json: data that will be encoded as JSON and passed in POST
+ request (GET will be sent by default)
+ :param response_key: the key to be looked up in response dictionary,
+ e.g., 'servers'. If response_key is None - all response body
+ will be used.
+ """
+ body = self.client.patch(url, json=json).json()
+ if response_key is not None:
+ return self.resource_class(self, body[response_key])
+ else:
+ return self.resource_class(self, body)
+
+ def _delete(self, url):
+ """Delete an object.
+
+ :param url: a partial URL, e.g., '/servers/my-server'
+ """
+ return self.client.delete(url)
+
+
+@six.add_metaclass(abc.ABCMeta)
+class ManagerWithFind(BaseManager):
+ """Manager with additional `find()`/`findall()` methods."""
+
+ @abc.abstractmethod
+ def list(self):
+ pass
+
+ def find(self, **kwargs):
+ """Find a single item with attributes matching ``**kwargs``.
+
+ This isn't very efficient: it loads the entire list then filters on
+ the Python side.
+ """
+ matches = self.findall(**kwargs)
+ num_matches = len(matches)
+ if num_matches == 0:
+ msg = _("No %(name)s matching %(args)s.") % {
+ 'name': self.resource_class.__name__,
+ 'args': kwargs
+ }
+ raise exceptions.NotFound(msg)
+ elif num_matches > 1:
+ raise exceptions.NoUniqueMatch()
+ else:
+ return matches[0]
+
+ def findall(self, **kwargs):
+ """Find all items with attributes matching ``**kwargs``.
+
+ This isn't very efficient: it loads the entire list then filters on
+ the Python side.
+ """
+ found = []
+ searches = kwargs.items()
+
+ for obj in self.list():
+ try:
+ if all(getattr(obj, attr) == value
+ for (attr, value) in searches):
+ found.append(obj)
+ except AttributeError:
+ continue
+
+ return found
+
+
+class CrudManager(BaseManager):
+ """Base manager class for manipulating entities.
+
+ Children of this class are expected to define a `collection_key` and `key`.
+ - `collection_key`: Usually a plural noun by convention (e.g. `entities`);
+ used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
+ objects containing a list of member resources (e.g. `{'entities': [{},
+ {}, {}]}`).
+ - `key`: Usually a singular noun by convention (e.g. `entity`); used to
+ refer to an individual member of the collection.
+ """
+ collection_key = None
+ key = None
+
+ def build_url(self, base_url=None, **kwargs):
+ """Builds a resource URL for the given kwargs.
+
+ Given an example collection where `collection_key = 'entities'` and
+ `key = 'entity'`, the following URL's could be generated.
+ By default, the URL will represent a collection of entities, e.g.::
+ /entities
+ If kwargs contains an `entity_id`, then the URL will represent a
+ specific member, e.g.::
+ /entities/{entity_id}
+ :param base_url: if provided, the generated URL will be appended to it
+ """
+ url = base_url if base_url is not None else ''
+
+ url += '/%s' % self.collection_key
+
+ # do we have a specific entity?
+ entity_id = kwargs.get('%s_id' % self.key)
+ if entity_id is not None:
+ url += '/%s' % entity_id
+
+ return url
+
+ def _filter_kwargs(self, kwargs):
+ """Drop null values and handle ids."""
+ for key, ref in six.iteritems(kwargs.copy()):
+ if ref is None:
+ kwargs.pop(key)
+ else:
+ if isinstance(ref, Resource):
+ kwargs.pop(key)
+ kwargs['%s_id' % key] = getid(ref)
+ return kwargs
+
+ def create(self, **kwargs):
+ kwargs = self._filter_kwargs(kwargs)
+ return self._post(
+ self.build_url(**kwargs),
+ {self.key: kwargs},
+ self.key)
+
+ def get(self, **kwargs):
+ kwargs = self._filter_kwargs(kwargs)
+ return self._get(
+ self.build_url(**kwargs),
+ self.key)
+
+ def head(self, **kwargs):
+ kwargs = self._filter_kwargs(kwargs)
+ return self._head(self.build_url(**kwargs))
+
+ def list(self, base_url=None, **kwargs):
+ """List the collection.
+
+ :param base_url: if provided, the generated URL will be appended to it
+ """
+ kwargs = self._filter_kwargs(kwargs)
+
+ return self._list(
+ '%(base_url)s%(query)s' % {
+ 'base_url': self.build_url(base_url=base_url, **kwargs),
+ 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
+ },
+ self.collection_key)
+
+ def put(self, base_url=None, **kwargs):
+ """Update an element.
+
+ :param base_url: if provided, the generated URL will be appended to it
+ """
+ kwargs = self._filter_kwargs(kwargs)
+
+ return self._put(self.build_url(base_url=base_url, **kwargs))
+
+ def update(self, **kwargs):
+ kwargs = self._filter_kwargs(kwargs)
+ params = kwargs.copy()
+ params.pop('%s_id' % self.key)
+
+ return self._patch(
+ self.build_url(**kwargs),
+ {self.key: params},
+ self.key)
+
+ def delete(self, **kwargs):
+ kwargs = self._filter_kwargs(kwargs)
+
+ return self._delete(
+ self.build_url(**kwargs))
+
+ def find(self, base_url=None, **kwargs):
+ """Find a single item with attributes matching ``**kwargs``.
+
+ :param base_url: if provided, the generated URL will be appended to it
+ """
+ kwargs = self._filter_kwargs(kwargs)
+
+ rl = self._list(
+ '%(base_url)s%(query)s' % {
+ 'base_url': self.build_url(base_url=base_url, **kwargs),
+ 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
+ },
+ self.collection_key)
+ num = len(rl)
+
+ if num == 0:
+ msg = _("No %(name)s matching %(args)s.") % {
+ 'name': self.resource_class.__name__,
+ 'args': kwargs
+ }
+ raise exceptions.NotFound(msg)
+ elif num > 1:
+ raise exceptions.NoUniqueMatch
+ else:
+ return rl[0]
+
+
+class Extension(HookableMixin):
+ """Extension descriptor."""
+
+ SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
+ manager_class = None
+
+ def __init__(self, name, module):
+ super(Extension, self).__init__()
+ self.name = name
+ self.module = module
+ self._parse_extension_module()
+
+ def _parse_extension_module(self):
+ self.manager_class = None
+ for attr_name, attr_value in self.module.__dict__.items():
+ if attr_name in self.SUPPORTED_HOOKS:
+ self.add_hook(attr_name, attr_value)
+ else:
+ try:
+ if issubclass(attr_value, BaseManager):
+ self.manager_class = attr_value
+ except TypeError:
+ pass
+
+ def __repr__(self):
+ return "<Extension '%s'>" % self.name
+
+
+class Resource(object):
+ """Base class for OpenStack resources (tenant, user, etc.).
+
+ This is pretty much just a bag for attributes.
+ """
+ HUMAN_ID = False
+ NAME_ATTR = 'name'
+
+ def __init__(self, manager, info, loaded=False):
+ """Populate and bind to a manager.
+
+ :param manager: BaseManager object
+ :param info: dictionary representing resource attributes
+ :param loaded: prevent lazy-loading if set to True
+ """
+ self.manager = manager
+ self._info = info
+ self._add_details(info)
+ self._loaded = loaded
+
+ def __repr__(self):
+ reprkeys = sorted(k
+ for k in self.__dict__.keys()
+ if k[0] != '_' and k != 'manager')
+ info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
+ class_name = reflection.get_class_name(self, fully_qualified=False)
+ return "<%s %s>" % (class_name, info)
+
+ @property
+ def human_id(self):
+ """Human-readable ID which can be used for bash completion. """
+ if self.HUMAN_ID:
+ name = getattr(self, self.NAME_ATTR, None)
+ if name is not None:
+ return strutils.to_slug(name)
+ return None
+
+ def _add_details(self, info):
+ for (k, v) in six.iteritems(info):
+ try:
+ setattr(self, k, v)
+ self._info[k] = v
+ except AttributeError:
+ # In this case we already defined the attribute on the class
+ pass
+
+ def __getattr__(self, k):
+ if k not in self.__dict__:
+ # NOTE(bcwaldon): disallow lazy-loading if already loaded once
+ if not self.is_loaded():
+ self.get()
+ return self.__getattr__(k)
+
+ raise AttributeError(k)
+ else:
+ return self.__dict__[k]
+
+ def get(self):
+ """Support for lazy loading details.
+
+ Some clients, such as novaclient have the option to lazy load the
+ details, details which can be loaded with this function.
+ """
+ # set_loaded() first ... so if we have to bail, we know we tried.
+ self.set_loaded(True)
+ if not hasattr(self.manager, 'get'):
+ return
+
+ new = self.manager.get(self.id)
+ if new:
+ self._add_details(new._info)
+ self._add_details(
+ {'x_request_id': self.manager.client.last_request_id})
+
+ def __eq__(self, other):
+ if not isinstance(other, Resource):
+ return NotImplemented
+ # two resources of different types are not equal
+ if not isinstance(other, self.__class__):
+ return False
+ LOG.warning(_LW("Two objects are equal when all of the attributes are "
+ "equal, if you want to identify whether two objects "
+ "are same one with same id, please use is_same_obj() "
+ "function."))
+ return self._info == other._info
+
+ def __ne__(self, other):
+ return not self.__eq__(other)
+
+ def is_same_obj(self, other):
+ """Identify the two objects are same one with same id."""
+ if isinstance(other, self.__class__):
+ if hasattr(self, 'id') and hasattr(other, 'id'):
+ return self.id == other.id
+ return False
+
+ def is_loaded(self):
+ return self._loaded
+
+ def set_loaded(self, val):
+ self._loaded = val
+
+ def to_dict(self):
+ return copy.deepcopy(self._info)
diff --git a/heatclient/common/utils.py b/heatclient/common/utils.py
index 91410ac..bc6f716 100644
--- a/heatclient/common/utils.py
+++ b/heatclient/common/utils.py
@@ -32,7 +32,6 @@ import yaml
from heatclient._i18n import _
from heatclient._i18n import _LE
from heatclient import exc
-from heatclient.openstack.common import cliutils
LOG = logging.getLogger(__name__)
@@ -42,10 +41,94 @@ supported_formats = {
"yaml": yaml.safe_dump
}
-# Using common methods from oslo cliutils
-arg = cliutils.arg
-env = cliutils.env
-print_list = cliutils.print_list
+
+def arg(*args, **kwargs):
+ """Decorator for CLI args.
+
+ Example:
+
+ >>> @arg("name", help="Name of the new entity")
+ ... def entity_create(args):
+ ... pass
+ """
+ def _decorator(func):
+ add_arg(func, *args, **kwargs)
+ return func
+ return _decorator
+
+
+def env(*args, **kwargs):
+ """Returns the first environment variable set.
+
+ If all are empty, defaults to '' or keyword arg `default`.
+ """
+ for arg in args:
+ value = os.environ.get(arg)
+ if value:
+ return value
+ return kwargs.get('default', '')
+
+
+def add_arg(func, *args, **kwargs):
+ """Bind CLI arguments to a shell.py `do_foo` function."""
+
+ if not hasattr(func, 'arguments'):
+ func.arguments = []
+
+ # NOTE(sirp): avoid dups that can occur when the module is shared across
+ # tests.
+ if (args, kwargs) not in func.arguments:
+ # Because of the semantics of decorator composition if we just append
+ # to the options list positional options will appear to be backwards.
+ func.arguments.insert(0, (args, kwargs))
+
+
+def print_list(objs, fields, formatters=None, sortby_index=0,
+ mixed_case_fields=None, field_labels=None):
+ """Print a list of objects as a table, one row per object.
+
+ :param objs: iterable of :class:`Resource`
+ :param fields: attributes that correspond to columns, in order
+ :param formatters: `dict` of callables for field formatting
+ :param sortby_index: index of the field for sorting table rows
+ :param mixed_case_fields: fields corresponding to object attributes that
+ have mixed case names (e.g., 'serverId')
+ :param field_labels: Labels to use in the heading of the table, default to
+ fields.
+ """
+ formatters = formatters or {}
+ mixed_case_fields = mixed_case_fields or []
+ field_labels = field_labels or fields
+ if len(field_labels) != len(fields):
+ raise ValueError(_("Field labels list %(labels)s has different number "
+ "of elements than fields list %(fields)s"),
+ {'labels': field_labels, 'fields': fields})
+
+ if sortby_index is None:
+ kwargs = {}
+ else:
+ kwargs = {'sortby': field_labels[sortby_index]}
+ pt = prettytable.PrettyTable(field_labels)
+ pt.align = 'l'
+
+ for o in objs:
+ row = []
+ for field in fields:
+ if field in formatters:
+ row.append(formatters[field](o))
+ else:
+ if field in mixed_case_fields:
+ field_name = field.replace(' ', '_')
+ else:
+ field_name = field.lower().replace(' ', '_')
+ data = getattr(o, field_name, '')
+ row.append(data)
+ pt.add_row(row)
+
+ if six.PY3:
+ print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode())
+ else:
+ print(encodeutils.safe_encode(pt.get_string(**kwargs)))
def link_formatter(links):