summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorjaypipes@gmail.com <>2010-10-05 20:38:43 +0000
committerTarmac <>2010-10-05 20:38:43 +0000
commit8e89d47958fc0e680582804ec07152ca05039854 (patch)
tree8ba2cbd584a998505250fd226ec2abbe8f6829f1
parent15cf92206627f2f56d30356ca974018d5b2244e9 (diff)
parentfbd1bc015bd5615963b9073eefb895ea04c55a3e (diff)
downloadnova-8e89d47958fc0e680582804ec07152ca05039854.tar.gz
nova-8e89d47958fc0e680582804ec07152ca05039854.tar.xz
nova-8e89d47958fc0e680582804ec07152ca05039854.zip
Adds stubs and tests for GlanceImageService and LocalImageService.
Adds basic plumbing for ParallaxClient and TellerClient and hooks that into the GlanceImageService. Fixes lp654843
-rw-r--r--nova/api/rackspace/images.py7
-rw-r--r--nova/api/rackspace/servers.py2
-rw-r--r--nova/flags.py4
-rw-r--r--nova/image/service.py223
-rw-r--r--nova/tests/api/rackspace/fakes.py74
-rw-r--r--nova/tests/api/rackspace/test_images.py118
-rw-r--r--nova/tests/api/rackspace/test_servers.py1
7 files changed, 404 insertions, 25 deletions
diff --git a/nova/api/rackspace/images.py b/nova/api/rackspace/images.py
index 4a7dd489c..d4ab8ce3c 100644
--- a/nova/api/rackspace/images.py
+++ b/nova/api/rackspace/images.py
@@ -17,12 +17,17 @@
from webob import exc
+from nova import flags
+from nova import utils
from nova import wsgi
from nova.api.rackspace import _id_translator
import nova.api.rackspace
import nova.image.service
from nova.api.rackspace import faults
+
+FLAGS = flags.FLAGS
+
class Controller(wsgi.Controller):
_serialization_metadata = {
@@ -35,7 +40,7 @@ class Controller(wsgi.Controller):
}
def __init__(self):
- self._service = nova.image.service.ImageService.load()
+ self._service = utils.import_object(FLAGS.image_service)
self._id_translator = _id_translator.RackspaceAPIIdTranslator(
"image", self._service.__class__.__name__)
diff --git a/nova/api/rackspace/servers.py b/nova/api/rackspace/servers.py
index 5cfb7a431..b23867bbf 100644
--- a/nova/api/rackspace/servers.py
+++ b/nova/api/rackspace/servers.py
@@ -42,7 +42,7 @@ def _instance_id_translator():
def _image_service():
""" Helper method for initializing the image id translator """
- service = nova.image.service.ImageService.load()
+ service = utils.import_object(FLAGS.image_service)
return (service, _id_translator.RackspaceAPIIdTranslator(
"image", service.__class__.__name__))
diff --git a/nova/flags.py b/nova/flags.py
index c32cdd7a4..ab80e83fb 100644
--- a/nova/flags.py
+++ b/nova/flags.py
@@ -222,6 +222,10 @@ DEFINE_string('volume_manager', 'nova.volume.manager.AOEManager',
DEFINE_string('scheduler_manager', 'nova.scheduler.manager.SchedulerManager',
'Manager for scheduler')
+# The service to use for image search and retrieval
+DEFINE_string('image_service', 'nova.image.service.LocalImageService',
+ 'The service to use for retrieving and searching for images.')
+
DEFINE_string('host', socket.gethostname(),
'name of this node')
diff --git a/nova/image/service.py b/nova/image/service.py
index 1a7a258b7..2e570e8a4 100644
--- a/nova/image/service.py
+++ b/nova/image/service.py
@@ -16,38 +16,215 @@
# under the License.
import cPickle as pickle
+import httplib
+import json
+import logging
import os.path
import random
import string
+import urlparse
-class ImageService(object):
- """Provides storage and retrieval of disk image objects."""
+import webob.exc
- @staticmethod
- def load():
- """Factory method to return image service."""
- #TODO(gundlach): read from config.
- class_ = LocalImageService
- return class_()
+from nova import utils
+from nova import flags
+from nova import exception
+
+
+FLAGS = flags.FLAGS
+
+
+flags.DEFINE_string('glance_teller_address', 'http://127.0.0.1',
+ 'IP address or URL where Glance\'s Teller service resides')
+flags.DEFINE_string('glance_teller_port', '9191',
+ 'Port for Glance\'s Teller service')
+flags.DEFINE_string('glance_parallax_address', 'http://127.0.0.1',
+ 'IP address or URL where Glance\'s Parallax service resides')
+flags.DEFINE_string('glance_parallax_port', '9292',
+ 'Port for Glance\'s Parallax service')
+
+
+class BaseImageService(object):
+
+ """Base class for providing image search and retrieval services"""
def index(self):
"""
Return a dict from opaque image id to image data.
"""
+ raise NotImplementedError
def show(self, id):
"""
Returns a dict containing image data for the given opaque image id.
+
+ :raises NotFound if the image does not exist
+ """
+ raise NotImplementedError
+
+ def create(self, data):
+ """
+ Store the image data and return the new image id.
+
+ :raises AlreadyExists if the image already exist.
+
+ """
+ raise NotImplementedError
+
+ def update(self, image_id, data):
+ """Replace the contents of the given image with the new data.
+
+ :raises NotFound if the image does not exist.
+
+ """
+ raise NotImplementedError
+
+ def delete(self, image_id):
+ """
+ Delete the given image.
+
+ :raises NotFound if the image does not exist.
+
+ """
+ raise NotImplementedError
+
+
+class TellerClient(object):
+
+ def __init__(self):
+ self.address = FLAGS.glance_teller_address
+ self.port = FLAGS.glance_teller_port
+ url = urlparse.urlparse(self.address)
+ self.netloc = url.netloc
+ self.connection_type = {'http': httplib.HTTPConnection,
+ 'https': httplib.HTTPSConnection}[url.scheme]
+
+
+class ParallaxClient(object):
+
+ def __init__(self):
+ self.address = FLAGS.glance_parallax_address
+ self.port = FLAGS.glance_parallax_port
+ url = urlparse.urlparse(self.address)
+ self.netloc = url.netloc
+ self.connection_type = {'http': httplib.HTTPConnection,
+ 'https': httplib.HTTPSConnection}[url.scheme]
+
+ def get_images(self):
+ """
+ Returns a list of image data mappings from Parallax
+ """
+ try:
+ c = self.connection_type(self.netloc, self.port)
+ c.request("GET", "images")
+ res = c.getresponse()
+ if res.status == 200:
+ # Parallax returns a JSONified dict(images=image_list)
+ data = json.loads(res.read())['images']
+ return data
+ else:
+ logging.warn("Parallax returned HTTP error %d from "
+ "request for /images", res.status_int)
+ return []
+ finally:
+ c.close()
+
+ def get_image_metadata(self, image_id):
+ """
+ Returns a mapping of image metadata from Parallax
+ """
+ try:
+ c = self.connection_type(self.netloc, self.port)
+ c.request("GET", "images/%s" % image_id)
+ res = c.getresponse()
+ if res.status == 200:
+ # Parallax returns a JSONified dict(image=image_info)
+ data = json.loads(res.read())['image']
+ return data
+ else:
+ # TODO(jaypipes): log the error?
+ return None
+ finally:
+ c.close()
+
+ def add_image_metadata(self, image_metadata):
+ """
+ Tells parallax about an image's metadata
"""
+ pass
+
+ def update_image_metadata(self, image_id, image_metadata):
+ """
+ Updates Parallax's information about an image
+ """
+ pass
+
+ def delete_image_metadata(self, image_id):
+ """
+ Deletes Parallax's information about an image
+ """
+ pass
-class GlanceImageService(ImageService):
+class GlanceImageService(BaseImageService):
+
"""Provides storage and retrieval of disk image objects within Glance."""
- # TODO(gundlach): once Glance has an API, build this.
- pass
+ def __init__(self):
+ self.teller = TellerClient()
+ self.parallax = ParallaxClient()
+
+ def index(self):
+ """
+ Calls out to Parallax for a list of images available
+ """
+ images = self.parallax.get_images()
+ return images
+
+ def show(self, id):
+ """
+ Returns a dict containing image data for the given opaque image id.
+ """
+ image = self.parallax.get_image_metadata(id)
+ if image:
+ return image
+ raise exception.NotFound
+
+ def create(self, data):
+ """
+ Store the image data and return the new image id.
+
+ :raises AlreadyExists if the image already exist.
+
+ """
+ return self.parallax.add_image_metadata(data)
+
+ def update(self, image_id, data):
+ """Replace the contents of the given image with the new data.
+
+ :raises NotFound if the image does not exist.
+
+ """
+ self.parallax.update_image_metadata(image_id, data)
+
+ def delete(self, image_id):
+ """
+ Delete the given image.
+
+ :raises NotFound if the image does not exist.
+
+ """
+ self.parallax.delete_image_metadata(image_id)
+
+ def delete_all(self):
+ """
+ Clears out all images
+ """
+ pass
+
+
+class LocalImageService(BaseImageService):
-class LocalImageService(ImageService):
"""Image service storing images to local disk."""
def __init__(self):
@@ -68,7 +245,10 @@ class LocalImageService(ImageService):
return [ self.show(id) for id in self._ids() ]
def show(self, id):
- return pickle.load(open(self._path_to(id)))
+ try:
+ return pickle.load(open(self._path_to(id)))
+ except IOError:
+ raise exception.NotFound
def create(self, data):
"""
@@ -81,10 +261,23 @@ class LocalImageService(ImageService):
def update(self, image_id, data):
"""Replace the contents of the given image with the new data."""
- pickle.dump(data, open(self._path_to(image_id), 'w'))
+ try:
+ pickle.dump(data, open(self._path_to(image_id), 'w'))
+ except IOError:
+ raise exception.NotFound
def delete(self, image_id):
"""
Delete the given image. Raises OSError if the image does not exist.
"""
- os.unlink(self._path_to(image_id))
+ try:
+ os.unlink(self._path_to(image_id))
+ except IOError:
+ raise exception.NotFound
+
+ def delete_all(self):
+ """
+ Clears out all images in local directory
+ """
+ for f in os.listdir(self._path):
+ os.unlink(self._path_to(f))
diff --git a/nova/tests/api/rackspace/fakes.py b/nova/tests/api/rackspace/fakes.py
index 2c4447920..c7d9216c8 100644
--- a/nova/tests/api/rackspace/fakes.py
+++ b/nova/tests/api/rackspace/fakes.py
@@ -1,5 +1,24 @@
+# vim: tabstop=4 shiftwidth=4 softtabstop=4
+
+# Copyright 2010 OpenStack LLC.
+# All Rights Reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License"); you may
+# not use this file except in compliance with the License. You may obtain
+# a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+# License for the specific language governing permissions and limitations
+# under the License.
+
import datetime
import json
+import random
+import string
import webob
import webob.dec
@@ -7,6 +26,7 @@ import webob.dec
from nova import auth
from nova import utils
from nova import flags
+from nova import exception as exc
import nova.api.rackspace.auth
import nova.api.rackspace._id_translator
from nova.image import service
@@ -105,6 +125,60 @@ def stub_out_networking(stubs):
FLAGS.FAKE_subdomain = 'rs'
+def stub_out_glance(stubs):
+
+ class FakeParallaxClient:
+
+ def __init__(self):
+ self.fixtures = {}
+
+ def fake_get_images(self):
+ return self.fixtures
+
+ def fake_get_image_metadata(self, image_id):
+ for k, f in self.fixtures.iteritems():
+ if k == image_id:
+ return f
+ return None
+
+ def fake_add_image_metadata(self, image_data):
+ id = ''.join(random.choice(string.letters) for _ in range(20))
+ image_data['id'] = id
+ self.fixtures[id] = image_data
+ return id
+
+ def fake_update_image_metadata(self, image_id, image_data):
+
+ if image_id not in self.fixtures.keys():
+ raise exc.NotFound
+
+ self.fixtures[image_id].update(image_data)
+
+ def fake_delete_image_metadata(self, image_id):
+
+ if image_id not in self.fixtures.keys():
+ raise exc.NotFound
+
+ del self.fixtures[image_id]
+
+ def fake_delete_all(self):
+ self.fixtures = {}
+
+ fake_parallax_client = FakeParallaxClient()
+ stubs.Set(nova.image.service.ParallaxClient, 'get_images',
+ fake_parallax_client.fake_get_images)
+ stubs.Set(nova.image.service.ParallaxClient, 'get_image_metadata',
+ fake_parallax_client.fake_get_image_metadata)
+ stubs.Set(nova.image.service.ParallaxClient, 'add_image_metadata',
+ fake_parallax_client.fake_add_image_metadata)
+ stubs.Set(nova.image.service.ParallaxClient, 'update_image_metadata',
+ fake_parallax_client.fake_update_image_metadata)
+ stubs.Set(nova.image.service.ParallaxClient, 'delete_image_metadata',
+ fake_parallax_client.fake_delete_image_metadata)
+ stubs.Set(nova.image.service.GlanceImageService, 'delete_all',
+ fake_parallax_client.fake_delete_all)
+
+
class FakeAuthDatabase(object):
data = {}
diff --git a/nova/tests/api/rackspace/test_images.py b/nova/tests/api/rackspace/test_images.py
index 489e35052..a7f320b46 100644
--- a/nova/tests/api/rackspace/test_images.py
+++ b/nova/tests/api/rackspace/test_images.py
@@ -15,25 +15,127 @@
# License for the specific language governing permissions and limitations
# under the License.
+import logging
import unittest
import stubout
+from nova import exception
+from nova import utils
from nova.api.rackspace import images
+from nova.tests.api.rackspace import fakes
-class ImagesTest(unittest.TestCase):
+class BaseImageServiceTests():
+
+ """Tasks to test for all image services"""
+
+ def test_create(self):
+
+ fixture = {'name': 'test image',
+ 'updated': None,
+ 'created': None,
+ 'status': None,
+ 'serverId': None,
+ 'progress': None}
+
+ num_images = len(self.service.index())
+
+ id = self.service.create(fixture)
+
+ self.assertNotEquals(None, id)
+ self.assertEquals(num_images + 1, len(self.service.index()))
+
+ def test_create_and_show_non_existing_image(self):
+
+ fixture = {'name': 'test image',
+ 'updated': None,
+ 'created': None,
+ 'status': None,
+ 'serverId': None,
+ 'progress': None}
+
+ num_images = len(self.service.index())
+
+ id = self.service.create(fixture)
+
+ self.assertNotEquals(None, id)
+
+ self.assertRaises(exception.NotFound,
+ self.service.show,
+ 'bad image id')
+
+ def test_update(self):
+
+ fixture = {'name': 'test image',
+ 'updated': None,
+ 'created': None,
+ 'status': None,
+ 'serverId': None,
+ 'progress': None}
+
+ id = self.service.create(fixture)
+
+ fixture['status'] = 'in progress'
+
+ self.service.update(id, fixture)
+ new_image_data = self.service.show(id)
+ self.assertEquals('in progress', new_image_data['status'])
+
+ def test_delete(self):
+
+ fixtures = [
+ {'name': 'test image 1',
+ 'updated': None,
+ 'created': None,
+ 'status': None,
+ 'serverId': None,
+ 'progress': None},
+ {'name': 'test image 2',
+ 'updated': None,
+ 'created': None,
+ 'status': None,
+ 'serverId': None,
+ 'progress': None}]
+
+ ids = []
+ for fixture in fixtures:
+ new_id = self.service.create(fixture)
+ ids.append(new_id)
+
+ num_images = len(self.service.index())
+ self.assertEquals(2, num_images)
+
+ self.service.delete(ids[0])
+
+ num_images = len(self.service.index())
+ self.assertEquals(1, num_images)
+
+
+class LocalImageServiceTest(unittest.TestCase,
+ BaseImageServiceTests):
+
+ """Tests the local image service"""
+
def setUp(self):
self.stubs = stubout.StubOutForTesting()
+ self.service = utils.import_object('nova.image.service.LocalImageService')
def tearDown(self):
+ self.service.delete_all()
self.stubs.UnsetAll()
- def test_get_image_list(self):
- pass
- def test_delete_image(self):
- pass
-
- def test_create_image(self):
- pass
+class GlanceImageServiceTest(unittest.TestCase,
+ BaseImageServiceTests):
+
+ """Tests the local image service"""
+
+ def setUp(self):
+ self.stubs = stubout.StubOutForTesting()
+ fakes.stub_out_glance(self.stubs)
+ self.service = utils.import_object('nova.image.service.GlanceImageService')
+
+ def tearDown(self):
+ self.service.delete_all()
+ self.stubs.UnsetAll()
diff --git a/nova/tests/api/rackspace/test_servers.py b/nova/tests/api/rackspace/test_servers.py
index 5a21356eb..1cc9ebc72 100644
--- a/nova/tests/api/rackspace/test_servers.py
+++ b/nova/tests/api/rackspace/test_servers.py
@@ -33,6 +33,7 @@ from nova.tests.api.rackspace import fakes
FLAGS = flags.FLAGS
+FLAGS.verbose = True
def return_server(context, id):
return stub_instance(id)