Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (21.1 kB)

1 cda71050 Christos Stavrakakis
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2 cda71050 Christos Stavrakakis
3 c34de90f Giorgos Verigakis
#
4 c34de90f Giorgos Verigakis
# Redistribution and use in source and binary forms, with or
5 c34de90f Giorgos Verigakis
# without modification, are permitted provided that the following
6 c34de90f Giorgos Verigakis
# conditions are met:
7 c34de90f Giorgos Verigakis
#
8 c34de90f Giorgos Verigakis
#   1. Redistributions of source code must retain the above
9 c34de90f Giorgos Verigakis
#      copyright notice, this list of conditions and the following
10 c34de90f Giorgos Verigakis
#      disclaimer.
11 c34de90f Giorgos Verigakis
#
12 c34de90f Giorgos Verigakis
#   2. Redistributions in binary form must reproduce the above
13 c34de90f Giorgos Verigakis
#      copyright notice, this list of conditions and the following
14 c34de90f Giorgos Verigakis
#      disclaimer in the documentation and/or other materials
15 c34de90f Giorgos Verigakis
#      provided with the distribution.
16 c34de90f Giorgos Verigakis
#
17 c34de90f Giorgos Verigakis
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
18 c34de90f Giorgos Verigakis
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 c34de90f Giorgos Verigakis
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
20 c34de90f Giorgos Verigakis
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
21 c34de90f Giorgos Verigakis
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
22 c34de90f Giorgos Verigakis
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
23 c34de90f Giorgos Verigakis
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
24 c34de90f Giorgos Verigakis
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
25 c34de90f Giorgos Verigakis
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
26 c34de90f Giorgos Verigakis
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
27 c34de90f Giorgos Verigakis
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
28 c34de90f Giorgos Verigakis
# POSSIBILITY OF SUCH DAMAGE.
29 c34de90f Giorgos Verigakis
#
30 c34de90f Giorgos Verigakis
# The views and conclusions contained in the software and
31 c34de90f Giorgos Verigakis
# documentation are those of the authors and should not be
32 c34de90f Giorgos Verigakis
# interpreted as representing official policies, either expressed
33 c34de90f Giorgos Verigakis
# or implied, of GRNET S.A.
34 c34de90f Giorgos Verigakis
35 f5afd99b Giorgos Verigakis
"""
36 bfd9f988 Giorgos Verigakis
The Plankton attributes are the following:
37 0a72907b Giorgos Verigakis
  - checksum: the 'hash' meta
38 7bd1d3b5 Giorgos Verigakis
  - container_format: stored as a user meta
39 0a72907b Giorgos Verigakis
  - created_at: the 'modified' meta of the first version
40 0a72907b Giorgos Verigakis
  - deleted_at: the timestamp of the last version
41 7bd1d3b5 Giorgos Verigakis
  - disk_format: stored as a user meta
42 0a72907b Giorgos Verigakis
  - id: the 'uuid' meta
43 f5afd99b Giorgos Verigakis
  - is_public: True if there is a * entry for the read permission
44 0a72907b Giorgos Verigakis
  - location: generated based on the file's path
45 7bd1d3b5 Giorgos Verigakis
  - name: stored as a user meta
46 0a72907b Giorgos Verigakis
  - owner: the file's account
47 7bd1d3b5 Giorgos Verigakis
  - properties: stored as user meta prefixed with PROPERTY_PREFIX
48 0a72907b Giorgos Verigakis
  - size: the 'bytes' meta
49 7bd1d3b5 Giorgos Verigakis
  - status: stored as a system meta
50 7bd1d3b5 Giorgos Verigakis
  - store: is always 'pithos'
51 0a72907b Giorgos Verigakis
  - updated_at: the 'modified' meta
52 f5afd99b Giorgos Verigakis
"""
53 f5afd99b Giorgos Verigakis
54 c34de90f Giorgos Verigakis
import json
55 c23d211a Giorgos Verigakis
import warnings
56 cda71050 Christos Stavrakakis
import logging
57 9393d4ee Kostas Papadimitriou
import os
58 9393d4ee Kostas Papadimitriou
59 3a528c36 Christos Stavrakakis
from time import time, gmtime, strftime
60 cda71050 Christos Stavrakakis
from functools import wraps
61 cda71050 Christos Stavrakakis
from operator import itemgetter
62 c34de90f Giorgos Verigakis
63 cda71050 Christos Stavrakakis
from django.conf import settings
64 9393d4ee Kostas Papadimitriou
from django.utils import importlib
65 cda71050 Christos Stavrakakis
from pithos.backends.base import NotAllowedError, VersionNotExists
66 f4366b6c Stratos Psomadakis
67 469d0997 Georgios D. Tsoukalas
logger = logging.getLogger(__name__)
68 c34de90f Giorgos Verigakis
69 c34de90f Giorgos Verigakis
70 0a72907b Giorgos Verigakis
PLANKTON_DOMAIN = 'plankton'
71 bfd9f988 Giorgos Verigakis
PLANKTON_PREFIX = 'plankton:'
72 7bd1d3b5 Giorgos Verigakis
PROPERTY_PREFIX = 'property:'
73 7bd1d3b5 Giorgos Verigakis
74 d58ea30a Christos Stavrakakis
PLANKTON_META = ('container_format', 'disk_format', 'name',
75 3a528c36 Christos Stavrakakis
                 'status', 'created_at')
