Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (16.8 kB)

1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2

    
3
#
4
# Redistribution and use in source and binary forms, with or
5
# without modification, are permitted provided that the following
6
# conditions are met:
7
#
8
#   1. Redistributions of source code must retain the above
9
#      copyright notice, this list of conditions and the following
10
#      disclaimer.
11
#
12
#   2. Redistributions in binary form must reproduce the above
13
#      copyright notice, this list of conditions and the following
14
#      disclaimer in the documentation and/or other materials
15
#      provided with the distribution.
16
#
17
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28
# POSSIBILITY OF SUCH DAMAGE.
29
#
30
# The views and conclusions contained in the software and
31
# documentation are those of the authors and should not be
32
# interpreted as representing official policies, either expressed
33
# or implied, of GRNET S.A.
34

    
35
"""
36
The Plankton attributes are the following:
37
  - checksum: the 'hash' meta
38
  - container_format: stored as a user meta
39
  - created_at: the 'modified' meta of the first version
40
  - deleted_at: the timestamp of the last version
41
  - disk_format: stored as a user meta
42
  - id: the 'uuid' meta
43
  - is_public: True if there is a * entry for the read permission
44
  - location: generated based on the file's path
45
  - name: stored as a user meta
46
  - owner: the file's account
47
  - properties: stored as user meta prefixed with PROPERTY_PREFIX
48
  - size: the 'bytes' meta
49
  - status: stored as a system meta
50
  - store: is always 'pithos'
51
  - updated_at: the 'modified' meta
52
"""
53

    
54
import json
55
import warnings
56
import logging
57
from time import gmtime, strftime
58
from functools import wraps
59
from operator import itemgetter
60

    
61
from django.conf import settings
62
from pithos.backends.base import NotAllowedError, VersionNotExists
63

    
64
logger = logging.getLogger(__name__)
65

    
66

    
67
PLANKTON_DOMAIN = 'plankton'
68
PLANKTON_PREFIX = 'plankton:'
69
PROPERTY_PREFIX = 'property:'
70

    
71
PLANKTON_META = ('container_format', 'disk_format', 'name', 'properties',
72
                 'status')
73

    
74
from pithos.backends.util import PithosBackendPool
75
POOL_SIZE = 8
76
_pithos_backend_pool = \
77
    PithosBackendPool(
78
        POOL_SIZE,
79
        astakos_url=settings.ASTAKOS_URL,
80
        service_token=settings.CYCLADES_ASTAKOS_SERVICE_TOKEN,
81
        astakosclient_poolsize=settings.ASTAKOS_POOLSIZE,
82
        db_connection=settings.BACKEND_DB_CONNECTION,
83
        block_path=settings.BACKEND_BLOCK_PATH)
84

    
85

    
86
def get_pithos_backend():
87
    return _pithos_backend_pool.pool_get()
88

    
89

    
90
def create_url(account, container, name):
91
    assert "/" not in account, "Invalid account"
92
    assert "/" not in container, "Invalid container"
93
    return "pithos://%s/%s/%s" % (account, container, name)
94

    
95

    
96
def split_url(url):
97
    """Returns (accout, container, object) from a url string"""
98
    t = url.split('/', 4)
99
    assert t[0] == "pithos:", "Invalid url"
100
    assert len(t) == 5, "Invalid url"
101
    return t[2:5]
102

    
103

    
104
def format_timestamp(t):
105
    return strftime('%Y-%m-%d %H:%M:%S', gmtime(t))
106

    
107

    
108
def handle_backend_exceptions(func):
109
    @wraps(func)
110
    def wrapper(*args, **kwargs):
111
        try:
112
            return func(*args, **kwargs)
113
        except NotAllowedError:
114
            raise Forbidden
115
        except NameError:
116
            raise ImageNotFound
117
        except VersionNotExists:
118
            raise ImageNotFound
119
    return wrapper
120

    
121

    
122
class ImageBackend(object):
123
    """A wrapper arround the pithos backend to simplify image handling."""
124

    
125
    def __init__(self, user):
126
        self.user = user
127

    
128
        original_filters = warnings.filters
129
        warnings.simplefilter('ignore')         # Suppress SQLAlchemy warnings
130
        self.backend = get_pithos_backend()
131
        warnings.filters = original_filters     # Restore warnings
