Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (16.3 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 ImageBackend(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
            print repr(size)
373
            print repr(meta['bytes'])
374
            raise BackendException("Invalid size")
375
        
376
        checksum = params.pop('checksum', meta['hash'])
377
        if checksum != meta['hash']:
378
            raise BackendException("Invalid checksum")
379
        
380
        is_public = params.pop('is_public', False)
381
        permissions = {'read': ['*']} if is_public else {}
382
        
383
        meta = {}
384
        meta['properties'] = params.pop('properties', {})
385
        meta.update(name=name, status='available', **params)
386
        
387
        self._update_meta(location, meta)
388
        self._update_permissions(location, permissions)
389
        return self._get_image(location)
390
    
391
    def remove_user(self, image_id, user):
392
        image = self.get_image(image_id)
393
        assert image, "Image not found"
394
        
395
        location = image['location']
396
        permissions = self._get_permissions(location)
397
        try:
398
            permissions.get('read', []).remove(user)
399
        except ValueError:
400
            return      # User did not have access anyway
401
        self._update_permissions(location, permissions)
402
    
403
    def replace_users(self, image_id, users):
404
        image = self.get_image(image_id)
405
        assert image, "Image not found"
406
        
407
        location = image['location']
408
        permissions = self._get_permissions(location)
409
        permissions['read'] = users
410
        if image.get('is_public', False):
411
            permissions['read'].append('*')
412
        self._update_permissions(location, permissions)
413
    
414
    def update(self, image_id, params):
415
        image = self.get_image(image_id)
416
        assert image, "Image not found"
417
        
418
        location = image['location']
419
        is_public = params.pop('is_public', None)
420
        if is_public is not None:
421
            permissions = self._get_permissions(location)
422
            read = set(permissions.get('read', []))
423
            if is_public:
424
                read.add('*')
425
            else:
426
                read.discard('*')
427
            permissions['read'] = list(read)
428
            self.backend._update_permissions(location, permissions)
429
        
430
        meta = {}
431
        meta['properties'] = params.pop('properties', {})
432
        meta.update(**params)
433
        
434
        self._update_meta(location, meta)
435
        return self.get_image(image_id)