Commit 73d42021 authored by Christos Stavrakakis's avatar Christos Stavrakakis
Browse files

plankton: Cleanup and improve code

Major refactor in plankton APP and ImageBackend code:
* Remove unused code
* Remove 'plankton_method' decorator that added an ImageBackend to
  requests an replace it with 'image_backend' context manager. This
  context manager is responsible for opening and closing connections
  to PithosBackend and converting ImageBackend errors to cloud faults.
* Update plankton and images API methods and tests.
parent 9b335f8f
......@@ -118,8 +118,8 @@ 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'))
if since:
images = []
for image in backend.iter():
......@@ -172,7 +172,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 +210,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 +229,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 +256,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 +287,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 +310,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):
......@@ -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,
......
......@@ -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):
......
......@@ -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,7 +127,7 @@ 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):
......
# 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,7 @@ 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
FILTERS = ('name', 'container_format', 'disk_format', 'status', 'size_min',
......@@ -117,7 +117,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
......@@ -146,10 +145,11 @@ def add_image(request):
location = params.pop('location', None)
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 +159,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 +172,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)
log.info("User '%s' deleted image '%s'" % (userid, image_id))
return HttpResponse(status=204)
@api.api_method(http_method="PUT", user_required=True, logger=log)
@plankton_method
def add_image_member(request, image_id, member):
"""Add a member to an image
......@@ -191,12 +190,12 @@ def add_image_member(request, image_id, member):
"""
log.debug('add_image_member %s %s', image_id, member)
request.backend.add_user(image_id, member)
with image_backend(request.user_uniq) as backend:
backend.add_user(image_id, member)
return HttpResponse(status=204)
@api.api_method(http_method="GET", user_required=True, logger=log)
@plankton_method
def get_image(request, image_id):
"""Retrieve a virtual machine image
......@@ -208,12 +207,12 @@ def get_image(request, image_id):
in memory.
"""
#image = request.backend.get_image(image_id)
#image = backend.get_image(image_id)
#if not image:
# return HttpResponseNotFound()
#
#response = _create_image_response(image)
#data = request.backend.get_data(image)
#data = backend.get_data(image)
#response.content = data
#response['Content-Length'] = len(data)
#response['Content-Type'] = 'application/octet-stream'
......@@ -223,7 +222,6 @@ def get_image(request, image_id):
@api.api_method(http_method="HEAD", user_required=True, logger=log)
@plankton_method
def get_image_meta(request, image_id):
"""Return detailed metadata on a specific image
......@@ -231,14 +229,12 @@ def get_image_meta(request, image_id):
3.4. Requesting Detailed Metadata on a Specific Image
"""
image = request.backend.get_image(image_id)
if not image:
raise faults.ItemNotFound()
with image_backend(request.user_uniq) as backend:
image = backend.get_image(image_id)
return _create_image_response(image)
@api.api_method(http_method="GET", user_required=True, logger=log)
@plankton_method
def list_image_members(request, image_id):
"""List image memberships
......@@ -246,14 +242,15 @@ def list_image_members(request, image_id):
3.7. Requesting Image Memberships
"""
members = [{'member_id': user, 'can_share': False}
for user in request.backend.list_users(image_id)]
with image_backend(request.user_uniq) as backend:
users = backend.list_users(image_id)
members = [{'member_id': u, 'can_share': False} for u in users]
data = json.dumps({'members': members}, indent=settings.DEBUG)
return HttpResponse(data)
@api.api_method(http_method="GET", user_required=True, logger=log)
@plankton_method
def list_images(request, detail=False):
"""Return a list of available images.
......@@ -293,7 +290,8 @@ def list_images(request, detail=False):
except ValueError:
raise faults.BadRequest("Malformed request.")
images = request.backend.list(filters, params)
with image_backend(request.user_uniq) as backend:
images = backend.list(filters, params)
# Remove keys that should not be returned
fields = DETAIL_FIELDS if detail else LIST_FIELDS
......@@ -307,7 +305,6 @@ def list_images(request, detail=False):
@api.api_method(http_method="GET", user_required=True, logger=log)
@plankton_method
def list_shared_images(request, member):
"""Request shared images
......@@ -322,16 +319,16 @@ def list_shared_images(request, member):
log.debug('list_shared_images %s', member)
images = []
for image in request.backend.iter_shared(member=member):
image_id = image['id']
images.append({'image_id': image_id, 'can_share': False})
with image_backend(request.user_uniq) as backend:
for image in backend.iter_shared(member=member):
image_id = image['id']
images.append({'image_id': image_id, 'can_share': False})
data = json.dumps({'shared_images': images}, indent=settings.DEBUG)
return HttpResponse(data)
@api.api_method(http_method="DELETE", user_required=True, logger=log)
@plankton_method
def remove_image_member(request, image_id, member):
"""Remove a member from an image
......@@ -340,12 +337,12 @@ def remove_image_member(request, image_id, member):
"""
log.debug('remove_image_member %s %s', image_id, member)
request.backend.remove_user(image_id, member)
with image_backend(request.user_uniq) as backend:
backend.remove_user(image_id, member)
return HttpResponse(status=204)
@api.api_method(http_method="PUT", user_required=True, logger=log)
@plankton_method
def update_image(request, image_id):
"""Update an image
......@@ -363,12 +360,12 @@ def update_image(request, image_id):
assert set(meta.keys()).issubset(set(UPDATE_FIELDS))
image = request.backend.update(image_id, meta)
with image_backend(request.user_uniq) as backend:
image = backend.update_metadata(image_id, meta)
return _create_image_response(image)
@api.api_method(http_method="PUT", user_required=True, logger=log)
@plankton_method
def update_image_members(request, image_id):
"""Replace a membership list for an image
......@@ -388,5 +385,6 @@ def update_image_members(request, image_id):
except (ValueError, KeyError, TypeError):
return HttpResponse(status=400)
request.backend.replace_users(image_id, members)
with image_backend(request.user_uniq) as backend:
backend.replace_users(image_id, members)
return HttpResponse(status=204)
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment