Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / plankton / backend.py @ 862afc75

History | View | Annotate | Download (23.6 kB)

1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2

    
3
#
4
# Redistribution and use in source and binary forms, with or
5
# without modification, are permitted provided that the following
6
# conditions are met:
7
#
8
#   1. Redistributions of source code must retain the above
9
#      copyright notice, this list of conditions and the following
10
#      disclaimer.
11
#
12
#   2. Redistributions in binary form must reproduce the above
13
#      copyright notice, this list of conditions and the following
14
#      disclaimer in the documentation and/or other materials
15
#      provided with the distribution.
16
#
17
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
# POSSIBILITY OF SUCH DAMAGE.
29
#
30
# The views and conclusions contained in the software and
31
# documentation are those of the authors and should not be
32
# interpreted as representing official policies, either expressed
33
# or implied, of GRNET S.A.
34

    
35
"""
36
The Plankton attributes are the following:
37
  - checksum: the 'hash' meta
38
  - container_format: stored as a user meta
39
  - created_at: the 'modified' meta of the first version
40
  - deleted_at: the timestamp of the last version
41
  - disk_format: stored as a user meta
42
  - id: the 'uuid' meta
43
  - is_public: True if there is a * entry for the read permission
44
  - location: generated based on the file's path
45
  - name: stored as a user meta
46
  - owner: the file's account
47
  - properties: stored as user meta prefixed with PROPERTY_PREFIX
48
  - size: the 'bytes' meta
49
  - status: stored as a system meta
50
  - store: is always 'pithos'
51
  - updated_at: the 'modified' meta
52
"""
53

    
54
import json
55
import warnings
56
import logging
57
import os
58

    
59
from time import time, gmtime, strftime
60
from functools import wraps
61
from operator import itemgetter
62

    
63
from django.conf import settings
64
from django.utils import importlib
65
from pithos.backends.base import NotAllowedError, VersionNotExists
66
from synnefo.util.text import uenc
67

    
68

    
69
logger = logging.getLogger(__name__)
70

    
71

    
72
PLANKTON_DOMAIN = 'plankton'
73
PLANKTON_PREFIX = 'plankton:'
74
PROPERTY_PREFIX = 'property:'
75

    
76
PLANKTON_META = ('container_format', 'disk_format', 'name',
77
                 'status', 'created_at')
78

    
79
MAX_META_KEY_LENGTH = 128 - len(PLANKTON_DOMAIN) - len(PROPERTY_PREFIX)
80
MAX_META_VALUE_LENGTH = 256
81

    
82
# TODO: Change domain!
83
SNAPSHOTS_DOMAIN = PLANKTON_DOMAIN
84
SNAPSHOTS_PREFIX = PLANKTON_PREFIX
85

    
86
from pithos.backends.util import PithosBackendPool
87
_pithos_backend_pool = \
88
    PithosBackendPool(
89
        settings.PITHOS_BACKEND_POOL_SIZE,
90
        astakos_auth_url=settings.ASTAKOS_AUTH_URL,
91
        service_token=settings.CYCLADES_SERVICE_TOKEN,
92
        astakosclient_poolsize=settings.CYCLADES_ASTAKOSCLIENT_POOLSIZE,
93
        db_connection=settings.BACKEND_DB_CONNECTION,
94
        block_path=settings.BACKEND_BLOCK_PATH)
95

    
96

    
97
def get_pithos_backend():
98
    return _pithos_backend_pool.pool_get()
99

    
100

    
101
def create_url(account, container, name):
102
    assert "/" not in account, "Invalid account"
103
    assert "/" not in container, "Invalid container"
104
    return "pithos://%s/%s/%s" % (account, container, name)
105

    
106

    
107
def split_url(url):
108
    """Returns (accout, container, object) from a url string"""
109
    try:
110
        assert(isinstance(url, basestring))
111
        t = url.split('/', 4)
112
        assert t[0] == "pithos:", "Invalid url"
113
        assert len(t) == 5, "Invalid url"
114
        return t[2:5]
115
    except AssertionError:
116
        raise InvalidLocation("Invalid location '%s" % url)
117

    
118

    
119
def format_timestamp(t):
120
    return strftime('%Y-%m-%d %H:%M:%S', gmtime(t))