76 c34de90f Giorgos Verigakis
77 0efb43cd Christos Stavrakakis
MAX_META_KEY_LENGTH = 128 - len(PLANKTON_DOMAIN) - len(PROPERTY_PREFIX)
78 0efb43cd Christos Stavrakakis
MAX_META_VALUE_LENGTH = 256
79 0efb43cd Christos Stavrakakis
80 7784ab88 Christos Stavrakakis
from pithos.backends.util import PithosBackendPool
81 7784ab88 Christos Stavrakakis
_pithos_backend_pool = \
82 b336e6fa Georgios D. Tsoukalas
    PithosBackendPool(
83 e3f006b0 Christos Stavrakakis
        settings.PITHOS_BACKEND_POOL_SIZE,
84 e407f159 Ilias Tsitsimpis
        astakos_auth_url=settings.ASTAKOS_AUTH_URL,
85 18c4414d Giorgos Korfiatis
        service_token=settings.CYCLADES_SERVICE_TOKEN,
86 b0c95903 Giorgos Korfiatis
        astakosclient_poolsize=settings.CYCLADES_ASTAKOSCLIENT_POOLSIZE,
87 b336e6fa Georgios D. Tsoukalas
        db_connection=settings.BACKEND_DB_CONNECTION,
88 b336e6fa Georgios D. Tsoukalas
        block_path=settings.BACKEND_BLOCK_PATH)
89 7784ab88 Christos Stavrakakis
90 7784ab88 Christos Stavrakakis
91 7784ab88 Christos Stavrakakis
def get_pithos_backend():
92 7784ab88 Christos Stavrakakis
    return _pithos_backend_pool.pool_get()
93 7784ab88 Christos Stavrakakis
94 7784ab88 Christos Stavrakakis
95 cda71050 Christos Stavrakakis
def create_url(account, container, name):
96 cda71050 Christos Stavrakakis
    assert "/" not in account, "Invalid account"
97 cda71050 Christos Stavrakakis
    assert "/" not in container, "Invalid container"
98 cda71050 Christos Stavrakakis
    return "pithos://%s/%s/%s" % (account, container, name)
99 cda71050 Christos Stavrakakis
100 cda71050 Christos Stavrakakis
101 cda71050 Christos Stavrakakis
def split_url(url):
102 cda71050 Christos Stavrakakis
    """Returns (accout, container, object) from a url string"""
103 cda71050 Christos Stavrakakis
    t = url.split('/', 4)
104 59573532 Christos Stavrakakis
    assert t[0] == "pithos:", "Invalid url"
105 cda71050 Christos Stavrakakis
    assert len(t) == 5, "Invalid url"
106 cda71050 Christos Stavrakakis
    return t[2:5]
107 cda71050 Christos Stavrakakis
108 cda71050 Christos Stavrakakis
109 cda71050 Christos Stavrakakis
def format_timestamp(t):
110 cda71050 Christos Stavrakakis
    return strftime('%Y-%m-%d %H:%M:%S', gmtime(t))
111 cda71050 Christos Stavrakakis
112 cda71050 Christos Stavrakakis
113 2db7d9df Christos Stavrakakis
def handle_backend_exceptions(func):
114 2db7d9df Christos Stavrakakis
    @wraps(func)
115 2db7d9df Christos Stavrakakis
    def wrapper(*args, **kwargs):
116 2db7d9df Christos Stavrakakis
        try:
117 2db7d9df Christos Stavrakakis
            return func(*args, **kwargs)
118 f6ff4b40 Christos Stavrakakis
        except NotAllowedError:
119 cda71050 Christos Stavrakakis
            raise Forbidden
120 cda71050 Christos Stavrakakis
        except NameError:
121 cda71050 Christos Stavrakakis
            raise ImageNotFound
122 cda71050 Christos Stavrakakis
        except VersionNotExists:
123 cda71050 Christos Stavrakakis
            raise ImageNotFound
124 2db7d9df Christos Stavrakakis
    return wrapper
125 2db7d9df Christos Stavrakakis
126 2db7d9df Christos Stavrakakis
127 3347a30c Georgios D. Tsoukalas
def commit_on_success(func):
128 3347a30c Georgios D. Tsoukalas
    def wrapper(self, *args, **kwargs):
129 3347a30c Georgios D. Tsoukalas
        backend = self.backend
130 3347a30c Georgios D. Tsoukalas
        backend.pre_exec()
131 3347a30c Georgios D. Tsoukalas
        try:
132 3347a30c Georgios D. Tsoukalas
            ret = func(self, *args, **kwargs)
133 3347a30c Georgios D. Tsoukalas
        except:
134 3347a30c Georgios D. Tsoukalas
            backend.post_exec(False)
135 3347a30c Georgios D. Tsoukalas
            raise
136 3347a30c Georgios D. Tsoukalas
        else:
137 3347a30c Georgios D. Tsoukalas
            backend.post_exec(True)
138 3347a30c Georgios D. Tsoukalas
        return ret
139 3347a30c Georgios D. Tsoukalas
    return wrapper
140 3347a30c Georgios D. Tsoukalas
141 3347a30c Georgios D. Tsoukalas
142 ffab341c Christos Stavrakakis
class ImageBackend(object):
143 f5afd99b Giorgos Verigakis
    """A wrapper arround the pithos backend to simplify image handling."""
144 1e28ba40 Christos Stavrakakis
145 c34de90f Giorgos Verigakis
    def __init__(self, user):
146 c34de90f Giorgos Verigakis
        self.user = user
147 7784ab88 Christos Stavrakakis
148 c23d211a Giorgos Verigakis
        original_filters = warnings.filters
149 c23d211a Giorgos Verigakis
        warnings.simplefilter('ignore')         # Suppress SQLAlchemy warnings
150 7784ab88 Christos Stavrakakis
        self.backend = get_pithos_backend()
151 c23d211a Giorgos Verigakis
        warnings.filters = original_filters     # Restore warnings
152 7784ab88 Christos Stavrakakis
153 cda71050 Christos Stavrakakis
    def close(self):
154 cda71050 Christos Stavrakakis
        """Close PithosBackend(return to pool)"""
