1 # Copyright 2010 Jacob Kaplan-Moss
2 # Copyright 2011 OpenStack Foundation
3 # Copyright 2012 Grid Dynamics
4 # Copyright 2013 OpenStack Foundation
7 # Licensed under the Apache License, Version 2.0 (the "License"); you may
8 # not use this file except in compliance with the License. You may obtain
9 # a copy of the License at
11 # http://www.apache.org/licenses/LICENSE-2.0
13 # Unless required by applicable law or agreed to in writing, software
14 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16 # License for the specific language governing permissions and limitations
20 Base utilities to build API operation managers and objects on top of.
23 ########################################################################
25 # THIS MODULE IS DEPRECATED
28 # https://etherpad.openstack.org/p/kilo-escalatorclient-library-proposals for
29 # the discussion leading to this deprecation.
31 # We recommend checking out the python-openstacksdk project
32 # (https://launchpad.net/python-openstacksdk) instead.
34 ########################################################################
37 # E1102: %s is not callable
38 # pylint: disable=E1102
43 from oslo.utils import strutils
45 from six.moves.urllib import parse
47 from escalatorclient.openstack.common._i18n import _
48 from escalatorclient.openstack.common.apiclient import exceptions
52 """Return id if argument is a Resource.
54 Abstracts the common pattern of allowing both an object or an object's ID
55 (UUID) as a parameter when dealing with relationships.
60 except AttributeError:
64 except AttributeError:
68 # TODO(aababilov): call run_hooks() in HookableMixin's child classes
69 class HookableMixin(object):
70 """Mixin so classes can register and run hooks."""
74 def add_hook(cls, hook_type, hook_func):
75 """Add a new hook of specified type.
77 :param cls: class that registers hooks
78 :param hook_type: hook type, e.g., '__pre_parse_args__'
79 :param hook_func: hook function
81 if hook_type not in cls._hooks_map:
82 cls._hooks_map[hook_type] = []
84 cls._hooks_map[hook_type].append(hook_func)
87 def run_hooks(cls, hook_type, *args, **kwargs):
88 """Run all hooks of specified type.
90 :param cls: class that registers hooks
91 :param hook_type: hook type, e.g., '__pre_parse_args__'
92 :param args: args to be passed to every hook function
93 :param kwargs: kwargs to be passed to every hook function
95 hook_funcs = cls._hooks_map.get(hook_type) or []
96 for hook_func in hook_funcs:
97 hook_func(*args, **kwargs)
100 class BaseManager(HookableMixin):
101 """Basic manager type providing common operations.
103 Managers interact with a particular type of API (servers, flavors, images,
104 etc.) and provide CRUD operations for them.
106 resource_class = None
108 def __init__(self, client):
109 """Initializes BaseManager with `client`.
111 :param client: instance of BaseClient descendant for HTTP requests
113 super(BaseManager, self).__init__()
116 def _list(self, url, response_key=None, obj_class=None, json=None):
117 """List the collection.
119 :param url: a partial URL, e.g., '/servers'
120 :param response_key: the key to be looked up in response dictionary,
121 e.g., 'servers'. If response_key is None - all response body
123 :param obj_class: class for constructing the returned objects
124 (self.resource_class will be used by default)
125 :param json: data that will be encoded as JSON and passed in POST
126 request (GET will be sent by default)
129 body = self.client.post(url, json=json).json()
131 body = self.client.get(url).json()
133 if obj_class is None:
134 obj_class = self.resource_class
136 data = body[response_key] if response_key is not None else body
137 # NOTE(ja): keystone returns values as list as {'values': [ ... ]}
138 # unlike other services which just return the list...
140 data = data['values']
141 except (KeyError, TypeError):
144 return [obj_class(self, res, loaded=True) for res in data if res]
146 def _get(self, url, response_key=None):
147 """Get an object from collection.
149 :param url: a partial URL, e.g., '/servers'
150 :param response_key: the key to be looked up in response dictionary,
151 e.g., 'server'. If response_key is None - all response body
154 body = self.client.get(url).json()
155 data = body[response_key] if response_key is not None else body
156 return self.resource_class(self, data, loaded=True)
158 def _head(self, url):
159 """Retrieve request headers for an object.
161 :param url: a partial URL, e.g., '/servers'
163 resp = self.client.head(url)
164 return resp.status_code == 204
166 def _post(self, url, json, response_key=None, return_raw=False):
169 :param url: a partial URL, e.g., '/servers'
170 :param json: data that will be encoded as JSON and passed in POST
171 request (GET will be sent by default)
172 :param response_key: the key to be looked up in response dictionary,
173 e.g., 'server'. If response_key is None - all response body
175 :param return_raw: flag to force returning raw JSON instead of
176 Python object of self.resource_class
178 body = self.client.post(url, json=json).json()
179 data = body[response_key] if response_key is not None else body
182 return self.resource_class(self, data)
184 def _put(self, url, json=None, response_key=None):
185 """Update an object with PUT method.
187 :param url: a partial URL, e.g., '/servers'
188 :param json: data that will be encoded as JSON and passed in POST
189 request (GET will be sent by default)
190 :param response_key: the key to be looked up in response dictionary,
191 e.g., 'servers'. If response_key is None - all response body
194 resp = self.client.put(url, json=json)
195 # PUT requests may not return a body
198 if response_key is not None:
199 return self.resource_class(self, body[response_key])
201 return self.resource_class(self, body)
203 def _patch(self, url, json=None, response_key=None):
204 """Update an object with PATCH method.
206 :param url: a partial URL, e.g., '/servers'
207 :param json: data that will be encoded as JSON and passed in POST
208 request (GET will be sent by default)
209 :param response_key: the key to be looked up in response dictionary,
210 e.g., 'servers'. If response_key is None - all response body
213 body = self.client.patch(url, json=json).json()
214 if response_key is not None:
215 return self.resource_class(self, body[response_key])
217 return self.resource_class(self, body)
219 def _delete(self, url):
222 :param url: a partial URL, e.g., '/servers/my-server'
224 return self.client.delete(url)
227 @six.add_metaclass(abc.ABCMeta)
228 class ManagerWithFind(BaseManager):
229 """Manager with additional `find()`/`findall()` methods."""
235 def find(self, **kwargs):
236 """Find a single item with attributes matching ``**kwargs``.
238 This isn't very efficient: it loads the entire list then filters on
241 matches = self.findall(**kwargs)
242 num_matches = len(matches)
244 msg = _("No %(name)s matching %(args)s.") % {
245 'name': self.resource_class.__name__,
248 raise exceptions.NotFound(msg)
249 elif num_matches > 1:
250 raise exceptions.NoUniqueMatch()
254 def findall(self, **kwargs):
255 """Find all items with attributes matching ``**kwargs``.
257 This isn't very efficient: it loads the entire list then filters on
261 searches = kwargs.items()
263 for obj in self.list():
265 if all(getattr(obj, attr) == value
266 for (attr, value) in searches):
268 except AttributeError:
274 class CrudManager(BaseManager):
275 """Base manager class for manipulating entities.
277 Children of this class are expected to define a `collection_key` and `key`.
279 - `collection_key`: Usually a plural noun by convention (e.g. `entities`);
280 used to refer collections in both URL's (e.g. `/v3/entities`) and JSON
281 objects containing a list of member resources (e.g. `{'entities': [{},
283 - `key`: Usually a singular noun by convention (e.g. `entity`); used to
284 refer to an individual member of the collection.
287 collection_key = None
290 def build_url(self, base_url=None, **kwargs):
291 """Builds a resource URL for the given kwargs.
293 Given an example collection where `collection_key = 'entities'` and
294 `key = 'entity'`, the following URL's could be generated.
296 By default, the URL will represent a collection of entities, e.g.::
300 If kwargs contains an `entity_id`, then the URL will represent a
301 specific member, e.g.::
303 /entities/{entity_id}
305 :param base_url: if provided, the generated URL will be appended to it
307 url = base_url if base_url is not None else ''
309 url += '/%s' % self.collection_key
311 # do we have a specific entity?
312 entity_id = kwargs.get('%s_id' % self.key)
313 if entity_id is not None:
314 url += '/%s' % entity_id
318 def _filter_kwargs(self, kwargs):
319 """Drop null values and handle ids."""
320 for key, ref in six.iteritems(kwargs.copy()):
324 if isinstance(ref, Resource):
326 kwargs['%s_id' % key] = getid(ref)
329 def create(self, **kwargs):
330 kwargs = self._filter_kwargs(kwargs)
332 self.build_url(**kwargs),
336 def get(self, **kwargs):
337 kwargs = self._filter_kwargs(kwargs)
339 self.build_url(**kwargs),
342 def head(self, **kwargs):
343 kwargs = self._filter_kwargs(kwargs)
344 return self._head(self.build_url(**kwargs))
346 def list(self, base_url=None, **kwargs):
347 """List the collection.
349 :param base_url: if provided, the generated URL will be appended to it
351 kwargs = self._filter_kwargs(kwargs)
354 '%(base_url)s%(query)s' % {
355 'base_url': self.build_url(base_url=base_url, **kwargs),
356 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
360 def put(self, base_url=None, **kwargs):
361 """Update an element.
363 :param base_url: if provided, the generated URL will be appended to it
365 kwargs = self._filter_kwargs(kwargs)
367 return self._put(self.build_url(base_url=base_url, **kwargs))
369 def update(self, **kwargs):
370 kwargs = self._filter_kwargs(kwargs)
371 params = kwargs.copy()
372 params.pop('%s_id' % self.key)
375 self.build_url(**kwargs),
379 def delete(self, **kwargs):
380 kwargs = self._filter_kwargs(kwargs)
383 self.build_url(**kwargs))
385 def find(self, base_url=None, **kwargs):
386 """Find a single item with attributes matching ``**kwargs``.
388 :param base_url: if provided, the generated URL will be appended to it
390 kwargs = self._filter_kwargs(kwargs)
393 '%(base_url)s%(query)s' % {
394 'base_url': self.build_url(base_url=base_url, **kwargs),
395 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '',
401 msg = _("No %(name)s matching %(args)s.") % {
402 'name': self.resource_class.__name__,
405 raise exceptions.NotFound(404, msg)
407 raise exceptions.NoUniqueMatch
412 class Extension(HookableMixin):
413 """Extension descriptor."""
415 SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
418 def __init__(self, name, module):
419 super(Extension, self).__init__()
422 self._parse_extension_module()
424 def _parse_extension_module(self):
425 self.manager_class = None
426 for attr_name, attr_value in self.module.__dict__.items():
427 if attr_name in self.SUPPORTED_HOOKS:
428 self.add_hook(attr_name, attr_value)
431 if issubclass(attr_value, BaseManager):
432 self.manager_class = attr_value
437 return "<Extension '%s'>" % self.name
440 class Resource(object):
441 """Base class for OpenStack resources (tenant, user, etc.).
443 This is pretty much just a bag for attributes.
449 def __init__(self, manager, info, loaded=False):
450 """Populate and bind to a manager.
452 :param manager: BaseManager object
453 :param info: dictionary representing resource attributes
454 :param loaded: prevent lazy-loading if set to True
456 self.manager = manager
458 self._add_details(info)
459 self._loaded = loaded
463 for k in self.__dict__.keys()
464 if k[0] != '_' and k != 'manager')
465 info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
466 return "<%s %s>" % (self.__class__.__name__, info)
470 """Human-readable ID which can be used for bash completion.
473 name = getattr(self, self.NAME_ATTR, None)
475 return strutils.to_slug(name)
478 def _add_details(self, info):
479 for (k, v) in six.iteritems(info):
483 except AttributeError:
484 # In this case we already defined the attribute on the class
487 def __getattr__(self, k):
488 if k not in self.__dict__:
489 # NOTE(bcwaldon): disallow lazy-loading if already loaded once
490 if not self.is_loaded():
492 return self.__getattr__(k)
494 raise AttributeError(k)
496 return self.__dict__[k]
499 """Support for lazy loading details.
501 Some clients, such as novaclient have the option to lazy load the
502 details, details which can be loaded with this function.
504 # set_loaded() first ... so if we have to bail, we know we tried.
505 self.set_loaded(True)
506 if not hasattr(self.manager, 'get'):
509 new = self.manager.get(self.id)
511 self._add_details(new._info)
513 {'x_request_id': self.manager.client.last_request_id})
515 def __eq__(self, other):
516 if not isinstance(other, Resource):
517 return NotImplemented
518 # two resources of different types are not equal
519 if not isinstance(other, self.__class__):
521 if hasattr(self, 'id') and hasattr(other, 'id'):
522 return self.id == other.id
523 return self._info == other._info
528 def set_loaded(self, val):
532 return copy.deepcopy(self._info)