Statistics
| Branch: | Tag: | Revision:

root / snf-cyclades-app / synnefo / plankton / backend.py @ 236223aa

History | View | Annotate | Download (23 kB)

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

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

    
53
import json
54
import warnings
55

    
56
from operator import itemgetter
57
from time import gmtime, strftime, time
58

    
59
from django.conf import settings
60

    
61
from pithos.backends import connect_backend
62
from pithos.backends.base import NotAllowedError
63

    
64

    
65
PLANKTON_DOMAIN = 'plankton'
66
PLANKTON_PREFIX = 'plankton:'
67
PROPERTY_PREFIX = 'property:'
68

    
69
PLANKTON_META = ('container_format', 'disk_format', 'name', 'properties',
70
                 'status')
71

    
72

    
73
def get_location(account, container, object):
74
    assert '/' not in account, "Invalid account"
75
    assert '/' not in container, "Invalid container"
76
    return 'pithos://%s/%s/%s' % (account, container, object)
77

    
78
def split_location(location):
79
    """Returns (accout, container, object) from a location string"""
80
    t = location.split('/', 4)
81
    assert len(t) == 5, "Invalid location"
82
    return t[2:5]
83

    
84

    
85
class BackendException(Exception):
86
    pass
87

    
88

    
89
class PithosImageBackend(object):
90
    """A wrapper arround the pithos backend to simplify image handling."""
91

    
92
    def __init__(self, user):
93
        self.user = user
94

    
95
        original_filters = warnings.filters
96
        warnings.simplefilter('ignore')         # Suppress SQLAlchemy warnings
97
        db_connection = settings.BACKEND_DB_CONNECTION
98
        block_path = settings.BACKEND_BLOCK_PATH
99
        self.backend = connect_backend(db_connection=db_connection,
100
                                       block_path=block_path)
101
        warnings.filters = original_filters     # Restore warnings
102

    
103
    def _get_image(self, location):
104
        def format_timestamp(t):
105
            return strftime('%Y-%m-%d %H:%M:%S', gmtime(t))
106

    
107
        account, container, object = split_location(location)
108

    
109
        try:
110
            versions = self.backend.list_versions(self.user, account,
111
                    container, object)
112
        except NameError:
113
            return None
114

    
115
        image = {}
116

    
117
        meta = self._get_meta(location)
118
        if meta:
119
            image['deleted_at'] = ''
120
        else:
121
            # Object was deleted, use the latest version
122
            version, timestamp = versions[-1]
123
            meta = self._get_meta(location, version)
124
            image['deleted_at'] = format_timestamp(timestamp)
125

    
126
        if PLANKTON_PREFIX + 'name' not in meta:
127
            return None     # Not a Plankton image
128

    
129
        permissions = self._get_permissions(location)
130

    
131
        image['checksum'] = meta['hash']
132
        image['created_at'] = format_timestamp(versions[0][1])
133
        image['id'] = meta['uuid']
134
        image['is_public'] = '*' in permissions.get('read', [])
135
        image['location'] = location
136
        image['owner'] = account
137
        image['size'] = meta['bytes']
138
        image['store'] = 'pithos'
139
        image['updated_at'] = format_timestamp(meta['modified'])
140
        image['properties'] = {}
141

    
142
        for key, val in meta.items():
143
            if not key.startswith(PLANKTON_PREFIX):
144
                continue
145
            key = key[len(PLANKTON_PREFIX):]
146
            if key == 'properties':
147
                val = json.loads(val)
148
            if key in PLANKTON_META:
149
                image[key] = val
150

    
151
        return image
152

    
153
    def _get_meta(self, location, version=None):
154
        account, container, object = split_location(location)
155
        try:
156
            return self.backend.get_object_meta(self.user, account, container,
157
                    object, PLANKTON_DOMAIN, version)
158
        except NameError:
159
            return None
160

    
161
    def _get_permissions(self, location):
162
        account, container, object = split_location(location)
163
        action, path, permissions = self.backend.get_object_permissions(
164
                self.user, account, container, object)
165
        return permissions
166

    
167
    def _store(self, f, size=None):
168
        """Breaks data into blocks and stores them in the backend"""
169

    
170
        bytes = 0
171
        hashmap = []
172
        backend = self.backend
173
        blocksize = backend.block_size
174

    
175
        data = f.read(blocksize)
176
        while data:
177
            hash = backend.put_block(data)
178
            hashmap.append(hash)
179
            bytes += len(data)
180
            data = f.read(blocksize)
181

    
182
        if size and size != bytes:
183
            raise BackendException("Invalid size")
184

    
185
        return hashmap, bytes
186

    
187
    def _update(self, location, size, hashmap, meta, permissions):
188
        account, container, object = split_location(location)
189
        self.backend.update_object_hashmap(self.user, account, container,
190
                object, size, hashmap, '', PLANKTON_DOMAIN,
191
                permissions=permissions)
192
        self._update_meta(location, meta, replace=True)
193

    
194
    def _update_meta(self, location, meta, replace=False):
195
        account, container, object = split_location(location)
196

    
197
        prefixed = {}
198
        for key, val in meta.items():
199
            if key == 'properties':
200
                val = json.dumps(val)
201
            if key in PLANKTON_META:
202
                prefixed[PLANKTON_PREFIX + key] = val
203

    
204
        self.backend.update_object_meta(self.user, account, container, object,
205
                PLANKTON_DOMAIN, prefixed, replace)
206

    
207
    def _update_permissions(self, location, permissions):
208
        account, container, object = split_location(location)
209
        self.backend.update_object_permissions(self.user, account, container,
210
                object, permissions)
211

    
212
    def add_user(self, image_id, user):
213
        image = self.get_image(image_id)
214
        assert image, "Image not found"
215

    
216
        location = image['location']
217
        permissions = self._get_permissions(location)
218
        read = set(permissions.get('read', []))
219
        read.add(user)
220
        permissions['read'] = list(read)
221
        self._update_permissions(location, permissions)
222

    
223
    def close(self):
224
        self.backend.close()
225

    
226
    def delete(self, image_id):
227
        image = self.get_image(image_id)
228
        account, container, object = split_location(image['location'])
229
        self.backend.delete_object(self.user, account, container, object)
230

    
231
    def get_data(self, location):
232
        account, container, object = split_location(location)
233
        size, hashmap = self.backend.get_object_hashmap(self.user, account,
234
                container, object)
235
        data = ''.join(self.backend.get_block(hash) for hash in hashmap)
236
        assert len(data) == size
237
        return data
238

    
239
    def get_image(self, image_id):
240
        try:
241
            account, container, object = self.backend.get_uuid(self.user,
242
                    image_id)
243
        except NameError:
244
            return None
245

    
246
        location = get_location(account, container, object)
247
        return self._get_image(location)
248

    
249
    def iter(self):
250
        """Iter over all images available to the user"""
251

    
252
        backend = self.backend
253
        for account in backend.list_accounts(self.user):
254
            for container in backend.list_containers(self.user, account,
255
                                                     shared=True):
256
                for path, version_id in backend.list_objects(self.user,
257
                        account, container, domain=PLANKTON_DOMAIN):
258
                    location = get_location(account, container, path)
259
                    image = self._get_image(location)
260
                    if image:
261
                        yield image
262

    
263
    def iter_public(self, filters=None):
264
        filters = filters or {}
265
        backend = self.backend
266

    
267
        keys = [PLANKTON_PREFIX + 'name']
268
        size_range = (None, None)
269

    
270
        for key, val in filters.items():
271
            if key == 'size_min':
272
                size_range = (int(val), size_range[1])
273
            elif key == 'size_max':
274
                size_range = (size_range[0], int(val))
275
            else:
276
                keys.append('%s = %s' % (PLANKTON_PREFIX + key, val))
277

    
278
        for account in backend.list_accounts(None):
279
            for container in backend.list_containers(None, account,
280
                                                     shared=True):
281
                for path, version_id in backend.list_objects(None, account,
282
                        container, domain=PLANKTON_DOMAIN, keys=keys,
283
                        shared=True, size_range=size_range):
284
                    location = get_location(account, container, path)
285
                    image = self._get_image(location)
286
                    if image:
287
                        yield image
288

    
289
    def iter_shared(self, member):
290
        """Iterate over image ids shared to this member"""
291

    
292
        backend = self.backend
293

    
294
        # To get the list we connect as member and get the list shared by us
295
        for container in  backend.list_containers(member, self.user):
296
            for object, version_id in backend.list_objects(member, self.user,
297
                    container, domain=PLANKTON_DOMAIN):
298
                try:
299
                    location = get_location(self.user, container, object)
300
                    meta = backend.get_object_meta(member, self.user,
301
                            container, object, PLANKTON_DOMAIN)
302
                    if PLANKTON_PREFIX + 'name' in meta:
303
                        yield meta['uuid']
304
                except (NameError, NotAllowedError):
305
                    continue
306

    
307
    def list(self):
308
        """Iter over all images available to the user"""
