Revision cda71050

b/snf-cyclades-app/synnefo/api/images.py
118 118
    #                       overLimit (413)
119 119

  
120 120
    log.debug('list_images detail=%s', detail)
121
    since = utils.isoparse(request.GET.get('changes-since'))
121 122
    with image_backend(request.user_uniq) as backend:
122
        since = utils.isoparse(request.GET.get('changes-since'))
123 123
        if since:
124 124
            images = []
125 125
            for image in backend.iter():
......
172 172
    #                       overLimit (413)
173 173

  
174 174
    log.debug('get_image_details %s', image_id)
175
    image = util.get_image(image_id, request.user_uniq)
175
    with image_backend(request.user_uniq) as backend:
176
        image = backend.get_image(image_id)
176 177
    reply = image_to_dict(image)
177 178

  
178 179
    if request.serialization == 'xml':
......
209 210
    #                       overLimit (413)
210 211

  
211 212
    log.debug('list_image_metadata %s', image_id)
212
    image = util.get_image(image_id, request.user_uniq)
213
    with image_backend(request.user_uniq) as backend:
214
        image = backend.get_image(image_id)
213 215
    metadata = image['properties']
214 216
    return util.render_metadata(request, metadata, use_values=True, status=200)
215 217

  
......
227 229

  
228 230
    req = utils.get_request_dict(request)
229 231
    log.info('update_image_metadata %s %s', image_id, req)
230
    image = util.get_image(image_id, request.user_uniq)
231
    try:
232
        metadata = req['metadata']
233
        assert isinstance(metadata, dict)
234
    except (KeyError, AssertionError):
235
        raise faults.BadRequest('Malformed request.')
232
    with image_backend(request.user_uniq) as backend:
233
        image = backend.get_image(image_id)
234
        try:
235
            metadata = req['metadata']
236
            assert isinstance(metadata, dict)
237
        except (KeyError, AssertionError):
238
            raise faults.BadRequest('Malformed request.')
236 239

  
237
    properties = image['properties']
238
    properties.update(metadata)
240
        properties = image['properties']
241
        properties.update(metadata)
239 242

  
240
    with image_backend(request.user_uniq) as backend:
241
        backend.update(image_id, dict(properties=properties))
243
        backend.update_metadata(image_id, dict(properties=properties))
242 244

  
243 245
    return util.render_metadata(request, properties, status=201)
244 246

  
......
254 256
    #                       overLimit (413)
255 257

  
256 258
    log.debug('get_image_metadata_item %s %s', image_id, key)
257
    image = util.get_image(image_id, request.user_uniq)
259
    with image_backend(request.user_uniq) as backend:
260
        image = backend.get_image(image_id)
258 261
    val = image['properties'].get(key)
259 262
    if val is None:
260 263
        raise faults.ItemNotFound('Metadata key not found.')
......
284 287
        raise faults.BadRequest('Malformed request.')
285 288

  
286 289
    val = metadict[key]
287
    image = util.get_image(image_id, request.user_uniq)
288
    properties = image['properties']
289
    properties[key] = val
290

  
291 290
    with image_backend(request.user_uniq) as backend:
292
        backend.update(image_id, dict(properties=properties))
291
        image = backend.get_image(image_id)
292
        properties = image['properties']
293
        properties[key] = val
294

  
295
        backend.update_metadata(image_id, dict(properties=properties))
293 296

  
294 297
    return util.render_meta(request, {key: val}, status=201)
295 298

  
......
307 310
    #                       overLimit (413),
308 311

  
309 312
    log.info('delete_image_metadata_item %s %s', image_id, key)
310
    image = util.get_image(image_id, request.user_uniq)
311
    properties = image['properties']
312
    properties.pop(key, None)
313

  
314 313
    with image_backend(request.user_uniq) as backend:
315
        backend.update(image_id, dict(properties=properties))
314
        image = backend.get_image(image_id)
315
        properties = image['properties']
316
        properties.pop(key, None)
317

  
318
        backend.update_metadata(image_id, dict(properties=properties))
316 319

  
317 320
    return HttpResponse(status=204)
b/snf-cyclades-app/synnefo/api/test/images.py
46 46
    def wrapper(self, backend):
47 47
        result = func(self, backend)
48 48
        if backend.called is True:
49
            num = len(backend.mock_calls) / 2
50
            assert(len(backend.return_value.close.mock_calls) == num)
49
            backend.return_value.close.assert_called_once_with()
51 50
        return result
52 51
    return wrapper
53 52

  
54 53

  
55
@patch('synnefo.plankton.utils.ImageBackend')
54
@patch('synnefo.plankton.backend.ImageBackend')
56 55
class ImageAPITest(BaseAPITest):
57 56
    @assert_backend_closed
58 57
    def test_create_image(self, mimage):
......
171 170
                   'created': '2012-11-26T11:52:54+00:00',
172 171
                   'updated': '2012-12-26T11:52:54+00:00',
173 172
                   'metadata': {'values': {'foo': 'bar'}}}
174
        with patch('synnefo.api.util.get_image') as m:
175
            m.return_value = image
176
            response = self.get('/api/v1.1/images/42', 'user')
173
        mimage.return_value.get_image.return_value = image
174
        response = self.get('/api/v1.1/images/42', 'user')
177 175
        self.assertSuccess(response)
178 176
        api_image = json.loads(response.content)['image']
179 177
        self.assertEqual(api_image, result_image)
180 178

  
181 179
    @assert_backend_closed
182 180
    def test_invalid_image(self, mimage):
183
        with patch('synnefo.api.util.get_image') as m:
