Statistics
| Branch: | Tag: | Revision:

root / snf-app / synnefo / plankton / backend.py @ bfd9f988

History | View | Annotate | Download (15.4 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 get_data(self, location):
227
        account, container, object = split_location(location)
228
        size, hashmap = self.backend.get_object_hashmap(self.user, account,
229
                container, object)
230
        data = ''.join(self.backend.get_block(hash) for hash in hashmap)
231
        assert len(data) == size
232
        return data
233
    
234
    def get_image(self, image_id):
235
        try:
236
            account, container, object = self.backend.get_uuid(self.user,
237
                    image_id)
238
        except NameError:
239
            return None
240
        
241
        location = get_location(account, container, object)
242
        return self._get_image(location)
243
    
244
    def iter_public(self, filters):
245
        backend = self.backend
246
        
247
        keys = [PLANKTON_PREFIX + 'name']
248
        for key, val in filters.items():
249
            if key == 'size_min':
250
                filter = 'bytes >= %d' % int(val)
251
            elif key == 'size_max':
252
                filter = 'bytes <= %d' % int(val)
253
            else:
254
                filter = '%s = %s' % (PLANKTON_PREFIX + key, val)
255
            keys.append(filter)
256
        
257
        for account in backend.list_accounts(None):
258
            for container in backend.list_containers(None, account,
259
                                                     shared=True):
260
                for path, version_id in backend.list_objects(None, account,
261
                        container, prefix='', delimiter='/',
262
                        domain=PLANKTON_DOMAIN,
263
                        keys=keys, shared=True):
264
                    location = get_location(account, container, path)
265
                    image = self._get_image(location)
266
                    if image:
267
                        yield image
268
    
269
    def iter_shared(self, member):
270
        """Iterate over image ids shared to this member"""
271
        
272
        backend = self.backend
273
        
274
        # To get the list we connect as member and get the list shared by us
275
        for container in  backend.list_containers(member, self.user):
276
            for object, version_id in backend.list_objects(member, self.user,
277
                    container, prefix='', delimiter='/',
278
                    domain=PLANKTON_DOMAIN):
279
                try:
280
                    location = get_location(self.user, container, object)
281
                    meta = backend.get_object_meta(member, self.user,
282
                            container, object, PLANKTON_DOMAIN)
283
                    if PLANKTON_PREFIX + 'name' in meta:
284
                        yield meta['uuid']
285
                except NotAllowedError:
286
                    continue
287
    
288
    def list_public(self, filters, params):
289
        images = list(self.iter_public(filters))
290
        key = itemgetter(params.get('sort_key', 'created_at'))
291
        reverse = params.get('sort_dir', 'desc') == 'desc'
292
        images.sort(key=key, reverse=reverse)
293
        return images
294
    
295
    def list_users(self, image_id):
296
        image = self.get_image(image_id)
297
        assert image, "Image not found"
298
        
299
        permissions = self._get_permissions(image['location'])
300
        return [user for user in permissions.get('read', []) if user != '*']
301
    
302
    def put(self, name, f, params):
303
        assert 'checksum' not in params, "Passing a checksum is not supported"
304
        assert 'id' not in params, "Passing an ID is not supported"
305
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
306
        assert params.setdefault('disk_format',
307
                settings.DEFAULT_DISK_FORMAT) in \
308
                settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
309
        assert params.setdefault('container_format',
310
                settings.DEFAULT_CONTAINER_FORMAT) in \
311
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
312
        
313
        container = settings.DEFAULT_PLANKTON_CONTAINER
314
        filename = params.pop('filename', name)
315
        location = 'pithos://%s/%s/%s' % (self.user, container, filename)
316
        is_public = params.pop('is_public', False)
317
        permissions = {'read': ['*']} if is_public else {}
318
        size = params.pop('size', None)
319
        
320
        hashmap, size = self._store(f, size)
321
        
322
        meta = {}
323
        meta['properties'] = params.pop('properties', {})
324
        meta.update(name=name, status='available', **params)
325
        
326
        self._update(location, size, hashmap, meta, permissions)
327
        return self._get_image(location)
328
    
329
    def register(self, name, location, params):
330
        assert 'id' not in params, "Passing an ID is not supported"
331
        assert location.startswith('pithos://'), "Invalid location"
332
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
333
        assert params.setdefault('disk_format',
334
                settings.DEFAULT_DISK_FORMAT) in \
335
                settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
336
        assert params.setdefault('container_format',
337
                settings.DEFAULT_CONTAINER_FORMAT) in \
338
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
339
        
340
        user = self.user
341
        account, container, object = split_location(location)
342
        
343
        meta = self._get_meta(location)
344
        assert meta, "File not found"
345
        
346
        size = int(params.pop('size', meta['bytes']))
347
        if size != meta['bytes']:
348
            print repr(size)
349
            print repr(meta['bytes'])
350
            raise BackendException("Invalid size")
351
        
352
        checksum = params.pop('checksum', meta['hash'])
353
        if checksum != meta['hash']:
354
            raise BackendException("Invalid checksum")
355
        
356
        is_public = params.pop('is_public', False)
357
        permissions = {'read': ['*']} if is_public else {}
358
        
359
        meta = {}
360
        meta['properties'] = params.pop('properties', {})
361
        meta.update(name=name, status='available', **params)
362
        
363
        self._update_meta(location, meta)
364
        self._update_permissions(location, permissions)
365
        return self._get_image(location)
366
    
367
    def remove_user(self, image_id, user):
368
        image = self.get_image(image_id)
369
        assert image, "Image not found"
370
        
371
        location = image['location']
372
        permissions = self._get_permissions(location)
373
        try:
374
            permissions.get('read', []).remove(user)
375
        except ValueError:
376
            return      # User did not have access anyway
377
        self._update_permissions(location, permissions)
378
    
379
    def replace_users(self, image_id, users):
380
        image = self.get_image(image_id)
381
        assert image, "Image not found"
382
        
383
        location = image['location']
384
        permissions = self._get_permissions(location)
385
        permissions['read'] = users
386
        if image.get('is_public', False):
387
            permissions['read'].append('*')
388
        self._update_permissions(location, permissions)
389
    
390
    def update(self, image_id, params):
391
        image = self.get_image(image_id)
392
        assert image, "Image not found"
393
        
394
        location = image['location']
395
        is_public = params.pop('is_public', None)
396
        if is_public is not None:
397
            permissions = self._get_permissions(location)
398
            read = set(permissions.get('read', []))
399
            if is_public:
400
                read.add('*')
401
            else:
402
                read.discard('*')
403
            permissions['read'] = list(read)
404
            self.backend._update_permissions(location, permissions)
405
        
406
        meta = {}
407
        meta['properties'] = params.pop('properties', {})
408
        meta.update(**params)
409
        
410
        self._update_meta(location, meta)
411
        return self.get_image(image_id)