309

    
310
        return list(self.iter())
311

    
312
    def list_public(self, filters, params):
313
        images = list(self.iter_public(filters))
314
        key = itemgetter(params.get('sort_key', 'created_at'))
315
        reverse = params.get('sort_dir', 'desc') == 'desc'
316
        images.sort(key=key, reverse=reverse)
317
        return images
318

    
319
    def list_users(self, image_id):
320
        image = self.get_image(image_id)
321
        assert image, "Image not found"
322

    
323
        permissions = self._get_permissions(image['location'])
324
        return [user for user in permissions.get('read', []) if user != '*']
325

    
326
    def put(self, name, f, params):
327
        assert 'checksum' not in params, "Passing a checksum is not supported"
328
        assert 'id' not in params, "Passing an ID is not supported"
329
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
330
        assert params.setdefault('disk_format',
331
                settings.DEFAULT_DISK_FORMAT) in \
332
                settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
333
        assert params.setdefault('container_format',
334
                settings.DEFAULT_CONTAINER_FORMAT) in \
335
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
336

    
337
        container = settings.DEFAULT_PLANKTON_CONTAINER
338
        filename = params.pop('filename', name)
339
        location = 'pithos://%s/%s/%s' % (self.user, container, filename)
340
        is_public = params.pop('is_public', False)
341
        permissions = {'read': ['*']} if is_public else {}
342
        size = params.pop('size', None)
343

    
344
        hashmap, size = self._store(f, size)
345

    
346
        meta = {}
347
        meta['properties'] = params.pop('properties', {})
348
        meta.update(name=name, status='available', **params)
349

    
350
        self._update(location, size, hashmap, meta, permissions)
351
        return self._get_image(location)
352

    
353
    def register(self, name, location, params):
354
        assert 'id' not in params, "Passing an ID is not supported"
355
        assert location.startswith('pithos://'), "Invalid location"
356
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
357
        assert params.setdefault('disk_format',
358
                settings.DEFAULT_DISK_FORMAT) in \
359
                settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
360
        assert params.setdefault('container_format',
361
                settings.DEFAULT_CONTAINER_FORMAT) in \
362
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
363

    
364
        user = self.user
365
        account, container, object = split_location(location)
366

    
367
        meta = self._get_meta(location)
368
        assert meta, "File not found"
369

    
370
        size = int(params.pop('size', meta['bytes']))
371
        if size != meta['bytes']:
372
            raise BackendException("Invalid size")
373

    
374
        checksum = params.pop('checksum', meta['hash'])
375
        if checksum != meta['hash']:
376
            raise BackendException("Invalid checksum")
377

    
378
        is_public = params.pop('is_public', False)
379
        permissions = {'read': ['*']} if is_public else {}
380

    
381
        meta = {}
382
        meta['properties'] = params.pop('properties', {})
383
        meta.update(name=name, status='available', **params)
384

    
385
        self._update_meta(location, meta)
386
        self._update_permissions(location, permissions)
387
        return self._get_image(location)
388

    
389
    def remove_user(self, image_id, user):
390
        image = self.get_image(image_id)
391
        assert image, "Image not found"
392

    
393
        location = image['location']
394
        permissions = self._get_permissions(location)
395
        try:
396
            permissions.get('read', []).remove(user)
397
        except ValueError:
398
            return      # User did not have access anyway
399
        self._update_permissions(location, permissions)
400

    
401
    def replace_users(self, image_id, users):
402
        image = self.get_image(image_id)
403
        assert image, "Image not found"
404

    
405
        location = image['location']
406
        permissions = self._get_permissions(location)
407
        permissions['read'] = users
408
        if image.get('is_public', False):
409
            permissions['read'].append('*')
410
        self._update_permissions(location, permissions)
411

    
412
    def update(self, image_id, params):
413
        image = self.get_image(image_id)
414
        assert image, "Image not found"
415

    
416
        location = image['location']
417
        is_public = params.pop('is_public', None)
418
        if is_public is not None:
419
            permissions = self._get_permissions(location)
420
            read = set(permissions.get('read', []))
421
            if is_public:
422
                read.add('*')
423
            else:
424
                read.discard('*')
425
            permissions['read'] = list(read)
426
            self.backend._update_permissions(location, permissions)
427

    
428
        meta = {}
429
        meta['properties'] = params.pop('properties', {})
430
        meta.update(**params)
431

    
432
        self._update_meta(location, meta)
433
        return self.get_image(image_id)
434

    
435

    
436
class DummyImageBackend():
437

    
438
    def __init__(self, user, images=None):
439
        self.user = user
440
        self.images = images or [{"status":"available","name":"Windows","checksum":"c4524d6f6e4f04ecf0212e50bb36f70114f459b223f932f1a3fa6a995e1334f7","created_at":"2012-03-28 14:39:43","disk_format":"diskdump","updated_at":"2012-03-28 16:56:34","properties":{"kernel":"Windows NT Kernel","osfamily":"windows","users":"Administrator","gui":"Windows, Aero Theme","sortorder":"7","size":"10537","os":"windows","root_partition":"2","description":"Windows 2008 R2, Aero Desktop Experience","OS":"windows"},"location":"pithos://images@okeanos.grnet.gr/pithos/windows-2008R2-7-x86_64.diskdump","container_format":"bare","owner":"images@okeanos.grnet.gr","is_public":True,"deleted_at":"","id":"34138811-299e-45db-9a53-acd5f1f7693c","size":11037310976,"metadata":{"values":{"kernel":"Windows NT Kernel","osfamily":"windows","users":"Administrator","gui":"Windows, Aero Theme","sortorder":"7","size":"10537","os":"windows","root_partition":"2","description":"Windows 2008 R2, Aero Desktop Experience","OS":"windows"}},"OS":"windows","description":"Windows 2008 R2, Aero Desktop Experience","kernel":"Windows NT Kernel","GUI":""},{"status":"available","name":"CentOS","checksum":"a7517d876f2387527f35e9d8c19cba4a37419279005354ca03bd625db1cf410e","created_at":"2012-03-28 13:37:28","disk_format":"diskdump","updated_at":"2012-03-28 16:56:33","properties":{"kernel":"2.6.32","osfamily":"linux","users":"root","gui":"No GUI","sortorder":"6","size":"601","os":"centos","root_partition":"1","description":"CentOS 6.0","OS":"centos"},"location":"pithos://images@okeanos.grnet.gr/pithos/centos-6.0-8-x86_64.diskdump","container_format":"bare","owner":"images@okeanos.grnet.gr","is_public":True,"deleted_at":"","id":"8f06e736-d890-4aed-8448-aa0e978695a9","size":628834304,"metadata":{"values":{"kernel":"2.6.32","osfamily":"linux","users":"root","gui":"No GUI","sortorder":"6","size":"601","os":"centos","root_partition":"1","description":"CentOS 6.0","OS":"centos"}},"OS":"centos","description":"CentOS 6.0","kernel":"2.6.32","GUI":""},{"status":"available","name":"Fedora","checksum":"407c7fa05c14937e213503783149aa74ad9bcfc8783ccb653a419fb86bffe0d9","created_at":"2012-03-28 13:52:45","disk_format":"diskdump","updated_at":"2012-03-28 16:56:32","properties":{"kernel":"3.1.9","osfamily":"linux","users":"root user","gui":"GNOME 3.2","sortorder":"5","size":"2641","os":"fedora","root_partition":"1","description":"Fedora 16 Desktop Edition","OS":"fedora"},"location":"debian_base-6.0-7-x86_64","container_format":"bare","owner":"images@okeanos.grnet.gr","is_public":True,"deleted_at":"","id":"1ba3666b-6e57-4d52-813b-e2a33185d12d","size":2765684736,"metadata":{"values":{"kernel":"3.1.9","osfamily":"linux","users":"root user","gui":"GNOME 3.2","sortorder":"5","size":"2641","os":"fedora","root_partition":"1","description":"Fedora 16 Desktop Edition","OS":"fedora"}},"OS":"fedora","description":"Fedora 16 Desktop Edition","kernel":"3.1.9","GUI":""},{"status":"available","name":"Kubuntu","checksum":"a149289f512d70c8f9f6acb0636d2ea9a5b5c3ec0b83e4398aed4a5678da6848","created_at":"2012-03-28 15:05:52","disk_format":"diskdump","updated_at":"2012-03-28 16:56:31","properties":{"kernel":"3.0.0","osfamily":"linux","users":"user","gui":"KDE 4.7.4","sortorder":"4","size":"2850","os":"kubuntu","root_partition":"1","description":"Kubuntu 11.10","OS":"kubuntu"},"location":"pithos://images@okeanos.grnet.gr/pithos/kubuntu-11.10-1-x86_64.diskdump","container_format":"bare","owner":"images@okeanos.grnet.gr","is_public":True,"deleted_at":"","id":"79d24739-af8f-436b-8f6e-eb2d908e0b7e","size":2985041920,"metadata":{"values":{"kernel":"3.0.0","osfamily":"linux","users":"user","gui":"KDE 4.7.4","sortorder":"4","size":"2850","os":"kubuntu","root_partition":"1","description":"Kubuntu 11.10","OS":"kubuntu"}},"OS":"kubuntu","description":"Kubuntu 11.10","kernel":"3.0.0","GUI":""},{"status":"available","name":"Ubuntu","checksum":"f508e1fc8d9cbbd360a9cfc3a68e475933063c77691dac652cb7a5c824791e1b","created_at":"2012-03-28 14:08:35","disk_format":"diskdump","updated_at":"2012-03-28 16:58:10","properties":{"kernel":"3.0.0","osfamily":"linux","users":"user","gui":"Unity 4.22","sortorder":"3","size":"2540","os":"ubuntu","root_partition":"1","description":"Ubuntu 11.10","OS":"ubuntu"},"location":"pithos://images@okeanos.grnet.gr/pithos/ubuntu-11.10-1-x86_64.diskdump","container_format":"bare","owner":"images@okeanos.grnet.gr","is_public":True,"deleted_at":"","id":"d8317451-820a-4c41-917b-665492ab0f81","size":2660171776,"metadata":{"values":{"kernel":"3.0.0","osfamily":"linux","users":"user","gui":"Unity 4.22","sortorder":"3","size":"2540","os":"ubuntu","root_partition":"1","description":"Ubuntu 11.10","OS":"ubuntu"}},"OS":"ubuntu","description":"Ubuntu 11.10","kernel":"3.0.0","GUI":""},{"status":"available","name":"Debian Desktop","checksum":"a7bea0bf6815168a281b505454cd9b2f07ffb9cad0d92e08e950a633c0f05bd2","created_at":"2012-03-28 14:55:37","disk_format":"diskdump","updated_at":"2012-04-03 15:52:09","properties":{"kernel":"2.6.32","osfamily":"linux","users":"root user","gui":"GNOME 2.30","sortorder":"2","size":"3314","os":"debian","root_partition":"1","description":"Debian Squeeze Desktop","OS":"debian"},"location":"pithos://images@okeanos.grnet.gr/pithos/debian_desktop-6.0-6-x86_64.diskdump","container_format":"bare","owner":"images@okeanos.grnet.gr","is_public":True,"deleted_at":"","id":"d7970a84-99b9-40be-b790-ce61001a5b9a","size":3482439680,"metadata":{"values":{"kernel":"2.6.32","osfamily":"linux","users":"root user","gui":"GNOME 2.30","sortorder":"2","size":"3314","os":"debian","root_partition":"1","description":"Debian Squeeze Desktop","OS":"debian"}},"OS":"debian","description":"Debian Squeeze Desktop","kernel":"2.6.32","GUI":""},{"status":"available","name":"Debian Base","checksum":"65352163c9842d6fbc5717437811495f163da2338c807819a1d4d7a50766e56c","created_at":"2012-03-28 13:39:38","disk_format":"diskdump","updated_at":"2012-03-28 16:56:29","properties":{"kernel":"2.6.32","osfamily":"linux","users":"root","gui":"No GUI","sortorder":"1","size":"451","os":"debian","root_partition":"1","description":"Debian Squeeze Base System","OS":"debian"},"location":"pithos://images@okeanos.grnet.gr/pithos/debian_base-6.0-7-x86_64.diskdump","container_format":"bare","owner":"images@okeanos.grnet.gr","is_public":True,"deleted_at":"","id":"49971ade-3bbc-4700-9a84-b3ca00133850","size":471891968,"metadata":{"values":{"kernel":"2.6.32","osfamily":"linux","users":"root","gui":"No GUI","sortorder":"1","size":"451","os":"debian","root_partition":"1","description":"Debian Squeeze Base System","OS":"debian"}},"OS":"debian","description":"Debian Squeeze Base System","kernel":"2.6.32","GUI":""},{"id":"20","name":"(deleted image)","size":-1,"progress":100,"status":"DELETED"},{"id":"21","name":"(deleted image)","size":-1,"progress":100,"status":"DELETED"}]
441

    
442

    
443
    def iter(self):
444
        return self.images
445

    
446
    def get_image(self, image_id):
447
        for i in self.images:
448
            if i['id'] == image_id:
449
                return i
450
        return None
451

    
452
    def close(self):
453
        pass
454

    
455
    def list_public(self, filters, params):
456
        return self.images
457

    
458

    
459
ImageBackend = PithosImageBackend
460
ImageBackend = DummyImageBackend
461