155 cda71050 Christos Stavrakakis
        self.backend.close()
156 cda71050 Christos Stavrakakis
157 2db7d9df Christos Stavrakakis
    @handle_backend_exceptions
158 3347a30c Georgios D. Tsoukalas
    @commit_on_success
159 cda71050 Christos Stavrakakis
    def get_image(self, image_uuid):
160 cda71050 Christos Stavrakakis
        """Retrieve information about an image."""
161 cda71050 Christos Stavrakakis
        image_url = self._get_image_url(image_uuid)
162 cda71050 Christos Stavrakakis
        return self._get_image(image_url)
163 1e28ba40 Christos Stavrakakis
164 cda71050 Christos Stavrakakis
    def _get_image_url(self, image_uuid):
165 cda71050 Christos Stavrakakis
        """Get the Pithos url that corresponds to an image UUID."""
166 cda71050 Christos Stavrakakis
        account, container, name = self.backend.get_uuid(self.user, image_uuid)
167 cda71050 Christos Stavrakakis
        return create_url(account, container, name)
168 1e28ba40 Christos Stavrakakis
169 cda71050 Christos Stavrakakis
    def _get_image(self, image_url):
170 cda71050 Christos Stavrakakis
        """Get information about an Image.
171 cda71050 Christos Stavrakakis

172 cda71050 Christos Stavrakakis
        Get all available information about an Image.
173 cda71050 Christos Stavrakakis
        """
174 cda71050 Christos Stavrakakis
        account, container, name = split_url(image_url)
175 cda71050 Christos Stavrakakis
        try:
176 cda71050 Christos Stavrakakis
            meta = self._get_meta(image_url)
177 14c94c48 Christos Stavrakakis
            meta["deleted"] = ""
178 cda71050 Christos Stavrakakis
        except NameError:
179 3a528c36 Christos Stavrakakis
            versions = self.backend.list_versions(self.user, account,
180 3a528c36 Christos Stavrakakis
                                                  container, name)
181 3a528c36 Christos Stavrakakis
            if not versions:
182 3a528c36 Christos Stavrakakis
                raise Exception("Image without versions %s" % image_url)
183 7bd1d3b5 Giorgos Verigakis
            # Object was deleted, use the latest version
184 7bd1d3b5 Giorgos Verigakis
            version, timestamp = versions[-1]
185 cda71050 Christos Stavrakakis
            meta = self._get_meta(image_url, version)
186 14c94c48 Christos Stavrakakis
            meta["deleted"] = timestamp
187 14c94c48 Christos Stavrakakis
188 72224dd6 Christos Stavrakakis
        # XXX: Check that an object is a plankton image! PithosBackend will
189 72224dd6 Christos Stavrakakis
        # return common metadata for an object, even if it has no metadata in
190 72224dd6 Christos Stavrakakis
        # plankton domain. All images must have a name, so we check if a file
191 72224dd6 Christos Stavrakakis
        # is an image by checking if they are having an image name.
192 0a72907b Giorgos Verigakis
        if PLANKTON_PREFIX + 'name' not in meta:
193 72224dd6 Christos Stavrakakis
            raise ImageNotFound
194 1e28ba40 Christos Stavrakakis
195 cda71050 Christos Stavrakakis
        permissions = self._get_permissions(image_url)
196 14c94c48 Christos Stavrakakis
        return image_to_dict(image_url, meta, permissions)
197 1e28ba40 Christos Stavrakakis
198 cda71050 Christos Stavrakakis
    def _get_meta(self, image_url, version=None):
199 cda71050 Christos Stavrakakis
        """Get object's metadata."""
200 cda71050 Christos Stavrakakis
        account, container, name = split_url(image_url)
201 cda71050 Christos Stavrakakis
        return self.backend.get_object_meta(self.user, account, container,
202 cda71050 Christos Stavrakakis
                                            name, PLANKTON_DOMAIN, version)
203 1e28ba40 Christos Stavrakakis
204 cda71050 Christos Stavrakakis
    def _update_meta(self, image_url, meta, replace=False):
205 cda71050 Christos Stavrakakis
        """Update object's metadata."""
206 cda71050 Christos Stavrakakis
        account, container, name = split_url(image_url)
207 1e28ba40 Christos Stavrakakis
208 d58ea30a Christos Stavrakakis
        prefixed = [(PLANKTON_PREFIX + k, v) for k, v in meta.items()
209 d58ea30a Christos Stavrakakis
                    if k in PLANKTON_META or k.startswith(PROPERTY_PREFIX)]
210 d58ea30a Christos Stavrakakis
        prefixed = dict(prefixed)
211 1e28ba40 Christos Stavrakakis
212 0efb43cd Christos Stavrakakis
        for k, v in prefixed.items():
213 0efb43cd Christos Stavrakakis
            if len(k) > 128:
214 0efb43cd Christos Stavrakakis
                raise InvalidMetadata('Metadata keys should be less than %s '
215 0efb43cd Christos Stavrakakis
                                      'characters' % MAX_META_KEY_LENGTH)
216 0efb43cd Christos Stavrakakis
            if len(v) > 256:
217 0efb43cd Christos Stavrakakis
                raise InvalidMetadata('Metadata values should be less than %s '
218 0efb43cd Christos Stavrakakis
                                      'characters.' % MAX_META_VALUE_LENGTH)
219 0efb43cd Christos Stavrakakis
220 cda71050 Christos Stavrakakis
        self.backend.update_object_meta(self.user, account, container, name,
221 cda71050 Christos Stavrakakis
                                        PLANKTON_DOMAIN, prefixed, replace)
222 62d3ea53 Christos Stavrakakis
        logger.debug("User '%s' updated image '%s', meta: '%s'", self.user,
223 62d3ea53 Christos Stavrakakis
                     image_url, prefixed)
