Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (17.1 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
58
from functools import wraps
59

    
60
from django.conf import settings
61

    
62
from pithos.backends.base import NotAllowedError as PithosNotAllowedError
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

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

    
85

    
86
class BackendException(Exception):
87
    pass
88

    
89

    
90
class NotAllowedError(BackendException):
91
    pass
92

    
93

    
94
from pithos.backends.util import PithosBackendPool
95
POOL_SIZE = 8
96
_pithos_backend_pool = \
97
    PithosBackendPool(POOL_SIZE,
98
                      db_connection=settings.BACKEND_DB_CONNECTION,
99
                      block_path=settings.BACKEND_BLOCK_PATH)
100

    
101

    
102
def get_pithos_backend():
103
    return _pithos_backend_pool.pool_get()
104

    
105

    
106
def handle_backend_exceptions(func):
107
    @wraps(func)
108
    def wrapper(*args, **kwargs):
109
        try:
110
            return func(*args, **kwargs)
111
        except PithosNotAllowedError:
112
            raise NotAllowedError()
113
    return wrapper
114

    
115

    
116
class ImageBackend(object):
117
    """A wrapper arround the pithos backend to simplify image handling."""
118

    
119
    def __init__(self, user):
120
        self.user = user
121

    
122
        original_filters = warnings.filters
123
        warnings.simplefilter('ignore')         # Suppress SQLAlchemy warnings
124
        self.backend = get_pithos_backend()
125
        warnings.filters = original_filters     # Restore warnings
126

    
127
    @handle_backend_exceptions
128
    def _get_image(self, location):
129
        def format_timestamp(t):
130
            return strftime('%Y-%m-%d %H:%M:%S', gmtime(t))
131

    
132
        account, container, object = split_location(location)
133

    
134
        try:
135
            versions = self.backend.list_versions(self.user, account,
136
                                                  container, object)
137
        except NameError:
138
            return None
139

    
140
        image = {}
141

    
142
        meta = self._get_meta(location)
143
        if meta:
144
            image['deleted_at'] = ''
145
        else:
146
            # Object was deleted, use the latest version
147
            version, timestamp = versions[-1]
148
            meta = self._get_meta(location, version)
149
            image['deleted_at'] = format_timestamp(timestamp)
150

    
151
        if PLANKTON_PREFIX + 'name' not in meta:
152
            return None     # Not a Plankton image
153

    
154
        permissions = self._get_permissions(location)
155

    
156
        image['checksum'] = meta['hash']
157
        image['created_at'] = format_timestamp(versions[0][1])
158
        image['id'] = meta['uuid']
159
        image['is_public'] = '*' in permissions.get('read', [])
160
        image['location'] = location
161
        image['owner'] = account
162
        image['size'] = meta['bytes']
163
        image['store'] = 'pithos'
164
        image['updated_at'] = format_timestamp(meta['modified'])
165
        image['properties'] = {}
166

    
167
        for key, val in meta.items():
168
            if not key.startswith(PLANKTON_PREFIX):
169
                continue
170
            key = key[len(PLANKTON_PREFIX):]
171
            if key == 'properties':
172
                val = json.loads(val)
173
            if key in PLANKTON_META:
174
                image[key] = val
175

    
176
        return image
177

    
178
    @handle_backend_exceptions
179
    def _get_meta(self, location, version=None):
180
        account, container, object = split_location(location)
181
        try:
182
            return self.backend.get_object_meta(self.user, account, container,
183
                                                object, PLANKTON_DOMAIN,
184
                                                version)
185
        except NameError:
186
            return None
187

    
188
    @handle_backend_exceptions
189
    def _get_permissions(self, location):
190
        account, container, object = split_location(location)
191
        _a, _p, permissions = self.backend.get_object_permissions(self.user,
192
                                                                  account,
193
                                                                  container,
194
                                                                  object)
195
        return permissions
196

    
197
    @handle_backend_exceptions
198
    def _store(self, f, size=None):
199
        """Breaks data into blocks and stores them in the backend"""
200

    
201
        bytes = 0
202
        hashmap = []
203
        backend = self.backend
204
        blocksize = backend.block_size
205

    
206
        data = f.read(blocksize)
207
        while data:
208
            hash = backend.put_block(data)
209
            hashmap.append(hash)
210
            bytes += len(data)
211
            data = f.read(blocksize)
212

    
213
        if size and size != bytes:
214
            raise BackendException("Invalid size")
215

    
216
        return hashmap, bytes
217

    
218
    @handle_backend_exceptions
219
    def _update(self, location, size, hashmap, meta, permissions):
220
        account, container, object = split_location(location)
221
        self.backend.update_object_hashmap(self.user, account, container,
222
                                           object, size, hashmap, '',
223
                                           PLANKTON_DOMAIN,
224
                                           permissions=permissions)
225
        self._update_meta(location, meta, replace=True)
226

    
227
    @handle_backend_exceptions
228
    def _update_meta(self, location, meta, replace=False):
229
        account, container, object = split_location(location)
230

    
231
        prefixed = {}
232
        for key, val in meta.items():
233
            if key == 'properties':
234
                val = json.dumps(val)
235
            if key in PLANKTON_META:
236
                prefixed[PLANKTON_PREFIX + key] = val
237

    
238
        self.backend.update_object_meta(self.user, account, container, object,
239
                                        PLANKTON_DOMAIN, prefixed, replace)
240

    
241
    @handle_backend_exceptions
242
    def _update_permissions(self, location, permissions):
243
        account, container, object = split_location(location)
244
        self.backend.update_object_permissions(self.user, account, container,
245
                                               object, permissions)
246

    
247
    @handle_backend_exceptions
248
    def add_user(self, image_id, user):
249
        image = self.get_image(image_id)
250
        assert image, "Image not found"
251

    
252
        location = image['location']
253
        permissions = self._get_permissions(location)
254
        read = set(permissions.get('read', []))
255
        read.add(user)
256
        permissions['read'] = list(read)
257
        self._update_permissions(location, permissions)
258

    
259
    def close(self):
260
        self.backend.close()
261

    
262
    @handle_backend_exceptions
263
    def delete(self, image_id):
264
        image = self.get_image(image_id)
265
        account, container, object = split_location(image['location'])
266
        self.backend.delete_object(self.user, account, container, object)
267

    
268
    @handle_backend_exceptions
269
    def get_data(self, location):
270
        account, container, object = split_location(location)
271
        size, hashmap = self.backend.get_object_hashmap(self.user, account,
272
                                                        container, object)
273
        data = ''.join(self.backend.get_block(hash) for hash in hashmap)
274
        assert len(data) == size
275
        return data
276

    
277
    @handle_backend_exceptions
278
    def get_image(self, image_id):
279
        try:
280
            account, container, object = self.backend.get_uuid(self.user,
281
                                                               image_id)
282
        except NameError:
283
            return None
284

    
285
        location = get_location(account, container, object)
286
        return self._get_image(location)
287

    
288
    @handle_backend_exceptions
289
    def _iter(self, public=False, filters=None, shared_from=None):
290
        filters = filters or {}
291

    
292
        # Fix keys
293
        keys = [PLANKTON_PREFIX + 'name']
294
        size_range = (None, None)
295
        for key, val in filters.items():
296
            if key == 'size_min':
297
                size_range = (val, size_range[1])
298
            elif key == 'size_max':
299
                size_range = (size_range[0], val)
300
            else:
301
                keys.append('%s = %s' % (PLANKTON_PREFIX + key, val))
302

    
303
        backend = self.backend
304
        if shared_from:
305
            # To get shared images, we connect as shared_from member and
306
            # get the list shared by us
307
            user = shared_from
308
            accounts = [self.user]
309
        else:
310
            user = None if public else self.user
311
            accounts = backend.list_accounts(user)
312

    
313
        for account in accounts:
314
            for container in backend.list_containers(user, account,
315
                                                     shared=True):
316
                for path, _ in backend.list_objects(user, account, container,
317
                                                    domain=PLANKTON_DOMAIN,
318
                                                    keys=keys, shared=True,
319
                                                    size_range=size_range):
320
                    location = get_location(account, container, path)
321
                    image = self._get_image(location)
322
                    if image:
323
                        yield image
324

    
325
    def iter(self, filters=None):
326
        """Iter over all images available to the user"""
327
        return self._iter(filters=filters)
328

    
329
    def iter_public(self, filters=None):
330
        """Iter over public images"""
331
        return self._iter(public=True, filters=filters)
332

    
333
    def iter_shared(self, filters=None, member=None):
334
        """Iter over images shared to member"""
335
        return self._iter(filters=filters, shared_from=member)
336

    
337
    def list(self, filters=None, params={}):
338
        """Return all images available to the user"""
339
        images = list(self.iter(filters))
340
        key = itemgetter(params.get('sort_key', 'created_at'))
341
        reverse = params.get('sort_dir', 'desc') == 'desc'
342
        images.sort(key=key, reverse=reverse)
343
        return images
344

    
345
    def list_public(self, filters, params={}):
346
        """Return public images"""
347
        images = list(self.iter_public(filters))
348
        key = itemgetter(params.get('sort_key', 'created_at'))
349
        reverse = params.get('sort_dir', 'desc') == 'desc'
350
        images.sort(key=key, reverse=reverse)
351
        return images
352

    
353
    def list_users(self, image_id):
354
        image = self.get_image(image_id)
355
        assert image, "Image not found"
356

    
357
        permissions = self._get_permissions(image['location'])
358
        return [user for user in permissions.get('read', []) if user != '*']
359

    
360
    @handle_backend_exceptions
361
    def put(self, name, f, params):
362
        assert 'checksum' not in params, "Passing a checksum is not supported"
363
        assert 'id' not in params, "Passing an ID is not supported"
364
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
365
        disk_format = params.setdefault('disk_format',
366
                                        settings.DEFAULT_DISK_FORMAT)
367
        assert disk_format in settings.ALLOWED_DISK_FORMATS,\
368
            "Invalid disk_format"
369
        assert params.setdefault('container_format',
370
                settings.DEFAULT_CONTAINER_FORMAT) in \
371
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
372

    
373
        container = settings.DEFAULT_PLANKTON_CONTAINER
374
        filename = params.pop('filename', name)
375
        location = 'pithos://%s/%s/%s' % (self.user, container, filename)
376
        is_public = params.pop('is_public', False)
377
        permissions = {'read': ['*']} if is_public else {}
378
        size = params.pop('size', None)
379

    
380
        hashmap, size = self._store(f, size)
381

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

    
386
        self._update(location, size, hashmap, meta, permissions)
387
        return self._get_image(location)
388

    
389
    @handle_backend_exceptions
390
    def register(self, name, location, params):
391
        assert 'id' not in params, "Passing an ID is not supported"
392
        assert location.startswith('pithos://'), "Invalid location"
393
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
394
        assert params.setdefault('disk_format',
395
                settings.DEFAULT_DISK_FORMAT) in \
396
                settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
397
        assert params.setdefault('container_format',
398
                settings.DEFAULT_CONTAINER_FORMAT) in \
399
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
400

    
401
        # user = self.user
402
        account, container, object = split_location(location)
403

    
404
        meta = self._get_meta(location)
405
        assert meta, "File not found"
406

    
407
        size = int(params.pop('size', meta['bytes']))
408
        if size != meta['bytes']:
409
            raise BackendException("Invalid size")
410

    
411
        checksum = params.pop('checksum', meta['hash'])
412
        if checksum != meta['hash']:
413
            raise BackendException("Invalid checksum")
414

    
415
        is_public = params.pop('is_public', False)
416
        if is_public:
417
            permissions = {'read': ['*']}
418
        else:
419
            permissions = {'read': [self.user]}
420

    
421
        meta = {}
422
        meta['properties'] = params.pop('properties', {})
423
        meta.update(name=name, status='available', **params)
424

    
425
        self._update_meta(location, meta)
426
        self._update_permissions(location, permissions)
427
        return self._get_image(location)
428

    
429
    @handle_backend_exceptions
430
    def remove_user(self, image_id, user):
431
        image = self.get_image(image_id)
432
        assert image, "Image not found"
433

    
434
        location = image['location']
435
        permissions = self._get_permissions(location)
436
        try:
437
            permissions.get('read', []).remove(user)
438
        except ValueError:
439
            return      # User did not have access anyway
440
        self._update_permissions(location, permissions)
441

    
442
    @handle_backend_exceptions
443
    def replace_users(self, image_id, users):
444
        image = self.get_image(image_id)
445
        assert image, "Image not found"
446

    
447
        location = image['location']
448
        permissions = self._get_permissions(location)
449
        permissions['read'] = users
450
        if image.get('is_public', False):
451
            permissions['read'].append('*')
452
        self._update_permissions(location, permissions)
453

    
454
    @handle_backend_exceptions
455
    def update(self, image_id, params):
456
        image = self.get_image(image_id)
457
        assert image, "Image not found"
458

    
459
        location = image['location']
460
        is_public = params.pop('is_public', None)
461
        if is_public is not None:
462
            permissions = self._get_permissions(location)
463
            read = set(permissions.get('read', []))
464
            if is_public:
465
                read.add('*')
466
            else:
467
                read.discard('*')
468
            permissions['read'] = list(read)
469
            self.backend._update_permissions(location, permissions)
470

    
471
        meta = {}
472
        meta['properties'] = params.pop('properties', {})
473
        meta.update(**params)
474

    
475
        self._update_meta(location, meta)
476
        return self.get_image(image_id)