Statistics
| Branch: | Tag: | Revision:

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

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