Statistics
| Branch: | Tag: | Revision:

root / snf-app / synnefo / plankton / backend.py @ 921355f8

History | View | Annotate | Download (16.6 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
Plankton attributes are divided in 3 categories:
36
  - generated: They are dynamically generated and not stored anywhere.
37
  - user: Stored as user accessible metadata and can be modified from within
38
            Pithos apps. They are visible as prefixed with PLANKTON_PREFIX.
39
  - system: Stored as metadata but can not be modified though Pithos.
40

41
In more detail, Plankton attributes are the following:
42
  - checksum: generated based on the merkle hash of the file
43
  - container_format: stored as a user meta
44
  - created_at: generated based on the modified attribute of the first version
45
  - deleted_at: generated based on the timestamp of the last version
46
  - disk_format: stored as a user meta
47
  - id: generated based on location and stored as system meta
48
  - is_public: True if there is a * entry for the read permission
49
  - location: generated based on the object's path
50
  - name: stored as a user meta
51
  - owner: identical to the object's account
52
  - properties: stored as user meta prefixed with PROPERTY_PREFIX
53
  - size: generated based from 'bytes' value
54
  - status: stored as a system meta
55
  - store: is always 'pithos'
56
  - updated_at: generated based on the modified attribute
57
"""
58

    
59
import json
60
import warnings
61

    
62
from binascii import hexlify
63
from functools import partial
64
from hashlib import md5
65
from operator import itemgetter
66
from time import gmtime, strftime, time
67
from uuid import UUID
68

    
69
from django.conf import settings
70

    
71
from pithos.backends import connect_backend
72
from pithos.backends.base import NotAllowedError
73

    
74

    
75
PLANKTON_PREFIX = 'plankton:'
76
PROPERTY_PREFIX = 'property:'
77

    
78
SYSTEM_META = set(['id', 'status'])
79
USER_META = set(['name', 'container_format', 'disk_format'])
80

    
81

    
82
def prefix_keys(keys):
83
    prefixed = []
84
    for key in keys:
85
        if key in SYSTEM_META:
86
            key = PLANKTON_PREFIX + key
87
        elif key in USER_META:
88
            key = 'X-Object-Meta-' + PLANKTON_PREFIX + key
89
        else:
90
            assert False, "Invalid filter key"
91
        prefixed.append(key)
92
    return prefixed
93

    
94
def prefix_meta(meta):
95
    prefixed = {}
96
    for key, val in meta.items():
97
        key = key.lower()
98
        if key in SYSTEM_META:
99
            key = PLANKTON_PREFIX + key
100
        elif key in USER_META:
101
            key = 'X-Object-Meta-' + PLANKTON_PREFIX + key
102
        elif key == 'properties':
103
            for k, v in val.items():
104
                k = k.lower()
105
                k = 'X-Object-Meta-' + PLANKTON_PREFIX + PROPERTY_PREFIX + k
106
                prefixed[k] = v
107
            continue
108
        else:
109
            assert False, "Invalid metadata key"
110
        prefixed[key] = val
111
    return prefixed
112

    
113

    
114
def get_image_id(location):
115
    return str(UUID(bytes=md5(location).digest()))
116

    
117

    
118
def get_location(account, container, object):
119
    assert '/' not in account, "Invalid account"
120
    assert '/' not in container, "Invalid container"
121
    return 'pithos://%s/%s/%s' % (account, container, object)
122

    
123
def split_location(location):
124
    """Returns (accout, container, object) from a location string"""
125
    t = location.split('/', 4)
126
    assert len(t) == 5, "Invalid location"
127
    return t[2:5]
128

    
129

    
130
class BackendException(Exception): pass
131

    
132

    
133
class ImageBackend(object):
134
    """A wrapper arround the pithos backend to simplify image handling."""
135
    
136
    def __init__(self, user):
137
        self.user = user
138
        self.container = settings.PITHOS_IMAGE_CONTAINER
139
        
140
        original_filters = warnings.filters
141
        warnings.simplefilter('ignore')         # Suppress SQLAlchemy warnings
142
        self.backend = connect_backend()
143
        warnings.filters = original_filters     # Restore warnings
144
        
145
        try:
146
            self.backend.put_container(self.user, self.user, self.container)
147
        except NameError:
148
            pass    # Container already exists
149
    
150
    def _get_image(self, location):
151
        def format_timestamp(t):
152
            return strftime('%Y-%m-%d %H:%M:%S', gmtime(t))
153
        
154
        account, container, object = split_location(location)
155
        
156
        try:
157
            versions = self.backend.list_versions(self.user, account,
158
                    container, object)
159
        except NameError:
160
            return None
161
        
162
        image = {}
163
        
164
        meta = self._get_meta(location)
165
        if meta:
166
            image['deleted_at'] = ''
167
        else:
168
            # Object was deleted, use the latest version
169
            version, timestamp = versions[-1]
170
            meta = self._get_meta(location, version)
171
            image['deleted_at'] = format_timestamp(timestamp)
172
        
173
        permissions = self._get_permissions(location)
174
        
175
        image['checksum'] = meta['_hash']
176
        image['created_at'] = format_timestamp(versions[0][1])
177
        image['is_public'] = '*' in permissions.get('read', [])
178
        image['location'] = location
179
        image['owner'] = account
180
        image['size'] = meta['_bytes']
181
        image['store'] = 'pithos'
182
        image['updated_at'] = format_timestamp(meta['_modified'])
183
        image['properties'] = {}
184
        
185
        for key, val in meta.items():
186
            if key in SYSTEM_META | USER_META:
187
                image[key] = val
188
            elif key.startswith(PROPERTY_PREFIX):
189
                key = key[len(PROPERTY_PREFIX):]
190
                image['properties'][key] = val
191
        
192
        if 'id' in image:
193
            return image
194
        else:
195
            return None
196
    
197
    def _get_meta(self, location, version=None):
198
        account, container, object = split_location(location)
199
        try:
200
            _meta = self.backend.get_object_meta(self.user, account, container,
201
                    object, version)
202
        except NameError:
203
            return None
204
        
205
        user_prefix = 'x-object-meta-' + PLANKTON_PREFIX
206
        system_prefix = PLANKTON_PREFIX
207
        meta = {}
208
        
209
        for key, val in _meta.items():
210
            key = key.lower()
211
            if key.startswith(user_prefix):
212
                key = key[len(user_prefix):]
213
            elif key.startswith(system_prefix):
214
                key = key[len(system_prefix):]
215
            else:
216
                key = '_' + key
217
            meta[key] = val
218
        
219
        return meta
220
    
221
    def _get_permissions(self, location):
222
        account, container, object = split_location(location)
223
        action, path, permissions = self.backend.get_object_permissions(
224
                self.user, account, container, object)
225
        return permissions
226
    
227
    def _iter(self, keys=[], public=False):
228
        backend = self.backend
229
        container = self.container
230
        user = None if public else self.user
231
        
232
        accounts = set(backend.list_accounts(user))
233
        if user:
234
            accounts.add(user)
235
        
236
        for account in accounts:
237
            for path, version_id in backend.list_objects(user, account,
238
                    container, prefix='', delimiter='/',
239
                    keys=prefix_keys(keys)):
240
                try:
241
                    location = get_location(account, container, path)
242
                    image = self._get_image(location)
243
                    if image:
244
                        yield image
245
                except NotAllowedError:
246
                    continue
247
    
248
    def _store(self, f, size=None):
249
        """Breaks data into blocks and stores them in the backend"""
250
        
251
        bytes = 0
252
        hashmap = []
253
        backend = self.backend
254
        blocksize = backend.block_size
255
        
256
        data = f.read(blocksize)
257
        while data:
258
            hash = backend.put_block(data)
259
            hashmap.append(hash)
260
            bytes += len(data)
261
            data = f.read(blocksize)
262
        
263
        if size and size != bytes:
264
            raise BackendException("Invalid size")
265
        
266
        return hashmap, bytes
267
    
268
    def _update(self, location, size, hashmap, meta, permissions):
269
        account, container, object = split_location(location)
270
        self.backend.update_object_hashmap(self.user, account, container,
271
                object, size, hashmap, meta=prefix_meta(meta),
272
                replace_meta=True, permissions=permissions)
273
    
274
    def _update_meta(self, location, meta):
275
        account, container, object = split_location(location)
276
        self.backend.update_object_meta(self.user, account, container, object,
277
                prefix_meta(meta))
278
    
279
    def _update_permissions(self, location, permissions):
280
        account, container, object = split_location(location)
281
        self.backend.update_object_permissions(self.user, account, container,
282
                object, permissions)
283
    
284
    def add_user(self, image_id, user):
285
        image = self.get_meta(image_id)
286
        assert image, "Image not found"
287
        
288
        location = image['location']
289
        permissions = self._get_permissions(location)
290
        read = set(permissions.get('read', []))
291
        read.add(user)
292
        permissions['read'] = list(read)
293
        self._update_permissions(location, permissions)
294
    
295
    def close(self):
296
        self.backend.close()
297
    
298
    def get_data(self, location):
299
        account, container, object = split_location(location)
300
        size, hashmap = self.backend.get_object_hashmap(self.user, account,
301
                container, object)
302
        data = ''.join(self.backend.get_block(hash) for hash in hashmap)
303
        assert len(data) == size
304
        return data
305
    
306
    def get_meta(self, image_id):
307
        # This is an inefficient implementation.
308
        # Due to limitations of the backend we have to iterate all files
309
        # in order to find the one with specific id.
310
        for image in self._iter(keys=['id']):
311
            if image and image['id'] == image_id:
312
                return image
313
        return None
314
    
315
    def iter_public(self, filters):
316
        keys = set()
317
        for key, val in filters.items():
318
            if key in ('size_min', 'size_max'):
319
                key = 'size'
320
            keys.add(key)
321
        
322
        for image in self._iter(keys=keys, public=True):
323
            for key, val in filters.items():
324
                if key == 'size_min':
325
                    if image['size'] < int(val):
326
                        break
327
                elif key == 'size_max':
328
                    if image['size'] > int(val):
329
                        break
330
                else:
331
                    if image[key] != val:
332
                        break
333
            else:
334
                yield image
335
    
336
    def iter_shared(self):
337
        for image in self._iter():
338
            yield image
339
    
340
    def list_public(self, filters, params):
341
        images = list(self.iter_public(filters))
342
        key = itemgetter(params.get('sort_key', 'created_at'))
343
        reverse = params.get('sort_dir', 'desc') == 'desc'
344
        images.sort(key=key, reverse=reverse)
345
        return images
346
    
347
    def list_users(self, image_id):
348
        image = self.get_meta(image_id)
349
        assert image, "Image not found"
350
        
351
        permissions = self._get_permissions(image['location'])
352
        return [user for user in permissions.get('read', []) if user != '*']
353
    
354
    def put(self, name, f, params):
355
        assert 'checksum' not in params, "Passing a checksum is not supported"
356
        assert 'id' not in params, "Passing an ID is not supported"
357
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
358
        assert params.setdefault('disk_format',
359
                settings.DEFAULT_DISK_FORMAT) in \
360
                settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
361
        assert params.setdefault('container_format',
362
                settings.DEFAULT_CONTAINER_FORMAT) in \
363
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
364
        
365
        filename = params.pop('filename', name)
366
        location = 'pithos://%s/%s/%s' % (self.user, self.container, filename)
367
        image_id = get_image_id(location)
368
        is_public = params.pop('is_public', False)
369
        permissions = {'read': ['*']} if is_public else {}
370
        size = params.pop('size', None)
371
        
372
        hashmap, size = self._store(f, size)
373
        
374
        meta = {}
375
        meta['properties'] = params.pop('properties', {})
376
        meta.update(id=image_id, name=name, status='available', **params)
377
        
378
        self._update(location, size, hashmap, meta, permissions)
379
        return self.get_meta(image_id)
380
    
381
    def register(self, name, location, params):
382
        assert 'id' not in params, "Passing an ID is not supported"
383
        assert location.startswith('pithos://'), "Invalid location"
384
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
385
        assert params.setdefault('disk_format',
386
                settings.DEFAULT_DISK_FORMAT) in \
387
                settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
388
        assert params.setdefault('container_format',
389
                settings.DEFAULT_CONTAINER_FORMAT) in \
390
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
391
        
392
        user = self.user
393
        account, container, object = split_location(location)
394
        image_id = get_image_id(location)
395
        
396
        meta = self._get_meta(location)
397
        
398
        size = params.pop('size', meta['_bytes'])
399
        if size != meta['_bytes']:
400
            raise BackendException("Invalid size")
401
        
402
        checksum = params.pop('checksum', meta['_hash'])
403
        if checksum != meta['_hash']:
404
            raise BackendException("Invalid checksum")
405
        
406
        is_public = params.pop('is_public', False)
407
        permissions = {'read': ['*']} if is_public else {}
408
        
409
        meta = {}
410
        meta['properties'] = params.pop('properties', {})
411
        meta.update(id=image_id, name=name, status='available', **params)
412
        
413
        self._update_meta(location, meta)
414
        self._update_permissions(location, permissions)
415
        return self.get_meta(image_id)
416
    
417
    def remove_user(self, image_id, user):
418
        image = self.get_meta(image_id)
419
        assert image, "Image not found"
420
        
421
        location = image['location']
422
        permissions = self._get_permissions(location)
423
        try:
424
            permissions.get('read', []).remove(user)
425
        except ValueError:
426
            return      # User did not have access anyway
427
        self._update_permissions(location, permissions)
428
    
429
    def replace_users(self, image_id, users):
430
        image = self.get_meta(image_id)
431
        assert image, "Image not found"
432
        
433
        location = image['location']
434
        permissions = self._get_permissions(location)
435
        permissions['read'] = users
436
        if image.get('is_public', False):
437
            permissions['read'].append('*')
438
        self._update_permissions(location, permissions)
439
    
440
    def update(self, image_id, params):
441
        image = self.get_meta(image_id)
442
        assert image, "Image not found"
443
        
444
        location = image['location']
445
        is_public = params.pop('is_public', None)
446
        if is_public is not None:
447
            permissions = self._get_permissions(location)
448
            read = set(permissions.get('read', []))
449
            if is_public:
450
                read.add('*')
451
            else:
452
                read.discard('*')
453
            permissions['read'] = list(read)
454
            self.backend._update_permissions(location, permissions)
455
        
456
        meta = {}
457
        meta['properties'] = params.pop('properties', {})
458
        meta.update(**params)
459
        
460
        self._update_meta(location, meta)
461
        return self.get_meta(image_id)