121

    
122

    
123
def handle_backend_exceptions(func):
124
    @wraps(func)
125
    def wrapper(*args, **kwargs):
126
        try:
127
            return func(*args, **kwargs)
128
        except NotAllowedError:
129
            raise Forbidden
130
        except NameError:
131
            raise ImageNotFound
132
        except VersionNotExists:
133
            raise ImageNotFound
134
    return wrapper
135

    
136

    
137
def commit_on_success(func):
138
    def wrapper(self, *args, **kwargs):
139
        backend = self.backend
140
        backend.pre_exec()
141
        try:
142
            ret = func(self, *args, **kwargs)
143
        except:
144
            backend.post_exec(False)
145
            raise
146
        else:
147
            backend.post_exec(True)
148
        return ret
149
    return wrapper
150

    
151

    
152
class ImageBackend(object):
153
    """A wrapper arround the pithos backend to simplify image handling."""
154

    
155
    def __init__(self, user):
156
        self.user = user
157

    
158
        original_filters = warnings.filters
159
        warnings.simplefilter('ignore')         # Suppress SQLAlchemy warnings
160
        self.backend = get_pithos_backend()
161
        warnings.filters = original_filters     # Restore warnings
162

    
163
    def close(self):
164
        """Close PithosBackend(return to pool)"""
165
        self.backend.close()
166

    
167
    @handle_backend_exceptions
168
    @commit_on_success
169
    def get_image(self, image_uuid):
170
        """Retrieve information about an image."""
171
        image_url = self._get_image_url(image_uuid)
172
        return self._get_image(image_url)
173

    
174
    def _get_image_url(self, image_uuid):
175
        """Get the Pithos url that corresponds to an image UUID."""
176
        account, container, name = self.backend.get_uuid(self.user, image_uuid)
177
        return create_url(account, container, name)
178

    
179
    def _get_image(self, image_url):
180
        """Get information about an Image.
181

182
        Get all available information about an Image.
183
        """
184
        account, container, name = split_url(image_url)
185
        try:
186
            meta = self._get_meta(image_url)
187
            meta["deleted"] = ""
188
        except NameError:
189
            versions = self.backend.list_versions(self.user, account,
190
                                                  container, name)
191
            if not versions:
192
                raise Exception("Image without versions %s" % image_url)
193
            # Object was deleted, use the latest version
194
            version, timestamp = versions[-1]
195
            meta = self._get_meta(image_url, version)
196
            meta["deleted"] = timestamp
197

    
198
        # XXX: Check that an object is a plankton image! PithosBackend will
199
        # return common metadata for an object, even if it has no metadata in
200
        # plankton domain. All images must have a name, so we check if a file
201
        # is an image by checking if they are having an image name.
202
        if PLANKTON_PREFIX + 'name' not in meta:
203
            raise ImageNotFound
204

    
205
        permissions = self._get_permissions(image_url)
206
        return image_to_dict(image_url, meta, permissions)
207

    
208
    def _get_meta(self, image_url, version=None):
209
        """Get object's metadata."""
210
        account, container, name = split_url(image_url)
211
        return self.backend.get_object_meta(self.user, account, container,
212
                                            name, PLANKTON_DOMAIN, version)
213

    
214
    def _update_meta(self, image_url, meta, replace=False):
215
        """Update object's metadata."""
216
        account, container, name = split_url(image_url)
217

    
218
        prefixed = [(PLANKTON_PREFIX + uenc(k), uenc(v))
219
                    for k, v in meta.items()
220
                    if k in PLANKTON_META or k.startswith(PROPERTY_PREFIX)]
221
        prefixed = dict(prefixed)
222

    
223
        for k, v in prefixed.items():
224
            if len(k) > 128:
225
                raise InvalidMetadata('Metadata keys should be less than %s '
226
                                      'characters' % MAX_META_KEY_LENGTH)
227
            if len(v) > 256:
228
                raise InvalidMetadata('Metadata values should be less than %s '
229
                                      'characters.' % MAX_META_VALUE_LENGTH)
230

    
231
        self.backend.update_object_meta(self.user, account, container, name,
232
                                        PLANKTON_DOMAIN, prefixed, replace)
233
        logger.debug("User '%s' updated image '%s', meta: '%s'", self.user,
234
                     image_url, prefixed)
235

    
236
    def _get_permissions(self, image_url):
237
        """Get object's permissions."""
238
        account, container, name = split_url(image_url)
239
        _a, path, permissions = \
240
            self.backend.get_object_permissions(self.user, account, container,
241
                                                name)
242

    
243
        if path is None and permissions != {}:
244
            logger.warning("Image '%s' got permissions '%s' from 'None' path.",
245
                           image_url, permissions)
246
            raise Exception("Database Inconsistency Error:"
247
                            " Image '%s' got permissions from 'None' path." %
248
                            image_url)
249

    
250
        return permissions
251

    
252
    def _update_permissions(self, image_url, permissions):
253
        """Update object's permissions."""
254
        account, container, name = split_url(image_url)
255
        self.backend.update_object_permissions(self.user, account, container,
256
                                               name, permissions)
257
        logger.debug("User '%s' updated image '%s', permissions: '%s'",
258
                     self.user, image_url, permissions)
259

    
260
    @handle_backend_exceptions
261
    @commit_on_success
262
    def unregister(self, image_uuid):
263
        """Unregister an image.
264

265
        Unregister an image, by removing all metadata from the Pithos
266
        file that exist in the PLANKTON_DOMAIN.
267

268
        """
269
        image_url = self._get_image_url(image_uuid)
270
        self._get_image(image_url)  # Assert that it is an image
271
        # Unregister the image by removing all metadata from domain
272
        # 'PLANKTON_DOMAIN'
273
        meta = {}
274
        self._update_meta(image_url, meta, True)
275
        logger.debug("User '%s' deleted image '%s'", self.user, image_url)
276

    
277
    @handle_backend_exceptions
278
    @commit_on_success
279
    def add_user(self, image_uuid, add_user):
280
        """Add a user as an image member.
281

282
        Update read permissions of Pithos file, to include the specified user.
283

284
        """
285
        image_url = self._get_image_url(image_uuid)
286
        self._get_image(image_url)  # Assert that it is an image
287
        permissions = self._get_permissions(image_url)
288
        read = set(permissions.get("read", []))
289
        assert(isinstance(add_user, (str, unicode)))
290
        read.add(add_user)
291
        permissions["read"] = list(read)
292
        self._update_permissions(image_url, permissions)
293

    
294
    @handle_backend_exceptions
295
    @commit_on_success
296
    def remove_user(self, image_uuid, remove_user):
297
        """Remove the user from image members.
298

299
        Remove the specified user from the read permissions of the Pithos file.
300

301
        """
302
        image_url = self._get_image_url(image_uuid)
303
        self._get_image(image_url)  # Assert that it is an image
304
        permissions = self._get_permissions(image_url)
305
        read = set(permissions.get("read", []))
306
        assert(isinstance(remove_user, (str, unicode)))
307
        try:
308
            read.remove(remove_user)
309
        except ValueError:
310
            return  # TODO: User did not have access
311
        permissions["read"] = list(read)
312
        self._update_permissions(image_url, permissions)
313

    
314
    @handle_backend_exceptions
315
    @commit_on_success
316
    def replace_users(self, image_uuid, replace_users):
317
        """Replace image members.
318

319
        Replace the read permissions of the Pithos files with the specified
320
        users. If image is specified as public, we must preserve * permission.
321

322
        """
323
        image_url = self._get_image_url(image_uuid)
324
        image = self._get_image(image_url)
325
        permissions = self._get_permissions(image_url)
326
        assert(isinstance(replace_users, list))
327
        permissions["read"] = replace_users
328
        if image.get("is_public", False):
329
            permissions["read"].append("*")
330
        self._update_permissions(image_url, permissions)
331

    
332
    @handle_backend_exceptions
333
    @commit_on_success
334
    def list_users(self, image_uuid):
335
        """List the image members.
336

337
        List the image members, by listing all users that have read permission
338
        to the corresponding Pithos file.
339

340
        """
341
        image_url = self._get_image_url(image_uuid)
342
        self._get_image(image_url)  # Assert that it is an image
