Revision 23808592

b/snf-cyclades-app/synnefo/admin/stats.py
43 43

  
44 44
from snf_django.lib.astakos import UserCache
45 45
from synnefo.db.models import VirtualMachine, Network, Backend
46
from synnefo.plankton.utils import image_backend
46
from synnefo.plankton.backend import PlanktonBackend
47 47
from synnefo.logic import backend as backend_mod
48 48

  
49 49

  
......
204 204
    def get_image(self, imageid, userid):
205 205
        if not imageid in self.images:
206 206
            try:
207
                with image_backend(userid) as ib:
207
                with PlanktonBackend(userid) as ib:
208 208
                    image = ib.get_image(imageid)
209 209
                properties = image.get("properties")
210 210
                os = properties.get("os",
b/snf-cyclades-app/synnefo/api/images.py
44 44
from snf_django.lib import api
45 45
from snf_django.lib.api import faults, utils
46 46
from synnefo.api import util
47
from synnefo.plankton.utils import image_backend
47
from synnefo.plankton import backend
48 48

  
49 49

  
50 50
log = getLogger(__name__)
......
131 131

  
132 132
    log.debug('list_images detail=%s', detail)
133 133
    since = utils.isoparse(request.GET.get('changes-since'))
134
    with image_backend(request.user_uniq) as backend:
135
        images = backend.list_images()
134
    with backend.PlanktonBackend(request.user_uniq) as b:
135
        images = b.list_images()
136 136
        if since:
137 137
            updated_since = lambda img: date_parse(img["updated_at"]) >= since
138 138
            images = ifilter(updated_since, images)
......
180 180
    #                       overLimit (413)
181 181

  
182 182
    log.debug('get_image_details %s', image_id)
183
    with image_backend(request.user_uniq) as backend:
184
        image = backend.get_image(image_id)
183
    with backend.PlanktonBackend(request.user_uniq) as b:
184
        image = b.get_image(image_id)
185 185
    reply = image_to_dict(image)
186 186

  
187 187
    if request.serialization == 'xml':
......
202 202
    #                       overLimit (413)
203 203

  
204 204
    log.info('delete_image %s', image_id)
205
    with image_backend(request.user_uniq) as backend:
206
        backend.unregister(image_id)
205
    with backend.PlanktonBackend(request.user_uniq) as b:
206
        b.unregister(image_id)
207 207
    log.info('User %s deleted image %s', request.user_uniq, image_id)
208 208
    return HttpResponse(status=204)
209 209

  
......
218 218
    #                       overLimit (413)
219 219

  
220 220
    log.debug('list_image_metadata %s', image_id)
221
    with image_backend(request.user_uniq) as backend:
222
        image = backend.get_image(image_id)
221
    with backend.PlanktonBackend(request.user_uniq) as b:
222
        image = b.get_image(image_id)
223 223
    metadata = image['properties']
224 224
    return util.render_metadata(request, metadata, use_values=False,
225 225
                                status=200)
......
238 238

  
239 239
    req = utils.get_request_dict(request)
240 240
    log.info('update_image_metadata %s %s', image_id, req)
241
    with image_backend(request.user_uniq) as backend:
242
        image = backend.get_image(image_id)
241
    with backend.PlanktonBackend(request.user_uniq) as b:
242
        image = b.get_image(image_id)
243 243
        try:
244 244
            metadata = req['metadata']
245 245
            assert isinstance(metadata, dict)
......
249 249
        properties = image['properties']
250 250
        properties.update(metadata)
251 251

  
252
        backend.update_metadata(image_id, dict(properties=properties))
252
        b.update_metadata(image_id, dict(properties=properties))
253 253

  
254 254
    return util.render_metadata(request, properties, status=201)
255 255

  
......
265 265
    #                       overLimit (413)
266 266

  
267 267
    log.debug('get_image_metadata_item %s %s', image_id, key)
268
    with image_backend(request.user_uniq) as backend:
269
        image = backend.get_image(image_id)
268
    with backend.PlanktonBackend(request.user_uniq) as b:
269
        image = b.get_image(image_id)
270 270
    val = image['properties'].get(key)
271 271
    if val is None:
272 272
        raise faults.ItemNotFound('Metadata key not found.')
......
296 296
        raise faults.BadRequest('Malformed request.')
297 297

  
298 298
    val = metadict[key]
299
    with image_backend(request.user_uniq) as backend:
300
        image = backend.get_image(image_id)
299
    with backend.PlanktonBackend(request.user_uniq) as b:
300
        image = b.get_image(image_id)
301 301
        properties = image['properties']
302 302
        properties[key] = val
303 303

  
304
        backend.update_metadata(image_id, dict(properties=properties))
304
        b.update_metadata(image_id, dict(properties=properties))
305 305

  
306 306
    return util.render_meta(request, {key: val}, status=201)
307 307

  
......
319 319
    #                       overLimit (413),
320 320

  
321 321
    log.info('delete_image_metadata_item %s %s', image_id, key)
322
    with image_backend(request.user_uniq) as backend:
323
        image = backend.get_image(image_id)
322
    with backend.PlanktonBackend(request.user_uniq) as b:
323
        image = b.get_image(image_id)
324 324
        properties = image['properties']
325 325
        properties.pop(key, None)
326 326

  
327
        backend.update_metadata(image_id, dict(properties=properties))
327
        b.update_metadata(image_id, dict(properties=properties))
328 328

  
329 329
    return HttpResponse(status=204)
b/snf-cyclades-app/synnefo/api/tests/images.py
42 42
from mock import patch
43 43
from functools import wraps
44 44

  
45
compute_path = get_service_path(cyclades_services, 'compute',
46
                                version='v2.0')
47
IMAGES_URL = join_urls(compute_path, "images/")
48

  
45 49

  
46 50
def assert_backend_closed(func):
47
    """Decorator for ensuring that ImageBackend is returned to pool."""
51
    """Decorator for ensuring that PlanktonBackend is returned to pool."""
48 52
    @wraps(func)
49 53
    def wrapper(self, backend):
50 54
        result = func(self, backend)
......
54 58
    return wrapper
55 59

  
56 60

  
57
class ComputeAPITest(BaseAPITest):
58
    def setUp(self, *args, **kwargs):
59
        super(ComputeAPITest, self).setUp(*args, **kwargs)
60
        self.compute_path = get_service_path(cyclades_services, 'compute',
61
                                             version='v2.0')
62
    def myget(self, path, *args, **kwargs):
63
        path = join_urls(self.compute_path, path)
64
        return self.get(path, *args, **kwargs)
65

  
66
    def myput(self, path, *args, **kwargs):
67
        path = join_urls(self.compute_path, path)
68
        return self.put(path, *args, **kwargs)
69

  
70
    def mypost(self, path, *args, **kwargs):
71
        path = join_urls(self.compute_path, path)
72
        return self.post(path, *args, **kwargs)
73

  
74
    def mydelete(self, path, *args, **kwargs):
75
        path = join_urls(self.compute_path, path)
76
        return self.delete(path, *args, **kwargs)
77

  
78

  
79
@patch('synnefo.plankton.backend.ImageBackend')
80
class ImageAPITest(ComputeAPITest):
61
@patch('synnefo.plankton.backend.PlanktonBackend')
62
class ImageAPITest(BaseAPITest):
81 63
    @assert_backend_closed
82 64
    def test_create_image(self, mimage):
83 65
        """Test that create image is not implemented"""
84
        response = self.mypost('images/', 'user', json.dumps(''), 'json')
66
        response = self.post(IMAGES_URL, 'user', json.dumps(''), 'json')
85 67
        self.assertEqual(response.status_code, 501)
86 68

  
87 69
    @assert_backend_closed
......
90 72
        images = [{'id': 1, 'name': 'image-1'},
91 73
                  {'id': 2, 'name': 'image-2'},
92 74
                  {'id': 3, 'name': 'image-3'}]
93
        mimage().list_images.return_value = images
94
        response = self.myget('images', 'user')
75
        mimage().__enter__().list_images.return_value = images
76
        response = self.get(IMAGES_URL, 'user')
95 77
        self.assertSuccess(response)
96 78
        api_images = json.loads(response.content)['images']
97 79
        self.assertEqual(images, api_images)
......
150 132
                   'created': '2012-11-26T11:52:54+00:00',
151 133
                   'updated': '2012-12-26T11:52:54+00:00',
152 134
                   'metadata': {}}]
