~ubuntu-branches/ubuntu/vivid/python-heatclient/vivid

« back to all changes in this revision

Viewing changes to heatclient/openstack/common/apiclient/base.py

  • Committer: Package Import Robot
  • Author(s): Chuck Short
  • Date: 2014-03-06 17:41:15 UTC
  • mto: This revision was merged to the branch mainline in revision 8.
  • Revision ID: package-import@ubuntu.com-20140306174115-ecpzxbyb30tl5i7a
Tags: upstream-0.2.8
ImportĀ upstreamĀ versionĀ 0.2.8

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# Copyright 2010 Jacob Kaplan-Moss
 
2
# Copyright 2011 OpenStack Foundation
 
3
# Copyright 2012 Grid Dynamics
 
4
# Copyright 2013 OpenStack Foundation
 
5
# All Rights Reserved.
 
6
#
 
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
 
10
#
 
11
#         http://www.apache.org/licenses/LICENSE-2.0
 
12
#
 
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
 
17
#    under the License.
 
18
 
 
19
"""
 
20
Base utilities to build API operation managers and objects on top of.
 
21
"""
 
22
 
 
23
# E1102: %s is not callable
 
24
# pylint: disable=E1102
 
25
 
 
26
import abc
 
27
import copy
 
28
 
 
29
import six
 
30
 
 
31
from heatclient.openstack.common.apiclient import exceptions
 
32
from heatclient.openstack.common.py3kcompat import urlutils
 
33
from heatclient.openstack.common import strutils
 
34
 
 
35
 
 
36
def getid(obj):
 
37
    """Return id if argument is a Resource.
 
38
 
 
39
    Abstracts the common pattern of allowing both an object or an object's ID
 
40
    (UUID) as a parameter when dealing with relationships.
 
41
    """
 
42
    try:
 
43
        if obj.uuid:
 
44
            return obj.uuid
 
45
    except AttributeError:
 
46
        pass
 
47
    try:
 
48
        return obj.id
 
49
    except AttributeError:
 
50
        return obj
 
51
 
 
52
 
 
53
# TODO(aababilov): call run_hooks() in HookableMixin's child classes
 
54
class HookableMixin(object):
 
55
    """Mixin so classes can register and run hooks."""
 
56
    _hooks_map = {}
 
57
 
 
58
    @classmethod
 
59
    def add_hook(cls, hook_type, hook_func):
 
60
        """Add a new hook of specified type.
 
61
 
 
62
        :param cls: class that registers hooks
 
63
        :param hook_type: hook type, e.g., '__pre_parse_args__'
 
64
        :param hook_func: hook function
 
65
        """
 
66
        if hook_type not in cls._hooks_map:
 
67
            cls._hooks_map[hook_type] = []
 
68
 
 
69
        cls._hooks_map[hook_type].append(hook_func)
 
70
 
 
71
    @classmethod
 
72
    def run_hooks(cls, hook_type, *args, **kwargs):
 
73
        """Run all hooks of specified type.
 
74
 
 
75
        :param cls: class that registers hooks
 
76
        :param hook_type: hook type, e.g., '__pre_parse_args__'
 
77
        :param **args: args to be passed to every hook function
 
78
        :param **kwargs: kwargs to be passed to every hook function
 
79
        """
 
80
        hook_funcs = cls._hooks_map.get(hook_type) or []
 
81
        for hook_func in hook_funcs:
 
82
            hook_func(*args, **kwargs)
 
83
 
 
84
 
 
85
class BaseManager(HookableMixin):
 
86
    """Basic manager type providing common operations.
 
87
 
 
88
    Managers interact with a particular type of API (servers, flavors, images,
 
89
    etc.) and provide CRUD operations for them.
 
90
    """
 
91
    resource_class = None
 
92
 
 
93
    def __init__(self, client):
 
94
        """Initializes BaseManager with `client`.
 
95
 
 
96
        :param client: instance of BaseClient descendant for HTTP requests
 
97
        """
 
98
        super(BaseManager, self).__init__()
 
99
        self.client = client
 
100
 
 
101
    def _list(self, url, response_key, obj_class=None, json=None):
 
102
        """List the collection.
 
103
 
 
104
        :param url: a partial URL, e.g., '/servers'
 
105
        :param response_key: the key to be looked up in response dictionary,
 
106
            e.g., 'servers'
 
107
        :param obj_class: class for constructing the returned objects
 
108
            (self.resource_class will be used by default)
 
109
        :param json: data that will be encoded as JSON and passed in POST
 
110
            request (GET will be sent by default)
 
111
        """
 
112
        if json:
 
113
            body = self.client.post(url, json=json).json()
 
114
        else:
 
115
            body = self.client.get(url).json()
 
116
 
 
117
        if obj_class is None:
 
