Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (17 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, version)
184
        except NameError:
185
            return None
186

    
187
    @handle_backend_exceptions
188
    def _get_permissions(self, location):
189
        account, container, object = split_location(location)
190
        action, path, permissions = self.backend.get_object_permissions(
191
                self.user, account, container, object)
192
        return permissions
193

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

    
198
        bytes = 0
199
        hashmap = []
200
        backend = self.backend
201
        blocksize = backend.block_size
202

    
203
        data = f.read(blocksize)
204
        while data:
205
            hash = backend.put_block(data)
206
            hashmap.append(hash)
207
            bytes += len(data)
208
            data = f.read(blocksize)
209

    
210
        if size and size != bytes:
211
            raise BackendException("Invalid size")
212

    
213
        return hashmap, bytes
214

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

    
223
    @handle_backend_exceptions
224
    def _update_meta(self, location, meta, replace=False):
225
        account, container, object = split_location(location)
226

    
227
        prefixed = {}
228
        for key, val in meta.items():
229
            if key == 'properties':
230
                val = json.dumps(val)
231
            if key in PLANKTON_META:
232
                prefixed[PLANKTON_PREFIX + key] = val
233

    
234
        self.backend.update_object_meta(self.user, account, container, object,
235
                PLANKTON_DOMAIN, prefixed, replace)
236

    
237
    @handle_backend_exceptions
238
    def _update_permissions(self, location, permissions):
239
        account, container, object = split_location(location)
240
        self.backend.update_object_permissions(self.user, account, container,
241
                object, permissions)
242

    
243
    @handle_backend_exceptions
244
    def add_user(self, image_id, user):
245
        image = self.get_image(image_id)
246
        assert image, "Image not found"
247

    
248
        location = image['location']
249
        permissions = self._get_permissions(location)
250
        read = set(permissions.get('read', []))
251
        read.add(user)
252
        permissions['read'] = list(read)
253
        self._update_permissions(location, permissions)
254

    
255
    def close(self):
256
        self.backend.close()
257

    
258
    @handle_backend_exceptions
259
    def delete(self, image_id):
260
        image = self.get_image(image_id)
261
        account, container, object = split_location(image['location'])
262
        self.backend.delete_object(self.user, account, container, object)
263

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

    
273
    @handle_backend_exceptions
274
    def get_image(self, image_id):
275
        try:
276
            account, container, object = self.backend.get_uuid(self.user,
277
                    image_id)
278
        except NameError:
279
            return None
280

    
281
        location = get_location(account, container, object)
282
        return self._get_image(location)
283

    
284
    @handle_backend_exceptions
285
    def iter(self):
286
        """Iter over all images available to the user"""
287

    
288
        backend = self.backend
289
        for account in backend.list_accounts(self.user):
290
            for container in backend.list_containers(self.user, account,
291
                                                     shared=True):
292
                for path, version_id in backend.list_objects(self.user,
293
                        account, container, domain=PLANKTON_DOMAIN):
294
                    location = get_location(account, container, path)
295
                    image = self._get_image(location)
296
                    if image:
297
                        yield image
298

    
299
    @handle_backend_exceptions
300
    def iter_public(self, filters=None):
301
        filters = filters or {}
302
        backend = self.backend
303

    
304
        keys = [PLANKTON_PREFIX + 'name']
305
        size_range = (None, None)
306

    
307
        for key, val in filters.items():
308
            if key == 'size_min':
309
                size_range = (int(val), size_range[1])
310
            elif key == 'size_max':
311
                size_range = (size_range[0], int(val))
312
            else:
313
                keys.append('%s = %s' % (PLANKTON_PREFIX + key, val))
314

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

    
326
    @handle_backend_exceptions
327
    def iter_shared(self, member):
328
        """Iterate over image ids shared to this member"""
329

    
330
        backend = self.backend
331

    
332
        # To get the list we connect as member and get the list shared by us
333
        for container in  backend.list_containers(member, self.user):
334
            for object, version_id in backend.list_objects(member, self.user,
335
                    container, domain=PLANKTON_DOMAIN):
336
                try:
337
                    location = get_location(self.user, container, object)
338
                    meta = backend.get_object_meta(member, self.user,
339
                            container, object, PLANKTON_DOMAIN)
