Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (17 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_BASE_URL,
80
        service_token=settings.CYCLADES_SERVICE_TOKEN,
81
        astakosclient_poolsize=settings.CYCLADES_ASTAKOSCLIENT_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
            logger.warning("Image without Plankton name! url %s meta %s",
171
                           image_url, meta)
172
            meta[PLANKTON_PREFIX + "name"] = ""
173

    
174
        permissions = self._get_permissions(image_url)
175
        return image_to_dict(image_url, meta, permissions)
176

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

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

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

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

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

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

    
210
        return permissions
211

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

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

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

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

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

240
        Update read permissions of Pithos file, to include the specified user.
241

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

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

256
        Remove the specified user from the read permissions of the Pithos file.
257

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

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

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

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

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

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

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

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

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

    
321
        self._update_meta(image_url, meta)
322
        return self.get_image(image_uuid)
323

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

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

    
347
        meta = self._get_meta(image_url)
348

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

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

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

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

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

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

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

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

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

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

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

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

    
422

    
423
class ImageBackendError(Exception):
424
    pass
425

    
426

    
427
class ImageNotFound(ImageBackendError):
428
    pass
429

    
430

    
431
class Forbidden(ImageBackendError):
432
    pass
433

    
434

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

    
439
    image = {}
440
    if PLANKTON_PREFIX + 'name' not in meta:
441
        logger.warning("Image without Plankton name!! url %s meta %s",
442
                       image_url, meta)
443
        image[PLANKTON_PREFIX + "name"] = ""
444

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

    
456
    # Permissions
457
    image["is_public"] = "*" in permissions.get('read', [])
458

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

    
470
    return image