118
            obj_class = self.resource_class
 
119
 
 
120
        data = body[response_key]
 
121
        # NOTE(ja): keystone returns values as list as {'values': [ ... ]}
 
122
        #           unlike other services which just return the list...
 
123
        try:
 
124
            data = data['values']
 
125
        except (KeyError, TypeError):
 
126
            pass
 
127
 
 
128
        return [obj_class(self, res, loaded=True) for res in data if res]
 
129
 
 
130
    def _get(self, url, response_key):
 
131
        """Get an object from collection.
 
132
 
 
133
        :param url: a partial URL, e.g., '/servers'
 
134
        :param response_key: the key to be looked up in response dictionary,
 
135
            e.g., 'server'
 
136
        """
 
137
        body = self.client.get(url).json()
 
138
        return self.resource_class(self, body[response_key], loaded=True)
 
139
 
 
140
    def _head(self, url):
 
141
        """Retrieve request headers for an object.
 
142
 
 
143
        :param url: a partial URL, e.g., '/servers'
 
144
        """
 
145
        resp = self.client.head(url)
 
146
        return resp.status_code == 204
 
147
 
 
148
    def _post(self, url, json, response_key, return_raw=False):
 
149
        """Create an object.
 
150
 
 
151
        :param url: a partial URL, e.g., '/servers'
 
152
        :param json: data that will be encoded as JSON and passed in POST
 
153
            request (GET will be sent by default)
 
154
        :param response_key: the key to be looked up in response dictionary,
 
155
            e.g., 'servers'
 
156
        :param return_raw: flag to force returning raw JSON instead of
 
157
            Python object of self.resource_class
 
158
        """
 
159
        body = self.client.post(url, json=json).json()
 
160
        if return_raw:
 
161
            return body[response_key]
 
162
        return self.resource_class(self, body[response_key])
 
163
 
 
164
    def _put(self, url, json=None, response_key=None):
 
165
        """Update an object with PUT method.
 
166
 
 
167
        :param url: a partial URL, e.g., '/servers'
 
168
        :param json: data that will be encoded as JSON and passed in POST
 
169
            request (GET will be sent by default)
 
170
        :param response_key: the key to be looked up in response dictionary,
 
171
            e.g., 'servers'
 
172
        """
 
173
        resp = self.client.put(url, json=json)
 
174
        # PUT requests may not return a body
 
175
        if resp.content:
 
176
            body = resp.json()
 
177
            if response_key is not None:
 
178
                return self.resource_class(self, body[response_key])
 
179
            else:
 
180
                return self.resource_class(self, body)
 
181
 
 
182
    def _patch(self, url, json=None, response_key=None):
 
183
        """Update an object with PATCH method.
 
184
 
 
185
        :param url: a partial URL, e.g., '/servers'
 
186
        :param json: data that will be encoded as JSON and passed in POST
 
187
            request (GET will be sent by default)
 
188
        :param response_key: the key to be looked up in response dictionary,
 
189
            e.g., 'servers'
 
190
        """
 
191
        body = self.client.patch(url, json=json).json()
 
192
        if response_key is not None:
 
193
            return self.resource_class(self, body[response_key])
 
194
        else:
 
195
            return self.resource_class(self, body)
 
196
 
 
197
    def _delete(self, url):
 
198
        """Delete an object.
 
199
 
 
200
        :param url: a partial URL, e.g., '/servers/my-server'
 
201
        """
 
202
        return self.client.delete(url)
 
203
 
 
204
 
 
205
@six.add_metaclass(abc.ABCMeta)
 
206
class ManagerWithFind(BaseManager):
 
207
    """Manager with additional `find()`/`findall()` methods."""
 
208
 
 
209
    @abc.abstractmethod
 
210
    def list(self):
 
211
        pass
 
212
 
 
213
    def find(self, **kwargs):
 
214
        """Find a single item with attributes matching ``**kwargs``.
 
215
 
 
216
        This isn't very efficient: it loads the entire list then filters on
 
217
        the Python side.
 
218
        """
 
219
        matches = self.findall(**kwargs)
 
220
        num_matches = len(matches)
 
221
        if num_matches == 0:
 
222
            msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
 
223
            raise exceptions.NotFound(msg)
 
224
        elif num_matches > 1:
 
225
            raise exceptions.NoUniqueMatch()
 
226
        else:
 
227
            return matches[0]
 
228
 
 
229
    def findall(self, **kwargs):
 
230
        """Find all items with attributes matching ``**kwargs``.
 
231
 
 
232
        This isn't very efficient: it loads the entire list then filters on
 
233
        the Python side.
 
234
        """
 
235
        found = []
 
236
        searches = kwargs.items()
 
237
 
 
238
        for obj in self.list():
 
239
            try:
 