153
        mimage().list_images.return_value = images
154
        response = self.myget('images/detail', 'user')
135
        mimage().__enter__().list_images.return_value = images
136
        response = self.get(join_urls(IMAGES_URL, "detail"), 'user')
155 137
        self.assertSuccess(response)
156 138
        api_images = json.loads(response.content)['images']
157 139
        self.assertEqual(len(result_images), len(api_images))
......
184 166
                   'updated_at': new_time.isoformat(),
185 167
                   'deleted_at': new_time.isoformat(),
186 168
                   'properties': ''}]
187
        mimage().list_images.return_value = images
169
        mimage().__enter__().list_images.return_value = images
188 170
        response =\
189
            self.myget('images/detail?changes-since=%sUTC' % new_time)
171
            self.get(join_urls(IMAGES_URL, 'detail?changes-since=%sUTC' %
172
                               new_time))
190 173
        self.assertSuccess(response)
191 174
        api_images = json.loads(response.content)['images']
192 175
        self.assertEqual(1, len(api_images))
......
211 194
                   'user_id': 'user1',
212 195
                   'tenant_id': 'user1',
213 196
                   'metadata': {'foo': 'bar'}}
214
        mimage.return_value.get_image.return_value = image
215
        response = self.myget('images/42', 'user')
197
        mimage().__enter__().get_image.return_value = image
198
        response = self.get(join_urls(IMAGES_URL, "42"), 'user')
216 199
        self.assertSuccess(response)
217 200
        api_image = json.loads(response.content)['image']
218 201
        api_image.pop("links")
219 202
        self.assertEqual(api_image, result_image)
220 203

  
221
    @assert_backend_closed
222 204
    def test_invalid_image(self, mimage):
223
        mimage.return_value.get_image.side_effect = faults.ItemNotFound('Image not found')
224
        response = self.myget('images/42', 'user')
205
        mimage().__enter__().get_image.side_effect = \
206
            faults.ItemNotFound('Image not found')
207
        response = self.get(join_urls(IMAGES_URL, "42"), 'user')
225 208
        self.assertItemNotFound(response)
226 209

  
227 210
    @assert_backend_closed
228 211
    def test_delete_image(self, mimage):
229
        response = self.mydelete("images/42", "user")
212
        response = self.delete(join_urls(IMAGES_URL, "42"), 'user')
230 213
        self.assertEqual(response.status_code, 204)
231
        mimage.return_value.unregister.assert_called_once_with('42')
232
        mimage.return_value._delete.assert_not_called('42')
214
        mimage().__enter__().unregister.assert_called_once_with('42')
233 215

  
234 216
    @assert_backend_closed
235 217
    def test_catch_wrong_api_paths(self, *args):
236
        response = self.myget('nonexistent')
218
        response = self.get(join_urls(IMAGES_URL, 'nonexistent/lala/foo'))
237 219
        self.assertEqual(response.status_code, 400)
238 220
        try:
239 221
            error = json.loads(response.content)
......
243 225
    @assert_backend_closed
244 226
    def test_method_not_allowed(self, *args):
245 227
        # /images/ allows only POST, GET
246
        response = self.myput('images', '', '')
228
        response = self.put(IMAGES_URL, '', '')
247 229
        self.assertMethodNotAllowed(response)
248
        response = self.mydelete('images')
230
        response = self.delete(IMAGES_URL, '')
249 231
        self.assertMethodNotAllowed(response)
250 232

  
251 233
        # /images/<imgid>/ allows only GET, DELETE
252
        response = self.mypost("images/42")
253
        self.assertMethodNotAllowed(response)
254
        response = self.myput('images/42', '', '')
255
        self.assertMethodNotAllowed(response)
256

  
257
        # /images/<imgid>/metadata/ allows only POST, GET
258
        response = self.myput('images/42/metadata', '', '')
234
        response = self.post(join_urls(IMAGES_URL, "42"), 'user')
259 235
        self.assertMethodNotAllowed(response)
260
        response = self.mydelete('images/42/metadata')
236
        response = self.put(join_urls(IMAGES_URL, "42"), 'user')
261 237
        self.assertMethodNotAllowed(response)
262 238

  
263 239
        # /images/<imgid>/metadata/ allows only POST, GET
264
        response = self.myput('images/42/metadata', '', '')
240
        response = self.put(join_urls(IMAGES_URL, "42", "metadata"), 'user')
265 241
        self.assertMethodNotAllowed(response)
266
        response = self.mydelete('images/42/metadata')
242
        response = self.delete(join_urls(IMAGES_URL, "42", "metadata"), 'user')
267 243
        self.assertMethodNotAllowed(response)
268 244

  
269 245
        # /images/<imgid>/metadata/<key> allows only PUT, GET, DELETE
270
        response = self.mypost('images/42/metadata/foo')
246
        response = self.post(join_urls(IMAGES_URL, "42", "metadata", "foo"),
247
                             'user')
271 248
        self.assertMethodNotAllowed(response)
272 249

  
273 250

  
274
@patch('synnefo.plankton.backend.ImageBackend')
275
class ImageMetadataAPITest(ComputeAPITest):
251
@patch('synnefo.plankton.backend.PlanktonBackend')
252
class ImageMetadataAPITest(BaseAPITest):
276 253
    def setUp(self):