224 1e28ba40 Christos Stavrakakis
225 cda71050 Christos Stavrakakis
    def _get_permissions(self, image_url):
226 cda71050 Christos Stavrakakis
        """Get object's permissions."""
227 cda71050 Christos Stavrakakis
        account, container, name = split_url(image_url)
228 cda71050 Christos Stavrakakis
        _a, path, permissions = \
229 cda71050 Christos Stavrakakis
            self.backend.get_object_permissions(self.user, account, container,
230 cda71050 Christos Stavrakakis
                                                name)
231 1e28ba40 Christos Stavrakakis
232 69cdf66c Nanakos Chrysostomos
        if path is None and permissions != {}:
233 69cdf66c Nanakos Chrysostomos
            logger.warning("Image '%s' got permissions '%s' from 'None' path.",
234 69cdf66c Nanakos Chrysostomos
                           image_url, permissions)
235 69cdf66c Nanakos Chrysostomos
            raise Exception("Database Inconsistency Error:"
236 69cdf66c Nanakos Chrysostomos
                            " Image '%s' got permissions from 'None' path." %
237 69cdf66c Nanakos Chrysostomos
                            image_url)
238 1e28ba40 Christos Stavrakakis
239 cda71050 Christos Stavrakakis
        return permissions
240 1e28ba40 Christos Stavrakakis
241 cda71050 Christos Stavrakakis
    def _update_permissions(self, image_url, permissions):
242 cda71050 Christos Stavrakakis
        """Update object's permissions."""
243 cda71050 Christos Stavrakakis
        account, container, name = split_url(image_url)
244 cda71050 Christos Stavrakakis
        self.backend.update_object_permissions(self.user, account, container,
245 cda71050 Christos Stavrakakis
                                               name, permissions)
246 62d3ea53 Christos Stavrakakis
        logger.debug("User '%s' updated image '%s', permissions: '%s'",
247 62d3ea53 Christos Stavrakakis
                     self.user, image_url, permissions)
248 1e28ba40 Christos Stavrakakis
249 2db7d9df Christos Stavrakakis
    @handle_backend_exceptions
250 3347a30c Georgios D. Tsoukalas
    @commit_on_success
251 cda71050 Christos Stavrakakis
    def unregister(self, image_uuid):
252 cda71050 Christos Stavrakakis
        """Unregister an image.
253 1e28ba40 Christos Stavrakakis

254 cda71050 Christos Stavrakakis
        Unregister an image, by removing all metadata from the Pithos
255 cda71050 Christos Stavrakakis
        file that exist in the PLANKTON_DOMAIN.
256 1e28ba40 Christos Stavrakakis

257 cda71050 Christos Stavrakakis
        """
258 cda71050 Christos Stavrakakis
        image_url = self._get_image_url(image_uuid)
259 cda71050 Christos Stavrakakis
        self._get_image(image_url)  # Assert that it is an image
260 cda71050 Christos Stavrakakis
        # Unregister the image by removing all metadata from domain
261 cda71050 Christos Stavrakakis
        # 'PLANKTON_DOMAIN'
262 22b5ac0b Christos Stavrakakis
        meta = {}
263 22b5ac0b Christos Stavrakakis
        self._update_meta(image_url, meta, True)
264 62d3ea53 Christos Stavrakakis
        logger.debug("User '%s' deleted image '%s'", self.user, image_url)
265 1e28ba40 Christos Stavrakakis
266 2db7d9df Christos Stavrakakis
    @handle_backend_exceptions
267 3347a30c Georgios D. Tsoukalas
    @commit_on_success
268 cda71050 Christos Stavrakakis
    def add_user(self, image_uuid, add_user):
269 cda71050 Christos Stavrakakis
        """Add a user as an image member.
270 cda71050 Christos Stavrakakis

271 cda71050 Christos Stavrakakis
        Update read permissions of Pithos file, to include the specified user.
272 cda71050 Christos Stavrakakis

273 cda71050 Christos Stavrakakis
        """
274 cda71050 Christos Stavrakakis
        image_url = self._get_image_url(image_uuid)
275 cda71050 Christos Stavrakakis
        self._get_image(image_url)  # Assert that it is an image
276 cda71050 Christos Stavrakakis
        permissions = self._get_permissions(image_url)
277 cda71050 Christos Stavrakakis
        read = set(permissions.get("read", []))
278 cda71050 Christos Stavrakakis
        assert(isinstance(add_user, (str, unicode)))
279 cda71050 Christos Stavrakakis
        read.add(add_user)
280 cda71050 Christos Stavrakakis
        permissions["read"] = list(read)
281 cda71050 Christos Stavrakakis
        self._update_permissions(image_url, permissions)
282 1e28ba40 Christos Stavrakakis
283 2db7d9df Christos Stavrakakis
    @handle_backend_exceptions
284 3347a30c Georgios D. Tsoukalas
    @commit_on_success
285 cda71050 Christos Stavrakakis
    def remove_user(self, image_uuid, remove_user):
286 cda71050 Christos Stavrakakis
        """Remove the user from image members.
287 1e28ba40 Christos Stavrakakis

288 cda71050 Christos Stavrakakis
        Remove the specified user from the read permissions of the Pithos file.
289 cda71050 Christos Stavrakakis

290 cda71050 Christos Stavrakakis
        """
291 cda71050 Christos Stavrakakis
        image_url = self._get_image_url(image_uuid)
292 cda71050 Christos Stavrakakis
        self._get_image(image_url)  # Assert that it is an image
293 cda71050 Christos Stavrakakis
        permissions = self._get_permissions(image_url)
294 cda71050 Christos Stavrakakis
        read = set(permissions.get("read", []))
295 cda71050 Christos Stavrakakis
        assert(isinstance(remove_user, (str, unicode)))
