Commit 2556931f authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

Merge branch 'feature-plankton-fixes' into develop

parents c117548d 011eae89
......@@ -32,6 +32,7 @@
# or implied, of GRNET S.A.
from logging import getLogger
from itertools import ifilter
from dateutil.parser import parse as date_parse
......@@ -118,18 +119,14 @@ def list_images(request, detail=False):
# overLimit (413)
log.debug('list_images detail=%s', detail)
since = utils.isoparse(request.GET.get('changes-since'))
with image_backend(request.user_uniq) as backend:
since = utils.isoparse(request.GET.get('changes-since'))
images = backend.list_images()
if since:
images = []
for image in backend.iter():
updated = date_parse(image['updated_at'])
if updated >= since:
images.append(image)
updated_since = lambda img: date_parse(img["updated_at"]) >= since
images = ifilter(updated_since, images)
if not images:
return HttpResponse(status=304)
else:
images = backend.list()
images = sorted(images, key=lambda x: x['id'])
reply = [image_to_dict(image, detail) for image in images]
......@@ -172,7 +169,8 @@ def get_image_details(request, image_id):
# overLimit (413)
log.debug('get_image_details %s', image_id)
image = util.get_image(image_id, request.user_uniq)
with image_backend(request.user_uniq) as backend:
image = backend.get_image(image_id)
reply = image_to_dict(image)
if request.serialization == 'xml':
......@@ -209,7 +207,8 @@ def list_metadata(request, image_id):
# overLimit (413)
log.debug('list_image_metadata %s', image_id)
image = util.get_image(image_id, request.user_uniq)
with image_backend(request.user_uniq) as backend:
image = backend.get_image(image_id)
metadata = image['properties']
return util.render_metadata(request, metadata, use_values=True, status=200)
......@@ -227,18 +226,18 @@ def update_metadata(request, image_id):
req = utils.get_request_dict(request)
log.info('update_image_metadata %s %s', image_id, req)
image = util.get_image(image_id, request.user_uniq)
try:
metadata = req['metadata']
assert isinstance(metadata, dict)
except (KeyError, AssertionError):
raise faults.BadRequest('Malformed request.')
with image_backend(request.user_uniq) as backend:
image = backend.get_image(image_id)
try:
metadata = req['metadata']
assert isinstance(metadata, dict)
except (KeyError, AssertionError):
raise faults.BadRequest('Malformed request.')
properties = image['properties']
properties.update(metadata)
properties = image['properties']
properties.update(metadata)
with image_backend(request.user_uniq) as backend:
backend.update(image_id, dict(properties=properties))
backend.update_metadata(image_id, dict(properties=properties))
return util.render_metadata(request, properties, status=201)
......@@ -254,7 +253,8 @@ def get_metadata_item(request, image_id, key):
# overLimit (413)
log.debug('get_image_metadata_item %s %s', image_id, key)
image = util.get_image(image_id, request.user_uniq)
with image_backend(request.user_uniq) as backend:
image = backend.get_image(image_id)
val = image['properties'].get(key)
if val is None:
raise faults.ItemNotFound('Metadata key not found.')
......@@ -284,12 +284,12 @@ def create_metadata_item(request, image_id, key):
raise faults.BadRequest('Malformed request.')
val = metadict[key]
image = util.get_image(image_id, request.user_uniq)
properties = image['properties']
properties[key] = val
with image_backend(request.user_uniq) as backend:
backend.update(image_id, dict(properties=properties))
image = backend.get_image(image_id)
properties = image['properties']
properties[key] = val
backend.update_metadata(image_id, dict(properties=properties))
return util.render_meta(request, {key: val}, status=201)
......@@ -307,11 +307,11 @@ def delete_metadata_item(request, image_id, key):
# overLimit (413),
log.info('delete_image_metadata_item %s %s', image_id, key)
image = util.get_image(image_id, request.user_uniq)
properties = image['properties']
properties.pop(key, None)
with image_backend(request.user_uniq) as backend:
backend.update(image_id, dict(properties=properties))
image = backend.get_image(image_id)
properties = image['properties']
properties.pop(key, None)
backend.update_metadata(image_id, dict(properties=properties))
return HttpResponse(status=204)
......@@ -46,13 +46,12 @@ def assert_backend_closed(func):
def wrapper(self, backend):
result = func(self, backend)
if backend.called is True:
num = len(backend.mock_calls) / 2
assert(len(backend.return_value.close.mock_calls) == num)
backend.return_value.close.assert_called_once_with()
return result
return wrapper
@patch('synnefo.plankton.utils.ImageBackend')
@patch('synnefo.plankton.backend.ImageBackend')
class ImageAPITest(BaseAPITest):
@assert_backend_closed
def test_create_image(self, mimage):
......@@ -67,7 +66,7 @@ class ImageAPITest(BaseAPITest):
images = [{'id': 1, 'name': 'image-1'},
{'id': 2, 'name': 'image-2'},
{'id': 3, 'name': 'image-3'}]
mimage().list.return_value = images
mimage().list_images.return_value = images
response = self.get('/api/v1.1/images/', 'user')
self.assertSuccess(response)
api_images = json.loads(response.content)['images']['values']
......@@ -116,7 +115,7 @@ class ImageAPITest(BaseAPITest):
'progress': 100,
'created': '2012-11-26T11:52:54+00:00',
'updated': '2012-12-26T11:52:54+00:00'}]
mimage().list.return_value = images
mimage().list_images.return_value = images
response = self.get('/api/v1.1/images/detail', 'user')
self.assertSuccess(response)
api_images = json.loads(response.content)['images']['values']
......@@ -147,7 +146,7 @@ class ImageAPITest(BaseAPITest):
'updated_at': new_time.isoformat(),
'deleted_at': new_time.isoformat(),
'properties': ''}]
mimage().iter.return_value = images
mimage().list_images.return_value = images
response =\
self.get('/api/v1.1/images/detail?changes-since=%sUTC' % new_time)
self.assertSuccess(response)
......@@ -171,20 +170,19 @@ class ImageAPITest(BaseAPITest):
'created': '2012-11-26T11:52:54+00:00',
'updated': '2012-12-26T11:52:54+00:00',
'metadata': {'values': {'foo': 'bar'}}}
with patch('synnefo.api.util.get_image') as m:
m.return_value = image
response = self.get('/api/v1.1/images/42', 'user')
mimage.return_value.get_image.return_value = image
response = self.get('/api/v1.1/images/42', 'user')
self.assertSuccess(response)
api_image = json.loads(response.content)['image']
self.assertEqual(api_image, result_image)
@assert_backend_closed
def test_invalid_image(self, mimage):
with patch('synnefo.api.util.get_image') as m:
m.side_effect = faults.ItemNotFound('Image not found')
response = self.get('/api/v1.1/images/42', 'user')
mimage.return_value.get_image.side_effect = faults.ItemNotFound('Image not found')
response = self.get('/api/v1.1/images/42', 'user')
self.assertItemNotFound(response)
@assert_backend_closed
def test_delete_image(self, mimage):
response = self.delete("/api/v1.1/images/42", "user")
self.assertEqual(response.status_code, 204)
......@@ -192,7 +190,7 @@ class ImageAPITest(BaseAPITest):
mimage.return_value._delete.assert_not_called('42')
@patch('synnefo.plankton.utils.ImageBackend')
@patch('synnefo.plankton.backend.ImageBackend')
class ImageMetadataAPITest(BaseAPITest):
def setUp(self):
self.image = {'id': 42,
......@@ -237,7 +235,7 @@ class ImageMetadataAPITest(BaseAPITest):
backend.return_value.get_image.return_value = self.image
response = self.delete('/api/v1.1/images/42/meta/foo', 'user')
self.assertEqual(response.status_code, 204)
backend.return_value.update.assert_called_once_with('42', {'properties': {'foo2':
backend.return_value.update_metadata.assert_called_once_with('42', {'properties': {'foo2':
'bar2'}})
@assert_backend_closed
......@@ -247,7 +245,7 @@ class ImageMetadataAPITest(BaseAPITest):
response = self.put('/api/v1.1/images/42/meta/foo3', 'user',
json.dumps(request), 'json')
self.assertEqual(response.status_code, 201)
backend.return_value.update.assert_called_once_with('42',
backend.return_value.update_metadata.assert_called_once_with('42',
{'properties':
{'foo': 'bar', 'foo2': 'bar2', 'foo3': 'bar3'}})
......@@ -290,7 +288,7 @@ class ImageMetadataAPITest(BaseAPITest):
response = self.post('/api/v1.1/images/42/meta', 'user',
json.dumps(request), 'json')
self.assertEqual(response.status_code, 201)
backend.return_value.update.assert_called_once_with('42',
backend.return_value.update_metadata.assert_called_once_with('42',
{'properties':
{'foo': 'bar_new', 'foo2': 'bar2', 'foo4': 'bar4'}
})
......
......@@ -151,10 +151,7 @@ def get_image(image_id, user_id):
"""Return an Image instance or raise ItemNotFound."""
with image_backend(user_id) as backend:
image = backend.get_image(image_id)
if not image:
raise faults.ItemNotFound('Image not found.')
return image
return backend.get_image(image_id)
def get_image_dict(image_id, user_id):
......
......@@ -32,7 +32,7 @@ from django.core.management.base import BaseCommand
from optparse import make_option
from synnefo.webproject.management.utils import pprint_table
from synnefo.plankton.backend import ImageBackend
from synnefo.plankton.utils import image_backend
class Command(BaseCommand):
......@@ -41,17 +41,18 @@ class Command(BaseCommand):
make_option(
'--user-id',
dest='userid',
default=None,
help="List all images available to that user."
" If no user is specified, only public images"
" are displayed."),
)
def handle(self, **options):
userid = options['userid']
user = options['userid']
c = ImageBackend(userid) if userid else ImageBackend("")
images = c.list()
images.sort(key=lambda x: x['created_at'], reverse=True)
with image_backend(user) as backend:
images = backend._list_images(user)
images.sort(key=lambda x: x['created_at'], reverse=True)
headers = ("id", "name", "owner", "public")
table = []
......
......@@ -29,21 +29,14 @@
#
from django.core.management.base import BaseCommand, CommandError
from optparse import make_option
from synnefo.plankton.backend import ImageBackend
from pprint import pprint
from synnefo.plankton.utils import image_backend
from synnefo.webproject.management import utils
class Command(BaseCommand):
args = "<image_id>"
help = "Display available information about an image"
option_list = BaseCommand.option_list + (
make_option(
'--user-id',
dest='userid',
help="The ID of the owner of the Image."),
)
def handle(self, *args, **options):
......@@ -51,11 +44,11 @@ class Command(BaseCommand):
raise CommandError("Please provide an image ID")
image_id = args[0]
userid = options['userid']
c = ImageBackend(userid) if userid else ImageBackend("")
image = c.get_image(image_id)
if not image:
raise CommandError("Image not Found")
pprint(image, stream=self.stdout)
with image_backend(None) as backend:
images = backend._list_images(None)
try:
image = filter(lambda x: x["id"] == image_id, images)[0]
except IndexError:
raise CommandError("Image not found. Use snf-manage image-list"
" to get the list of all images.")
utils.pprint_table(out=self.stdout, table=[image.values()], headers=image.keys(), vertical=True)
......@@ -33,13 +33,10 @@
import json
from django.test import TestCase
from contextlib import contextmanager
from mock import patch
from functools import wraps
from copy import deepcopy
from snf_django.utils.testing import astakos_user, BaseAPITest
from snf_django.utils.testing import BaseAPITest
FILTERS = ('name', 'container_format', 'disk_format', 'status', 'size_min',
......@@ -130,11 +127,11 @@ def assert_backend_closed(func):
return wrapper
@patch("synnefo.plankton.utils.ImageBackend")
@patch("synnefo.plankton.backend.ImageBackend")
class PlanktonTest(BaseAPITest):
@assert_backend_closed
def test_list_images(self, backend):
backend.return_value.list.return_value =\
backend.return_value.list_images.return_value =\
deepcopy(DummyImages).values()
response = self.get("/plankton/images/")
self.assertSuccess(response)
......@@ -146,12 +143,12 @@ class PlanktonTest(BaseAPITest):
if key in LIST_FIELDS])
self.assertEqual(api_image, pithos_image)
backend.return_value\
.list.assert_called_once_with({}, {'sort_key': 'created_at',
.list_images.assert_called_once_with({}, {'sort_key': 'created_at',
'sort_dir': 'desc'})
@assert_backend_closed
def test_list_images_detail(self, backend):
backend.return_value.list.return_value =\
backend.return_value.list_images.return_value =\
deepcopy(DummyImages).values()
response = self.get("/plankton/images/detail")
self.assertSuccess(response)
......@@ -163,19 +160,19 @@ class PlanktonTest(BaseAPITest):
if key in DETAIL_FIELDS])
self.assertEqual(api_image, pithos_image)
backend.return_value\
.list.assert_called_once_with({}, {'sort_key': 'created_at',
.list_images.assert_called_once_with({}, {'sort_key': 'created_at',
'sort_dir': 'desc'})
@assert_backend_closed
def test_list_images_filters(self, backend):
backend.return_value.list.return_value =\
backend.return_value.list_images.return_value =\
deepcopy(DummyImages).values()
response = self.get("/plankton/images/?size_max=1000")
self.assertSuccess(response)
backend.return_value\
.list.assert_called_once_with({'size_max': 1000},
{'sort_key': 'created_at',
'sort_dir': 'desc'})
.list_images.assert_called_once_with({'size_max': 1000},
{'sort_key': 'created_at',
'sort_dir': 'desc'})
@assert_backend_closed
def test_list_images_filters_error_1(self, backend):
......@@ -194,8 +191,8 @@ class PlanktonTest(BaseAPITest):
json.dumps({}),
'json', HTTP_X_IMAGE_META_OWNER='user2')
self.assertSuccess(response)
backend.return_value.update.assert_called_once_with(db_image['id'],
{"owner": "user2"})
backend.return_value.update_metadata.assert_called_once_with(db_image['id'],
{"owner": "user2"})
@assert_backend_closed
def test_add_image_member(self, backend):
......@@ -216,15 +213,16 @@ class PlanktonTest(BaseAPITest):
@assert_backend_closed
def test_add_image(self, backend):
location = "pithos://uuid/container/name/"
response = self.post("/plankton/images/",
json.dumps({}),
'json',
HTTP_X_IMAGE_META_NAME='dummy_name',
HTTP_X_IMAGE_META_OWNER='dummy_owner',
HTTP_X_IMAGE_META_LOCATION='dummy_location')
HTTP_X_IMAGE_META_LOCATION=location)
self.assertSuccess(response)
backend.return_value.register.assert_called_once_with('dummy_name',
'dummy_location',
location,
{'owner': 'dummy_owner'})
@assert_backend_closed
......
# Copyright 2011 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
# conditions are met:
#
# 1. Redistributions of source code must retain the above
# copyright notice, this list of conditions and the following
# disclaimer.
#
# 2. Redistributions in binary form must reproduce the above
# copyright notice, this list of conditions and the following
# disclaimer in the documentation and/or other materials
# provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY GRNET S.A. ``AS IS'' AND ANY EXPRESS
# OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL GRNET S.A OR
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
# USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
# AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
# POSSIBILITY OF SUCH DAMAGE.
#
# The views and conclusions contained in the software and
# documentation are those of the authors and should not be
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from functools import wraps
from logging import getLogger
from traceback import format_exc
from django.conf import settings
from django.http import (HttpResponse, HttpResponseBadRequest,
HttpResponseServerError, HttpResponseForbidden)
from snf_django.lib.api import faults
from snf_django.lib.astakos import get_user
from synnefo.plankton.backend import (ImageBackend, BackendException,
NotAllowedError)
log = getLogger('synnefo.plankton')
def plankton_method(method):
def decorator(func):
@wraps(func)
def wrapper(request, *args, **kwargs):
try:
get_user(request, settings.ASTAKOS_URL)
if not request.user_uniq:
return HttpResponse(status=401)
if request.method != method:
return HttpResponse(status=405)
request.backend = ImageBackend(request.user_uniq)
return func(request, *args, **kwargs)
except (AssertionError, BackendException) as e:
message = e.args[0] if e.args else ''
return HttpResponseBadRequest(message)
except NotAllowedError:
return HttpResponseForbidden()
except faults.Fault, fault:
return HttpResponse(status=fault.code)
except Exception as e:
if settings.DEBUG:
message = format_exc(e)
else:
message = ''
log.exception(e)
return HttpResponseServerError(message)
finally:
if hasattr(request, 'backend'):
request.backend.close()
return wrapper
return decorator
# Copyright 2011 GRNET S.A. All rights reserved.
# Copyright 2011-2013 GRNET S.A. All rights reserved.
#
# Redistribution and use in source and binary forms, with or
# without modification, are permitted provided that the following
......@@ -31,29 +31,26 @@
# interpreted as representing official policies, either expressed
# or implied, of GRNET S.A.
from functools import wraps
from synnefo.plankton.backend import ImageBackend
from contextlib import contextmanager
def plankton_method(func):
"""Decorator function for API methods using ImageBackend.
Decorator function that creates and closes an ImageBackend, needed
by all API methods that handle images.
"""
@wraps(func)
def wrapper(request, *args, **kwargs):
with image_backend(request.user_uniq) as backend:
request.backend = backend
return func(request, *args, **kwargs)
return wrapper
from synnefo.plankton import backend
from snf_django.lib.api import faults
@contextmanager
def image_backend(user_id):
"""Context manager for ImageBackend"""
backend = ImageBackend(user_id)
"""Context manager for ImageBackend.
Context manager for using ImageBackend in API methods. Handles
opening and closing a connection to Pithos and converting backend
erros to cloud faults.
"""
image_backend = backend.ImageBackend(user_id)
try:
yield backend
yield image_backend
except backend.Forbidden:
raise faults.Forbidden
except backend.ImageNotFound:
raise faults.ItemNotFound
finally:
backend.close()
image_backend.close()
......@@ -42,7 +42,8 @@ from django.http import HttpResponse
from snf_django.lib import api
from snf_django.lib.api import faults
from synnefo.plankton.utils import plankton_method
from synnefo.plankton.utils import image_backend
from synnefo.plankton.backend import split_url
FILTERS = ('name', 'container_format', 'disk_format', 'status', 'size_min',
......@@ -117,7 +118,6 @@ def _get_image_headers(request):
@api.api_method(http_method="POST", user_required=True, logger=log)
@plankton_method
def add_image(request):
"""Add a new virtual machine image
......@@ -144,12 +144,17 @@ def add_image(request):
name = params.pop('name')
location = params.pop('location', None)
try:
split_url(location)
except AssertionError:
raise faults.BadRequest("Invalid location '%s'" % location)
if location:
image = request.backend.register(name, location, params)
with image_backend(request.user_uniq) as backend:
image = backend.register(name, location, params)
else:
#f = StringIO(request.raw_post_data)
#image = request.backend.put(name, f, params)
#image = backend.put(name, f, params)
return HttpResponse(status=501) # Not Implemented
if not image:
......@@ -159,7 +164,6 @@ def add_image(request):
@api.api_method(http_method="DELETE", user_required=True, logger=log)
@plankton_method
def delete_image(request, image_id):
"""Delete an Image.
......@@ -173,13 +177,13 @@ def delete_image(request, image_id):
"""
log.info("delete_image '%s'" % image_id)
userid = request.user_uniq
request.backend.unregister(image_id)
with image_backend(userid) as backend:
backend.unregister(image_id)