343
        permissions = self._get_permissions(image_url)
344
        return [user for user in permissions.get('read', []) if user != '*']
345

    
346
    @handle_backend_exceptions
347
    @commit_on_success
348
    def update_metadata(self, image_uuid, metadata):
349
        """Update Image metadata."""
350
        image_url = self._get_image_url(image_uuid)
351
        self._get_image(image_url)  # Assert that it is an image
352

    
353
        # 'is_public' metadata is translated in proper file permissions
354
        is_public = metadata.pop("is_public", None)
355
        if is_public is not None:
356
            permissions = self._get_permissions(image_url)
357
            read = set(permissions.get("read", []))
358
            if is_public:
359
                read.add("*")
360
            else:
361
                read.discard("*")
362
            permissions["read"] = list(read)
363
            self._update_permissions(image_url, permissions)
364

    
365
        # Extract the properties dictionary from metadata, and store each
366
        # property as a separeted, prefixed metadata
367
        properties = metadata.pop("properties", {})
368
        meta = dict([(PROPERTY_PREFIX + k, v) for k, v in properties.items()])
369
        # Also add the following metadata
370
        meta.update(**metadata)
371

    
372
        self._update_meta(image_url, meta)
373
        image_url = self._get_image_url(image_uuid)
374
        return self._get_image(image_url)
375

    
376
    @handle_backend_exceptions
377
    @commit_on_success
378
    def register(self, name, image_url, metadata):
379
        # Validate that metadata are allowed
380
        if "id" in metadata:
381
            raise ValueError("Passing an ID is not supported")
382
        store = metadata.pop("store", "pithos")
383
        if store != "pithos":
384
            raise ValueError("Invalid store '%s'. Only 'pithos' store is"
385
                             "supported" % store)
386
        disk_format = metadata.setdefault("disk_format",
387
                                          settings.DEFAULT_DISK_FORMAT)
388
        if disk_format not in settings.ALLOWED_DISK_FORMATS:
389
            raise ValueError("Invalid disk format '%s'" % disk_format)
390
        container_format =\
391
            metadata.setdefault("container_format",
392
                                settings.DEFAULT_CONTAINER_FORMAT)
393
        if container_format not in settings.ALLOWED_CONTAINER_FORMATS:
394
            raise ValueError("Invalid container format '%s'" %
395
                             container_format)
396

    
397
        # Validate that 'size' and 'checksum' are valid
398
        account, container, object = split_url(image_url)
399

    
400
        meta = self._get_meta(image_url)
401

    
402
        size = int(metadata.pop('size', meta['bytes']))
403
        if size != meta['bytes']:
404
            raise ValueError("Invalid size")
405

    
406
        checksum = metadata.pop('checksum', meta['hash'])
407
        if checksum != meta['hash']:
408
            raise ValueError("Invalid checksum")
409

    
410
        # Fix permissions
411
        is_public = metadata.pop('is_public', False)
412
        if is_public:
413
            permissions = {'read': ['*']}
414
        else:
415
            permissions = {'read': [self.user]}
416

    
417
        # Extract the properties dictionary from metadata, and store each
418
        # property as a separeted, prefixed metadata
419
        properties = metadata.pop("properties", {})
420
        meta = dict([(PROPERTY_PREFIX + k, v) for k, v in properties.items()])
421
        # Add creation(register) timestamp as a metadata, to avoid extra
422
        # queries when retrieving the list of images.
423
        meta['created_at'] = time()
424
        # Update rest metadata
425
        meta.update(name=name, status='available', **metadata)
426

    
427
        # Do the actualy update in the Pithos backend
428
        self._update_meta(image_url, meta)
429
        self._update_permissions(image_url, permissions)
430
        logger.debug("User '%s' created image '%s'('%s')", self.user,
431
                     image_url, name)
432
        return self._get_image(image_url)
433

    
434
    def list_snapshots(self, user=None):
435
        _snapshots = self.backend.get_domain_objects(domain=SNAPSHOTS_DOMAIN,
436
                                                     user=user)
437
        snapshots = []
438
        for (location, meta, permissions) in _snapshots:
439
            snapshot_url = "pithos://" + location