296 cda71050 Christos Stavrakakis
        try:
297 cda71050 Christos Stavrakakis
            read.remove(remove_user)
298 cda71050 Christos Stavrakakis
        except ValueError:
299 cda71050 Christos Stavrakakis
            return  # TODO: User did not have access
300 cda71050 Christos Stavrakakis
        permissions["read"] = list(read)
301 cda71050 Christos Stavrakakis
        self._update_permissions(image_url, permissions)
302 1e28ba40 Christos Stavrakakis
303 2db7d9df Christos Stavrakakis
    @handle_backend_exceptions
304 3347a30c Georgios D. Tsoukalas
    @commit_on_success
305 cda71050 Christos Stavrakakis
    def replace_users(self, image_uuid, replace_users):
306 cda71050 Christos Stavrakakis
        """Replace image members.
307 f13aab5d Christos Stavrakakis

308 cda71050 Christos Stavrakakis
        Replace the read permissions of the Pithos files with the specified
309 cda71050 Christos Stavrakakis
        users. If image is specified as public, we must preserve * permission.
310 f13aab5d Christos Stavrakakis

311 f13aab5d Christos Stavrakakis
        """
312 cda71050 Christos Stavrakakis
        image_url = self._get_image_url(image_uuid)
313 cda71050 Christos Stavrakakis
        image = self._get_image(image_url)
314 cda71050 Christos Stavrakakis
        permissions = self._get_permissions(image_url)
315 cda71050 Christos Stavrakakis
        assert(isinstance(replace_users, list))
316 cda71050 Christos Stavrakakis
        permissions["read"] = replace_users
317 cda71050 Christos Stavrakakis
        if image.get("is_public", False):
318 cda71050 Christos Stavrakakis
            permissions["read"].append("*")
319 cda71050 Christos Stavrakakis
        self._update_permissions(image_url, permissions)
320 1e28ba40 Christos Stavrakakis
321 2db7d9df Christos Stavrakakis
    @handle_backend_exceptions
322 3347a30c Georgios D. Tsoukalas
    @commit_on_success
323 cda71050 Christos Stavrakakis
    def list_users(self, image_uuid):
324 cda71050 Christos Stavrakakis
        """List the image members.
325 cda71050 Christos Stavrakakis

326 cda71050 Christos Stavrakakis
        List the image members, by listing all users that have read permission
327 cda71050 Christos Stavrakakis
        to the corresponding Pithos file.
328 cda71050 Christos Stavrakakis

329 cda71050 Christos Stavrakakis
        """
330 cda71050 Christos Stavrakakis
        image_url = self._get_image_url(image_uuid)
331 cda71050 Christos Stavrakakis
        self._get_image(image_url)  # Assert that it is an image
332 cda71050 Christos Stavrakakis
        permissions = self._get_permissions(image_url)
333 cda71050 Christos Stavrakakis
        return [user for user in permissions.get('read', []) if user != '*']
334 1e28ba40 Christos Stavrakakis
335 2db7d9df Christos Stavrakakis
    @handle_backend_exceptions
336 3347a30c Georgios D. Tsoukalas
    @commit_on_success
337 cda71050 Christos Stavrakakis
    def update_metadata(self, image_uuid, metadata):
338 cda71050 Christos Stavrakakis
        """Update Image metadata."""
339 cda71050 Christos Stavrakakis
        image_url = self._get_image_url(image_uuid)
340 cda71050 Christos Stavrakakis
        self._get_image(image_url)  # Assert that it is an image
341 1e28ba40 Christos Stavrakakis
342 d58ea30a Christos Stavrakakis
        # 'is_public' metadata is translated in proper file permissions
343 cda71050 Christos Stavrakakis
        is_public = metadata.pop("is_public", None)
344 cda71050 Christos Stavrakakis
        if is_public is not None:
345 cda71050 Christos Stavrakakis
            permissions = self._get_permissions(image_url)
346 cda71050 Christos Stavrakakis
            read = set(permissions.get("read", []))
347 cda71050 Christos Stavrakakis
            if is_public:
348 cda71050 Christos Stavrakakis
                read.add("*")
349 cda71050 Christos Stavrakakis
            else:
350 cda71050 Christos Stavrakakis
                read.discard("*")
351 cda71050 Christos Stavrakakis
            permissions["read"] = list(read)
352 cda71050 Christos Stavrakakis
            self._update_permissions(image_url, permissions)
353 d58ea30a Christos Stavrakakis
354 d58ea30a Christos Stavrakakis
        # Extract the properties dictionary from metadata, and store each
355 d58ea30a Christos Stavrakakis
        # property as a separeted, prefixed metadata
356 d58ea30a Christos Stavrakakis
        properties = metadata.pop("properties", {})
357 d58ea30a Christos Stavrakakis
        meta = dict([(PROPERTY_PREFIX + k, v) for k, v in properties.items()])
358 d58ea30a Christos Stavrakakis
        # Also add the following metadata
359 cda71050 Christos Stavrakakis
        meta.update(**metadata)
360 cda71050 Christos Stavrakakis
361 cda71050 Christos Stavrakakis
        self._update_meta(image_url, meta)
362 3347a30c Georgios D. Tsoukalas
        image_url = self._get_image_url(image_uuid)
363 3347a30c Georgios D. Tsoukalas
        return self._get_image(image_url)
364 1e28ba40 Christos Stavrakakis
365 2db7d9df Christos Stavrakakis
    @handle_backend_exceptions
366 3347a30c Georgios D. Tsoukalas
    @commit_on_success
367 cda71050 Christos Stavrakakis
    def register(self, name, image_url, metadata):
368 cda71050 Christos Stavrakakis
        # Validate that metadata are allowed
369 cda71050 Christos Stavrakakis
        if "id" in metadata:
370 cda71050 Christos Stavrakakis
            raise ValueError("Passing an ID is not supported")
371 cda71050 Christos Stavrakakis
        store = metadata.pop("store", "pithos")
372 cda71050 Christos Stavrakakis
        if store != "pithos":
373 cda71050 Christos Stavrakakis
            raise ValueError("Invalid store '%s'. Only 'pithos' store is"
374 cda71050 Christos Stavrakakis
                             "supported" % store)
375 cda71050 Christos Stavrakakis
        disk_format = metadata.setdefault("disk_format",
376 cda71050 Christos Stavrakakis
                                          settings.DEFAULT_DISK_FORMAT)
377 cda71050 Christos Stavrakakis
        if disk_format not in settings.ALLOWED_DISK_FORMATS:
378 cda71050 Christos Stavrakakis
            raise ValueError("Invalid disk format '%s'" % disk_format)
379 cda71050 Christos Stavrakakis
        container_format =\
380 cda71050 Christos Stavrakakis
            metadata.setdefault("container_format",
381 cda71050 Christos Stavrakakis
                                settings.DEFAULT_CONTAINER_FORMAT)
382 cda71050 Christos Stavrakakis
        if container_format not in settings.ALLOWED_CONTAINER_FORMATS:
383 cda71050 Christos Stavrakakis
            raise ValueError("Invalid container format '%s'" %
384 cda71050 Christos Stavrakakis
                             container_format)
385 cda71050 Christos Stavrakakis
386 cda71050 Christos Stavrakakis
        # Validate that 'size' and 'checksum' are valid
387 cda71050 Christos Stavrakakis
        account, container, object = split_url(image_url)
388 cda71050 Christos Stavrakakis
389 cda71050 Christos Stavrakakis
        meta = self._get_meta(image_url)
390 cda71050 Christos Stavrakakis
391 cda71050 Christos Stavrakakis
        size = int(metadata.pop('size', meta['bytes']))
392 cda71050 Christos Stavrakakis
        if size != meta['bytes']:
393 cda71050 Christos Stavrakakis
            raise ValueError("Invalid size")
394 cda71050 Christos Stavrakakis
395 cda71050 Christos Stavrakakis
        checksum = metadata.pop('checksum', meta['hash'])
396 cda71050 Christos Stavrakakis
        if checksum != meta['hash']:
397 cda71050 Christos Stavrakakis
            raise ValueError("Invalid checksum")
398 cda71050 Christos Stavrakakis
399 cda71050 Christos Stavrakakis
        # Fix permissions
400 cda71050 Christos Stavrakakis
        is_public = metadata.pop('is_public', False)
401 cda71050 Christos Stavrakakis
        if is_public:
402 cda71050 Christos Stavrakakis
            permissions = {'read': ['*']}
403 cda71050 Christos Stavrakakis
        else:
404 cda71050 Christos Stavrakakis
            permissions = {'read': [self.user]}
405 cda71050 Christos Stavrakakis
406 d58ea30a Christos Stavrakakis
        # Extract the properties dictionary from metadata, and store each
407 d58ea30a Christos Stavrakakis
        # property as a separeted, prefixed metadata
408 d58ea30a Christos Stavrakakis
        properties = metadata.pop("properties", {})
409 d58ea30a Christos Stavrakakis
        meta = dict([(PROPERTY_PREFIX + k, v) for k, v in properties.items()])
410 3a528c36 Christos Stavrakakis
        # Add creation(register) timestamp as a metadata, to avoid extra
411 3a528c36 Christos Stavrakakis
        # queries when retrieving the list of images.
412 3a528c36 Christos Stavrakakis
        meta['created_at'] = time()
413 d58ea30a Christos Stavrakakis
        # Update rest metadata
414 cda71050 Christos Stavrakakis
        meta.update(name=name, status='available', **metadata)
415 cda71050 Christos Stavrakakis
416 cda71050 Christos Stavrakakis
        # Do the actualy update in the Pithos backend
417 cda71050 Christos Stavrakakis
        self._update_meta(image_url, meta)
418 cda71050 Christos Stavrakakis
        self._update_permissions(image_url, permissions)
419 62d3ea53 Christos Stavrakakis
        logger.debug("User '%s' created image '%s'('%s')", self.user,
420 62d3ea53 Christos Stavrakakis
                     image_url, name)
421 cda71050 Christos Stavrakakis
        return self._get_image(image_url)
422 cda71050 Christos Stavrakakis
423 14c94c48 Christos Stavrakakis
    def _list_images(self, user=None, filters=None, params=None):
424 d19e8f77 Giorgos Verigakis
        filters = filters or {}
425 1e28ba40 Christos Stavrakakis
426 14c94c48 Christos Stavrakakis
        # TODO: Use filters
427 14c94c48 Christos Stavrakakis
        # # Fix keys
428 14c94c48 Christos Stavrakakis
        # keys = [PLANKTON_PREFIX + 'name']
429 14c94c48 Christos Stavrakakis
        # size_range = (None, None)
430 14c94c48 Christos Stavrakakis
        # for key, val in filters.items():
431 14c94c48 Christos Stavrakakis
        #     if key == 'size_min':
432 14c94c48 Christos Stavrakakis
        #         size_range = (val, size_range[1])
433 14c94c48 Christos Stavrakakis
        #     elif key == 'size_max':
434 14c94c48 Christos Stavrakakis
        #         size_range = (size_range[0], val)
435 14c94c48 Christos Stavrakakis
        #     else:
436 14c94c48 Christos Stavrakakis
        #         keys.append('%s = %s' % (PLANKTON_PREFIX + key, val))
437 14c94c48 Christos Stavrakakis
        _images = self.backend.get_domain_objects(domain=PLANKTON_DOMAIN,
438 14c94c48 Christos Stavrakakis
                                                  user=user)
