From 8e1b88cc228f9ed55c3b6e4fdd790a572d63e6fe Mon Sep 17 00:00:00 2001 From: Eric Day Date: Thu, 18 Nov 2010 13:27:52 -0800 Subject: First step to getting the image APIs consolidated. The EC2 API was using a one-off S3 image service wrapper, but this should be moved into the nova.image space and use the same interface as the others. There are still some mismatches between the various image service implementations, but this patch was getting large and wanted to keep it within a resonable size. --- nova/image/glance.py | 227 +++++++++++++++++++++++++++++++++ nova/image/local.py | 88 +++++++++++++ nova/image/s3.py | 108 ++++++++++++++++ nova/image/service.py | 97 +------------- nova/image/services/__init__.py | 0 nova/image/services/glance/__init__.py | 216 ------------------------------- 6 files changed, 429 insertions(+), 307 deletions(-) create mode 100644 nova/image/glance.py create mode 100644 nova/image/local.py create mode 100644 nova/image/s3.py delete mode 100644 nova/image/services/__init__.py delete mode 100644 nova/image/services/glance/__init__.py (limited to 'nova/image') diff --git a/nova/image/glance.py b/nova/image/glance.py new file mode 100644 index 000000000..547381a4a --- /dev/null +++ b/nova/image/glance.py @@ -0,0 +1,227 @@ +# 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. + +"""Implementation of an image service that uses Glance as the backend""" + +import httplib +import json +import urlparse + +import webob.exc + +from nova import utils +from nova import flags +from nova import exception +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): + 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_image_index(self): + """ + Returns a list of image id/name 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_details(self): + """ + Returns a list of detailed image data mappings from Parallax + """ + try: + c = self.connection_type(self.netloc, self.port) + c.request("GET", "images/detail") + 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/detail", 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 + """ + try: + c = self.connection_type(self.netloc, self.port) + body = json.dumps(image_metadata) + c.request("POST", "images", body) + res = c.getresponse() + if res.status == 200: + # Parallax returns a JSONified dict(image=image_info) + data = json.loads(res.read())['image'] + return data['id'] + else: + # TODO(jaypipes): log the error? + return None + finally: + c.close() + + def update_image_metadata(self, image_id, image_metadata): + """ + Updates Parallax's information about an image + """ + try: + c = self.connection_type(self.netloc, self.port) + body = json.dumps(image_metadata) + c.request("PUT", "images/%s" % image_id, body) + res = c.getresponse() + return res.status == 200 + finally: + c.close() + + def delete_image_metadata(self, image_id): + """ + Deletes Parallax's information about an image + """ + try: + c = self.connection_type(self.netloc, self.port) + c.request("DELETE", "images/%s" % image_id) + res = c.getresponse() + return res.status == 200 + finally: + c.close() + + +class GlanceImageService(nova.image.service.BaseImageService): + """Provides storage and retrieval of disk image objects within Glance.""" + + def __init__(self): + self.teller = TellerClient() + self.parallax = ParallaxClient() + + 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, context): + """ + Calls out to Parallax for a list of detailed image information + """ + images = self.parallax.get_image_details() + return images + + def show(self, context, 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, context, 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, context, 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, context, 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 diff --git a/nova/image/local.py b/nova/image/local.py new file mode 100644 index 000000000..9b0cdcc50 --- /dev/null +++ b/nova/image/local.py @@ -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)) diff --git a/nova/image/s3.py b/nova/image/s3.py new file mode 100644 index 000000000..ba1086aca --- /dev/null +++ b/nova/image/s3.py @@ -0,0 +1,108 @@ +# 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) diff --git a/nova/image/service.py b/nova/image/service.py index 52ddd4e0f..ebee2228d 100644 --- a/nova/image/service.py +++ b/nova/image/service.py @@ -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)) diff --git a/nova/image/services/__init__.py b/nova/image/services/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/nova/image/services/glance/__init__.py b/nova/image/services/glance/__init__.py deleted file mode 100644 index f1d05f0bc..000000000 --- a/nova/image/services/glance/__init__.py +++ /dev/null @@ -1,216 +0,0 @@ -# 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. - -"""Implementation of an image service that uses Glance as the backend""" - -import httplib -import json -import urlparse - -import webob.exc - -from nova import utils -from nova import flags -from nova import exception -import nova.image.service - -FLAGS = flags.FLAGS - - -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_image_index(self): - """ - Returns a list of image id/name 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_details(self): - """ - Returns a list of detailed image data mappings from Parallax - """ - try: - c = self.connection_type(self.netloc, self.port) - c.request("GET", "images/detail") - 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/detail", 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 - """ - try: - c = self.connection_type(self.netloc, self.port) - body = json.dumps(image_metadata) - c.request("POST", "images", body) - res = c.getresponse() - if res.status == 200: - # Parallax returns a JSONified dict(image=image_info) - data = json.loads(res.read())['image'] - return data['id'] - else: - # TODO(jaypipes): log the error? - return None - finally: - c.close() - - def update_image_metadata(self, image_id, image_metadata): - """ - Updates Parallax's information about an image - """ - try: - c = self.connection_type(self.netloc, self.port) - body = json.dumps(image_metadata) - c.request("PUT", "images/%s" % image_id, body) - res = c.getresponse() - return res.status == 200 - finally: - c.close() - - def delete_image_metadata(self, image_id): - """ - Deletes Parallax's information about an image - """ - try: - c = self.connection_type(self.netloc, self.port) - c.request("DELETE", "images/%s" % image_id) - res = c.getresponse() - return res.status == 200 - finally: - c.close() - - -class GlanceImageService(nova.image.service.BaseImageService): - """Provides storage and retrieval of disk image objects within Glance.""" - - 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_image_index() - return images - - def detail(self): - """ - Calls out to Parallax for a list of detailed image information - """ - images = self.parallax.get_image_details() - 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 -- cgit From 9c7ddf24acfdbdb220bcc56d8e4d6421cd46e1d7 Mon Sep 17 00:00:00 2001 From: Eric Day Date: Thu, 18 Nov 2010 21:27:00 -0800 Subject: PEP8 fixes, 2 lines were too long. --- nova/image/s3.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) (limited to 'nova/image') diff --git a/nova/image/s3.py b/nova/image/s3.py index ba1086aca..0a25161de 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -42,7 +42,8 @@ class S3ImageService(service.BaseImageService): self._conn(context).make_request( method='POST', bucket='_images', - query_args=self._qs({'image_id': image_id, 'operation': operation})) + query_args=self._qs({'image_id': image_id, + 'operation': operation})) return True def update(self, context, image_id, attributes): -- cgit