440
            if not (SNAPSHOTS_PREFIX + "is_snapshot") in meta:
441
                continue
442
            snapshots.append(snapshot_to_dict(snapshot_url, meta, permissions))
443
        snapshots.sort(key="uuid", reverse=False)
444
        return snapshots
445

    
446
    @handle_backend_exceptions
447
    def get_snapshot(self, user, snapshot_uuid):
448
        #snapshot = self.backend.get_version_props_for_uuid(self.user,
449
        #                                                   snapshot_uuid,
450
        #                                                   SNAPSHOT_DOMAIN)
451
        snapshot_url = self._get_image_url(snapshot_uuid)
452
        meta = self._get_meta(snapshot_url)
453
        permissions = self._get_permissions(snapshot_url)
454
        return snapshot_to_dict(snapshot_url, meta, permissions)
455

    
456
    @handle_backend_exceptions
457
    def delete_snapshot(self, snapshot_uuid):
458
        self.backend.delete_object_for_uuid(self.user, snapshot_uuid)
459

    
460
    def _list_images(self, user=None, filters=None, params=None):
461
        filters = filters or {}
462

    
463
        # TODO: Use filters
464
        # # Fix keys
465
        # keys = [PLANKTON_PREFIX + 'name']
466
        # size_range = (None, None)
467
        # for key, val in filters.items():
468
        #     if key == 'size_min':
469
        #         size_range = (val, size_range[1])
470
        #     elif key == 'size_max':
471
        #         size_range = (size_range[0], val)
472
        #     else:
473
        #         keys.append('%s = %s' % (PLANKTON_PREFIX + key, val))
474
        _images = self.backend.get_domain_objects(domain=PLANKTON_DOMAIN,
475
                                                  user=user)
476

    
477
        images = []
478
        for (location, meta, permissions) in _images:
479
            image_url = "pithos://" + location
480
            meta["modified"] = meta["version_timestamp"]
481
            images.append(image_to_dict(image_url, meta, permissions))
482

    
483
        if params is None:
484
            params = {}
485
        key = itemgetter(params.get('sort_key', 'created_at'))
486
        reverse = params.get('sort_dir', 'desc') == 'desc'
487
        images.sort(key=key, reverse=reverse)
488
        return images
489

    
490
    @commit_on_success
491
    def list_images(self, filters=None, params=None):
492
        return self._list_images(user=self.user, filters=filters,
493
                                 params=params)
494

    
495
    @commit_on_success
496
    def list_shared_images(self, member, filters=None, params=None):
497
        images = self._list_images(user=self.user, filters=filters,
498
                                   params=params)
499
        is_shared = lambda img: not img["is_public"] and img["owner"] == member
500
        return filter(is_shared, images)
501

    
502
    @commit_on_success
503
    def list_public_images(self, filters=None, params=None):
504
        images = self._list_images(user=None, filters=filters, params=params)
505
        return filter(lambda img: img["is_public"], images)
506

    
507

    
508
class ImageBackendError(Exception):
509
    pass
510

    
511

    
512
class ImageNotFound(ImageBackendError):
513
    pass
514

    
515

    
516
class Forbidden(ImageBackendError):
517
    pass
518

    
519

    
520
class InvalidMetadata(ImageBackendError):
521
    pass
522

    
523

    
524
class InvalidLocation(ImageBackendError):
525
    pass
526

    
527

    
528
def image_to_dict(image_url, meta, permissions):
529
    """Render an image to a dictionary"""
530
    account, container, name = split_url(image_url)
531

    
532
    image = {}
533
    if PLANKTON_PREFIX + 'name' not in meta:
534
        logger.warning("Image without Plankton name!! url %s meta %s",
535
                       image_url, meta)
536
        image[PLANKTON_PREFIX + "name"] = ""
537

    
538
    image["id"] = meta["uuid"]
539
    image["location"] = image_url
540
    image["checksum"] = meta["hash"]
541
    created = meta.get("created_at", meta["modified"])
542
    image["created_at"] = format_timestamp(created)
543
    deleted = meta.get("deleted", None)
544
    image["deleted_at"] = format_timestamp(deleted) if deleted else ""
545
    image["updated_at"] = format_timestamp(meta["modified"])