340
                    if PLANKTON_PREFIX + 'name' in meta:
341
                        yield meta['uuid']
342
                except (NameError, NotAllowedError):
343
                    continue
344

    
345
    @handle_backend_exceptions
346
    def list(self):
347
        """Iter over all images available to the user"""
348

    
349
        return list(self.iter())
350

    
351
    @handle_backend_exceptions
352
    def list_public(self, filters, params):
353
        images = list(self.iter_public(filters))
354
        key = itemgetter(params.get('sort_key', 'created_at'))
355
        reverse = params.get('sort_dir', 'desc') == 'desc'
356
        images.sort(key=key, reverse=reverse)
357
        return images
358

    
359
    @handle_backend_exceptions
360
    def list_users(self, image_id):
361
        image = self.get_image(image_id)
362
        assert image, "Image not found"
363

    
364
        permissions = self._get_permissions(image['location'])
365
        return [user for user in permissions.get('read', []) if user != '*']
366

    
367
    @handle_backend_exceptions
368
    def put(self, name, f, params):
369
        assert 'checksum' not in params, "Passing a checksum is not supported"
370
        assert 'id' not in params, "Passing an ID is not supported"
371
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
372
        assert params.setdefault('disk_format',
373
                settings.DEFAULT_DISK_FORMAT) in \
374
                settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
375
        assert params.setdefault('container_format',
376
                settings.DEFAULT_CONTAINER_FORMAT) in \
377
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
378

    
379
        container = settings.DEFAULT_PLANKTON_CONTAINER
380
        filename = params.pop('filename', name)
381
        location = 'pithos://%s/%s/%s' % (self.user, container, filename)
382
        is_public = params.pop('is_public', False)
383
        permissions = {'read': ['*']} if is_public else {}
384
        size = params.pop('size', None)
385

    
386
        hashmap, size = self._store(f, size)
387

    
388
        meta = {}
389
        meta['properties'] = params.pop('properties', {})
390
        meta.update(name=name, status='available', **params)
391

    
392
        self._update(location, size, hashmap, meta, permissions)
393
        return self._get_image(location)
394

    
395
    @handle_backend_exceptions
396
    def register(self, name, location, params):
397
        assert 'id' not in params, "Passing an ID is not supported"
398
        assert location.startswith('pithos://'), "Invalid location"
399
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
400
        assert params.setdefault('disk_format',
401
                settings.DEFAULT_DISK_FORMAT) in \
402
                settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
403
        assert params.setdefault('container_format',
404
                settings.DEFAULT_CONTAINER_FORMAT) in \
405
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
406

    
407
        # user = self.user
408
        account, container, object = split_location(location)
409

    
410
        meta = self._get_meta(location)
411
        assert meta, "File not found"
412

    
413
        size = int(params.pop('size', meta['bytes']))
414
        if size != meta['bytes']:
415
            raise BackendException("Invalid size")
416

    
417
        checksum = params.pop('checksum', meta['hash'])
418
        if checksum != meta['hash']:
419
            raise BackendException("Invalid checksum")
420

    
421
        is_public = params.pop('is_public', False)
422
        permissions = {'read': ['*']} if is_public else {}
423

    
424
        meta = {}
425
        meta['properties'] = params.pop('properties', {})
426
        meta.update(name=name, status='available', **params)
427

    
428
        self._update_meta(location, meta)
429
        self._update_permissions(location, permissions)
430
        return self._get_image(location)
431

    
432
    @handle_backend_exceptions
433
    def remove_user(self, image_id, user):
434
        image = self.get_image(image_id)
435
        assert image, "Image not found"
436

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

    
445
    @handle_backend_exceptions
446
    def replace_users(self, image_id, users):
447
        image = self.get_image(image_id)
448
        assert image, "Image not found"
449

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

    
457
    @handle_backend_exceptions
458
    def update(self, image_id, params):
459
        image = self.get_image(image_id)
460
        assert image, "Image not found"
461

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

    
474
        meta = {}
475
        meta['properties'] = params.pop('properties', {})
476
        meta.update(**params)
477

    
478
        self._update_meta(location, meta)
479
        return self.get_image(image_id)