Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (17.9 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, partial
59

    
60
from django.conf import settings
61

    
62
from pithos.backends.base import NotAllowedError as PithosNotAllowedError
63
import synnefo.lib.astakos as lib_astakos
64
import logging
65

    
66
logger = logging.getLogger(__name__)
67

    
68

    
69
PLANKTON_DOMAIN = 'plankton'
70
PLANKTON_PREFIX = 'plankton:'
71
PROPERTY_PREFIX = 'property:'
72

    
73
PLANKTON_META = ('container_format', 'disk_format', 'name', 'properties',
74
                 'status')
75

    
76
TRANSLATE_UUIDS = getattr(settings, 'TRANSLATE_UUIDS', False)
77

    
78
def get_displaynames(names):
79
    try:
80
        auth_url = settings.ASTAKOS_URL
81
        url = auth_url.replace('im/authenticate', 'service/api/user_catalogs')
82
        token = settings.CYCLADES_ASTAKOS_SERVICE_TOKEN
83
        uuids = lib_astakos.get_displaynames(token, names, url=url)
84
    except Exception, e:
85
        logger.exception(e)
86
        return {}
87

    
88
    return uuids
89

    
90

    
91
def get_location(account, container, object):
92
    assert '/' not in account, "Invalid account"
93
    assert '/' not in container, "Invalid container"
94
    return 'pithos://%s/%s/%s' % (account, container, object)
95

    
96

    
97
def split_location(location):
98
    """Returns (accout, container, object) from a location string"""
99
    t = location.split('/', 4)
100
    assert len(t) == 5, "Invalid location"
101
    return t[2:5]
102

    
103

    
104
class BackendException(Exception):
105
    pass
106

    
107

    
108
class NotAllowedError(BackendException):
109
    pass
110

    
111

    
112
from pithos.backends.util import PithosBackendPool
113
POOL_SIZE = 8
114
_pithos_backend_pool = \
115
    PithosBackendPool(POOL_SIZE,
116
                      db_connection=settings.BACKEND_DB_CONNECTION,
117
                      block_path=settings.BACKEND_BLOCK_PATH)
118

    
119

    
120
def get_pithos_backend():
121
    return _pithos_backend_pool.pool_get()
122

    
123

    
124
def handle_backend_exceptions(func):
125
    @wraps(func)
126
    def wrapper(*args, **kwargs):
127
        try:
128
            return func(*args, **kwargs)
129
        except PithosNotAllowedError:
130
            raise NotAllowedError()
131
    return wrapper
132

    
133

    
134
class ImageBackend(object):
135
    """A wrapper arround the pithos backend to simplify image handling."""
136

    
137
    def __init__(self, user):
138
        self.user = user
139

    
140
        original_filters = warnings.filters
141
        warnings.simplefilter('ignore')         # Suppress SQLAlchemy warnings
142
        self.backend = get_pithos_backend()
143
        warnings.filters = original_filters     # Restore warnings
144

    
145
    @handle_backend_exceptions
146
    def _get_image(self, location):
147
        def format_timestamp(t):
148
            return strftime('%Y-%m-%d %H:%M:%S', gmtime(t))
149

    
150
        account, container, object = split_location(location)
151

    
152
        try:
153
            versions = self.backend.list_versions(self.user, account,
154
                                                  container, object)
155
        except NameError:
156
            return None
157

    
158
        image = {}
159

    
160
        meta = self._get_meta(location)
161
        if meta:
162
            image['deleted_at'] = ''
163
        else:
164
            # Object was deleted, use the latest version
165
            version, timestamp = versions[-1]
166
            meta = self._get_meta(location, version)
167
            image['deleted_at'] = format_timestamp(timestamp)
168

    
169
        if PLANKTON_PREFIX + 'name' not in meta:
170
            return None     # Not a Plankton image
171

    
172
        permissions = self._get_permissions(location)
173

    
174
        image['checksum'] = meta['hash']
175
        image['created_at'] = format_timestamp(versions[0][1])
176
        image['id'] = meta['uuid']
177
        image['is_public'] = '*' in permissions.get('read', [])
178
        image['location'] = location
179
        if TRANSLATE_UUIDS:
180
            displaynames = get_displaynames([account])
181
            if account in displaynames:
182
                display_account = displaynames[account]
183
            else:
184
                display_account = 'unknown'
185
            image['owner'] = display_account
186
        else:
187
            image['owner'] = account
188
        image['size'] = meta['bytes']
189
        image['store'] = 'pithos'
190
        image['updated_at'] = format_timestamp(meta['modified'])
191
        image['properties'] = {}
192

    
193
        for key, val in meta.items():
194
            if not key.startswith(PLANKTON_PREFIX):
195
                continue
196
            key = key[len(PLANKTON_PREFIX):]
197
            if key == 'properties':
198
                val = json.loads(val)
199
            if key in PLANKTON_META:
200
                image[key] = val
201

    
202
        return image
203

    
204
    @handle_backend_exceptions
205
    def _get_meta(self, location, version=None):
206
        account, container, object = split_location(location)
207
        try:
208
            return self.backend.get_object_meta(self.user, account, container,
209
                                                object, PLANKTON_DOMAIN,
210
                                                version)
211
        except NameError:
212
            return None
213

    
214
    @handle_backend_exceptions
215
    def _get_permissions(self, location):
216
        account, container, object = split_location(location)
217
        _a, _p, permissions = self.backend.get_object_permissions(self.user,
218
                                                                  account,
219
                                                                  container,
220
                                                                  object)
221
        return permissions
222

    
223
    @handle_backend_exceptions
224
    def _store(self, f, size=None):
225
        """Breaks data into blocks and stores them in the backend"""
226

    
227
        bytes = 0
228
        hashmap = []
229
        backend = self.backend
230
        blocksize = backend.block_size
231

    
232
        data = f.read(blocksize)
233
        while data:
234
            hash = backend.put_block(data)
235
            hashmap.append(hash)
236
            bytes += len(data)
237
            data = f.read(blocksize)
238

    
239
        if size and size != bytes:
240
            raise BackendException("Invalid size")
241

    
242
        return hashmap, bytes
243

    
244
    @handle_backend_exceptions
245
    def _update(self, location, size, hashmap, meta, permissions):
246
        account, container, object = split_location(location)
247
        self.backend.update_object_hashmap(self.user, account, container,
248
                                           object, size, hashmap, '',
249
                                           PLANKTON_DOMAIN,
250
                                           permissions=permissions)
251
        self._update_meta(location, meta, replace=True)
252

    
253
    @handle_backend_exceptions
254
    def _update_meta(self, location, meta, replace=False):
255
        account, container, object = split_location(location)
256

    
257
        prefixed = {}
258
        for key, val in meta.items():
259
            if key == 'properties':
260
                val = json.dumps(val)
261
            if key in PLANKTON_META:
262
                prefixed[PLANKTON_PREFIX + key] = val
263

    
264
        self.backend.update_object_meta(self.user, account, container, object,
265
                                        PLANKTON_DOMAIN, prefixed, replace)
266

    
267
    @handle_backend_exceptions
268
    def _update_permissions(self, location, permissions):
269
        account, container, object = split_location(location)
270
        self.backend.update_object_permissions(self.user, account, container,
271
                                               object, permissions)
272

    
273
    @handle_backend_exceptions
274
    def add_user(self, image_id, user):
275
        image = self.get_image(image_id)
276
        assert image, "Image not found"
277

    
278
        location = image['location']
279
        permissions = self._get_permissions(location)
280
        read = set(permissions.get('read', []))
281
        read.add(user)
282
        permissions['read'] = list(read)
283
        self._update_permissions(location, permissions)
284

    
285
    def close(self):
286
        self.backend.close()
287

    
288
    @handle_backend_exceptions
289
    def delete(self, image_id):
290
        image = self.get_image(image_id)
291
        account, container, object = split_location(image['location'])
292
        self.backend.delete_object(self.user, account, container, object)
293

    
294
    @handle_backend_exceptions
295
    def get_data(self, location):
296
        account, container, object = split_location(location)
297
        size, hashmap = self.backend.get_object_hashmap(self.user, account,
298
                                                        container, object)
299
        data = ''.join(self.backend.get_block(hash) for hash in hashmap)
300
        assert len(data) == size
301
        return data
302

    
303
    @handle_backend_exceptions
304
    def get_image(self, image_id):
305
        try:
306
            account, container, object = self.backend.get_uuid(self.user,
307
                                                               image_id)
308
        except NameError:
309
            return None
310

    
311
        location = get_location(account, container, object)
312
        return self._get_image(location)
313

    
314
    @handle_backend_exceptions
315
    def _iter(self, public=False, filters=None, shared_from=None):
316
        filters = filters or {}
317

    
318
        # Fix keys
319
        keys = [PLANKTON_PREFIX + 'name']
320
        size_range = (None, None)
321
        for key, val in filters.items():
322
            if key == 'size_min':
323
                size_range = (val, size_range[1])
324
            elif key == 'size_max':
325
                size_range = (size_range[0], val)
326
            else:
327
                keys.append('%s = %s' % (PLANKTON_PREFIX + key, val))
328

    
329
        backend = self.backend
330
        if shared_from:
331
            # To get shared images, we connect as shared_from member and
332
            # get the list shared by us
333
            user = shared_from
334
        else:
335
            user = None if public else self.user
336
            accounts = backend.list_accounts(user)
337

    
338
        for account in accounts:
339
            for container in backend.list_containers(user, account,
340
                                                     shared=True):
341
                for path, _ in backend.list_objects(user, account, container,
342
                                                    domain=PLANKTON_DOMAIN,
343
                                                    keys=keys, shared=True,
344
                                                    size_range=size_range):
345
                    location = get_location(account, container, path)
346
                    image = self._get_image(location)
347
                    if image:
348
                        yield image
349

    
350
    def iter(self, filters=None):
351
        """Iter over all images available to the user"""
352
        return self._iter(filters=filters)
353

    
354
    def iter_public(self, filters=None):
355
        """Iter over public images"""
356
        return self._iter(public=True, filters=filters)
357

    
358
    def iter_shared(self, filters=None, member=None):
359
        """Iter over images shared to member"""
360
        return self._iter(filters=filters, shared_from=member)
361

    
362
    def list(self, filters=None, params={}):
363
        """Return all images available to the user"""
364
        images = list(self.iter(filters))
365
        key = itemgetter(params.get('sort_key', 'created_at'))
366
        reverse = params.get('sort_dir', 'desc') == 'desc'
367
        images.sort(key=key, reverse=reverse)
368
        return images
369

    
370
    def list_public(self, filters, params={}):
371
        """Return public images"""
372
        images = list(self.iter_public(filters))
373
        key = itemgetter(params.get('sort_key', 'created_at'))
374
        reverse = params.get('sort_dir', 'desc') == 'desc'
375
        images.sort(key=key, reverse=reverse)
376
        return images
377

    
378
    def list_users(self, image_id):
379
        image = self.get_image(image_id)
380
        assert image, "Image not found"
381

    
382
        permissions = self._get_permissions(image['location'])
383
        return [user for user in permissions.get('read', []) if user != '*']
384

    
385
    @handle_backend_exceptions
386
    def put(self, name, f, params):
387
        assert 'checksum' not in params, "Passing a checksum is not supported"
388
        assert 'id' not in params, "Passing an ID is not supported"
389
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
390
        disk_format = params.setdefault('disk_format',
391
                                        settings.DEFAULT_DISK_FORMAT)
392
        assert disk_format in settings.ALLOWED_DISK_FORMATS,\
393
            "Invalid disk_format"
394
        assert params.setdefault('container_format',
395
                settings.DEFAULT_CONTAINER_FORMAT) in \
396
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
397

    
398
        container = settings.DEFAULT_PLANKTON_CONTAINER
399
        filename = params.pop('filename', name)
400
        location = 'pithos://%s/%s/%s' % (self.user, container, filename)
401
        is_public = params.pop('is_public', False)
402
        permissions = {'read': ['*']} if is_public else {}
403
        size = params.pop('size', None)
404

    
405
        hashmap, size = self._store(f, size)
406

    
407
        meta = {}
408
        meta['properties'] = params.pop('properties', {})
409
        meta.update(name=name, status='available', **params)
410

    
411
        self._update(location, size, hashmap, meta, permissions)
412
        return self._get_image(location)
413

    
414
    @handle_backend_exceptions
415
    def register(self, name, location, params):
416
        assert 'id' not in params, "Passing an ID is not supported"
417
        assert location.startswith('pithos://'), "Invalid location"
418
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
419
        assert params.setdefault('disk_format',
420
                settings.DEFAULT_DISK_FORMAT) in \
421
                settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
422
        assert params.setdefault('container_format',
423
                settings.DEFAULT_CONTAINER_FORMAT) in \
424
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
425

    
426
        # user = self.user
427
        account, container, object = split_location(location)
428

    
429
        meta = self._get_meta(location)
430
        assert meta, "File not found"
431

    
432
        size = int(params.pop('size', meta['bytes']))
433
        if size != meta['bytes']:
434
            raise BackendException("Invalid size")
435

    
436
        checksum = params.pop('checksum', meta['hash'])
437
        if checksum != meta['hash']:
438
            raise BackendException("Invalid checksum")
439

    
440
        is_public = params.pop('is_public', False)
441
        if is_public:
442
            permissions = {'read': ['*']}
443
        else:
444
            permissions = {'read': [self.user]}
445

    
446
        meta = {}
447
        meta['properties'] = params.pop('properties', {})
448
        meta.update(name=name, status='available', **params)
449

    
450
        self._update_meta(location, meta)
451
        self._update_permissions(location, permissions)
452
        return self._get_image(location)
453

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

    
459
        location = image['location']
460
        permissions = self._get_permissions(location)
461
        try:
462
            permissions.get('read', []).remove(user)
463
        except ValueError:
464
            return      # User did not have access anyway
465
        self._update_permissions(location, permissions)
466

    
467
    @handle_backend_exceptions
468
    def replace_users(self, image_id, users):
469
        image = self.get_image(image_id)
470
        assert image, "Image not found"
471

    
472
        location = image['location']
473
        permissions = self._get_permissions(location)
474
        permissions['read'] = users
475
        if image.get('is_public', False):
476
            permissions['read'].append('*')
477
        self._update_permissions(location, permissions)
478

    
479
    @handle_backend_exceptions
480
    def update(self, image_id, params):
481
        image = self.get_image(image_id)
482
        assert image, "Image not found"
483

    
484
        location = image['location']
485
        is_public = params.pop('is_public', None)
486
        if is_public is not None:
487
            permissions = self._get_permissions(location)
488
            read = set(permissions.get('read', []))
489
            if is_public:
490
                read.add('*')
491
            else:
492
                read.discard('*')
493
            permissions['read'] = list(read)
494
            self.backend._update_permissions(location, permissions)
495

    
496
        meta = {}
497
        meta['properties'] = params.pop('properties', {})
498
        meta.update(**params)
499

    
500
        self._update_meta(location, meta)
501
        return self.get_image(image_id)