546
    image["size"] = meta["bytes"]
547
    image["store"] = "pithos"
548
    image['owner'] = account
549

    
550
    # Distinquish between images and snapshots
551
    image["is_snapshot"] = (SNAPSHOTS_PREFIX + "is_snapshot") in meta
552

    
553
    # Permissions
554
    image["is_public"] = "*" in permissions.get('read', [])
555

    
556
    properties = {}
557
    for key, val in meta.items():
558
        # Get plankton properties
559
        if key.startswith(PLANKTON_PREFIX):
560
            # Remove plankton prefix
561
            key = key.replace(PLANKTON_PREFIX, "")
562
            # Keep only those in plankton meta
563
            if key in PLANKTON_META:
564
                if key != "created_at":
565
                    # created timestamp is return in 'created_at' field
566
                    image[key] = val
567
            elif key.startswith(PROPERTY_PREFIX):
568
                key = key.replace(PROPERTY_PREFIX, "")
569
                properties[key] = val
570
    image["properties"] = properties
571

    
572
    return image
573

    
574

    
575
def snapshot_to_dict(snapshot_url, meta, permissions):
576
    """Render an snapshot to a dictionary"""
577
    account, container, name = split_url(snapshot_url)
578

    
579
    snapshot = {}
580
    snapshot["uuid"] = meta["uuid"]
581
    snapshot["map"] = meta["hash"]
582
    snapshot["size"] = meta["bytes"]
583

    
584
    snapshot['owner'] = account
585
    snapshot["location"] = snapshot_url
586
    snapshot["file_name"] = name
587

    
588
    created = meta.get("created_at", meta["modified"])
589
    snapshot["created_at"] = format_timestamp(created)
590

    
591
    for key, val in meta.items():
592
        if key.startswith(SNAPSHOTS_PREFIX):
593
            # Remove plankton prefix
594
            key = key.replace(SNAPSHOTS_PREFIX, "")
595
            if key == "metadata":
596
                snapshot[key] = json.loads(val)
597
            else:
598
                snapshot[key] = val
599

    
600
    return snapshot
601

    
602

    
603
class JSONFileBackend(object):
604
    """
605
    A dummy image backend that loads available images from a file with json
606
    formatted content.
607

608
    usage:
609
        PLANKTON_BACKEND_MODULE = 'synnefo.plankton.backend.JSONFileBackend'
610
        PLANKTON_IMAGES_JSON_BACKEND_FILE = '/tmp/images.json'
611

612
        # loading images from an existing plankton service
613
        $ curl -H "X-Auth-Token: <MYTOKEN>" \
614
                https://cyclades.synnefo.org/plankton/images/detail | \
615
                python -m json.tool > /tmp/images.json
616
    """
617
    def __init__(self, userid):
618
        self.images_file = getattr(settings,
619
                                   'PLANKTON_IMAGES_JSON_BACKEND_FILE', '')
620
        if not os.path.exists(self.images_file):
621
            raise Exception("Invalid plankgon images json backend file: %s",
622
                            self.images_file)
623
        fp = file(self.images_file)
624
        self.images = json.load(fp)
625
        fp.close()
626

    
627
    def iter(self, *args, **kwargs):
628
        return self.images.__iter__()
629

    
630
    def list_images(self, *args, **kwargs):
631
        return self.images
632

    
633
    def get_image(self, image_uuid):
634
        try:
635
            return filter(lambda i: i['id'] == image_uuid, self.images)[0]
636
        except IndexError:
637
            raise Exception("Unknown image uuid: %s" % image_uuid)
638

    
639
    def close(self):
640
        pass
641

    
642

    
643
def get_backend():
644
    backend_module = getattr(settings, 'PLANKTON_BACKEND_MODULE', None)
645
    if not backend_module:
646
        # no setting set
647
        return ImageBackend
648

    
649
    parts = backend_module.split(".")
650
    module = ".".join(parts[:-1])
651
    cls = parts[-1]
652
    try:
653
        return getattr(importlib.import_module(module), cls)
654
    except (ImportError, AttributeError), e:
655
        raise ImportError("Cannot import plankton module: %s (%s)" %
656
                          (backend_module, e.message))