439 14c94c48 Christos Stavrakakis
440 14c94c48 Christos Stavrakakis
        images = []
441 14c94c48 Christos Stavrakakis
        for (location, meta, permissions) in _images:
442 14c94c48 Christos Stavrakakis
            image_url = "pithos://" + location
443 14c94c48 Christos Stavrakakis
            meta["modified"] = meta["version_timestamp"]
444 14c94c48 Christos Stavrakakis
            images.append(image_to_dict(image_url, meta, permissions))
445 14c94c48 Christos Stavrakakis
446 14c94c48 Christos Stavrakakis
        if params is None:
447 14c94c48 Christos Stavrakakis
            params = {}
448 aed77afe Christos Stavrakakis
        key = itemgetter(params.get('sort_key', 'created_at'))
449 aed77afe Christos Stavrakakis
        reverse = params.get('sort_dir', 'desc') == 'desc'
450 aed77afe Christos Stavrakakis
        images.sort(key=key, reverse=reverse)
451 aed77afe Christos Stavrakakis
        return images
452 1e28ba40 Christos Stavrakakis
453 3347a30c Georgios D. Tsoukalas
    @commit_on_success
454 14c94c48 Christos Stavrakakis
    def list_images(self, filters=None, params=None):
455 14c94c48 Christos Stavrakakis
        return self._list_images(user=self.user, filters=filters,
456 14c94c48 Christos Stavrakakis
                                 params=params)
457 14c94c48 Christos Stavrakakis
458 3347a30c Georgios D. Tsoukalas
    @commit_on_success
459 14c94c48 Christos Stavrakakis
    def list_shared_images(self, member, filters=None, params=None):
460 14c94c48 Christos Stavrakakis
        images = self._list_images(user=self.user, filters=filters,
461 14c94c48 Christos Stavrakakis
                                   params=params)
462 14c94c48 Christos Stavrakakis
        is_shared = lambda img: not img["is_public"] and img["owner"] == member
463 14c94c48 Christos Stavrakakis
        return filter(is_shared, images)
464 14c94c48 Christos Stavrakakis
465 3347a30c Georgios D. Tsoukalas
    @commit_on_success
466 14c94c48 Christos Stavrakakis
    def list_public_images(self, filters=None, params=None):
467 14c94c48 Christos Stavrakakis
        images = self._list_images(user=None, filters=filters, params=params)
468 14c94c48 Christos Stavrakakis
        return filter(lambda img: img["is_public"], images)
469 1e28ba40 Christos Stavrakakis
470 1e28ba40 Christos Stavrakakis
471 cda71050 Christos Stavrakakis
class ImageBackendError(Exception):
472 cda71050 Christos Stavrakakis
    pass
473 1e28ba40 Christos Stavrakakis
474 1e28ba40 Christos Stavrakakis
475 cda71050 Christos Stavrakakis
class ImageNotFound(ImageBackendError):
476 cda71050 Christos Stavrakakis
    pass
477 f13aab5d Christos Stavrakakis
478 f13aab5d Christos Stavrakakis
479 cda71050 Christos Stavrakakis
class Forbidden(ImageBackendError):
480 cda71050 Christos Stavrakakis
    pass
481 14c94c48 Christos Stavrakakis
482 14c94c48 Christos Stavrakakis
483 0efb43cd Christos Stavrakakis
class InvalidMetadata(ImageBackendError):
484 0efb43cd Christos Stavrakakis
    pass
485 0efb43cd Christos Stavrakakis
486 0efb43cd Christos Stavrakakis
487 14c94c48 Christos Stavrakakis
def image_to_dict(image_url, meta, permissions):
488 14c94c48 Christos Stavrakakis
    """Render an image to a dictionary"""
489 14c94c48 Christos Stavrakakis
    account, container, name = split_url(image_url)
490 14c94c48 Christos Stavrakakis
491 14c94c48 Christos Stavrakakis
    image = {}
492 14c94c48 Christos Stavrakakis
    if PLANKTON_PREFIX + 'name' not in meta:
493 125c682c Christos Stavrakakis
        logger.warning("Image without Plankton name!! url %s meta %s",
494 125c682c Christos Stavrakakis
                       image_url, meta)
495 125c682c Christos Stavrakakis
        image[PLANKTON_PREFIX + "name"] = ""
496 14c94c48 Christos Stavrakakis
497 14c94c48 Christos Stavrakakis
    image["id"] = meta["uuid"]
498 14c94c48 Christos Stavrakakis
    image["location"] = image_url
499 14c94c48 Christos Stavrakakis
    image["checksum"] = meta["hash"]
500 3a528c36 Christos Stavrakakis
    created = meta.get("created_at", meta["modified"])
501 3a528c36 Christos Stavrakakis
    image["created_at"] = format_timestamp(created)
502 14c94c48 Christos Stavrakakis
    deleted = meta.get("deleted", None)
503 14c94c48 Christos Stavrakakis
    image["deleted_at"] = format_timestamp(deleted) if deleted else ""
504 14c94c48 Christos Stavrakakis
    image["updated_at"] = format_timestamp(meta["modified"])
505 14c94c48 Christos Stavrakakis
    image["size"] = meta["bytes"]
506 14c94c48 Christos Stavrakakis
    image["store"] = "pithos"
507 14c94c48 Christos Stavrakakis
    image['owner'] = account
508 14c94c48 Christos Stavrakakis
509 14c94c48 Christos Stavrakakis
    # Permissions
510 14c94c48 Christos Stavrakakis
    image["is_public"] = "*" in permissions.get('read', [])
511 14c94c48 Christos Stavrakakis
512 d58ea30a Christos Stavrakakis
    properties = {}
513 14c94c48 Christos Stavrakakis
    for key, val in meta.items():
