Moves the EC2 API S3 image service into nova.service. There is still work to be done to make the APIs align, but this is the first step.

This commit is contained in:
Eric Day
2010-11-19 06:09:40 +00:00
committed by Tarmac
11 changed files with 273 additions and 267 deletions
+11 -13
View File
@@ -41,7 +41,7 @@ from nova import rpc
from nova import utils
from nova.compute.instance_types import INSTANCE_TYPES
from nova.api import cloud
from nova.api.ec2 import images
from nova.image.s3 import S3ImageService
FLAGS = flags.FLAGS
@@ -100,6 +100,7 @@ class CloudController(object):
def __init__(self):
self.network_manager = utils.import_object(FLAGS.network_manager)
self.compute_manager = utils.import_object(FLAGS.compute_manager)
self.image_service = S3ImageService()
self.setup()
def __str__(self):
@@ -785,7 +786,7 @@ class CloudController(object):
vpn = kwargs['image_id'] == FLAGS.vpn_image_id
if not vpn:
image = images.get(context, kwargs['image_id'])
image = self.image_service.show(context, kwargs['image_id'])
# FIXME(ja): if image is vpn, this breaks
# get defaults from imagestore
@@ -798,8 +799,8 @@ class CloudController(object):
ramdisk_id = kwargs.get('ramdisk_id', ramdisk_id)
# make sure we have access to kernel and ramdisk
images.get(context, kernel_id)
images.get(context, ramdisk_id)
self.image_service.show(context, kernel_id)
self.image_service.show(context, ramdisk_id)
logging.debug("Going to run %s instances...", num_instances)
launch_time = time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime())
@@ -993,20 +994,17 @@ class CloudController(object):
return True
def describe_images(self, context, image_id=None, **kwargs):
# The objectstore does its own authorization for describe
imageSet = images.list(context, image_id)
imageSet = self.image_service.index(context, image_id)
return {'imagesSet': imageSet}
def deregister_image(self, context, image_id, **kwargs):
# FIXME: should the objectstore be doing these authorization checks?
images.deregister(context, image_id)
self.image_service.deregister(context, image_id)
return {'imageId': image_id}
def register_image(self, context, image_location=None, **kwargs):
# FIXME: should the objectstore be doing these authorization checks?
if image_location is None and 'name' in kwargs:
image_location = kwargs['name']
image_id = images.register(context, image_location)
image_id = self.image_service.register(context, image_location)
logging.debug("Registered %s as %s" % (image_location, image_id))
return {'imageId': image_id}
@@ -1014,7 +1012,7 @@ class CloudController(object):
if attribute != 'launchPermission':
raise exception.ApiError('attribute not supported: %s' % attribute)
try:
image = images.list(context, image_id)[0]
image = self.image_service.show(context, image_id)
except IndexError:
raise exception.ApiError('invalid id: %s' % image_id)
result = {'image_id': image_id, 'launchPermission': []}
@@ -1033,8 +1031,8 @@ class CloudController(object):
raise exception.ApiError('only group "all" is supported')
if not operation_type in ['add', 'remove']:
raise exception.ApiError('operation_type must be add or remove')
return images.modify(context, image_id, operation_type)
return self.image_service.modify(context, image_id, operation_type)
def update_image(self, context, image_id, **kwargs):
result = images.update(context, image_id, dict(kwargs))
result = self.image_service.update(context, image_id, dict(kwargs))
return result
-123
View File
@@ -1,123 +0,0 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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.
"""
Proxy AMI-related calls from the cloud controller, to the running
objectstore service.
"""
import json
import urllib
import boto.s3.connection
from nova import exception
from nova import flags
from nova import utils
from nova.auth import manager
FLAGS = flags.FLAGS
def modify(context, image_id, operation):
conn(context).make_request(
method='POST',
bucket='_images',
query_args=qs({'image_id': image_id, 'operation': operation}))
return True
def update(context, image_id, attributes):
"""update an image's attributes / info.json"""
attributes.update({"image_id": image_id})
conn(context).make_request(
method='POST',
bucket='_images',
query_args=qs(attributes))
return True
def register(context, image_location):
""" rpc call to register a new image based from a manifest """
image_id = utils.generate_uid('ami')
conn(context).make_request(
method='PUT',
bucket='_images',
query_args=qs({'image_location': image_location,
'image_id': image_id}))
return image_id
def list(context, filter_list=[]):
""" return a list of all images that a user can see
optionally filtered by a list of image_id """
if FLAGS.connection_type == 'fake':
return [{'imageId': 'bar'}]
# FIXME: send along the list of only_images to check for
response = conn(context).make_request(
method='GET',
bucket='_images')
result = json.loads(response.read())
if not filter_list is None:
return [i for i in result if i['imageId'] in filter_list]
return result
def get(context, image_id):
"""return a image object if the context has permissions"""
result = list(context, [image_id])
if not result:
raise exception.NotFound('Image %s could not be found' % image_id)
image = result[0]
return image
def deregister(context, image_id):
""" unregister an image """
conn(context).make_request(
method='DELETE',
bucket='_images',
query_args=qs({'image_id': image_id}))
def conn(context):
access = manager.AuthManager().get_access_key(context.user,
context.project)
secret = str(context.user.secret)
calling = boto.s3.connection.OrdinaryCallingFormat()
return boto.s3.connection.S3Connection(aws_access_key_id=access,
aws_secret_access_key=secret,
is_secure=False,
calling_format=calling,
port=FLAGS.s3_port,
host=FLAGS.s3_host)
def qs(params):
pairs = []
for key in params.keys():
pairs.append(key + '=' + urllib.quote(params[key]))
return '&'.join(pairs)
+9 -4
View File
@@ -17,6 +17,7 @@
from webob import exc
from nova import context
from nova import flags
from nova import utils
from nova import wsgi
@@ -46,19 +47,23 @@ class Controller(wsgi.Controller):
def detail(self, req):
"""Return all public images in detail."""
user_id = req.environ['nova.context']['user']['id']
ctxt = context.RequestContext(user_id, user_id)
try:
images = self._service.detail()
images = self._service.detail(ctxt)
images = nova.api.openstack.limited(images, req)
except NotImplementedError:
# Emulate detail() using repeated calls to show()
images = self._service.index()
images = self._service.index(ctxt)
images = nova.api.openstack.limited(images, req)
images = [self._service.show(i['id']) for i in images]
images = [self._service.show(ctxt, i['id']) for i in images]
return dict(images=images)
def show(self, req, id):
"""Return data about the given image id."""
return dict(image=self._service.show(id))
user_id = req.environ['nova.context']['user']['id']
ctxt = context.RequestContext(user_id, user_id)
return dict(image=self._service.show(ctxt, id))
def delete(self, req, id):
# Only public images are supported for now.
+1 -1
View File
@@ -232,7 +232,7 @@ 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',
DEFINE_string('image_service', 'nova.image.local.LocalImageService',
'The service to use for retrieving and searching for images.')
DEFINE_string('host', socket.gethostname(),
@@ -32,6 +32,17 @@ import nova.image.service
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 TellerClient(object):
def __init__(self):
@@ -161,21 +172,21 @@ class GlanceImageService(nova.image.service.BaseImageService):
self.teller = TellerClient()
self.parallax = ParallaxClient()
def index(self):
def index(self, context):
"""
Calls out to Parallax for a list of images available
"""
images = self.parallax.get_image_index()
return images
def detail(self):
def detail(self, context):
"""
Calls out to Parallax for a list of detailed image information
"""
images = self.parallax.get_image_details()
return images
def show(self, id):
def show(self, context, id):
"""
Returns a dict containing image data for the given opaque image id.
"""
@@ -184,7 +195,7 @@ class GlanceImageService(nova.image.service.BaseImageService):
return image
raise exception.NotFound
def create(self, data):
def create(self, context, data):
"""
Store the image data and return the new image id.
@@ -193,7 +204,7 @@ class GlanceImageService(nova.image.service.BaseImageService):
"""
return self.parallax.add_image_metadata(data)
def update(self, image_id, data):
def update(self, context, image_id, data):
"""Replace the contents of the given image with the new data.
:raises NotFound if the image does not exist.
@@ -201,7 +212,7 @@ class GlanceImageService(nova.image.service.BaseImageService):
"""
self.parallax.update_image_metadata(image_id, data)
def delete(self, image_id):
def delete(self, context, image_id):
"""
Delete the given image.
+88
View File
@@ -0,0 +1,88 @@
# 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 cPickle as pickle
import os.path
import random
from nova import exception
from nova.image import service
class LocalImageService(service.BaseImageService):
"""Image service storing images to local disk.
It assumes that image_ids are integers."""
def __init__(self):
self._path = "/tmp/nova/images"
try:
os.makedirs(self._path)
except OSError: # Exists
pass
def _path_to(self, image_id):
return os.path.join(self._path, str(image_id))
def _ids(self):
"""The list of all image ids."""
return [int(i) for i in os.listdir(self._path)]
def index(self, context):
return [dict(id=i['id'], name=i['name']) for i in self.detail(context)]
def detail(self, context):
return [self.show(context, id) for id in self._ids()]
def show(self, context, id):
try:
return pickle.load(open(self._path_to(id)))
except IOError:
raise exception.NotFound
def create(self, context, data):
"""
Store the image data and return the new image id.
"""
id = random.randint(0, 2 ** 32 - 1)
data['id'] = id
self.update(context, id, data)
return id
def update(self, context, image_id, data):
"""Replace the contents of the given image with the new data."""
try:
pickle.dump(data, open(self._path_to(image_id), 'w'))
except IOError:
raise exception.NotFound
def delete(self, context, image_id):
"""
Delete the given image. Raises OSError if the image does not exist.
"""
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 id in self._ids():
os.unlink(self._path_to(id))
+109
View File
@@ -0,0 +1,109 @@
# vim: tabstop=4 shiftwidth=4 softtabstop=4
# Copyright 2010 United States Government as represented by the
# Administrator of the National Aeronautics and Space Administration.
# 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.
"""
Proxy AMI-related calls from the cloud controller, to the running
objectstore service.
"""
import json
import urllib
import boto.s3.connection
from nova import exception
from nova import flags
from nova import utils
from nova.auth import manager
from nova.image import service
FLAGS = flags.FLAGS
class S3ImageService(service.BaseImageService):
def modify(self, context, image_id, operation):
self._conn(context).make_request(
method='POST',
bucket='_images',
query_args=self._qs({'image_id': image_id,
'operation': operation}))
return True
def update(self, context, image_id, attributes):
"""update an image's attributes / info.json"""
attributes.update({"image_id": image_id})
self._conn(context).make_request(
method='POST',
bucket='_images',
query_args=self._qs(attributes))
return True
def register(self, context, image_location):
""" rpc call to register a new image based from a manifest """
image_id = utils.generate_uid('ami')
self._conn(context).make_request(
method='PUT',
bucket='_images',
query_args=self._qs({'image_location': image_location,
'image_id': image_id}))
return image_id
def index(self, context):
"""Return a list of all images that a user can see."""
response = self._conn(context).make_request(
method='GET',
bucket='_images')
return json.loads(response.read())
def show(self, context, image_id):
"""return a image object if the context has permissions"""
if FLAGS.connection_type == 'fake':
return {'imageId': 'bar'}
result = self.index(context)
result = [i for i in result if i['imageId'] == image_id]
if not result:
raise exception.NotFound('Image %s could not be found' % image_id)
image = result[0]
return image
def deregister(self, context, image_id):
""" unregister an image """
self._conn(context).make_request(
method='DELETE',
bucket='_images',
query_args=self._qs({'image_id': image_id}))
def _conn(self, context):
access = manager.AuthManager().get_access_key(context.user,
context.project)
secret = str(context.user.secret)
calling = boto.s3.connection.OrdinaryCallingFormat()
return boto.s3.connection.S3Connection(aws_access_key_id=access,
aws_secret_access_key=secret,
is_secure=False,
calling_format=calling,
port=FLAGS.s3_port,
host=FLAGS.s3_host)
def _qs(self, params):
pairs = []
for key in params.keys():
pairs.append(key + '=' + urllib.quote(params[key]))
return '&'.join(pairs)
+6 -91
View File
@@ -15,32 +15,12 @@
# License for the specific language governing permissions and limitations
# under the License.
import cPickle as pickle
import os.path
import random
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):
def index(self, context):
"""
Returns a sequence of mappings of id and name information about
images.
@@ -52,7 +32,7 @@ class BaseImageService(object):
"""
raise NotImplementedError
def detail(self):
def detail(self, context):
"""
Returns a sequence of mappings of detailed information about images.
@@ -76,7 +56,7 @@ class BaseImageService(object):
"""
raise NotImplementedError
def show(self, id):
def show(self, context, id):
"""
Returns a dict containing image data for the given opaque image id.
@@ -96,7 +76,7 @@ class BaseImageService(object):
"""
raise NotImplementedError
def create(self, data):
def create(self, context, data):
"""
Store the image data and return the new image id.
@@ -105,7 +85,7 @@ class BaseImageService(object):
"""
raise NotImplementedError
def update(self, image_id, data):
def update(self, context, image_id, data):
"""Replace the contents of the given image with the new data.
:raises NotFound if the image does not exist.
@@ -113,7 +93,7 @@ class BaseImageService(object):
"""
raise NotImplementedError
def delete(self, image_id):
def delete(self, context, image_id):
"""
Delete the given image.
@@ -121,68 +101,3 @@ class BaseImageService(object):
"""
raise NotImplementedError
class LocalImageService(BaseImageService):
"""Image service storing images to local disk.
It assumes that image_ids are integers."""
def __init__(self):
self._path = "/tmp/nova/images"
try:
os.makedirs(self._path)
except OSError: # Exists
pass
def _path_to(self, image_id):
return os.path.join(self._path, str(image_id))
def _ids(self):
"""The list of all image ids."""
return [int(i) for i in os.listdir(self._path)]
def index(self):
return [dict(id=i['id'], name=i['name']) for i in self.detail()]
def detail(self):
return [self.show(id) for id in self._ids()]
def show(self, id):
try:
return pickle.load(open(self._path_to(id)))
except IOError:
raise exception.NotFound
def create(self, data):
"""
Store the image data and return the new image id.
"""
id = random.randint(0, 2 ** 32 - 1)
data['id'] = id
self.update(id, data)
return id
def update(self, image_id, data):
"""Replace the contents of the given image with the new data."""
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.
"""
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 id in self._ids():
os.unlink(self._path_to(id))
View File
+9 -11
View File
@@ -29,7 +29,7 @@ from nova import flags
from nova import exception as exc
import nova.api.openstack.auth
from nova.image import service
from nova.image.services import glance
from nova.image import glance
from nova.tests import fake_flags
from nova.wsgi import Router
@@ -76,7 +76,7 @@ def stub_out_image_service(stubs):
def fake_image_show(meh, id):
return dict(kernelId=1, ramdiskId=1)
stubs.Set(nova.image.service.LocalImageService, 'show', fake_image_show)
stubs.Set(nova.image.local.LocalImageService, 'show', fake_image_show)
def stub_out_auth(stubs):
@@ -151,21 +151,19 @@ def stub_out_glance(stubs, initial_fixtures=[]):
self.fixtures = []
fake_parallax_client = FakeParallaxClient(initial_fixtures)
stubs.Set(nova.image.services.glance.ParallaxClient, 'get_image_index',
stubs.Set(nova.image.glance.ParallaxClient, 'get_image_index',
fake_parallax_client.fake_get_image_index)
stubs.Set(nova.image.services.glance.ParallaxClient, 'get_image_details',
stubs.Set(nova.image.glance.ParallaxClient, 'get_image_details',
fake_parallax_client.fake_get_image_details)
stubs.Set(nova.image.services.glance.ParallaxClient, 'get_image_metadata',
stubs.Set(nova.image.glance.ParallaxClient, 'get_image_metadata',
fake_parallax_client.fake_get_image_metadata)
stubs.Set(nova.image.services.glance.ParallaxClient, 'add_image_metadata',
stubs.Set(nova.image.glance.ParallaxClient, 'add_image_metadata',
fake_parallax_client.fake_add_image_metadata)
stubs.Set(nova.image.services.glance.ParallaxClient,
'update_image_metadata',
stubs.Set(nova.image.glance.ParallaxClient, 'update_image_metadata',
fake_parallax_client.fake_update_image_metadata)
stubs.Set(nova.image.services.glance.ParallaxClient,
'delete_image_metadata',
stubs.Set(nova.image.glance.ParallaxClient, 'delete_image_metadata',
fake_parallax_client.fake_delete_image_metadata)
stubs.Set(nova.image.services.glance.GlanceImageService, 'delete_all',
stubs.Set(nova.image.glance.GlanceImageService, 'delete_all',
fake_parallax_client.fake_delete_all)
+23 -18
View File
@@ -28,6 +28,7 @@ import unittest
import stubout
import webob
from nova import context
from nova import exception
from nova import flags
from nova import utils
@@ -52,12 +53,13 @@ class BaseImageServiceTests(object):
'serverId': None,
'progress': None}
num_images = len(self.service.index())
num_images = len(self.service.index(self.context))
id = self.service.create(fixture)
id = self.service.create(self.context, fixture)
self.assertNotEquals(None, id)
self.assertEquals(num_images + 1, len(self.service.index()))
self.assertEquals(num_images + 1,
len(self.service.index(self.context)))
def test_create_and_show_non_existing_image(self):
@@ -68,14 +70,15 @@ class BaseImageServiceTests(object):
'serverId': None,
'progress': None}
num_images = len(self.service.index())
num_images = len(self.service.index(self.context))
id = self.service.create(fixture)
id = self.service.create(self.context, fixture)
self.assertNotEquals(None, id)
self.assertRaises(exception.NotFound,
self.service.show,
self.context,
'bad image id')
def test_update(self):
@@ -87,12 +90,12 @@ class BaseImageServiceTests(object):
'serverId': None,
'progress': None}
id = self.service.create(fixture)
id = self.service.create(self.context, fixture)
fixture['status'] = 'in progress'
self.service.update(id, fixture)
new_image_data = self.service.show(id)
self.service.update(self.context, id, fixture)
new_image_data = self.service.show(self.context, id)
self.assertEquals('in progress', new_image_data['status'])
def test_delete(self):
@@ -111,20 +114,20 @@ class BaseImageServiceTests(object):
'serverId': None,
'progress': None}]
num_images = len(self.service.index())
self.assertEquals(0, num_images, str(self.service.index()))
num_images = len(self.service.index(self.context))
self.assertEquals(0, num_images, str(self.service.index(self.context)))
ids = []
for fixture in fixtures:
new_id = self.service.create(fixture)
new_id = self.service.create(self.context, fixture)
ids.append(new_id)
num_images = len(self.service.index())
self.assertEquals(2, num_images, str(self.service.index()))
num_images = len(self.service.index(self.context))
self.assertEquals(2, num_images, str(self.service.index(self.context)))
self.service.delete(ids[0])
self.service.delete(self.context, ids[0])
num_images = len(self.service.index())
num_images = len(self.service.index(self.context))
self.assertEquals(1, num_images)
@@ -135,8 +138,9 @@ class LocalImageServiceTest(unittest.TestCase,
def setUp(self):
self.stubs = stubout.StubOutForTesting()
service_class = 'nova.image.service.LocalImageService'
service_class = 'nova.image.local.LocalImageService'
self.service = utils.import_object(service_class)
self.context = context.RequestContext(None, None)
def tearDown(self):
self.service.delete_all()
@@ -151,8 +155,9 @@ class GlanceImageServiceTest(unittest.TestCase,
def setUp(self):
self.stubs = stubout.StubOutForTesting()
fakes.stub_out_glance(self.stubs)
service_class = 'nova.image.services.glance.GlanceImageService'
service_class = 'nova.image.glance.GlanceImageService'
self.service = utils.import_object(service_class)
self.context = context.RequestContext(None, None)
self.service.delete_all()
def tearDown(self):
@@ -187,7 +192,7 @@ class ImageControllerWithGlanceServiceTest(unittest.TestCase):
def setUp(self):
self.orig_image_service = FLAGS.image_service
FLAGS.image_service = 'nova.image.services.glance.GlanceImageService'
FLAGS.image_service = 'nova.image.glance.GlanceImageService'
self.stubs = stubout.StubOutForTesting()
fakes.FakeAuthManager.auth_data = {}
fakes.FakeAuthDatabase.data = {}