240
                if all(getattr(obj, attr) == value
 
241
                       for (attr, value) in searches):
 
242
                    found.append(obj)
 
243
            except AttributeError:
 
244
                continue
 
245
 
 
246
        return found
 
247
 
 
248
 
 
249
class CrudManager(BaseManager):
 
250
    """Base manager class for manipulating entities.
 
251
 
 
252
    Children of this class are expected to define a `collection_key` and `key`.
 
253
 
 
254
    - `collection_key`: Usually a plural noun by convention (e.g. `entities`);
 
255
      used to refer collections in both URL's (e.g.  `/v3/entities`) and JSON
 
256
      objects containing a list of member resources (e.g. `{'entities': [{},
 
257
      {}, {}]}`).
 
258
    - `key`: Usually a singular noun by convention (e.g. `entity`); used to
 
259
      refer to an individual member of the collection.
 
260
 
 
261
    """
 
262
    collection_key = None
 
263
    key = None
 
264
 
 
265
    def build_url(self, base_url=None, **kwargs):
 
266
        """Builds a resource URL for the given kwargs.
 
267
 
 
268
        Given an example collection where `collection_key = 'entities'` and
 
269
        `key = 'entity'`, the following URL's could be generated.
 
270
 
 
271
        By default, the URL will represent a collection of entities, e.g.::
 
272
 
 
273
            /entities
 
274
 
 
275
        If kwargs contains an `entity_id`, then the URL will represent a
 
276
        specific member, e.g.::
 
277
 
 
278
            /entities/{entity_id}
 
279
 
 
280
        :param base_url: if provided, the generated URL will be appended to it
 
281
        """
 
282
        url = base_url if base_url is not None else ''
 
283
 
 
284
        url += '/%s' % self.collection_key
 
285
 
 
286
        # do we have a specific entity?
 
287
        entity_id = kwargs.get('%s_id' % self.key)
 
288
        if entity_id is not None:
 
289
            url += '/%s' % entity_id
 
290
 
 
291
        return url
 
292
 
 
293
    def _filter_kwargs(self, kwargs):
 
294
        """Drop null values and handle ids."""
 
295
        for key, ref in six.iteritems(kwargs.copy()):
 
296
            if ref is None:
 
297
                kwargs.pop(key)
 
298
            else:
 
299
                if isinstance(ref, Resource):
 
300
                    kwargs.pop(key)
 
301
                    kwargs['%s_id' % key] = getid(ref)
 
302
        return kwargs
 
303
 
 
304
    def create(self, **kwargs):
 
305
        kwargs = self._filter_kwargs(kwargs)
 
306
        return self._post(
 
307
            self.build_url(**kwargs),
 
308
            {self.key: kwargs},
 
309
            self.key)
 
310
 
 
311
    def get(self, **kwargs):
 
312
        kwargs = self._filter_kwargs(kwargs)
 
313
        return self._get(
 
314
            self.build_url(**kwargs),
 
315
            self.key)
 
316
 
 
317
    def head(self, **kwargs):
 
318
        kwargs = self._filter_kwargs(kwargs)
 
319
        return self._head(self.build_url(**kwargs))
 
320
 
 
321
    def list(self, base_url=None, **kwargs):
 
322
        """List the collection.
 
323
 
 
324
        :param base_url: if provided, the generated URL will be appended to it
 
325
        """
 
326
        kwargs = self._filter_kwargs(kwargs)
 
327
 
 
328
        return self._list(
 
329
            '%(base_url)s%(query)s' % {
 
330
                'base_url': self.build_url(base_url=base_url, **kwargs),
 
331
                'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '',
 
332
            },
 
333
            self.collection_key)
 
334
 
 
335
    def put(self, base_url=None, **kwargs):
 
336
        """Update an element.
 
337
 
 
338
        :param base_url: if provided, the generated URL will be appended to it
 
339
        """
 
340
        kwargs = self._filter_kwargs(kwargs)
 
341
 
 
342
        return self._put(self.build_url(base_url=base_url, **kwargs))
 
343
 
 
344
    def update(self, **kwargs):
 
345
        kwargs = self._filter_kwargs(kwargs)
 
346
        params = kwargs.copy()
 
347
        params.pop('%s_id' % self.key)
 
348
 
 
349
        return self._patch(
 
350
            self.build_url(**kwargs),
 
351
            {self.key: params},
 
352
            self.key)
 
353
 
 
354
    def delete(self, **kwargs):
 
355
        kwargs = self._filter_kwargs(kwargs)
 
356
 
 
357
        return self._delete(
 
358
            self.build_url(**kwargs))
 
359
 
 
360
    def find(self, base_url=None, **kwargs):
 
361
        """Find a single item with attributes matching ``**kwargs``.
 
362
 
 
363
        :param base_url: if provided, the generated URL will be appended to it
 
364
        """
 