514 14c94c48 Christos Stavrakakis
        # Get plankton properties
515 14c94c48 Christos Stavrakakis
        if key.startswith(PLANKTON_PREFIX):
516 14c94c48 Christos Stavrakakis
            # Remove plankton prefix
517 14c94c48 Christos Stavrakakis
            key = key.replace(PLANKTON_PREFIX, "")
518 14c94c48 Christos Stavrakakis
            # Keep only those in plankton meta
519 14c94c48 Christos Stavrakakis
            if key in PLANKTON_META:
520 d58ea30a Christos Stavrakakis
                if key != "created_at":
521 3a528c36 Christos Stavrakakis
                    # created timestamp is return in 'created_at' field
522 3a528c36 Christos Stavrakakis
                    image[key] = val
523 d58ea30a Christos Stavrakakis
            elif key.startswith(PROPERTY_PREFIX):
524 d58ea30a Christos Stavrakakis
                key = key.replace(PROPERTY_PREFIX, "")
525 d58ea30a Christos Stavrakakis
                properties[key] = val
526 d58ea30a Christos Stavrakakis
    image["properties"] = properties
527 14c94c48 Christos Stavrakakis
528 14c94c48 Christos Stavrakakis
    return image
529 9393d4ee Kostas Papadimitriou
530 9393d4ee Kostas Papadimitriou
531 9393d4ee Kostas Papadimitriou
class JSONFileBackend(object):
532 9393d4ee Kostas Papadimitriou
    """
533 9393d4ee Kostas Papadimitriou
    A dummy image backend that loads available images from a file with json
534 9393d4ee Kostas Papadimitriou
    formatted content.
535 9393d4ee Kostas Papadimitriou

536 9393d4ee Kostas Papadimitriou
    usage:
537 9393d4ee Kostas Papadimitriou
        PLANKTON_BACKEND_MODULE = 'synnefo.plankton.backend.JSONFileBackend'
538 9393d4ee Kostas Papadimitriou
        PLANKTON_IMAGES_JSON_BACKEND_FILE = '/tmp/images.json'
539 9393d4ee Kostas Papadimitriou

540 9393d4ee Kostas Papadimitriou
        # loading images from an existing plankton service
541 9393d4ee Kostas Papadimitriou
        $ curl -H "X-Auth-Token: <MYTOKEN>" \
542 9393d4ee Kostas Papadimitriou
                https://cyclades.synnefo.org/plankton/images/detail | \
543 9393d4ee Kostas Papadimitriou
                python -m json.tool > /tmp/images.json
544 9393d4ee Kostas Papadimitriou
    """
545 9393d4ee Kostas Papadimitriou
    def __init__(self, userid):
546 9393d4ee Kostas Papadimitriou
        self.images_file = getattr(settings,
547 9393d4ee Kostas Papadimitriou
                                   'PLANKTON_IMAGES_JSON_BACKEND_FILE', '')
548 9393d4ee Kostas Papadimitriou
        if not os.path.exists(self.images_file):
549 9393d4ee Kostas Papadimitriou
            raise Exception("Invalid plankgon images json backend file: %s",
550 9393d4ee Kostas Papadimitriou
                            self.images_file)
551 9393d4ee Kostas Papadimitriou
        fp = file(self.images_file)
552 9393d4ee Kostas Papadimitriou
        self.images = json.load(fp)
553 9393d4ee Kostas Papadimitriou
        fp.close()
554 9393d4ee Kostas Papadimitriou
555 9393d4ee Kostas Papadimitriou
    def iter(self, *args, **kwargs):
556 9393d4ee Kostas Papadimitriou
        return self.images.__iter__()
557 9393d4ee Kostas Papadimitriou
558 9393d4ee Kostas Papadimitriou
    def list_images(self, *args, **kwargs):
559 9393d4ee Kostas Papadimitriou
        return self.images
560 9393d4ee Kostas Papadimitriou
561 9393d4ee Kostas Papadimitriou
    def get_image(self, image_uuid):
562 9393d4ee Kostas Papadimitriou
        try:
563 9393d4ee Kostas Papadimitriou
            return filter(lambda i: i['id'] == image_uuid, self.images)[0]
564 9393d4ee Kostas Papadimitriou
        except IndexError:
565 9393d4ee Kostas Papadimitriou
            raise Exception("Unknown image uuid: %s" % image_uuid)
566 9393d4ee Kostas Papadimitriou
567 9393d4ee Kostas Papadimitriou
    def close(self):
568 9393d4ee Kostas Papadimitriou
        pass
569 9393d4ee Kostas Papadimitriou
570 9393d4ee Kostas Papadimitriou
571 9393d4ee Kostas Papadimitriou
def get_backend():
572 9393d4ee Kostas Papadimitriou
    backend_module = getattr(settings, 'PLANKTON_BACKEND_MODULE', None)
573 9393d4ee Kostas Papadimitriou
    if not backend_module:
574 9393d4ee Kostas Papadimitriou
        # no setting set
575 9393d4ee Kostas Papadimitriou
        return ImageBackend
576 9393d4ee Kostas Papadimitriou
577 9393d4ee Kostas Papadimitriou
    parts = backend_module.split(".")
578 9393d4ee Kostas Papadimitriou
    module = ".".join(parts[:-1])
579 9393d4ee Kostas Papadimitriou
    cls = parts[-1]
580 9393d4ee Kostas Papadimitriou
    try:
581 9393d4ee Kostas Papadimitriou
        return getattr(importlib.import_module(module), cls)
582 9393d4ee Kostas Papadimitriou
    except (ImportError, AttributeError), e:
583 9393d4ee Kostas Papadimitriou
        raise ImportError("Cannot import plankton module: %s (%s)" %
584 9393d4ee Kostas Papadimitriou
                          (backend_module, e.message))