132

    
133
    def close(self):
134
        """Close PithosBackend(return to pool)"""
135
        self.backend.close()
136

    
137
    @handle_backend_exceptions
138
    def get_image(self, image_uuid):
139
        """Retrieve information about an image."""
140
        image_url = self._get_image_url(image_uuid)
141
        return self._get_image(image_url)
142

    
143
    def _get_image_url(self, image_uuid):
144
        """Get the Pithos url that corresponds to an image UUID."""
145
        account, container, name = self.backend.get_uuid(self.user, image_uuid)
146
        return create_url(account, container, name)
147

    
148
    def _get_image(self, image_url):
149
        """Get information about an Image.
150

151
        Get all available information about an Image.
152
        """
153
        account, container, name = split_url(image_url)
154
        versions = self.backend.list_versions(self.user, account, container,
155
                                              name)
156
        if not versions:
157
            raise Exception("Image without versions %s" % image_url)
158
        try:
159
            meta = self._get_meta(image_url)
160
            meta["deleted"] = ""
161
        except NameError:
162
            # Object was deleted, use the latest version
163
            version, timestamp = versions[-1]
164
            meta = self._get_meta(image_url, version)
165
            meta["deleted"] = timestamp
166

    
167
        meta["created"] = versions[0][1]
168

    
169
        if PLANKTON_PREFIX + 'name' not in meta:
170
            raise ImageNotFound("'%s' is not a Plankton image" % image_url)
171

    
172
        permissions = self._get_permissions(image_url)
173
        return image_to_dict(image_url, meta, permissions)
174

    
175
    def _get_meta(self, image_url, version=None):
176
        """Get object's metadata."""
177
        account, container, name = split_url(image_url)
178
        return self.backend.get_object_meta(self.user, account, container,
179
                                            name, PLANKTON_DOMAIN, version)
180

    
181
    def _update_meta(self, image_url, meta, replace=False):
182
        """Update object's metadata."""
183
        account, container, name = split_url(image_url)
184

    
185
        prefixed = {}
186
        for key, val in meta.items():
187
            if key in PLANKTON_META:
188
                if key == "properties":
189
                    val = json.dumps(val)
190
                prefixed[PLANKTON_PREFIX + key] = val
191

    
192
        self.backend.update_object_meta(self.user, account, container, name,
193
                                        PLANKTON_DOMAIN, prefixed, replace)
194
        logger.debug("User '%s' updated image '%s', meta: '%s'", self.user,
195
                     image_url, prefixed)
196

    
197
    def _get_permissions(self, image_url):
198
        """Get object's permissions."""
199
        account, container, name = split_url(image_url)
200
        _a, path, permissions = \
201
            self.backend.get_object_permissions(self.user, account, container,
202
                                                name)
203

    
204
        if path is None:
205
            logger.warning("Image '%s' got permissions from None path",
206
                           image_url)
207

    
208
        return permissions
209

    
210
    def _update_permissions(self, image_url, permissions):
211
        """Update object's permissions."""
212
        account, container, name = split_url(image_url)
213
        self.backend.update_object_permissions(self.user, account, container,
214
                                               name, permissions)
215
        logger.debug("User '%s' updated image '%s', permissions: '%s'",
216
                     self.user, image_url, permissions)
217

    
218
    @handle_backend_exceptions
219
    def unregister(self, image_uuid):
220
        """Unregister an image.
221

222
        Unregister an image, by removing all metadata from the Pithos
223
        file that exist in the PLANKTON_DOMAIN.
224

225
        """
226
        image_url = self._get_image_url(image_uuid)
227
        self._get_image(image_url)  # Assert that it is an image
228
        # Unregister the image by removing all metadata from domain
229
        # 'PLANKTON_DOMAIN'
230
        meta = {}
231
        self._update_meta(image_url, meta, True)
232
        logger.debug("User '%s' deleted image '%s'", self.user, image_url)
233

    
234
    @handle_backend_exceptions
235
    def add_user(self, image_uuid, add_user):
236
        """Add a user as an image member.
237

238
        Update read permissions of Pithos file, to include the specified user.
239

240
        """
241
        image_url = self._get_image_url(image_uuid)
242
        self._get_image(image_url)  # Assert that it is an image
243
        permissions = self._get_permissions(image_url)