365
        kwargs = self._filter_kwargs(kwargs)
 
366
 
 
367
        rl = self._list(
 
368
            '%(base_url)s%(query)s' % {
 
369
                'base_url': self.build_url(base_url=base_url, **kwargs),
 
370
                'query': '?%s' % urlutils.urlencode(kwargs) if kwargs else '',
 
371
            },
 
372
            self.collection_key)
 
373
        num = len(rl)
 
374
 
 
375
        if num == 0:
 
376
            msg = "No %s matching %s." % (self.resource_class.__name__, kwargs)
 
377
            raise exceptions.NotFound(404, msg)
 
378
        elif num > 1:
 
379
            raise exceptions.NoUniqueMatch
 
380
        else:
 
381
            return rl[0]
 
382
 
 
383
 
 
384
class Extension(HookableMixin):
 
385
    """Extension descriptor."""
 
386
 
 
387
    SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__')
 
388
    manager_class = None
 
389
 
 
390
    def __init__(self, name, module):
 
391
        super(Extension, self).__init__()
 
392
        self.name = name
 
393
        self.module = module
 
394
        self._parse_extension_module()
 
395
 
 
396
    def _parse_extension_module(self):
 
397
        self.manager_class = None
 
398
        for attr_name, attr_value in self.module.__dict__.items():
 
399
            if attr_name in self.SUPPORTED_HOOKS:
 
400
                self.add_hook(attr_name, attr_value)
 
401
            else:
 
402
                try:
 
403
                    if issubclass(attr_value, BaseManager):
 
404
                        self.manager_class = attr_value
 
405
                except TypeError:
 
406
                    pass
 
407
 
 
408
    def __repr__(self):
 
409
        return "<Extension '%s'>" % self.name
 
410
 
 
411
 
 
412
class Resource(object):
 
413
    """Base class for OpenStack resources (tenant, user, etc.).
 
414
 
 
415
    This is pretty much just a bag for attributes.
 
416
    """
 
417
 
 
418
    HUMAN_ID = False
 
419
    NAME_ATTR = 'name'
 
420
 
 
421
    def __init__(self, manager, info, loaded=False):
 
422
        """Populate and bind to a manager.
 
423
 
 
424
        :param manager: BaseManager object
 
425
        :param info: dictionary representing resource attributes
 
426
        :param loaded: prevent lazy-loading if set to True
 
427
        """
 
428
        self.manager = manager
 
429
        self._info = info
 
430
        self._add_details(info)
 
431
        self._loaded = loaded
 
432
 
 
433
    def __repr__(self):
 
434
        reprkeys = sorted(k
 
435
                          for k in self.__dict__.keys()
 
436
                          if k[0] != '_' and k != 'manager')
 
437
        info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys)
 
438
        return "<%s %s>" % (self.__class__.__name__, info)
 
439
 
 
440
    @property
 
441
    def human_id(self):
 
442
        """Human-readable ID which can be used for bash completion.
 
443
        """
 
444
        if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID:
 
445
            return strutils.to_slug(getattr(self, self.NAME_ATTR))
 
446
        return None
 
447
 
 
448
    def _add_details(self, info):
 
449
        for (k, v) in six.iteritems(info):
 
450
            try:
 
451
                setattr(self, k, v)
 
452
                self._info[k] = v
 
453
            except AttributeError:
 
454
                # In this case we already defined the attribute on the class
 
455
                pass
 
456
 
 
457
    def __getattr__(self, k):
 
458
        if k not in self.__dict__:
 
459
            #NOTE(bcwaldon): disallow lazy-loading if already loaded once
 
460
            if not self.is_loaded:
 
461
                self._get()
 
462
                return self.__getattr__(k)
 
463
 
 
464
            raise AttributeError(k)
 
465
        else:
 
466
            return self.__dict__[k]
 
467
 
 
468
    def _get(self):
 
469
        # set _loaded first ... so if we have to bail, we know we tried.
 
470
        self._loaded = True
 
471
        if not hasattr(self.manager, 'get'):
 
472
            return
 
473
 
 
474
        new = self.manager.get(self.id)
 
475
        if new:
 
476
            self._add_details(new._info)
 
477
 
 
478
    def __eq__(self, other):
 
479
        if not isinstance(other, Resource):
 
480
            return NotImplemented
 
481
        # two resources of different types are not equal
 
482
        if not isinstance(other, self.__class__):
 
483
            return False
 
484
        if hasattr(self, 'id') and hasattr(other, 'id'):
 
485
            return self.id == other.id
 
486
        return self._info == other._info
 
487
 
 
488
    @property
 
489
    def is_loaded(self):
 
490
        return self._loaded
 
491
 
 
492
    def to_dict(self):
 
493
        return copy.deepcopy(self._info)