Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (22.4 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 78fa9134 Christos Stavrakakis
from synnefo.util.text import uenc
67 78fa9134 Christos Stavrakakis
68 f4366b6c Stratos Psomadakis
69 469d0997 Georgios D. Tsoukalas
logger = logging.getLogger(__name__)
70 c34de90f Giorgos Verigakis
71 c34de90f Giorgos Verigakis
72 0a72907b Giorgos Verigakis
PLANKTON_DOMAIN = 'plankton'
73 bfd9f988 Giorgos Verigakis
PLANKTON_PREFIX = 'plankton:'
74 7bd1d3b5 Giorgos Verigakis
PROPERTY_PREFIX = 'property:'
75 7bd1d3b5 Giorgos Verigakis
76 d58ea30a Christos Stavrakakis
PLANKTON_META = ('container_format', 'disk_format', 'name',
77 c449760f Christos Stavrakakis
                 'status', 'created_at', 'volume_id', 'description')
78 c34de90f Giorgos Verigakis
79 0efb43cd Christos Stavrakakis
MAX_META_KEY_LENGTH = 128 - len(PLANKTON_DOMAIN) - len(PROPERTY_PREFIX)
80 0efb43cd Christos Stavrakakis
MAX_META_VALUE_LENGTH = 256
81 0efb43cd Christos Stavrakakis
82 862afc75 Christos Stavrakakis
83 7784ab88 Christos Stavrakakis
from pithos.backends.util import PithosBackendPool
84 7784ab88 Christos Stavrakakis
_pithos_backend_pool = \
85 b336e6fa Georgios D. Tsoukalas
    PithosBackendPool(
86 e3f006b0 Christos Stavrakakis
        settings.PITHOS_BACKEND_POOL_SIZE,
87 e407f159 Ilias Tsitsimpis
        astakos_auth_url=settings.ASTAKOS_AUTH_URL,
88 18c4414d Giorgos Korfiatis
        service_token=settings.CYCLADES_SERVICE_TOKEN,
89 b0c95903 Giorgos Korfiatis
        astakosclient_poolsize=settings.CYCLADES_ASTAKOSCLIENT_POOLSIZE,
90 b336e6fa Georgios D. Tsoukalas
        db_connection=settings.BACKEND_DB_CONNECTION,
91 b336e6fa Georgios D. Tsoukalas
        block_path=settings.BACKEND_BLOCK_PATH)
92 7784ab88 Christos Stavrakakis
93 7784ab88 Christos Stavrakakis
94 7784ab88 Christos Stavrakakis
def get_pithos_backend():
95 7784ab88 Christos Stavrakakis
    return _pithos_backend_pool.pool_get()
96 7784ab88 Christos Stavrakakis
97 7784ab88 Christos Stavrakakis
98 cda71050 Christos Stavrakakis
def create_url(account, container, name):
99 cda71050 Christos Stavrakakis
    assert "/" not in account, "Invalid account"
100 cda71050 Christos Stavrakakis
    assert "/" not in container, "Invalid container"
101 cda71050 Christos Stavrakakis
    return "pithos://%s/%s/%s" % (account, container, name)
102 cda71050 Christos Stavrakakis
103 cda71050 Christos Stavrakakis
104 cda71050 Christos Stavrakakis
def split_url(url):
105 cda71050 Christos Stavrakakis
    """Returns (accout, container, object) from a url string"""
106 78fa9134 Christos Stavrakakis
    try:
107 78fa9134 Christos Stavrakakis
        assert(isinstance(url, basestring))
108 78fa9134 Christos Stavrakakis
        t = url.split('/', 4)
109 78fa9134 Christos Stavrakakis
        assert t[0] == "pithos:", "Invalid url"
110 78fa9134 Christos Stavrakakis
        assert len(t) == 5, "Invalid url"
111 78fa9134 Christos Stavrakakis
        return t[2:5]
112 78fa9134 Christos Stavrakakis
    except AssertionError:
113 78fa9134 Christos Stavrakakis
        raise InvalidLocation("Invalid location '%s" % url)
114 cda71050 Christos Stavrakakis
115 cda71050 Christos Stavrakakis
116 cda71050 Christos Stavrakakis
def format_timestamp(t):
117 cda71050 Christos Stavrakakis
    return strftime('%Y-%m-%d %H:%M:%S', gmtime(t))
118 cda71050 Christos Stavrakakis
119 cda71050 Christos Stavrakakis
120 2db7d9df Christos Stavrakakis
def handle_backend_exceptions(func):
121 2db7d9df Christos Stavrakakis
    @wraps(func)
122 2db7d9df Christos Stavrakakis
    def wrapper(*args, **kwargs):
123 2db7d9df Christos Stavrakakis
        try:
124 2db7d9df Christos Stavrakakis
            return func(*args, **kwargs)
125 f6ff4b40 Christos Stavrakakis
        except NotAllowedError:
126 cda71050 Christos Stavrakakis
            raise Forbidden
127 cda71050 Christos Stavrakakis
        except NameError:
128 cda71050 Christos Stavrakakis
            raise ImageNotFound
129 cda71050 Christos Stavrakakis
        except VersionNotExists:
130 cda71050 Christos Stavrakakis
            raise ImageNotFound
131 2db7d9df Christos Stavrakakis
    return wrapper
132 2db7d9df Christos Stavrakakis
133 2db7d9df Christos Stavrakakis
134 3347a30c Georgios D. Tsoukalas
def commit_on_success(func):
135 3347a30c Georgios D. Tsoukalas
    def wrapper(self, *args, **kwargs):
136 3347a30c Georgios D. Tsoukalas
        backend = self.backend
137 3347a30c Georgios D. Tsoukalas
        backend.pre_exec()
138 3347a30c Georgios D. Tsoukalas
        try:
139 3347a30c Georgios D. Tsoukalas
            ret = func(self, *args, **kwargs)
140 3347a30c Georgios D. Tsoukalas
        except:
141 3347a30c Georgios D. Tsoukalas
            backend.post_exec(False)
142 3347a30c Georgios D. Tsoukalas
            raise
143 3347a30c Georgios D. Tsoukalas
        else:
144 3347a30c Georgios D. Tsoukalas
            backend.post_exec(True)
145 3347a30c Georgios D. Tsoukalas
        return ret
146 3347a30c Georgios D. Tsoukalas
    return wrapper
147 3347a30c Georgios D. Tsoukalas
148 3347a30c Georgios D. Tsoukalas
149 ffab341c Christos Stavrakakis
class ImageBackend(object):
150 f5afd99b Giorgos Verigakis
    """A wrapper arround the pithos backend to simplify image handling."""
151 1e28ba40 Christos Stavrakakis
152 c34de90f Giorgos Verigakis
    def __init__(self, user):
153 c34de90f Giorgos Verigakis
        self.user = user
154 7784ab88 Christos Stavrakakis
155 c23d211a Giorgos Verigakis
        original_filters = warnings.filters
156 c23d211a Giorgos Verigakis
        warnings.simplefilter('ignore')         # Suppress SQLAlchemy warnings
157 7784ab88 Christos Stavrakakis
        self.backend = get_pithos_backend()
158 c23d211a Giorgos Verigakis
        warnings.filters = original_filters     # Restore warnings
159 7784ab88 Christos Stavrakakis
160 cda71050 Christos Stavrakakis
    def close(self):
161 cda71050 Christos Stavrakakis
        """Close PithosBackend(return to pool)"""
162 cda71050 Christos Stavrakakis
        self.backend.close()
163 cda71050 Christos Stavrakakis
164 2db7d9df Christos Stavrakakis
    @handle_backend_exceptions
165 3347a30c Georgios D. Tsoukalas
    @commit_on_success
166 cda71050 Christos Stavrakakis
    def get_image(self, image_uuid):
167 cda71050 Christos Stavrakakis
        """Retrieve information about an image."""
168 cda71050 Christos Stavrakakis
        image_url = self._get_image_url(image_uuid)
169 cda71050 Christos Stavrakakis
        return self._get_image(image_url)
170 1e28ba40 Christos Stavrakakis
171 cda71050 Christos Stavrakakis
    def _get_image_url(self, image_uuid):
172 cda71050 Christos Stavrakakis
        """Get the Pithos url that corresponds to an image UUID."""
173 cda71050 Christos Stavrakakis
        account, container, name = self.backend.get_uuid(self.user, image_uuid)
174 cda71050 Christos Stavrakakis
        return create_url(account, container, name)
175 1e28ba40 Christos Stavrakakis
176 cda71050 Christos Stavrakakis
    def _get_image(self, image_url):
177 cda71050 Christos Stavrakakis
        """Get information about an Image.
178 cda71050 Christos Stavrakakis

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

262 cda71050 Christos Stavrakakis
        Unregister an image, by removing all metadata from the Pithos
263 cda71050 Christos Stavrakakis
        file that exist in the PLANKTON_DOMAIN.
264 1e28ba40 Christos Stavrakakis

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

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

281 cda71050 Christos Stavrakakis
        """
282 cda71050 Christos Stavrakakis
        image_url = self._get_image_url(image_uuid)
283 cda71050 Christos Stavrakakis
        self._get_image(image_url)  # Assert that it is an image
284 cda71050 Christos Stavrakakis
        permissions = self._get_permissions(image_url)
285 cda71050 Christos Stavrakakis
        read = set(permissions.get("read", []))
286 cda71050 Christos Stavrakakis
        assert(isinstance(add_user, (str, unicode)))
287 cda71050 Christos Stavrakakis
        read.add(add_user)
288 cda71050 Christos Stavrakakis
        permissions["read"] = list(read)
289 cda71050 Christos Stavrakakis
        self._update_permissions(image_url, permissions)
290 1e28ba40 Christos Stavrakakis
291 2db7d9df Christos Stavrakakis
    @handle_backend_exceptions
292 3347a30c Georgios D. Tsoukalas
    @commit_on_success
293 cda71050 Christos Stavrakakis
    def remove_user(self, image_uuid, remove_user):
294 cda71050 Christos Stavrakakis
        """Remove the user from image members.
295 1e28ba40 Christos Stavrakakis

296 cda71050 Christos Stavrakakis
        Remove the specified user from the read permissions of the Pithos file.
297 cda71050 Christos Stavrakakis

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

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

319 f13aab5d Christos Stavrakakis
        """
320 cda71050 Christos Stavrakakis
        image_url = self._get_image_url(image_uuid)
321 cda71050 Christos Stavrakakis
        image = self._get_image(image_url)
322 cda71050 Christos Stavrakakis
        permissions = self._get_permissions(image_url)
323 cda71050 Christos Stavrakakis
        assert(isinstance(replace_users, list))
324 cda71050 Christos Stavrakakis
        permissions["read"] = replace_users
325 cda71050 Christos Stavrakakis
        if image.get("is_public", False):
326 cda71050 Christos Stavrakakis
            permissions["read"].append("*")
327 cda71050 Christos Stavrakakis
        self._update_permissions(image_url, permissions)
328 1e28ba40 Christos Stavrakakis
329 2db7d9df Christos Stavrakakis
    @handle_backend_exceptions
330 3347a30c Georgios D. Tsoukalas
    @commit_on_success
331 cda71050 Christos Stavrakakis
    def list_users(self, image_uuid):
332 cda71050 Christos Stavrakakis
        """List the image members.
333 cda71050 Christos Stavrakakis

334 cda71050 Christos Stavrakakis
        List the image members, by listing all users that have read permission
335 cda71050 Christos Stavrakakis
        to the corresponding Pithos file.
336 cda71050 Christos Stavrakakis

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

577 9393d4ee Kostas Papadimitriou
    usage:
578 9393d4ee Kostas Papadimitriou
        PLANKTON_BACKEND_MODULE = 'synnefo.plankton.backend.JSONFileBackend'
579 9393d4ee Kostas Papadimitriou
        PLANKTON_IMAGES_JSON_BACKEND_FILE = '/tmp/images.json'
580 9393d4ee Kostas Papadimitriou

581 9393d4ee Kostas Papadimitriou
        # loading images from an existing plankton service
582 9393d4ee Kostas Papadimitriou
        $ curl -H "X-Auth-Token: <MYTOKEN>" \
583 9393d4ee Kostas Papadimitriou
                https://cyclades.synnefo.org/plankton/images/detail | \
584 9393d4ee Kostas Papadimitriou
                python -m json.tool > /tmp/images.json
585 9393d4ee Kostas Papadimitriou
    """
586 9393d4ee Kostas Papadimitriou
    def __init__(self, userid):
587 9393d4ee Kostas Papadimitriou
        self.images_file = getattr(settings,
588 9393d4ee Kostas Papadimitriou
                                   'PLANKTON_IMAGES_JSON_BACKEND_FILE', '')
589 9393d4ee Kostas Papadimitriou
        if not os.path.exists(self.images_file):
590 9393d4ee Kostas Papadimitriou
            raise Exception("Invalid plankgon images json backend file: %s",
591 9393d4ee Kostas Papadimitriou
                            self.images_file)
592 9393d4ee Kostas Papadimitriou
        fp = file(self.images_file)
593 9393d4ee Kostas Papadimitriou
        self.images = json.load(fp)
594 9393d4ee Kostas Papadimitriou
        fp.close()
595 9393d4ee Kostas Papadimitriou
596 9393d4ee Kostas Papadimitriou
    def iter(self, *args, **kwargs):
597 9393d4ee Kostas Papadimitriou
        return self.images.__iter__()
598 9393d4ee Kostas Papadimitriou
599 9393d4ee Kostas Papadimitriou
    def list_images(self, *args, **kwargs):
600 9393d4ee Kostas Papadimitriou
        return self.images
601 9393d4ee Kostas Papadimitriou
602 9393d4ee Kostas Papadimitriou
    def get_image(self, image_uuid):
603 9393d4ee Kostas Papadimitriou
        try:
604 9393d4ee Kostas Papadimitriou
            return filter(lambda i: i['id'] == image_uuid, self.images)[0]
605 9393d4ee Kostas Papadimitriou
        except IndexError:
606 9393d4ee Kostas Papadimitriou
            raise Exception("Unknown image uuid: %s" % image_uuid)
607 9393d4ee Kostas Papadimitriou
608 9393d4ee Kostas Papadimitriou
    def close(self):
609 9393d4ee Kostas Papadimitriou
        pass
610 9393d4ee Kostas Papadimitriou
611 9393d4ee Kostas Papadimitriou
612 9393d4ee Kostas Papadimitriou
def get_backend():
613 9393d4ee Kostas Papadimitriou
    backend_module = getattr(settings, 'PLANKTON_BACKEND_MODULE', None)
614 9393d4ee Kostas Papadimitriou
    if not backend_module:
615 9393d4ee Kostas Papadimitriou
        # no setting set
616 9393d4ee Kostas Papadimitriou
        return ImageBackend
617 9393d4ee Kostas Papadimitriou
618 9393d4ee Kostas Papadimitriou
    parts = backend_module.split(".")
619 9393d4ee Kostas Papadimitriou
    module = ".".join(parts[:-1])
620 9393d4ee Kostas Papadimitriou
    cls = parts[-1]
621 9393d4ee Kostas Papadimitriou
    try:
622 9393d4ee Kostas Papadimitriou
        return getattr(importlib.import_module(module), cls)
623 9393d4ee Kostas Papadimitriou
    except (ImportError, AttributeError), e:
624 9393d4ee Kostas Papadimitriou
        raise ImportError("Cannot import plankton module: %s (%s)" %
625 9393d4ee Kostas Papadimitriou
                          (backend_module, e.message))