244
        read = set(permissions.get("read", []))
245
        assert(isinstance(add_user, (str, unicode)))
246
        read.add(add_user)
247
        permissions["read"] = list(read)
248
        self._update_permissions(image_url, permissions)
249

    
250
    @handle_backend_exceptions
251
    def remove_user(self, image_uuid, remove_user):
252
        """Remove the user from image members.
253

254
        Remove the specified user from the read permissions of the Pithos file.
255

256
        """
257
        image_url = self._get_image_url(image_uuid)
258
        self._get_image(image_url)  # Assert that it is an image
259
        permissions = self._get_permissions(image_url)
260
        read = set(permissions.get("read", []))
261
        assert(isinstance(remove_user, (str, unicode)))
262
        try:
263
            read.remove(remove_user)
264
        except ValueError:
265
            return  # TODO: User did not have access
266
        permissions["read"] = list(read)
267
        self._update_permissions(image_url, permissions)
268

    
269
    @handle_backend_exceptions
270
    def replace_users(self, image_uuid, replace_users):
271
        """Replace image members.
272

273
        Replace the read permissions of the Pithos files with the specified
274
        users. If image is specified as public, we must preserve * permission.
275

276
        """
277
        image_url = self._get_image_url(image_uuid)
278
        image = self._get_image(image_url)
279
        permissions = self._get_permissions(image_url)
280
        assert(isinstance(replace_users, list))
281
        permissions["read"] = replace_users
282
        if image.get("is_public", False):
283
            permissions["read"].append("*")
284
        self._update_permissions(image_url, permissions)
285

    
286
    @handle_backend_exceptions
287
    def list_users(self, image_uuid):
288
        """List the image members.
289

290
        List the image members, by listing all users that have read permission
291
        to the corresponding Pithos file.
292

293
        """
294
        image_url = self._get_image_url(image_uuid)
295
        self._get_image(image_url)  # Assert that it is an image
296
        permissions = self._get_permissions(image_url)
297
        return [user for user in permissions.get('read', []) if user != '*']
298

    
299
    @handle_backend_exceptions
300
    def update_metadata(self, image_uuid, metadata):
301
        """Update Image metadata."""
302
        image_url = self._get_image_url(image_uuid)
303
        self._get_image(image_url)  # Assert that it is an image
304

    
305
        is_public = metadata.pop("is_public", None)
306
        if is_public is not None:
307
            permissions = self._get_permissions(image_url)
308
            read = set(permissions.get("read", []))
309
            if is_public:
310
                read.add("*")
311
            else:
312
                read.discard("*")
313
            permissions["read"] = list(read)
314
            self._update_permissions(image_url, permissions)
315
        meta = {}
316
        meta["properties"] = metadata.pop("properties", {})
317
        meta.update(**metadata)
318

    
319
        self._update_meta(image_url, meta)
320
        return self.get_image(image_uuid)
321

    
322
    @handle_backend_exceptions
323
    def register(self, name, image_url, metadata):
324
        # Validate that metadata are allowed
325
        if "id" in metadata:
326
            raise ValueError("Passing an ID is not supported")
327
        store = metadata.pop("store", "pithos")
328
        if store != "pithos":
329
            raise ValueError("Invalid store '%s'. Only 'pithos' store is"
330
                             "supported" % store)
331
        disk_format = metadata.setdefault("disk_format",
332
                                          settings.DEFAULT_DISK_FORMAT)
333
        if disk_format not in settings.ALLOWED_DISK_FORMATS:
334
            raise ValueError("Invalid disk format '%s'" % disk_format)
335
        container_format =\
336
            metadata.setdefault("container_format",
337
                                settings.DEFAULT_CONTAINER_FORMAT)
338
        if container_format not in settings.ALLOWED_CONTAINER_FORMATS:
339
            raise ValueError("Invalid container format '%s'" %
340
                             container_format)
341

    
342
        # Validate that 'size' and 'checksum' are valid
343
        account, container, object = split_url(image_url)
344

    
345
        meta = self._get_meta(image_url)
346

    
347
        size = int(metadata.pop('size', meta['bytes']))
348
        if size != meta['bytes']:
349
            raise ValueError("Invalid size")
350

    
351
        checksum = metadata.pop('checksum', meta['hash'])
352
        if checksum != meta['hash']:
353
            raise ValueError("Invalid checksum")
354

    
355
        # Fix permissions
356
        is_public = metadata.pop('is_public', False)
357
        if is_public:
358
            permissions = {'read': ['*']}
359
        else:
360
            permissions = {'read': [self.user]}
361

    
362
        # Update rest metadata
363
        meta = {}
364
        meta['properties'] = metadata.pop('properties', {})
365
        meta.update(name=name, status='available', **metadata)
366

    
367
        # Do the actualy update in the Pithos backend
368
        self._update_meta(image_url, meta)
369
        self._update_permissions(image_url, permissions)
370
        logger.debug("User '%s' created image '%s'('%s')", self.user,
371
                     image_url, name)
372
        return self._get_image(image_url)
373

    
374
    def _list_images(self, user=None, filters=None, params=None):
375
        filters = filters or {}
376

    
377
        # TODO: Use filters
378
        # # Fix keys
379
        # keys = [PLANKTON_PREFIX + 'name']
380
        # size_range = (None, None)
381
        # for key, val in filters.items():
382
        #     if key == 'size_min':
383
        #         size_range = (val, size_range[1])
384
        #     elif key == 'size_max':
385
        #         size_range = (size_range[0], val)
386
        #     else:
387
        #         keys.append('%s = %s' % (PLANKTON_PREFIX + key, val))
388
        _images = self.backend.get_domain_objects(domain=PLANKTON_DOMAIN,
389
                                                  user=user)
390

    
391
        images = []
392
        for (location, meta, permissions) in _images:
393
            image_url = "pithos://" + location
394
            meta["modified"] = meta["version_timestamp"]
395
            # TODO: Create metadata when registering an Image
396
            meta["created"] = meta["version_timestamp"]
397
            images.append(image_to_dict(image_url, meta, permissions))
398

    
399
        if params is None:
400
            params = {}
401
        key = itemgetter(params.get('sort_key', 'created_at'))
402
        reverse = params.get('sort_dir', 'desc') == 'desc'
403
        images.sort(key=key, reverse=reverse)
404
        return images
405

    
406
    def list_images(self, filters=None, params=None):
407
        return self._list_images(user=self.user, filters=filters,
408
                                 params=params)
409

    
410
    def list_shared_images(self, member, filters=None, params=None):
411
        images = self._list_images(user=self.user, filters=filters,
412
                                   params=params)
413
        is_shared = lambda img: not img["is_public"] and img["owner"] == member
414
        return filter(is_shared, images)
415

    
416
    def list_public_images(self, filters=None, params=None):
417
        images = self._list_images(user=None, filters=filters, params=params)
418
        return filter(lambda img: img["is_public"], images)
419

    
420

    
421
class ImageBackendError(Exception):
422
    pass
423

    
424

    
425
class ImageNotFound(ImageBackendError):
426
    pass
427

    
428

    
429
class Forbidden(ImageBackendError):
430
    pass
431

    
432

    
433
def image_to_dict(image_url, meta, permissions):
434
    """Render an image to a dictionary"""
435
    account, container, name = split_url(image_url)
436

    
437
    image = {}
438
    if PLANKTON_PREFIX + 'name' not in meta:
439
        raise ImageNotFound("'%s' is not a Plankton image" % image_url)
440

    
441
    image["id"] = meta["uuid"]
442
    image["location"] = image_url
443
    image["checksum"] = meta["hash"]
444
    image["created_at"] = format_timestamp(meta["created"])
445
    deleted = meta.get("deleted", None)
446
    image["deleted_at"] = format_timestamp(deleted) if deleted else ""
447
    image["updated_at"] = format_timestamp(meta["modified"])
448
    image["size"] = meta["bytes"]
449
    image["store"] = "pithos"
450
    image['owner'] = account
451

    
452
    # Permissions
453
    image["is_public"] = "*" in permissions.get('read', [])
454

    
455
    for key, val in meta.items():
456
        # Get plankton properties
457
        if key.startswith(PLANKTON_PREFIX):
458
            # Remove plankton prefix
459
            key = key.replace(PLANKTON_PREFIX, "")
460
            # Keep only those in plankton meta
461
            if key in PLANKTON_META:
462
                if key == "properties":
463
                    val = json.loads(val)
464
                image[key] = val
465

    
466
    return image