184
            m.side_effect = faults.ItemNotFound('Image not found')
185
            response = self.get('/api/v1.1/images/42', 'user')
181
        mimage.return_value.get_image.side_effect = faults.ItemNotFound('Image not found')
182
        response = self.get('/api/v1.1/images/42', 'user')
186 183
        self.assertItemNotFound(response)
187 184

  
185
    @assert_backend_closed
188 186
    def test_delete_image(self, mimage):
189 187
        response = self.delete("/api/v1.1/images/42", "user")
190 188
        self.assertEqual(response.status_code, 204)
......
192 190
        mimage.return_value._delete.assert_not_called('42')
193 191

  
194 192

  
195
@patch('synnefo.plankton.utils.ImageBackend')
193
@patch('synnefo.plankton.backend.ImageBackend')
196 194
class ImageMetadataAPITest(BaseAPITest):
197 195
    def setUp(self):
198 196
        self.image = {'id': 42,
b/snf-cyclades-app/synnefo/api/util.py
151 151
    """Return an Image instance or raise ItemNotFound."""
152 152

  
153 153
    with image_backend(user_id) as backend:
154
        image = backend.get_image(image_id)
155
        if not image:
156
            raise faults.ItemNotFound('Image not found.')
157
        return image
154
        return backend.get_image(image_id)
158 155

  
159 156

  
160 157
def get_image_dict(image_id, user_id):
b/snf-cyclades-app/synnefo/plankton/backend.py
1
# Copyright 2011 GRNET S.A. All rights reserved.
1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2

  
2 3
#
3 4
# Redistribution and use in source and binary forms, with or
4 5
# without modification, are permitted provided that the following
......
52 53

  
53 54
import json
54 55
import warnings
55

  
56
from operator import itemgetter
56
import logging
57 57
from time import gmtime, strftime
58
from functools import wraps, partial
59
from snf_django.lib.api import faults
60

  
61
from django.conf import settings
58
from functools import wraps
59
from operator import itemgetter
62 60

  
63
from pithos.backends.base import NotAllowedError
64 61

  
62
from django.conf import settings
63
from pithos.backends.base import NotAllowedError, VersionNotExists
65 64
import snf_django.lib.astakos as lib_astakos
66
import logging
67

  
68
from synnefo.settings import (CYCLADES_USE_QUOTAHOLDER,
69
                              CYCLADES_QUOTAHOLDER_URL,
70
                              CYCLADES_QUOTAHOLDER_TOKEN,
71
                              CYCLADES_QUOTAHOLDER_POOLSIZE)
72 65

  
73 66
logger = logging.getLogger(__name__)
74 67

  
......
89 82
        url = auth_url.replace('im/authenticate', 'service/api/user_catalogs')
90 83
        token = settings.CYCLADES_ASTAKOS_SERVICE_TOKEN
91 84
        uuids = lib_astakos.get_displaynames(token, names, url=url)
92
    except Exception, e:
93
        logger.exception(e)
85
    except Exception:
94 86
        return {}
95 87

  
96 88
    return uuids
......
109 101
    return t[2:5]
110 102

  
111 103

  
112
class BackendException(Exception):
113
    pass
114

  
115

  
116 104
from pithos.backends.util import PithosBackendPool
117 105
POOL_SIZE = 8
118 106
_pithos_backend_pool = \
119 107
    PithosBackendPool(
120 108
        POOL_SIZE,
121
        quotaholder_enabled=CYCLADES_USE_QUOTAHOLDER,
122
        quotaholder_url=CYCLADES_QUOTAHOLDER_URL,
123
        quotaholder_token=CYCLADES_QUOTAHOLDER_TOKEN,
124
        quotaholder_client_poolsize=CYCLADES_QUOTAHOLDER_POOLSIZE,
109
        quotaholder_enabled=settings.CYCLADES_USE_QUOTAHOLDER,
110
        quotaholder_url=settings.CYCLADES_QUOTAHOLDER_URL,
111
        quotaholder_token=settings.CYCLADES_QUOTAHOLDER_TOKEN,
112
        quotaholder_client_poolsize=settings.CYCLADES_QUOTAHOLDER_POOLSIZE,
125 113
        db_connection=settings.BACKEND_DB_CONNECTION,
126 114
        block_path=settings.BACKEND_BLOCK_PATH)
127 115

  
......
130 118
    return _pithos_backend_pool.pool_get()
131 119

  
132 120

  
121
def create_url(account, container, name):
122
    assert "/" not in account, "Invalid account"
123
    assert "/" not in container, "Invalid container"
124
    return "pithos://%s/%s/%s" % (account, container, name)
125

  
126

  
127
def split_url(url):
128
    """Returns (accout, container, object) from a url string"""
129
    t = url.split('/', 4)
130
    assert len(t) == 5, "Invalid url"
131
    return t[2:5]
132

  
133

  
134
def format_timestamp(t):
135
    return strftime('%Y-%m-%d %H:%M:%S', gmtime(t))
136

  
137

  
133 138
def handle_backend_exceptions(func):
134 139
    @wraps(func)
135 140
    def wrapper(*args, **kwargs):
136 141
        try:
137 142
            return func(*args, **kwargs)
138 143
        except NotAllowedError:
139
            raise faults.Forbidden("Request not allowed")
144
            raise Forbidden
145
        except NameError:
146
            raise ImageNotFound
147
        except VersionNotExists:
148
            raise ImageNotFound
140 149
    return wrapper
141 150

  
142 151

  
......
151 160
        self.backend = get_pithos_backend()
152 161
        warnings.filters = original_filters     # Restore warnings
153 162

  
163
    def close(self):
164
        """Close PithosBackend(return to pool)"""
165
        self.backend.close()
166

  
154 167
    @handle_backend_exceptions
155
    def _get_image(self, location):
156
        def format_timestamp(t):
157
            return strftime('%Y-%m-%d %H:%M:%S', gmtime(t))
168
    def get_image(self, image_uuid):
169
        """Retrieve information about an image."""
170
        image_url = self._get_image_url(image_uuid)
171
        return self._get_image(image_url)
158 172

  
159
        account, container, object = split_location(location)
173
    def _get_image_url(self, image_uuid):
174
        """Get the Pithos url that corresponds to an image UUID."""
175
        account, container, name = self.backend.get_uuid(self.user, image_uuid)
176
        return create_url(account, container, name)
160 177

  
161
        try:
162
            versions = self.backend.list_versions(self.user, account,
163
                                                  container, object)
164
        except NameError:
165
            return None
178
    def _get_image(self, image_url):
179
        """Get information about an Image.
180

  
181
        Get all available information about an Image.
182
        """
183
        account, container, name = split_url(image_url)
184
        versions = self.backend.list_versions(self.user, account, container,
185
                                              name)
186

  
187
        if not versions:
188
            raise Exception("Image without versions %s" % image_url)
166 189

  
167 190
        image = {}
168 191

  
169
        meta = self._get_meta(location)
170
        if meta:
171
            image['deleted_at'] = ''
172
        else:
192
        try:
193
            meta = self._get_meta(image_url)
194
            image["deleted_at"] = ""
195
        except NameError:
173 196
            # Object was deleted, use the latest version
174 197
            version, timestamp = versions[-1]
175
            meta = self._get_meta(location, version)
176
            image['deleted_at'] = format_timestamp(timestamp)
198
            meta = self._get_meta(image_url, version)
199
            image["deleted_at"] = format_timestamp(timestamp)
177 200

  
178 201
        if PLANKTON_PREFIX + 'name' not in meta:
179
            return None     # Not a Plankton image
202
            raise ImageNotFound("'%s' is not a Plankton image" % image_url)
180 203

  
181
        permissions = self._get_permissions(location)
204
        image["id"] = meta["uuid"]
205
        image["location"] = image_url
206
        image["checksum"] = meta["hash"]
207
        image["created_at"] = format_timestamp(versions[0][1])
208
        image["updated_at"] = format_timestamp(meta["modified"])
209
        image["size"] = meta["bytes"]
210
        image["store"] = "pithos"
182 211

  
183
        image['checksum'] = meta['hash']
184
        image['created_at'] = format_timestamp(versions[0][1])
185
        image['id'] = meta['uuid']
186
        image['is_public'] = '*' in permissions.get('read', [])
187
        image['location'] = location
188 212
        if TRANSLATE_UUIDS:
189 213
            displaynames = get_displaynames([account])
190 214
            if account in displaynames:
......
194 218
            image['owner'] = display_account
195 219
        else:
196 220
            image['owner'] = account
197
        image['size'] = meta['bytes']
198
        image['store'] = 'pithos'
199
        image['updated_at'] = format_timestamp(meta['modified'])
200
        image['properties'] = {}
221

  
222
        # Permissions
223
        permissions = self._get_permissions(image_url)
224
        image["is_public"] = "*" in permissions.get('read', [])
201 225

  
202 226
        for key, val in meta.items():
203
            if not key.startswith(PLANKTON_PREFIX):
204
                continue
205
            key = key[len(PLANKTON_PREFIX):]
206
            if key == 'properties':
207
                val = json.loads(val)
208
            if key in PLANKTON_META:
209
                image[key] = val
227
            # Get plankton properties
228
            if key.startswith(PLANKTON_PREFIX):
229
                # Remove plankton prefix
230
                key = key.replace(PLANKTON_PREFIX, "")
231
                # Keep only those in plankton meta
232
                if key in PLANKTON_META:
233
                    if key == "properties":
234
                        val = json.loads(val)
235
                    image[key] = val
210 236

  
211 237
        return image
212 238

  
213
    @handle_backend_exceptions
214
    def _get_meta(self, location, version=None):
215
        account, container, object = split_location(location)
216
        try:
217
            return self.backend.get_object_meta(self.user, account, container,
218
                                                object, PLANKTON_DOMAIN,
219
                                                version)
220
        except NameError:
221
            return None
239
    def _get_meta(self, image_url, version=None):
240
        """Get object's metadata."""
241
        account, container, name = split_url(image_url)
242
        return self.backend.get_object_meta(self.user, account, container,
243
                                            name, PLANKTON_DOMAIN, version)
222 244

  
223
    @handle_backend_exceptions
224
    def _get_permissions(self, location):
225
        account, container, object = split_location(location)
226
        _a, _p, permissions = self.backend.get_object_permissions(self.user,
227
                                                                  account,
228
                                                                  container,
229
                                                                  object)
230
        return permissions
245
    def _update_meta(self, image_url, meta, replace=False):
246
        """Update object's metadata."""
247
        account, container, name = split_url(image_url)
231 248

  
232
    @handle_backend_exceptions
233
    def _store(self, f, size=None):
234
        """Breaks data into blocks and stores them in the backend"""
249
        prefixed = {}
250
        for key, val in meta.items():
251
            if key in PLANKTON_META:
252
                if key == "properties":
253
                    val = json.dumps(val)
254
                prefixed[PLANKTON_PREFIX + key] = val
235 255

  
236
        bytes = 0
237
        hashmap = []
238
        backend = self.backend
239
        blocksize = backend.block_size
256
        self.backend.update_object_meta(self.user, account, container, name,
257
                                        PLANKTON_DOMAIN, prefixed, replace)
240 258

  
241
        data = f.read(blocksize)
242
        while data:
243
            hash = backend.put_block(data)
244
            hashmap.append(hash)
245
            bytes += len(data)
246
            data = f.read(blocksize)
259
    def _get_permissions(self, image_url):
260
        """Get object's permissions."""
261
        account, container, name = split_url(image_url)
262
        _a, path, permissions = \
263
            self.backend.get_object_permissions(self.user, account, container,
264
                                                name)
247 265

  
248
        if size and size != bytes:
249
            raise BackendException("Invalid size")
266
        if path is None:
267
            logger.warning("Image '%s' got permissions from None path",
268
                           image_url)
250 269

  
251
        return hashmap, bytes
270
        return permissions
252 271

  
253
    @handle_backend_exceptions
254
    def _update(self, location, size, hashmap, meta, permissions):
255
        account, container, object = split_location(location)
256
        self.backend.update_object_hashmap(self.user, account, container,
257
                                           object, size, hashmap, '',
258
                                           PLANKTON_DOMAIN,
259
                                           permissions=permissions)
260
        self._update_meta(location, meta, replace=True)
272
    def _update_permissions(self, image_url, permissions):
273
        """Update object's permissions."""
274
        account, container, name = split_url(image_url)
275
        self.backend.update_object_permissions(self.user, account, container,
276
                                               name, permissions)
261 277

  
262 278
    @handle_backend_exceptions
263
    def _update_meta(self, location, meta, replace=False):
264
        account, container, object = split_location(location)
279
    def unregister(self, image_uuid):
280
        """Unregister an image.
265 281

  
266
        prefixed = {}
267
        for key, val in meta.items():
268
            if key == 'properties':
269
                val = json.dumps(val)
270
            if key in PLANKTON_META:
271
                prefixed[PLANKTON_PREFIX + key] = val
282
        Unregister an image, by removing all metadata from the Pithos
283
        file that exist in the PLANKTON_DOMAIN.
272 284

  
273
        self.backend.update_object_meta(self.user, account, container, object,
274
                                        PLANKTON_DOMAIN, prefixed, replace)
285
        """
286
        image_url = self._get_image_url(image_uuid)
287
        self._get_image(image_url)  # Assert that it is an image
288
        # Unregister the image by removing all metadata from domain
289
        # 'PLANKTON_DOMAIN'
290
        meta = self._get_meta(image_url)
291
        for k in meta.keys():
292
            meta[k] = ""
293
        self._update_meta(image_url, meta, False)
275 294

  
276 295
    @handle_backend_exceptions
277
    def _update_permissions(self, location, permissions):
278
        account, container, object = split_location(location)
279
        self.backend.update_object_permissions(self.user, account, container,
280
                                               object, permissions)
296
    def add_user(self, image_uuid, add_user):
297
        """Add a user as an image member.
298

  
299
        Update read permissions of Pithos file, to include the specified user.
300

  
301
        """
302
        image_url = self._get_image_url(image_uuid)
303
        self._get_image(image_url)  # Assert that it is an image
304
        permissions = self._get_permissions(image_url)
305
        read = set(permissions.get("read", []))
306
        assert(isinstance(add_user, (str, unicode)))
307
        read.add(add_user)
308
        permissions["read"] = list(read)
309
        self._update_permissions(image_url, permissions)
281 310

  
282 311
    @handle_backend_exceptions
283
    def add_user(self, image_id, user):
284
        image = self.get_image(image_id)
285
        if not image:
286
            raise faults.ItemNotFound
287

  
288
        location = image['location']
289
        permissions = self._get_permissions(location)
290
        read = set(permissions.get('read', []))
291
        read.add(user)
292
        permissions['read'] = list(read)
293
        self._update_permissions(location, permissions)
312
    def remove_user(self, image_uuid, remove_user):
313
        """Remove the user from image members.
294 314

  
295
    def close(self):
296
        self.backend.close()
315
        Remove the specified user from the read permissions of the Pithos file.
316

  
317
        """
318
        image_url = self._get_image_url(image_uuid)
319
        self._get_image(image_url)  # Assert that it is an image
320
        permissions = self._get_permissions(image_url)
321
        read = set(permissions.get("read", []))
322
        assert(isinstance(remove_user, (str, unicode)))
323
        try:
324
            read.remove(remove_user)
325
        except ValueError:
326
            return  # TODO: User did not have access
327
        permissions["read"] = list(read)
328
        self._update_permissions(image_url, permissions)
297 329

  
298 330
    @handle_backend_exceptions
299
    def _delete(self, image_id):
300
        """Delete an Image.
331
    def replace_users(self, image_uuid, replace_users):
332
        """Replace image members.
301 333

  
302
        This method will delete the Image from the Storage backend.
334
        Replace the read permissions of the Pithos files with the specified
335
        users. If image is specified as public, we must preserve * permission.
303 336

  
304 337
        """
305
        image = self.get_image(image_id)
306
        account, container, object = split_location(image['location'])
307
        self.backend.delete_object(self.user, account, container, object)
338
        image_url = self._get_image_url(image_uuid)
339
        image = self._get_image(image_url)
340
        permissions = self._get_permissions(image_url)
341
        assert(isinstance(replace_users, list))
342
        permissions["read"] = replace_users
343
        if image.get("is_public", False):
344
            permissions["read"].append("*")
345
        self._update_permissions(image_url, permissions)
308 346

  
309 347
    @handle_backend_exceptions
310
    def get_data(self, location):
311
        account, container, object = split_location(location)
312
        size, hashmap = self.backend.get_object_hashmap(self.user, account,
313
                                                        container, object)
314
        data = ''.join(self.backend.get_block(hash) for hash in hashmap)
315
        assert len(data) == size
316
        return data
348
    def list_users(self, image_uuid):
349
        """List the image members.
350

  
351
        List the image members, by listing all users that have read permission
352
        to the corresponding Pithos file.
353

  
354
        """
355
        image_url = self._get_image_url(image_uuid)
356
        self._get_image(image_url)  # Assert that it is an image
357
        permissions = self._get_permissions(image_url)
358
        return [user for user in permissions.get('read', []) if user != '*']
317 359

  
318 360
    @handle_backend_exceptions
319
    def get_image(self, image_id):
320
        try:
321
            account, container, object = self.backend.get_uuid(self.user,
322
                                                               image_id)
323
        except NameError:
324
            return None
361
    def update_metadata(self, image_uuid, metadata):
362
        """Update Image metadata."""
363
        image_url = self._get_image_url(image_uuid)
364
        self._get_image(image_url)  # Assert that it is an image
325 365

  
326
        location = get_location(account, container, object)
327
        return self._get_image(location)
366
        is_public = metadata.pop("is_public", None)
367
        if is_public is not None:
368
            permissions = self._get_permissions(image_url)
369
            read = set(permissions.get("read", []))
370
            if is_public:
371
                read.add("*")
372
            else:
373
                read.discard("*")
374
            permissions["read"] = list(read)
375
            self._update_permissions(image_url, permissions)
376
        meta = {}
377
        meta["properties"] = metadata.pop("properties", {})
378
        meta.update(**metadata)
379

  
380
        self._update_meta(image_url, meta)
381
        return self.get_image(image_uuid)
328 382

  
329 383
    @handle_backend_exceptions
384
    def register(self, name, image_url, metadata):
385
        # Validate that metadata are allowed
386
        if "id" in metadata:
387
            raise ValueError("Passing an ID is not supported")
388
        store = metadata.pop("store", "pithos")
389
        if store != "pithos":
390
            raise ValueError("Invalid store '%s'. Only 'pithos' store is"
391
                             "supported" % store)
392
        disk_format = metadata.setdefault("disk_format",
393
                                          settings.DEFAULT_DISK_FORMAT)
394
        if disk_format not in settings.ALLOWED_DISK_FORMATS:
395
            raise ValueError("Invalid disk format '%s'" % disk_format)
396
        container_format =\
397
            metadata.setdefault("container_format",
398
                                settings.DEFAULT_CONTAINER_FORMAT)
399
        if container_format not in settings.ALLOWED_CONTAINER_FORMATS:
400
            raise ValueError("Invalid container format '%s'" %
401
                             container_format)
402

  
403
        # Validate that 'size' and 'checksum' are valid
404
        account, container, object = split_url(image_url)
405

  
406
        meta = self._get_meta(image_url)
407

  
408
        size = int(metadata.pop('size', meta['bytes']))
409
        if size != meta['bytes']:
410
            raise ValueError("Invalid size")
411

  
412
        checksum = metadata.pop('checksum', meta['hash'])
413
        if checksum != meta['hash']:
414
            raise ValueError("Invalid checksum")
415

  
416
        # Fix permissions
417
        is_public = metadata.pop('is_public', False)
418
        if is_public:
419
            permissions = {'read': ['*']}
420
        else:
421
            permissions = {'read': [self.user]}
422

  
423
        # Update rest metadata
424
        meta = {}
425
        meta['properties'] = metadata.pop('properties', {})
426
        meta.update(name=name, status='available', **metadata)
427

  
428
        # Do the actualy update in the Pithos backend
429
        self._update_meta(image_url, meta)
430
        self._update_permissions(image_url, permissions)
431
        return self._get_image(image_url)
432

  
433
    # TODO: Fix all these
330 434
    def _iter(self, public=False, filters=None, shared_from=None):
331 435
        filters = filters or {}
332 436

  
......
363 467
                    if image:
364 468
                        yield image
365 469

  
470
    @handle_backend_exceptions
366 471
    def iter(self, filters=None):
367 472
        """Iter over all images available to the user"""
368 473
        return self._iter(filters=filters)
369 474

  
475
    @handle_backend_exceptions
370 476
    def iter_public(self, filters=None):
371 477
        """Iter over public images"""
372 478
        return self._iter(public=True, filters=filters)
373 479

  
480
    @handle_backend_exceptions
374 481
    def iter_shared(self, filters=None, member=None):
375 482
        """Iter over images shared to member"""
376 483
        return self._iter(filters=filters, shared_from=member)
377 484

  
485
    @handle_backend_exceptions
378 486
    def list(self, filters=None, params={}):
379 487
        """Return all images available to the user"""
380 488
        images = list(self.iter(filters))
......
383 491
        images.sort(key=key, reverse=reverse)
384 492
        return images
385 493

  
494
    @handle_backend_exceptions
386 495
    def list_public(self, filters, params={}):
387 496
        """Return public images"""
388 497
        images = list(self.iter_public(filters))
......
391 500
        images.sort(key=key, reverse=reverse)
392 501
        return images
393 502

  
394
    def list_users(self, image_id):
395
        image = self.get_image(image_id)
396
        if not image:
397
            raise faults.ItemNotFound
398

  
399
        permissions = self._get_permissions(image['location'])
400
        return [user for user in permissions.get('read', []) if user != '*']
401

  
402
    @handle_backend_exceptions
403
    def put(self, name, f, params):
404
        assert 'checksum' not in params, "Passing a checksum is not supported"
405
        assert 'id' not in params, "Passing an ID is not supported"
406
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
407
        disk_format = params.setdefault('disk_format',
408
                                        settings.DEFAULT_DISK_FORMAT)
409
        assert disk_format in settings.ALLOWED_DISK_FORMATS,\
410
            "Invalid disk_format"
411
        assert params.setdefault('container_format',
412
                settings.DEFAULT_CONTAINER_FORMAT) in \
413
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
414

  
415
        container = settings.DEFAULT_PLANKTON_CONTAINER
416
        filename = params.pop('filename', name)
417
        location = 'pithos://%s/%s/%s' % (self.user, container, filename)
418
        is_public = params.pop('is_public', False)
419
        permissions = {'read': ['*']} if is_public else {}
420
        size = params.pop('size', None)
421

  
422
        hashmap, size = self._store(f, size)
423

  
424
        meta = {}
425
        meta['properties'] = params.pop('properties', {})
426
        meta.update(name=name, status='available', **params)
427

  
428
        self._update(location, size, hashmap, meta, permissions)
429
        return self._get_image(location)
430

  
431
    @handle_backend_exceptions
432
    def register(self, name, location, params):
433
        assert 'id' not in params, "Passing an ID is not supported"
434
        assert location.startswith('pithos://'), "Invalid location"
435
        assert params.pop('store', 'pithos') == 'pithos', "Invalid store"
436
        assert params.setdefault('disk_format',
437
                settings.DEFAULT_DISK_FORMAT) in \
438
                settings.ALLOWED_DISK_FORMATS, "Invalid disk_format"
439
        assert params.setdefault('container_format',
440
                settings.DEFAULT_CONTAINER_FORMAT) in \
441
                settings.ALLOWED_CONTAINER_FORMATS, "Invalid container_format"
442

  
443
        # user = self.user
444
        account, container, object = split_location(location)
445

  
446
        meta = self._get_meta(location)
447
        assert meta, "File not found"
448

  
449
        size = int(params.pop('size', meta['bytes']))
450
        if size != meta['bytes']:
451
            raise BackendException("Invalid size")
452

  
453
        checksum = params.pop('checksum', meta['hash'])
454
        if checksum != meta['hash']:
455
            raise BackendException("Invalid checksum")
456

  
457
        is_public = params.pop('is_public', False)
458
        if is_public:
459
            permissions = {'read': ['*']}
460
        else:
461
            permissions = {'read': [self.user]}
462

  
463
        meta = {}
464
        meta['properties'] = params.pop('properties', {})
465
        meta.update(name=name, status='available', **params)
466

  
467
        self._update_meta(location, meta)
468
        self._update_permissions(location, permissions)
469
        return self._get_image(location)
470

  
471
    @handle_backend_exceptions
472
    def remove_user(self, image_id, user):
473
        image = self.get_image(image_id)
474
        if not image:
475
            raise faults.ItemNotFound
476

  
477
        location = image['location']
478
        permissions = self._get_permissions(location)
479
        try:
480
            permissions.get('read', []).remove(user)
481
        except ValueError:
482
            return      # User did not have access anyway
483
        self._update_permissions(location, permissions)
484 503

  
485
    @handle_backend_exceptions
486
    def replace_users(self, image_id, users):
487
        image = self.get_image(image_id)
488
        if not image:
489
            raise faults.ItemNotFound
490

  
491
        location = image['location']
492
        permissions = self._get_permissions(location)
493
        permissions['read'] = users
494
        if image.get('is_public', False):
495
            permissions['read'].append('*')
496
        self._update_permissions(location, permissions)
504
class ImageBackendError(Exception):
505
    pass
497 506

  
498
    @handle_backend_exceptions
499
    def update(self, image_id, params):
500
        image = self.get_image(image_id)
501
        assert image, "Image not found"
502
        if not image:
503
            raise faults.ItemNotFound
504

  
505
        location = image['location']
506
        is_public = params.pop('is_public', None)
507
        if is_public is not None:
508
            permissions = self._get_permissions(location)
509
            read = set(permissions.get('read', []))
510
            if is_public:
511
                read.add('*')
512
            else:
513
                read.discard('*')
514
            permissions['read'] = list(read)
515
            self.backend._update_permissions(location, permissions)
516 507

  
517
        meta = {}
518
        meta['properties'] = params.pop('properties', {})
519
        meta.update(**params)
520

  
521
        self._update_meta(location, meta)
522
        return self.get_image(image_id)
508
class ImageNotFound(ImageBackendError):
509
    pass
523 510

  
524
    @handle_backend_exceptions
525
    def unregister(self, image_id):
526
        """Unregister an image."""
527
        image = self.get_image(image_id)
528
        if not image:
529
            raise faults.ItemNotFound
530 511

  
531
        location = image["location"]
532
        # Unregister the image by removing all metadata from domain
533
        # 'PLANKTON_DOMAIN'
534
        meta = self._get_meta(location)
535
        for k in meta.keys():
536
            meta[k] = ""
537
        self._update_meta(location, meta, False)
512
class Forbidden(ImageBackendError):
513
    pass
b/snf-cyclades-app/synnefo/plankton/tests.py
33 33

  
34 34
import json
35 35

  
36
from django.test import TestCase
37

  
38
from contextlib import contextmanager
39 36
from mock import patch
40 37
from functools import wraps
41 38
from copy import deepcopy
42
from snf_django.utils.testing import astakos_user, BaseAPITest
39
from snf_django.utils.testing import BaseAPITest
43 40

  
44 41

  
45 42
FILTERS = ('name', 'container_format', 'disk_format', 'status', 'size_min',
......
130 127
    return wrapper
131 128

  
132 129

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

  
34
from functools import wraps
35
from logging import getLogger
36
from traceback import format_exc
37

  
38
from django.conf import settings
39
from django.http import (HttpResponse, HttpResponseBadRequest,
40
                         HttpResponseServerError, HttpResponseForbidden)
41

  
42
from snf_django.lib.api import faults
43
from snf_django.lib.astakos import get_user
44
from synnefo.plankton.backend import (ImageBackend, BackendException,
45
                                      NotAllowedError)
46

  
47
log = getLogger('synnefo.plankton')
48

  
49

  
50
def plankton_method(method):
51
    def decorator(func):
52
        @wraps(func)
53
        def wrapper(request, *args, **kwargs):
54
            try:
55
                get_user(request, settings.ASTAKOS_URL)
56
                if not request.user_uniq:
57
                    return HttpResponse(status=401)
58
                if request.method != method:
59
                    return HttpResponse(status=405)
60
                request.backend = ImageBackend(request.user_uniq)
61
                return func(request, *args, **kwargs)
62
            except (AssertionError, BackendException) as e:
63
                message = e.args[0] if e.args else ''
64
                return HttpResponseBadRequest(message)
65
            except NotAllowedError:
66
                return HttpResponseForbidden()
67
            except faults.Fault, fault:
68
                return HttpResponse(status=fault.code)
69
            except Exception as e:
70
                if settings.DEBUG:
71
                    message = format_exc(e)
72
                else:
73
                    message = ''
74
                log.exception(e)
75
                return HttpResponseServerError(message)
76
            finally:
77
                if hasattr(request, 'backend'):
78
                    request.backend.close()
79
        return wrapper
80
    return decorator
b/snf-cyclades-app/synnefo/plankton/utils.py
1
# Copyright 2011 GRNET S.A. All rights reserved.
1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
2 2
#
3 3
# Redistribution and use in source and binary forms, with or
4 4
# without modification, are permitted provided that the following
......
31 31
# interpreted as representing official policies, either expressed
32 32
# or implied, of GRNET S.A.
33 33

  
34
from functools import wraps
35
from synnefo.plankton.backend import ImageBackend
36 34
from contextlib import contextmanager
37

  
38
def plankton_method(func):
39
    """Decorator function for API methods using ImageBackend.
40

  
41
    Decorator function that creates and closes an ImageBackend, needed
42
    by all API methods that handle images.
43
    """
44
    @wraps(func)
45
    def wrapper(request, *args, **kwargs):
46
        with image_backend(request.user_uniq) as backend:
47
            request.backend = backend
48
            return func(request, *args, **kwargs)
49
    return wrapper
35
from synnefo.plankton import backend
36
from snf_django.lib.api import faults
50 37

  
51 38

  
52 39
@contextmanager
53 40
def image_backend(user_id):
54
    """Context manager for ImageBackend"""
55
    backend = ImageBackend(user_id)
41
    """Context manager for ImageBackend.
42

  
43
    Context manager for using ImageBackend in API methods. Handles
44
    opening and closing a connection to Pithos and converting backend
45
    erros to cloud faults.
46

  
47
    """
48
    image_backend = backend.ImageBackend(user_id)
56 49
    try:
57
        yield backend
50
        yield image_backend
51
    except backend.Forbidden:
52
        raise faults.Forbidden
53
    except backend.ImageNotFound:
54
        raise faults.ItemNotFound
58 55
    finally:
59
        backend.close()
56
        image_backend.close()
b/snf-cyclades-app/synnefo/plankton/views.py
42 42

  
43 43
from snf_django.lib import api
44 44
from snf_django.lib.api import faults
45
from synnefo.plankton.utils import plankton_method
45
from synnefo.plankton.utils import image_backend
46 46

  
47 47

  
48 48
FILTERS = ('name', 'container_format', 'disk_format', 'status', 'size_min',
......
117 117

  
118 118

  
119 119
@api.api_method(http_method="POST", user_required=True, logger=log)
120
@plankton_method
121 120
def add_image(request):
122 121
    """Add a new virtual machine image
123 122

  
......
146 145
    location = params.pop('location', None)
147 146

  
148 147
    if location:
149
        image = request.backend.register(name, location, params)
148
        with image_backend(request.user_uniq) as backend:
149
            image = backend.register(name, location, params)
150 150
    else:
151 151
        #f = StringIO(request.raw_post_data)
152
        #image = request.backend.put(name, f, params)
152
        #image = backend.put(name, f, params)
153 153
        return HttpResponse(status=501)     # Not Implemented
154 154

  
155 155
    if not image:
......
159 159

  
160 160

  
161 161
@api.api_method(http_method="DELETE", user_required=True, logger=log)
162
@plankton_method
163 162
def delete_image(request, image_id):
164 163
    """Delete an Image.
165 164

  
......
173 172
    """
174 173
    log.info("delete_image '%s'" % image_id)
175 174
    userid = request.user_uniq
176
    request.backend.unregister(image_id)
175
    with image_backend(userid) as backend:
176
        backend.unregister(image_id)
177 177
    log.info("User '%s' deleted image '%s'" % (userid, image_id))
178 178
    return HttpResponse(status=204)
179 179

  
180 180

  
181 181
@api.api_method(http_method="PUT", user_required=True, logger=log)
182
@plankton_method
183 182
def add_image_member(request, image_id, member):
184 183
    """Add a member to an image
185 184

  
......
191 190
    """
192 191

  
193 192
    log.debug('add_image_member %s %s', image_id, member)
194
    request.backend.add_user(image_id, member)
193
    with image_backend(request.user_uniq) as backend:
194
        backend.add_user(image_id, member)
195 195
    return HttpResponse(status=204)
196 196

  
197 197

  
198 198
@api.api_method(http_method="GET", user_required=True, logger=log)
199
@plankton_method
200 199
def get_image(request, image_id):
201 200
    """Retrieve a virtual machine image
202 201

  
......
208 207
        in memory.
209 208
    """
210 209

  
211
    #image = request.backend.get_image(image_id)
210
    #image = backend.get_image(image_id)
212 211
    #if not image:
213 212
    #    return HttpResponseNotFound()
214 213
    #
215 214
    #response = _create_image_response(image)
216
    #data = request.backend.get_data(image)
215
    #data = backend.get_data(image)
217 216
    #response.content = data
218 217
    #response['Content-Length'] = len(data)
219 218
    #response['Content-Type'] = 'application/octet-stream'
......
223 222

  
224 223

  
225 224
@api.api_method(http_method="HEAD", user_required=True, logger=log)
226
@plankton_method
227 225
def get_image_meta(request, image_id):
228 226
    """Return detailed metadata on a specific image
229 227

  
......
231 229
    3.4. Requesting Detailed Metadata on a Specific Image
232 230
    """
233 231

  
234
    image = request.backend.get_image(image_id)
235
    if not image:
236
        raise faults.ItemNotFound()
232
    with image_backend(request.user_uniq) as backend:
233
        image = backend.get_image(image_id)
237 234
    return _create_image_response(image)
238 235

  
239 236

  
240 237
@api.api_method(http_method="GET", user_required=True, logger=log)
241
@plankton_method
242 238
def list_image_members(request, image_id):
243 239
    """List image memberships
244 240

  
......
246 242
    3.7. Requesting Image Memberships
247 243
    """
248 244

  
249
    members = [{'member_id': user, 'can_share': False}
250
               for user in request.backend.list_users(image_id)]
245
    with image_backend(request.user_uniq) as backend:
246
        users = backend.list_users(image_id)
247

  
248
    members = [{'member_id': u, 'can_share': False} for u in users]
251 249
    data = json.dumps({'members': members}, indent=settings.DEBUG)
252 250
    return HttpResponse(data)
253 251

  
254 252

  
255 253
@api.api_method(http_method="GET", user_required=True, logger=log)
256
@plankton_method
257 254
def list_images(request, detail=False):
258 255
    """Return a list of available images.
259 256

  
......
293 290
        except ValueError:
294 291
            raise faults.BadRequest("Malformed request.")
295 292

  
296
    images = request.backend.list(filters, params)
293
    with image_backend(request.user_uniq) as backend:
294
        images = backend.list(filters, params)
297 295

  
298 296
    # Remove keys that should not be returned
299 297
    fields = DETAIL_FIELDS if detail else LIST_FIELDS
......
307 305

  
308 306

  
309 307
@api.api_method(http_method="GET", user_required=True, logger=log)
310
@plankton_method
311 308
def list_shared_images(request, member):
312 309
    """Request shared images
313 310

  
......
322 319
    log.debug('list_shared_images %s', member)
323 320

  
324 321
    images = []
325
    for image in request.backend.iter_shared(member=member):
326
        image_id = image['id']
327
        images.append({'image_id': image_id, 'can_share': False})
322
    with image_backend(request.user_uniq) as backend:
323
        for image in backend.iter_shared(member=member):
324
            image_id = image['id']
325
            images.append({'image_id': image_id, 'can_share': False})
328 326

  
329 327
    data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
330 328
    return HttpResponse(data)
331 329

  
332 330

  
333 331
@api.api_method(http_method="DELETE", user_required=True, logger=log)
334
@plankton_method
335 332
def remove_image_member(request, image_id, member):
336 333
    """Remove a member from an image
337 334

  
......
340 337
    """
341 338

  
342 339
    log.debug('remove_image_member %s %s', image_id, member)
343
    request.backend.remove_user(image_id, member)
340
    with image_backend(request.user_uniq) as backend:
341
        backend.remove_user(image_id, member)
344 342
    return HttpResponse(status=204)
345 343

  
346 344

  
347 345
@api.api_method(http_method="PUT", user_required=True, logger=log)
348
@plankton_method
349 346
def update_image(request, image_id):
350 347
    """Update an image
351 348

  
......
363 360

  
364 361
    assert set(meta.keys()).issubset(set(UPDATE_FIELDS))
365 362

  
366
    image = request.backend.update(image_id, meta)
363
    with image_backend(request.user_uniq) as backend:
364
        image = backend.update_metadata(image_id, meta)
367 365
    return _create_image_response(image)
368 366

  
369 367

  
370 368
@api.api_method(http_method="PUT", user_required=True, logger=log)
371
@plankton_method
372 369
def update_image_members(request, image_id):
373 370
    """Replace a membership list for an image
374 371

  
......
388 385
    except (ValueError, KeyError, TypeError):
389 386
        return HttpResponse(status=400)
390 387

  
391
    request.backend.replace_users(image_id, members)
388
    with image_backend(request.user_uniq) as backend:
389
        backend.replace_users(image_id, members)
392 390
    return HttpResponse(status=204)

Also available in: Unified diff