277 254
        self.image = {'id': 42,
278 255
                 'name': 'image-1',
......
293 270

  
294 271
    @assert_backend_closed
295 272
    def test_list_metadata(self, backend):
296
        backend.return_value.get_image.return_value = self.image
297
        response = self.myget('images/42/metadata', 'user')
273
        backend().__enter__().get_image.return_value = self.image
274
        response = self.get(join_urls(IMAGES_URL, '42/metadata'), 'user')
298 275
        self.assertSuccess(response)
299 276
        meta = json.loads(response.content)['metadata']
300 277
        self.assertEqual(meta, self.image['properties'])
301 278

  
302 279
    @assert_backend_closed
303 280
    def test_get_metadata(self, backend):
304
        backend.return_value.get_image.return_value = self.image
305
        response = self.myget('images/42/metadata/foo', 'user')
281
        backend().__enter__().get_image.return_value = self.image
282
        response = self.get(join_urls(IMAGES_URL, '42/metadata/foo'), 'user')
306 283
        self.assertSuccess(response)
307 284
        meta = json.loads(response.content)['meta']
308 285
        self.assertEqual(meta['foo'], 'bar')
309 286

  
310 287
    @assert_backend_closed
311 288
    def test_get_invalid_metadata(self, backend):
312
        backend.return_value.get_image.return_value = self.image
313
        response = self.myget('images/42/metadata/not_found', 'user')
289
        backend().__enter__().get_image.return_value = self.image
290
        response = self.get(join_urls(IMAGES_URL, '42/metadata/not_found'),
291
                            'user')
314 292
        self.assertItemNotFound(response)
315 293

  
316 294
    def test_delete_metadata_item(self, backend):
317
        backend.return_value.get_image.return_value = self.image
318
        response = self.mydelete('images/42/metadata/foo', 'user')
295
        backend().__enter__().get_image.return_value = self.image
296
        response = self.delete(join_urls(IMAGES_URL, '42/metadata/foo'),
297
                               'user')
319 298
        self.assertEqual(response.status_code, 204)
320
        backend.return_value.update_metadata.assert_called_once_with('42', {'properties': {'foo2':
321
                                                    'bar2'}})
299
        backend().__enter__().update_metadata\
300
               .assert_called_once_with('42', {'properties': {'foo2': 'bar2'}})
322 301

  
323 302
    @assert_backend_closed
324 303
    def test_create_metadata_item(self, backend):
325
        backend.return_value.get_image.return_value = self.image
304
        backend().__enter__().get_image.return_value = self.image
326 305
        request = {'meta': {'foo3': 'bar3'}}
327
        response = self.myput('images/42/metadata/foo3', 'user',
328
                              json.dumps(request), 'json')
306
        response = self.put(join_urls(IMAGES_URL, '42/metadata/foo3'), 'user',
307
                            json.dumps(request), 'json')
329 308
        self.assertEqual(response.status_code, 201)
330
        backend.return_value.update_metadata.assert_called_once_with('42',
309
        backend().__enter__().update_metadata.assert_called_once_with('42',
331 310
                {'properties':
332 311
                    {'foo': 'bar', 'foo2': 'bar2', 'foo3': 'bar3'}})
333 312

  
334 313
    @assert_backend_closed
335 314
    def test_create_metadata_malformed_1(self, backend):
336
        backend.return_value.get_image.return_value = self.image
315
        backend().__enter__().get_image.return_value = self.image
337 316
        request = {'met': {'foo3': 'bar3'}}
338
        response = self.myput('images/42/metadata/foo3', 'user',
339
                              json.dumps(request), 'json')
317
        response = self.put(join_urls(IMAGES_URL, '42/metadata/foo3'), 'user',
318
                            json.dumps(request), 'json')
340 319
        self.assertBadRequest(response)
341 320

  
342 321
    @assert_backend_closed
343 322
    def test_create_metadata_malformed_2(self, backend):
344
        backend.return_value.get_image.return_value = self.image
323
        backend().__enter__().get_image.return_value = self.image
345 324
        request = {'metadata': [('foo3', 'bar3')]}
346
        response = self.myput('images/42/metadata/foo3', 'user',
347
                              json.dumps(request), 'json')
325
        response = self.put(join_urls(IMAGES_URL, '42/metadata/foo3'), 'user',
326
                            json.dumps(request), 'json')
348 327
        self.assertBadRequest(response)
349 328

  
350 329
    @assert_backend_closed
351 330
    def test_create_metadata_malformed_3(self, backend):
352
        backend.return_value.get_image.return_value = self.image
331
        backend().__enter__().get_image.return_value = self.image
353 332
        request = {'met': {'foo3': 'bar3', 'foo4': 'bar4'}}
354
        response = self.myput('images/42/metadata/foo3', 'user',
355
                                json.dumps(request), 'json')
333
        response = self.put(join_urls(IMAGES_URL, '42/metadata/foo3'), 'user',
334
                            json.dumps(request), 'json')
356 335
        self.assertBadRequest(response)
357 336

  
358 337
    @assert_backend_closed
359 338
    def test_create_metadata_malformed_4(self, backend):
360
        backend.return_value.get_image.return_value = self.image
339
        backend().__enter__().get_image.return_value = self.image
361 340
        request = {'met': {'foo3': 'bar3'}}
362
        response = self.myput('images/42/metadata/foo4', 'user',
363
                              json.dumps(request), 'json')
341
        response = self.put(join_urls(IMAGES_URL, '42/metadata/foo4'), 'user',
342
                            json.dumps(request), 'json')
364 343
        self.assertBadRequest(response)
365 344

  
366 345
    @assert_backend_closed
367 346
    def test_update_metadata_item(self, backend):
368
        backend.return_value.get_image.return_value = self.image
347
        backend().__enter__().get_image.return_value = self.image
369 348
        request = {'metadata': {'foo': 'bar_new', 'foo4': 'bar4'}}
370
        response = self.mypost('images/42/metadata', 'user',
371
                               json.dumps(request), 'json')
349
        response = self.post(join_urls(IMAGES_URL, '42/metadata'), 'user',
350
                             json.dumps(request), 'json')
372 351
        self.assertEqual(response.status_code, 201)
373
        backend.return_value.update_metadata.assert_called_once_with('42',
352
        backend().__enter__().update_metadata.assert_called_once_with('42',
374 353
                {'properties':
375 354
                    {'foo': 'bar_new', 'foo2': 'bar2', 'foo4': 'bar4'}
376 355
                })
377 356

  
378 357
    @assert_backend_closed
379 358
    def test_update_metadata_malformed(self, backend):
380
        backend.return_value.get_image.return_value = self.image
359
        backend().__enter__().get_image.return_value = self.image
381 360
        request = {'meta': {'foo': 'bar_new', 'foo4': 'bar4'}}
382
        response = self.mypost('images/42/metadata', 'user',
383
                               json.dumps(request), 'json')
361
        response = self.post(join_urls(IMAGES_URL, '42/metadata'), 'user',
362
                             json.dumps(request), 'json')
384 363
        self.assertBadRequest(response)
b/snf-cyclades-app/synnefo/api/util.py
51 51
                               Network, NetworkInterface, SecurityGroup,
52 52
                               BridgePoolTable, MacPrefixPoolTable, IPAddress,
53 53
                               IPPoolTable)
54
from synnefo.plankton.utils import image_backend
54
from synnefo.plankton.backend import PlanktonBackend
55 55

  
56 56
from synnefo.cyclades_settings import cyclades_services, BASE_HOST
57 57
from synnefo.lib.services import get_service_path
......
163 163
def get_image(image_id, user_id):
164 164
    """Return an Image instance or raise ItemNotFound."""
165 165

  
166
    with image_backend(user_id) as backend:
166
    with PlanktonBackend(user_id) as backend:
167 167
        return backend.get_image(image_id)
168 168

  
169 169

  
b/snf-cyclades-app/synnefo/plankton/backend.py
1
# Copyright 2011-2013 GRNET S.A. All rights reserved.
1
# Copyright 2011-2014 GRNET S.A. All rights reserved.
2 2

  
3 3
#
4 4
# Redistribution and use in source and binary forms, with or
......
52 52
"""
53 53

  
54 54
import json
55
import warnings
56 55
import logging
57 56
import os
58 57

  
59 58
from time import time, gmtime, strftime
60 59
from functools import wraps
61 60
from operator import itemgetter
61
from collections import namedtuple
62 62

  
63 63
from django.conf import settings
64 64
from django.utils import importlib
65
from pithos.backends.base import NotAllowedError, VersionNotExists
65
from pithos.backends.base import NotAllowedError, VersionNotExists, QuotaError
66 66
from synnefo.util.text import uenc
67
from copy import deepcopy
68
from snf_django.lib.api import faults
67 69

  
70
Location = namedtuple("ObjectLocation", ["account", "container", "path"])
68 71

  
69 72
logger = logging.getLogger(__name__)
70 73

  
......
94 97
    return _pithos_backend_pool.pool_get()
95 98

  
96 99

  
97
def create_url(account, container, name):
98
    assert "/" not in account, "Invalid account"
99
    assert "/" not in container, "Invalid container"
100
    return "pithos://%s/%s/%s" % (account, container, name)
101

  
102

  
103
def split_url(url):
104
    """Returns (accout, container, object) from a url string"""
105
    try:
106
        assert(isinstance(url, basestring))
107
        t = url.split('/', 4)
108
        assert t[0] == "pithos:", "Invalid url"
109
        assert len(t) == 5, "Invalid url"
110
        return t[2:5]
111
    except AssertionError:
112
        raise InvalidLocation("Invalid location '%s" % url)
113

  
114

  
115 100
def format_timestamp(t):
116 101
    return strftime('%Y-%m-%d %H:%M:%S', gmtime(t))
117 102

  
118 103

  
119
def handle_backend_exceptions(func):
104
def handle_pithos_backend(func):
120 105
    @wraps(func)
121
    def wrapper(*args, **kwargs):
122
        try:
123
            return func(*args, **kwargs)
124
        except NotAllowedError:
125
            raise Forbidden
126
        except NameError:
127
            raise ImageNotFound
128
        except VersionNotExists:
129
            raise ImageNotFound
130
    return wrapper
131

  
132

  
133
def commit_on_success(func):
134 106
    def wrapper(self, *args, **kwargs):
135 107
        backend = self.backend
136 108
        backend.pre_exec()
109
        commit = False
137 110
        try:
138 111
            ret = func(self, *args, **kwargs)
139
        except:
140
            backend.post_exec(False)
141
            raise
112
        except NotAllowedError:
113
            raise faults.Forbidden
114
        except (NameError, VersionNotExists):
115
            raise faults.ItemNotFound
116
        except (AssertionError, ValueError):
117
            raise faults.BadRequest
118
        except QuotaError:
119
            raise faults.OverLimit
142 120
        else:
143
            backend.post_exec(True)
121
            commit = True
122
        finally:
123
            backend.post_exec(commit)
144 124
        return ret
145 125
    return wrapper
146 126

  
147 127

  
148
class ImageBackend(object):
128
class PlanktonBackend(object):
149 129
    """A wrapper arround the pithos backend to simplify image handling."""
150 130

  
151 131
    def __init__(self, user):
152 132
        self.user = user
153

  
154
        original_filters = warnings.filters
155
        warnings.simplefilter('ignore')         # Suppress SQLAlchemy warnings
156 133
        self.backend = get_pithos_backend()
157
        warnings.filters = original_filters     # Restore warnings
158 134

  
159 135
    def close(self):
160 136
        """Close PithosBackend(return to pool)"""
161 137
        self.backend.close()
162 138

  
163
    @handle_backend_exceptions
164
    @commit_on_success
165
    def get_image(self, image_uuid):
166
        """Retrieve information about an image."""
167
        image_url = self._get_image_url(image_uuid)
168
        return self._get_image(image_url)
139
    def __enter__(self):
140
        return self
141

  
142
    def __exit__(self, exc_type, exc_val, exc_tb):
143
        self.close()
144
        self.backend = None
145
        return False
146

  
147
    @handle_pithos_backend
148
    def get_image(self, uuid):
149
        return self._get_image(uuid)
150

  
151
    def _get_image(self, uuid):
152
        location, metadata = self._get_raw_metadata(uuid)
153
        permissions = self._get_raw_permissions(uuid, location)
154
        return image_to_dict(location, metadata, permissions)
155

  
156
    @handle_pithos_backend
157
    def add_property(self, uuid, key, value):
158
        location, _ = self._get_raw_metadata(uuid)
159
        properties = self._prefix_properties({key: value})
160
        self._update_metadata(uuid, location, properties, replace=False)
161

  
162
    @handle_pithos_backend
163
    def remove_property(self, uuid, key):
164
        location, _ = self._get_raw_metadata(uuid)
165
        # Use empty string to delete a property
166
        properties = self._prefix_properties({key: ""})
167
        self._update_metadata(uuid, location, properties, replace=False)
168

  
169
    @handle_pithos_backend
170
    def update_properties(self, uuid, properties):
171
        location, _ = self._get_raw_metadata(uuid)
172
        properties = self._prefix_properties(properties)
173
        self._update_metadata(uuid, location, properties, replace=False)
174

  
175
    @staticmethod
176
    def _prefix_properties(properties):
177
        """Add property prefix to properties."""
178
        return dict([(PROPERTY_PREFIX + k, v) for k, v in properties.items()])
179

  
180
    @staticmethod
181
    def _unprefix_properties(properties):
182
        """Remove property prefix from properties."""
183
        return dict([(k.replace(PROPERTY_PREFIX, "", 1), v)
184
                     for k, v in properties.items()])
185

  
186
    @staticmethod
187
    def _prefix_metadata(metadata):
188
        """Add plankton prefix to metadata."""
189
        return dict([(PLANKTON_PREFIX + k, v) for k, v in metadata.items()])
190

  
191
    @staticmethod
192
    def _unprefix_metadata(metadata):
193
        """Remove plankton prefix from metadata."""
194
        return dict([(k.replace(PLANKTON_PREFIX, "", 1), v)
195
                     for k, v in metadata.items()])
196

  
197
    @handle_pithos_backend
198
    def update_metadata(self, uuid, metadata):
199
        location, _ = self._get_raw_metadata(uuid)
169 200

  
170
    def _get_image_url(self, image_uuid):
171
        """Get the Pithos url that corresponds to an image UUID."""
172
        account, container, name = self.backend.get_uuid(self.user, image_uuid)
173
        return create_url(account, container, name)
201
        is_public = metadata.pop("is_public", None)
202
        if is_public is not None:
203
            self._set_public(uuid, location, public=is_public)
174 204

  
175
    def _get_image(self, image_url):
176
        """Get information about an Image.
205
        # Each property is stored as a separate prefixed metadata
206
        meta = deepcopy(metadata)
207
        properties = meta.pop("properties", {})
208
        meta.update(self._prefix_properties(properties))
177 209

  
178
        Get all available information about an Image.
179
        """
180
        account, container, name = split_url(image_url)
181
        try:
182
            meta = self._get_meta(image_url)
183
            meta["deleted"] = ""
184
        except NameError:
185
            versions = self.backend.list_versions(self.user, account,
186
                                                  container, name)
187
            if not versions:
188
                raise Exception("Image without versions %s" % image_url)
189
            # Object was deleted, use the latest version
190
            version, timestamp = versions[-1]
191
            meta = self._get_meta(image_url, version)
192
            meta["deleted"] = timestamp
193

  
194
        # XXX: Check that an object is a plankton image! PithosBackend will
195
        # return common metadata for an object, even if it has no metadata in
196
        # plankton domain. All images must have a name, so we check if a file
197
        # is an image by checking if they are having an image name.
198
        if PLANKTON_PREFIX + 'name' not in meta:
199
            raise ImageNotFound
200

  
201
        permissions = self._get_permissions(image_url)
202
        return image_to_dict(image_url, meta, permissions)
203

  
204
    def _get_meta(self, image_url, version=None):
205
        """Get object's metadata."""
206
        account, container, name = split_url(image_url)
207
        return self.backend.get_object_meta(self.user, account, container,
208
                                            name, PLANKTON_DOMAIN, version)
209

  
210
    def _update_meta(self, image_url, meta, replace=False):
211
        """Update object's metadata."""
212
        account, container, name = split_url(image_url)
213

  
214
        prefixed = [(PLANKTON_PREFIX + uenc(k), uenc(v))
215
                    for k, v in meta.items()
216
                    if k in PLANKTON_META or k.startswith(PROPERTY_PREFIX)]
217
        prefixed = dict(prefixed)
218

  
219
        for k, v in prefixed.items():
210
        self._update_metadata(uuid, location, metadata=meta, replace=False)
211

  
212
        return self._get_image(uuid)
213

  
214
    def _update_metadata(self, uuid, location, metadata, replace=False):
215
        _prefixed_metadata = self._prefix_metadata(metadata)
216
        prefixed = {}
217
        for k, v in _prefixed_metadata.items():
218
            # Encode to UTF-8
219
            k, v = uenc(k), uenc(v)
220
            # Check the length of key/value
220 221
            if len(k) > 128:
221
                raise InvalidMetadata('Metadata keys should be less than %s '
222
                                      'characters' % MAX_META_KEY_LENGTH)
222
                raise faults.BadRequest('Metadata keys should be less than %s'
223
                                        ' characters' % MAX_META_KEY_LENGTH)
223 224
            if len(v) > 256:
224
                raise InvalidMetadata('Metadata values should be less than %s '
225
                                      'characters.' % MAX_META_VALUE_LENGTH)
225
                raise faults.BadRequest('Metadata values should be less than'
226
                                        ' %scharacters.'
227
                                        % MAX_META_VALUE_LENGTH)
228
            prefixed[k] = v
226 229

  
227
        self.backend.update_object_meta(self.user, account, container, name,
230
        account, container, path = location
231
        self.backend.update_object_meta(self.user, account, container, path,
228 232
                                        PLANKTON_DOMAIN, prefixed, replace)
229
        logger.debug("User '%s' updated image '%s', meta: '%s'", self.user,
230
                     image_url, prefixed)
231

  
232
    def _get_permissions(self, image_url):
233
        """Get object's permissions."""
234
        account, container, name = split_url(image_url)
235
        _a, path, permissions = \
236
            self.backend.get_object_permissions(self.user, account, container,
237
                                                name)
238

  
239
        if path is None and permissions != {}:
240
            logger.warning("Image '%s' got permissions '%s' from 'None' path.",
241
                           image_url, permissions)
242
            raise Exception("Database Inconsistency Error:"
243
                            " Image '%s' got permissions from 'None' path." %
244
                            image_url)
245

  
246
        return permissions
247

  
248
    def _update_permissions(self, image_url, permissions):
249
        """Update object's permissions."""
250
        account, container, name = split_url(image_url)
251
        self.backend.update_object_permissions(self.user, account, container,
252
                                               name, permissions)
253
        logger.debug("User '%s' updated image '%s', permissions: '%s'",
254
                     self.user, image_url, permissions)
233
        logger.debug("User '%s' updated image '%s', metadata: '%s'", self.user,
234
                     uuid, prefixed)
255 235

  
256
    @handle_backend_exceptions
257
    @commit_on_success
258
    def unregister(self, image_uuid):
259
        """Unregister an image.
236
    def _get_raw_metadata(self, uuid, version=None, check_image=True):
237
        """Get info and metadata in Plankton doamin for the Pithos object.
260 238

  
261
        Unregister an image, by removing all metadata from the Pithos
262
        file that exist in the PLANKTON_DOMAIN.
239
        Return the location and the metadata of the Pithos object.
240
        If 'check_image' is set, check that the Pithos object is a registered
241
        Plankton Image.
263 242

  
264 243
        """
265
        image_url = self._get_image_url(image_uuid)
266
        self._get_image(image_url)  # Assert that it is an image
267
        # Unregister the image by removing all metadata from domain
268
        # 'PLANKTON_DOMAIN'
269
        meta = {}
270
        self._update_meta(image_url, meta, True)
271
        logger.debug("User '%s' deleted image '%s'", self.user, image_url)
272

  
273
    @handle_backend_exceptions
274
    @commit_on_success
275
    def add_user(self, image_uuid, add_user):
276
        """Add a user as an image member.
277

  
278
        Update read permissions of Pithos file, to include the specified user.
279

  
280
        """
281
        image_url = self._get_image_url(image_uuid)
282
        self._get_image(image_url)  # Assert that it is an image
283
        permissions = self._get_permissions(image_url)
244
        # Convert uuid to location
245
        account, container, path = self.backend.get_uuid(self.user, uuid)
246
        try:
247
            meta = self.backend.get_object_meta(self.user, account, container,
248
                                                path, PLANKTON_DOMAIN, version)
249
            meta["deleted"] = False
250
        except NameError:
251
            if version is not None:
252
                raise
253
            versions = self.backend.list_versions(self.user, account,
254
                                                  container, path)
255
            assert(versions), ("Object without versions: %s/%s/%s" %
256
                               (account, container, path))
257
            # Object was deleted, use the latest version
258
            version, timestamp = versions[-1]
259
            meta = self.backend.get_object_meta(self.user, account, container,
260
                                                path, PLANKTON_DOMAIN, version)
261
            meta["deleted"] = True
262

  
263
        if check_image and PLANKTON_PREFIX + "name" not in meta:
264
            # Check that object is an image by checking if it has an Image name
265
            # in Plankton metadata
266
            raise faults.ItemNotFound("Image '%s' does not exist." % uuid)
267

  
268
        return Location(account, container, path), meta
269

  
270
    # Users and Permissions
271
    @handle_pithos_backend
272
    def add_user(self, uuid, user):
273
        assert(isinstance(user, basestring))
274
        location, _ = self._get_raw_metadata(uuid)
275
        permissions = self._get_raw_permissions(uuid, location)
284 276
        read = set(permissions.get("read", []))
285
        assert(isinstance(add_user, (str, unicode)))
286
        read.add(add_user)
287
        permissions["read"] = list(read)
288
        self._update_permissions(image_url, permissions)
277
        if not user in read:
278
            read.add(user)
279
            permissions["read"] = list(read)
280
            self._update_permissions(uuid, location, permissions)
289 281

  
290
    @handle_backend_exceptions
291
    @commit_on_success
292
    def remove_user(self, image_uuid, remove_user):
293
        """Remove the user from image members.
282
    @handle_pithos_backend
283
    def remove_user(self, uuid, user):
284
        assert(isinstance(user, basestring))
285
        location, _ = self._get_raw_metadata(uuid)
286
        permissions = self._get_raw_permissions(uuid, location)
287
        read = set(permissions.get("read", []))
288
        if user in read:
289
            read.remove(user)
290
            permissions["read"] = list(read)
291
            self._update_permissions(uuid, location, permissions)
294 292

  
295
        Remove the specified user from the read permissions of the Pithos file.
293
    @handle_pithos_backend
294
    def replace_users(self, uuid, users):
295
        assert(isinstance(users, list))
296
        location, _ = self._get_raw_metadata(uuid)
297
        permissions = self._get_raw_permissions(uuid, location)
298
        read = set(permissions.get("read", []))
299
        if "*" in read:  # Retain public permissions
300
            users.append("*")
301
        permissions["read"] = list(users)
302
        self._update_permissions(uuid, location, permissions)
303

  
304
    @handle_pithos_backend
305
    def list_users(self, uuid):
306
        location, _ = self._get_raw_metadata(uuid)
307
        permissions = self._get_raw_permissions(uuid, location)
308
        return [user for user in permissions.get('read', []) if user != '*']
296 309

  
297
        """
298
        image_url = self._get_image_url(image_uuid)
299
        self._get_image(image_url)  # Assert that it is an image
300
        permissions = self._get_permissions(image_url)
310
    def _set_public(self, uuid, location, public):
311
        permissions = self._get_raw_permissions(uuid, location)
312
        assert(isinstance(public, bool))
301 313
        read = set(permissions.get("read", []))
302
        assert(isinstance(remove_user, (str, unicode)))
303
        try:
304
            read.remove(remove_user)
305
        except ValueError:
306
            return  # TODO: User did not have access
314
        if public and "*" not in read:
315
            read.add("*")
316
        elif not public and "*" in read:
317
            read.discard("*")
307 318
        permissions["read"] = list(read)
308
        self._update_permissions(image_url, permissions)
309

  
310
    @handle_backend_exceptions
311
    @commit_on_success
312
    def replace_users(self, image_uuid, replace_users):
313
        """Replace image members.
314

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

  
318
        """
319
        image_url = self._get_image_url(image_uuid)
320
        image = self._get_image(image_url)
321
        permissions = self._get_permissions(image_url)
322
        assert(isinstance(replace_users, list))
323
        permissions["read"] = replace_users
324
        if image.get("is_public", False):
325
            permissions["read"].append("*")
326
        self._update_permissions(image_url, permissions)
327

  
328
    @handle_backend_exceptions
329
    @commit_on_success
330
    def list_users(self, image_uuid):
331
        """List the image members.
332

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

  
336
        """
337
        image_url = self._get_image_url(image_uuid)
338
        self._get_image(image_url)  # Assert that it is an image
339
        permissions = self._get_permissions(image_url)
340
        return [user for user in permissions.get('read', []) if user != '*']
319
        self._update_permissions(uuid, location, permissions)
320
        return permissions
341 321

  
342
    @handle_backend_exceptions
343
    @commit_on_success
344
    def update_metadata(self, image_uuid, metadata):
345
        """Update Image metadata."""
346
        image_url = self._get_image_url(image_uuid)
347
        self._get_image(image_url)  # Assert that it is an image
322
    def _get_raw_permissions(self, uuid, location):
323
        account, container, path = location
324
        _a, path, permissions = \
325
            self.backend.get_object_permissions(self.user, account, container,
326
                                                path)
348 327

  
349
        # 'is_public' metadata is translated in proper file permissions
350
        is_public = metadata.pop("is_public", None)
351
        if is_public is not None:
352
            permissions = self._get_permissions(image_url)
353
            read = set(permissions.get("read", []))
354
            if is_public:
355
                read.add("*")
356
            else:
357
                read.discard("*")
358
            permissions["read"] = list(read)
359
            self._update_permissions(image_url, permissions)
328
        if path is None and permissions != {}:
329
            raise Exception("Database Inconsistency Error:"
330
                            " Image '%s' got permissions from 'None' path." %
331
                            uuid)
360 332

  
361
        # Extract the properties dictionary from metadata, and store each
362
        # property as a separeted, prefixed metadata
363
        properties = metadata.pop("properties", {})
364
        meta = dict([(PROPERTY_PREFIX + k, v) for k, v in properties.items()])
365
        # Also add the following metadata
366
        meta.update(**metadata)
333
        return permissions
367 334

  
368
        self._update_meta(image_url, meta)
369
        image_url = self._get_image_url(image_uuid)
370
        return self._get_image(image_url)
335
    def _update_permissions(self, uuid, location, permissions):
336
        account, container, path = location
337
        self.backend.update_object_permissions(self.user, account, container,
338
                                               path, permissions)
339
        logger.debug("User '%s' updated image '%s' permissions: '%s'",
340
                     self.user, uuid, permissions)
371 341

  
372
    @handle_backend_exceptions
373
    @commit_on_success
342
    @handle_pithos_backend
374 343
    def register(self, name, image_url, metadata):
375 344
        # Validate that metadata are allowed
376 345
        if "id" in metadata:
377
            raise ValueError("Passing an ID is not supported")
346
            raise faults.BadRequest("Passing an ID is not supported")
378 347
        store = metadata.pop("store", "pithos")
379 348
        if store != "pithos":
380
            raise ValueError("Invalid store '%s'. Only 'pithos' store is"
381
                             "supported" % store)
349
            raise faults.BadRequest("Invalid store '%s'. Only 'pithos' store"
350
                                    " is supported" % store)
382 351
        disk_format = metadata.setdefault("disk_format",
383 352
                                          settings.DEFAULT_DISK_FORMAT)
384 353
        if disk_format not in settings.ALLOWED_DISK_FORMATS:
385
            raise ValueError("Invalid disk format '%s'" % disk_format)
354
            raise faults.BadRequest("Invalid disk format '%s'" % disk_format)
386 355
        container_format =\
387 356
            metadata.setdefault("container_format",
388 357
                                settings.DEFAULT_CONTAINER_FORMAT)
389 358
        if container_format not in settings.ALLOWED_CONTAINER_FORMATS:
390
            raise ValueError("Invalid container format '%s'" %
391
                             container_format)
392

  
393
        # Validate that 'size' and 'checksum' are valid
394
        account, container, object = split_url(image_url)
359
            raise faults.BadRequest("Invalid container format '%s'" %
360
                                    container_format)
395 361

  
396
        meta = self._get_meta(image_url)
362
        account, container, path = split_url(image_url)
363
        location = Location(account, container, path)
364
        meta = self.backend.get_object_meta(self.user, account, container,
365
                                            path, PLANKTON_DOMAIN, None)
366
        uuid = meta["uuid"]
397 367

  
398
        size = int(metadata.pop('size', meta['bytes']))
399
        if size != meta['bytes']:
400
            raise ValueError("Invalid size")
368
        # Validate that 'size' and 'checksum'
369
        size = metadata.pop('size', int(meta['bytes']))
370
        if not isinstance(size, int) or int(size) != int(meta["bytes"]):
371
            raise faults.BadRequest("Invalid 'size' field")
401 372

  
402 373
        checksum = metadata.pop('checksum', meta['hash'])
403
        if checksum != meta['hash']:
404
            raise ValueError("Invalid checksum")
374
        if not isinstance(checksum, basestring) or checksum != meta['hash']:
375
            raise faults.BadRequest("Invalid checksum field")
376

  
377
        users = [self.user]
378
        public = metadata.pop("is_public", False)
379
        if not isinstance(public, bool):
380
            raise faults.BadRequest("Invalid value for 'is_public' metadata")
381
        if public:
382
            users.append("*")
383
        permissions = {'read': users}
384
        self._update_permissions(uuid, location, permissions)
385

  
386
        # Each property is stored as a separate prefixed metadata
387
        meta = deepcopy(metadata)
388
        properties = meta.pop("properties", {})
389
        meta.update(self._prefix_properties(properties))
390
        # Add extra metadata
391
        meta["name"] = name
392
        meta["status"] = "AVAILABLE"
393
        meta['created_at'] = str(time())
394
        #meta["is_snapshot"] = False
395
        self._update_metadata(uuid, location, metadata=meta, replace=False)
396

  
397
        logger.debug("User '%s' registered image '%s'('%s')", self.user,
398
                     uuid, location)
399
        return self._get_image(uuid)
400

  
401
    @handle_pithos_backend
402
    def unregister(self, uuid):
403
        """Unregister an Image.
404

  
405
        Unregister an Image by removing all the metadata in the Plankton
406
        domain. The Pithos file is not deleted.
405 407

  
406
        # Fix permissions
407
        is_public = metadata.pop('is_public', False)
408
        if is_public:
409
            permissions = {'read': ['*']}
410
        else:
411
            permissions = {'read': [self.user]}
412

  
413
        # Extract the properties dictionary from metadata, and store each
414
        # property as a separeted, prefixed metadata
415
        properties = metadata.pop("properties", {})
416
        meta = dict([(PROPERTY_PREFIX + k, v) for k, v in properties.items()])
417
        # Add creation(register) timestamp as a metadata, to avoid extra
418
        # queries when retrieving the list of images.
419
        meta['created_at'] = time()
420
        # Update rest metadata
421
        meta.update(name=name, status='available', **metadata)
422

  
423
        # Do the actualy update in the Pithos backend
424
        self._update_meta(image_url, meta)
425
        self._update_permissions(image_url, permissions)
426
        logger.debug("User '%s' created image '%s'('%s')", self.user,
427
                     image_url, name)
428
        return self._get_image(image_url)
408
        """
409
        location, _ = self._get_raw_metadata(uuid)
410
        self._update_metadata(uuid, location, metadata={}, replace=True)
411
        logger.debug("User '%s' unregistered image '%s'", self.user, uuid)
429 412

  
413
    # List functions
430 414
    def _list_images(self, user=None, filters=None, params=None):
431 415
        filters = filters or {}
432 416

  
......
445 429
                                                  user=user)
446 430

  
447 431
        images = []
448
        for (location, meta, permissions) in _images:
449
            image_url = "pithos://" + location
450
            meta["modified"] = meta["version_timestamp"]
451
            images.append(image_to_dict(image_url, meta, permissions))
432
        for (location, metadata, permissions) in _images:
433
            location = Location(*location.split("/", 2))
434
            images.append(image_to_dict(location, metadata, permissions))
452 435

  
453 436
        if params is None:
454 437
            params = {}
438

  
455 439
        key = itemgetter(params.get('sort_key', 'created_at'))
456 440
        reverse = params.get('sort_dir', 'desc') == 'desc'
457 441
        images.sort(key=key, reverse=reverse)
458 442
        return images
459 443

  
460
    @commit_on_success
444
    @handle_pithos_backend
461 445
    def list_images(self, filters=None, params=None):
462 446
        return self._list_images(user=self.user, filters=filters,
463 447
                                 params=params)
464 448

  
465
    @commit_on_success
449
    @handle_pithos_backend
466 450
    def list_shared_images(self, member, filters=None, params=None):
467 451
        images = self._list_images(user=self.user, filters=filters,
468 452
                                   params=params)
469 453
        is_shared = lambda img: not img["is_public"] and img["owner"] == member
470 454
        return filter(is_shared, images)
471 455

  
472
    @commit_on_success
456
    @handle_pithos_backend
473 457
    def list_public_images(self, filters=None, params=None):
474 458
        images = self._list_images(user=None, filters=filters, params=params)
475 459
        return filter(lambda img: img["is_public"], images)
476 460

  
461
    # # Snapshots
462
    # def list_snapshots(self, user=None):
463
    #     _snapshots = self.list_images()
464
    #     return [s for s in _snapshots if s["is_snapshot"]]
465

  
466
    # @handle_pithos_backend
467
    # def get_snapshot(self, user, snapshot_uuid):
468
    #     snap = self._get_image(snapshot_uuid)
469
    #     if snap.get("is_snapshot", False) is False:
470
    #         raise faults.ItemNotFound("Snapshots '%s' does not exist" %
471
    #                                   snapshot_uuid)
472
    #     return snap
473

  
474
    # @handle_pithos_backend
475
    # def delete_snapshot(self, snapshot_uuid):
476
    #     self.backend.delete_object_for_uuid(self.user, snapshot_uuid)
477

  
478
    # @handle_pithos_backend
479
    # def update_status(self, image_uuid, status):
480
    #     """Update status of snapshot"""
481
    #     location, _ = self._get_raw_metadata(image_uuid)
482
    #     properties = {"status": status.upper()}
483
    #     self._update_metadata(image_uuid, location, properties,
484
    #     replace=False)
485
    #     return self._get_image(image_uuid)
477 486

  
478
class ImageBackendError(Exception):
479
    pass
480 487

  
481

  
482
class ImageNotFound(ImageBackendError):
483
    pass
484

  
485

  
486
class Forbidden(ImageBackendError):
487
    pass
488

  
489

  
490
class InvalidMetadata(ImageBackendError):
491
    pass
488
def create_url(account, container, name):
489
    """Create a Pithos URL from the object info"""
490
    assert "/" not in account, "Invalid account"
491
    assert "/" not in container, "Invalid container"
492
    return "pithos://%s/%s/%s" % (account, container, name)
492 493

  
493 494

  
494
class InvalidLocation(ImageBackendError):
495
    pass
495
def split_url(url):
496
    """Get object info from the Pithos URL"""
497
    assert(isinstance(url, basestring))
498
    t = url.split('/', 4)
499
    assert t[0] == "pithos:", "Invalid url"
500
    assert len(t) == 5, "Invalid url"
501
    return t[2:5]
496 502

  
497 503

  
498
def image_to_dict(image_url, meta, permissions):
504
def image_to_dict(location, metadata, permissions):
499 505
    """Render an image to a dictionary"""
500
    account, container, name = split_url(image_url)
506
    account, container, name = location
501 507

  
502 508
    image = {}
503
    if PLANKTON_PREFIX + 'name' not in meta:
504
        logger.warning("Image without Plankton name!! url %s meta %s",
505
                       image_url, meta)
506
        image[PLANKTON_PREFIX + "name"] = ""
507

  
508
    image["id"] = meta["uuid"]
509
    image["location"] = image_url
510
    image["checksum"] = meta["hash"]
511
    created = meta.get("created_at", meta["modified"])
512
    image["created_at"] = format_timestamp(created)
513
    deleted = meta.get("deleted", None)
514
    image["deleted_at"] = format_timestamp(deleted) if deleted else ""
515
    image["updated_at"] = format_timestamp(meta["modified"])
516
    image["size"] = meta["bytes"]
517
    image["store"] = "pithos"
509
    image["id"] = metadata["uuid"]
510
    image["mapfile"] = metadata["hash"]
511
    image["checksum"] = metadata["hash"]
512
    image["location"] = create_url(account, container, name)
513
    image["size"] = metadata["bytes"]
518 514
    image['owner'] = account
519

  
515
    image["store"] = u"pithos"
516
    #image["is_snapshot"] = metadata.pop(PLANKTON_PREFIX + "is_snapshot",
517
    #False)
520 518
    # Permissions
521
    image["is_public"] = "*" in permissions.get('read', [])
519
    users = list(permissions.get("read", []))
520
    image["is_public"] = "*" in users
521
    image["users"] = [u for u in users if u != "*"]
522
    # Timestamps
523
    updated_at = metadata["version_timestamp"]
524
    created_at = metadata.get("created_at", updated_at)
525
    image["created_at"] = format_timestamp(created_at)
526
    image["updated_at"] = format_timestamp(updated_at)
527
    if metadata.get("deleted", False):
528
        image["deleted_at"] = image["updated_at"]
529
    else:
530
        image["deleted_at"] = ""
522 531

  
523 532
    properties = {}
524
    for key, val in meta.items():
533
    for key, val in metadata.items():
525 534
        # Get plankton properties
526 535
        if key.startswith(PLANKTON_PREFIX):
527 536
            # Remove plankton prefix
528 537
            key = key.replace(PLANKTON_PREFIX, "")
529
            # Keep only those in plankton meta
538
            # Keep only those in plankton metadata
530 539
            if key in PLANKTON_META:
531 540
                if key != "created_at":
532 541
                    # created timestamp is return in 'created_at' field
......
583 592
    backend_module = getattr(settings, 'PLANKTON_BACKEND_MODULE', None)
584 593
    if not backend_module:
585 594
        # no setting set
586
        return ImageBackend
595
        return PlanktonBackend
587 596

  
588 597
    parts = backend_module.split(".")
589 598
    module = ".".join(parts[:-1])
b/snf-cyclades-app/synnefo/plankton/management/commands/image-list.py
32 32
from optparse import make_option
33 33

  
34 34
from snf_django.management.utils import pprint_table
35
from synnefo.plankton.utils import image_backend
35
from synnefo.plankton.backend import PlanktonBackend
36 36

  
37 37

  
38 38
class Command(BaseCommand):
......
50 50
    def handle(self, **options):
51 51
        user = options['userid']
52 52

  
53
        with image_backend(user) as backend:
53
        with PlanktonBackend(user) as backend:
54 54
            images = backend._list_images(user)
55 55
            images.sort(key=lambda x: x['created_at'], reverse=True)
56 56

  
b/snf-cyclades-app/synnefo/plankton/management/commands/image-show.py
1
# Copyright 2012 GRNET S.A. All rights reserved.
1
# Copyright 2012-2014 GRNET S.A. All rights reserved.
2 2
#
3 3
# Redistribution and use in source and binary forms, with or without
4 4
# modification, are permitted provided that the following conditions
......
30 30

  
31 31
from django.core.management.base import BaseCommand, CommandError
32 32

  
33
from synnefo.plankton.utils import image_backend
33
from synnefo.plankton.backend import PlanktonBackend
34 34
from snf_django.management import utils
35 35

  
36 36

  
......
44 44
            raise CommandError("Please provide an image ID")
45 45
        image_id = args[0]
46 46

  
47
        with image_backend(None) as backend:
48
            images = backend._list_images(None)
49
            try:
50
                image = filter(lambda x: x["id"] == image_id, images)[0]
51
            except IndexError:
52
                raise CommandError("Image not found. Use snf-manage image-list"
53
                                   " to get the list of all images.")
54
        utils.pprint_table(out=self.stdout, table=[image.values()], headers=image.keys(), vertical=True)
47
        with PlanktonBackend(None) as backend:
48
            image = backend.get_image(image_id)
49
        utils.pprint_table(out=self.stdout, table=[image.values()],
50
                           headers=image.keys(), vertical=True)
b/snf-cyclades-app/synnefo/plankton/tests.py
1
# Copyright 2012 GRNET S.A. All rights reserved.
1
# Copyright 2012-2014 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
......
36 36
from mock import patch
37 37
from functools import wraps
38 38
from copy import deepcopy
39
from decimal import Decimal
39 40
from snf_django.utils.testing import BaseAPITest
40 41
from synnefo.cyclades_settings import cyclades_services
41 42
from synnefo.lib.services import get_service_path
42 43
from synnefo.lib import join_urls
43 44

  
44

  
45
class PlanktonAPITest(BaseAPITest):
46
    def setUp(self, *args, **kwargs):
47
        super(PlanktonAPITest, self).setUp(*args, **kwargs)
48
        self.api_path = get_service_path(cyclades_services, 'image',
49
                                             version='v1.0')
50
    def myget(self, path, *args, **kwargs):
51
        path = join_urls(self.api_path, path)
52
        return self.get(path, *args, **kwargs)
53

  
54
    def myput(self, path, *args, **kwargs):
55
        path = join_urls(self.api_path, path)
56
        return self.put(path, *args, **kwargs)
57

  
58
    def mypost(self, path, *args, **kwargs):
59
        path = join_urls(self.api_path, path)
60
        return self.post(path, *args, **kwargs)
61

  
62
    def mydelete(self, path, *args, **kwargs):
63
        path = join_urls(self.api_path, path)
64
        return self.delete(path, *args, **kwargs)
65

  
66

  
67
FILTERS = ('name', 'container_format', 'disk_format', 'status', 'size_min',
68
           'size_max')
69
PARAMS = ('sort_key', 'sort_dir')
70
SORT_KEY_OPTIONS = ('id', 'name', 'status', 'size', 'disk_format',
71
                    'container_format', 'created_at', 'updated_at')
72
SORT_DIR_OPTIONS = ('asc', 'desc')
73
LIST_FIELDS = ('status', 'name', 'disk_format', 'container_format', 'size',
74
               'id')
75
DETAIL_FIELDS = ('name', 'disk_format', 'container_format', 'size', 'checksum',
76
                 'location', 'created_at', 'updated_at', 'deleted_at',
77
                 'status', 'is_public', 'owner', 'properties', 'id')
78
ADD_FIELDS = ('name', 'id', 'store', 'disk_format', 'container_format', 'size',
79
              'checksum', 'is_public', 'owner', 'properties', 'location')
80
UPDATE_FIELDS = ('name', 'disk_format', 'container_format', 'is_public',
81
                 'owner', 'properties', 'status')
82

  
83

  
84
DummyImages = {
85
 '0786a349-9725-48ec-8b86-8598eefc4043':
86
 {'checksum': u'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
87
  u'container_format': u'bare',
88
  'created_at': '2012-12-04 09:50:20',
89
  'deleted_at': '',
90
  u'disk_format': u'diskdump',
91
  'id': u'0786a349-9725-48ec-8b86-8598eefc4043',
92
  'is_public': True,
93
  'location': u'pithos://foo@example.com/container/foo3',
94
  u'name': u'dummyname',
95
  'owner': u'foo@example.com',
96
  'properties': {},
97
  'size': 500L,
98
  u'status': u'available',
99
  'store': 'pithos',
100
  'updated_at': '2012-12-04 09:50:54'},
101

  
102
 'd8aa85b8-410b-4550-953d-6797572534e6':
103
 {'checksum': u'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855',
104
  u'container_format': u'bare',
105
  'created_at': '2012-11-26 11:56:42',
106
  'deleted_at': '',
107
  u'disk_format': u'diskdump',
108
  'id': u'd8aa85b8-410b-4550-953d-6797572534e6',
109
  'is_public': False,
110
  'location': u'pithos://foo@example.com/container/private',
111
  u'name': u'dummyname2',
112
  'owner': u'foo@example.com',
113
  'properties': {},
114
  'size': 10000L,
115
  u'status': u'available',
116
  'store': 'pithos',
117
  'updated_at': '2012-11-26 11:57:09'},
118

  
119
 '264fb9ac-2458-421c-b460-6a765a92825c':
120
 {'checksum': u'0c6d0586744781218672fff2d7ed94cc32efb02a6a8eb589a0628f0e22bd5a7f',
121
  u'container_format': u'bare',
122
  'created_at': '2012-11-26 11:52:54',
123
  'deleted_at': '',
124
  u'disk_format': u'diskdump',
125
  'id': u'264fb9ac-2458-421c-b460-6a765a92825c',
126
  'is_public': True,
127
  'location': u'pithos://foo@example.com/container/baz.diskdump',
128
  u'name': u'"dummyname3"',
129
  'owner': u'foo@example.com',
130
  'properties': {u'description': u'Debian Squeeze Base System',
131
                 u'gui': u'No GUI',
132
                 u'kernel': u'2.6.32',
133
                 u'os': u'debian',
134
                 u'osfamily': u'linux',
135
                 u'root_partition': u'1',
136
                 u'size': u'451',
137
                 u'sortorder': u'1',
138
                 u'users': u'root'},
139
  'size': 473772032L,
140
  u'status': u'available',
141
  'store': 'pithos',
142
  'updated_at': '2012-11-26 11:55:40'}}
45
PLANKTON_URL = get_service_path(cyclades_services, 'image',
46
                                version='v1.0')
47
IMAGES_URL = join_urls(PLANKTON_URL, "images/")
143 48

  
144 49

  
145 50
def assert_backend_closed(func):
......
152 57
    return wrapper
153 58

  
154 59

  
155
@patch("synnefo.plankton.backend.ImageBackend")
156
class PlanktonTest(PlanktonAPITest):
157
    @assert_backend_closed
158
    def test_list_images(self, backend):
159
        backend.return_value.list_images.return_value =\
160
                deepcopy(DummyImages).values()
161
        response = self.myget("images/")
162
        self.assertSuccess(response)
163
        images = json.loads(response.content)
164
        for api_image in images:
165
            id = api_image['id']
166
            pithos_image = dict([(key, val)\
167
                                for key, val in DummyImages[id].items()\
168
                                if key in LIST_FIELDS])
169
            self.assertEqual(api_image, pithos_image)
170
        backend.return_value\
171
                .list_images.assert_called_once_with({}, {'sort_key': 'created_at',
172
                                                   'sort_dir': 'desc'})
60
@patch("synnefo.plankton.backend.get_pithos_backend")
61
class PlanktonTest(BaseAPITest):
62
    def test_register_image(self, backend):
63
        required = {
64
            "HTTP_X_IMAGE_META_NAME": u"TestImage\u2602",
65
            "HTTP_X_IMAGE_META_LOCATION": "pithos://4321-4321/images/foo"}
66
        # Check valid name
67
        headers = deepcopy(required)
68
        headers.pop("HTTP_X_IMAGE_META_NAME")
69
        response = self.post(IMAGES_URL, **headers)
70
        self.assertBadRequest(response)
71
        self.assertTrue("name" in response.content)
72
        headers["HTTP_X_IMAGE_META_NAME"] = ""
73
        response = self.post(IMAGES_URL, **headers)
74
        self.assertBadRequest(response)
75
        self.assertTrue("name" in response.content)
76
        # Check valid location
77
        headers = deepcopy(required)
78
        headers.pop("HTTP_X_IMAGE_META_LOCATION")
79
        response = self.post(IMAGES_URL, **headers)
80
        self.assertBadRequest(response)
81
        self.assertTrue("location" in response.content)
82
        headers["HTTP_X_IMAGE_META_LOCATION"] = ""
83
        response = self.post(IMAGES_URL, **headers)
84
        self.assertBadRequest(response)
... This diff was truncated because it exceeds the maximum size that can be displayed.

Also available in: Unified diff