diff options
| author | Naveed Massjouni <naveedm9@gmail.com> | 2011-09-13 14:00:20 -0400 |
|---|---|---|
| committer | Naveed Massjouni <naveedm9@gmail.com> | 2011-09-13 14:00:20 -0400 |
| commit | 59cd446cc61fdbf933c504d498852835a187ea6f (patch) | |
| tree | a45d2ae2dab40bb885337dd8ef456a810a2b0b44 | |
| parent | 613bcfc45865c9d7c7577e124b187920970d031e (diff) | |
| parent | 61e5825a43fff1ad60dcd26454dc4881bdc13ef6 (diff) | |
| download | nova-59cd446cc61fdbf933c504d498852835a187ea6f.tar.gz nova-59cd446cc61fdbf933c504d498852835a187ea6f.tar.xz nova-59cd446cc61fdbf933c504d498852835a187ea6f.zip | |
Merge from trunk.
37 files changed, 1060 insertions, 1321 deletions
diff --git a/MANIFEST.in b/MANIFEST.in index 883aba8a1..5451ace4b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -37,7 +37,7 @@ include nova/tests/bundle/1mb.manifest.xml include nova/tests/bundle/1mb.no_kernel_or_ramdisk.manifest.xml include nova/tests/bundle/1mb.part.0 include nova/tests/bundle/1mb.part.1 -include nova/tests/public_key/* +include nova/tests/api/ec2/public_key/* include nova/tests/db/nova.austin.sqlite include plugins/xenapi/README include plugins/xenapi/etc/xapi.d/plugins/objectstore diff --git a/nova/api/auth.py b/nova/api/auth.py index cd0d38b3f..f73cae01e 100644 --- a/nova/api/auth.py +++ b/nova/api/auth.py @@ -70,6 +70,7 @@ class KeystoneContext(wsgi.Middleware): project_id, roles=roles, auth_token=auth_token, + strategy='keystone', remote_address=remote_address) req.environ['nova.context'] = ctx diff --git a/nova/api/ec2/cloud.py b/nova/api/ec2/cloud.py index 4f7030a5a..eacfdc0df 100644 --- a/nova/api/ec2/cloud.py +++ b/nova/api/ec2/cloud.py @@ -1467,7 +1467,7 @@ class CloudController(object): return image def _format_image(self, image): - """Convert from format defined by BaseImageService to S3 format.""" + """Convert from format defined by GlanceImageService to S3 format.""" i = {} image_type = self._image_type(image.get('container_format')) ec2_id = self.image_ec2_id(image.get('id'), image_type) diff --git a/nova/api/openstack/create_instance_helper.py b/nova/api/openstack/create_instance_helper.py index 67e669c17..e27ddf78b 100644 --- a/nova/api/openstack/create_instance_helper.py +++ b/nova/api/openstack/create_instance_helper.py @@ -92,7 +92,8 @@ class CreateInstanceHelper(object): if str(image_href).startswith(req.application_url): image_href = image_href.split('/').pop() try: - image_service, image_id = nova.image.get_image_service(image_href) + image_service, image_id = nova.image.get_image_service(context, + image_href) kernel_id, ramdisk_id = self._get_kernel_ramdisk_from_image( req, image_service, image_id) images = set([str(x['id']) for x in image_service.index(context)]) diff --git a/nova/api/openstack/image_metadata.py b/nova/api/openstack/image_metadata.py index 4d615ea96..adb6bee4b 100644 --- a/nova/api/openstack/image_metadata.py +++ b/nova/api/openstack/image_metadata.py @@ -17,6 +17,7 @@ from webob import exc +from nova import exception from nova import flags from nova import image from nova import utils @@ -33,21 +34,22 @@ class Controller(object): def __init__(self): self.image_service = image.get_default_image_service() - def _get_metadata(self, context, image_id, image=None): - if not image: - image = self.image_service.show(context, image_id) - metadata = image.get('properties', {}) - return metadata + def _get_image(self, context, image_id): + try: + return self.image_service.show(context, image_id) + except exception.NotFound: + msg = _("Image not found.") + raise exc.HTTPNotFound(explanation=msg) def index(self, req, image_id): """Returns the list of metadata for a given instance""" context = req.environ['nova.context'] - metadata = self._get_metadata(context, image_id) + metadata = self._get_image(context, image_id)['properties'] return dict(metadata=metadata) def show(self, req, image_id, id): context = req.environ['nova.context'] - metadata = self._get_metadata(context, image_id) + metadata = self._get_image(context, image_id)['properties'] if id in metadata: return {'meta': {id: metadata[id]}} else: @@ -55,15 +57,13 @@ class Controller(object): def create(self, req, image_id, body): context = req.environ['nova.context'] - img = self.image_service.show(context, image_id) - metadata = self._get_metadata(context, image_id, img) + image = self._get_image(context, image_id) if 'metadata' in body: for key, value in body['metadata'].iteritems(): - metadata[key] = value - common.check_img_metadata_quota_limit(context, metadata) - img['properties'] = metadata - self.image_service.update(context, image_id, img, None) - return dict(metadata=metadata) + image['properties'][key] = value + common.check_img_metadata_quota_limit(context, image['properties']) + self.image_service.update(context, image_id, image, None) + return dict(metadata=image['properties']) def update(self, req, image_id, id, body): context = req.environ['nova.context'] @@ -80,32 +80,30 @@ class Controller(object): if len(meta) > 1: expl = _('Request body contains too many items') raise exc.HTTPBadRequest(explanation=expl) - img = self.image_service.show(context, image_id) - metadata = self._get_metadata(context, image_id, img) - metadata[id] = meta[id] - common.check_img_metadata_quota_limit(context, metadata) - img['properties'] = metadata - self.image_service.update(context, image_id, img, None) + + image = self._get_image(context, image_id) + image['properties'][id] = meta[id] + common.check_img_metadata_quota_limit(context, image['properties']) + self.image_service.update(context, image_id, image, None) return dict(meta=meta) def update_all(self, req, image_id, body): context = req.environ['nova.context'] - img = self.image_service.show(context, image_id) + image = self._get_image(context, image_id) metadata = body.get('metadata', {}) common.check_img_metadata_quota_limit(context, metadata) - img['properties'] = metadata - self.image_service.update(context, image_id, img, None) + image['properties'] = metadata + self.image_service.update(context, image_id, image, None) return dict(metadata=metadata) def delete(self, req, image_id, id): context = req.environ['nova.context'] - img = self.image_service.show(context, image_id) - metadata = self._get_metadata(context, image_id) - if not id in metadata: - raise exc.HTTPNotFound() - metadata.pop(id) - img['properties'] = metadata - self.image_service.update(context, image_id, img, None) + image = self._get_image(context, image_id) + if not id in image['properties']: + msg = _("Invalid metadata key") + raise exc.HTTPNotFound(explanation=msg) + image['properties'].pop(id) + self.image_service.update(context, image_id, image, None) def create_resource(): diff --git a/nova/api/openstack/images.py b/nova/api/openstack/images.py index 893674f21..4340cbe3e 100644 --- a/nova/api/openstack/images.py +++ b/nova/api/openstack/images.py @@ -51,7 +51,7 @@ class Controller(object): """Initialize new `ImageController`. :param compute_service: `nova.compute.api:API` - :param image_service: `nova.image.service:BaseImageService` + :param image_service: `nova.image.glance:GlancemageService` """ self._compute_service = compute_service or compute.API() diff --git a/nova/api/openstack/views/images.py b/nova/api/openstack/views/images.py index 21f1b2d3e..8983b2957 100644 --- a/nova/api/openstack/views/images.py +++ b/nova/api/openstack/views/images.py @@ -37,17 +37,18 @@ class ViewBuilder(object): def _format_status(self, image): """Update the status field to standardize format.""" status_mapping = { - 'pending': 'QUEUED', - 'decrypting': 'PREPARING', - 'untarring': 'SAVING', - 'available': 'ACTIVE', - 'killed': 'FAILED', + 'active': 'ACTIVE', + 'queued': 'SAVING', + 'saving': 'SAVING', + 'deleted': 'DELETED', + 'pending_delete': 'DELETED', + 'killed': 'ERROR', } try: - image['status'] = status_mapping[image['status']].upper() + image['status'] = status_mapping[image['status']] except KeyError: - image['status'] = image['status'].upper() + image['status'] = 'UNKNOWN' def _build_server(self, image, image_obj): """Indicates that you must use a ViewBuilder subclass.""" diff --git a/nova/compute/api.py b/nova/compute/api.py index b0ea044c5..d674224e5 100644 --- a/nova/compute/api.py +++ b/nova/compute/api.py @@ -202,7 +202,8 @@ class API(base.Base): self._check_injected_file_quota(context, injected_files) self._check_requested_networks(context, requested_networks) - (image_service, image_id) = nova.image.get_image_service(image_href) + (image_service, image_id) = nova.image.get_image_service(context, + image_href) image = image_service.show(context, image_id) config_drive_id = None diff --git a/nova/compute/manager.py b/nova/compute/manager.py index 0be12297f..7915830ec 100644 --- a/nova/compute/manager.py +++ b/nova/compute/manager.py @@ -322,7 +322,8 @@ class ComputeManager(manager.SchedulerDependentManager): # used by the image service. This should be refactored to be # consistent. image_href = instance['image_ref'] - image_service, image_id = nova.image.get_image_service(image_href) + image_service, image_id = nova.image.get_image_service(context, + image_href) image_meta = image_service.show(context, image_id) try: diff --git a/nova/context.py b/nova/context.py index 5c22641a0..de5b791c4 100644 --- a/nova/context.py +++ b/nova/context.py @@ -32,7 +32,7 @@ class RequestContext(object): def __init__(self, user_id, project_id, is_admin=None, read_deleted=False, roles=None, remote_address=None, timestamp=None, - request_id=None, auth_token=None): + request_id=None, auth_token=None, strategy='noauth'): self.user_id = user_id self.project_id = project_id self.roles = roles or [] @@ -50,6 +50,7 @@ class RequestContext(object): request_id = unicode(uuid.uuid4()) self.request_id = request_id self.auth_token = auth_token + self.strategy = strategy def to_dict(self): return {'user_id': self.user_id, @@ -60,7 +61,8 @@ class RequestContext(object): 'remote_address': self.remote_address, 'timestamp': utils.strtime(self.timestamp), 'request_id': self.request_id, - 'auth_token': self.auth_token} + 'auth_token': self.auth_token, + 'strategy': self.strategy} @classmethod def from_dict(cls, values): @@ -77,7 +79,8 @@ class RequestContext(object): remote_address=self.remote_address, timestamp=self.timestamp, request_id=self.request_id, - auth_token=self.auth_token) + auth_token=self.auth_token, + strategy=self.strategy) def get_admin_context(read_deleted=False): diff --git a/nova/db/sqlalchemy/api.py b/nova/db/sqlalchemy/api.py index 40e2ca167..e5a661c7f 100644 --- a/nova/db/sqlalchemy/api.py +++ b/nova/db/sqlalchemy/api.py @@ -672,7 +672,7 @@ def floating_ip_update(context, address, values): def fixed_ip_associate(context, address, instance_id, network_id=None, reserved=False): """Keyword arguments: - reserved -- should be a boolean value(True or False), exact value will be + reserved -- should be a boolean value(True or False), exact value will be used to filter on the fixed ip address """ session = get_session() diff --git a/nova/image/__init__.py b/nova/image/__init__.py index 5447c8a3a..307b73f01 100644 --- a/nova/image/__init__.py +++ b/nova/image/__init__.py @@ -16,70 +16,20 @@ # under the License. -from urlparse import urlparse - import nova -from nova import exception from nova import utils from nova import flags -from nova.image import glance as glance_image_service +from nova.image import glance FLAGS = flags.FLAGS -GlanceClient = utils.import_class('glance.client.Client') - - -def _parse_image_ref(image_href): - """Parse an image href into composite parts. - - :param image_href: href of an image - :returns: a tuple of the form (image_id, host, port) - :raises ValueError - - """ - o = urlparse(image_href) - port = o.port or 80 - host = o.netloc.split(':', 1)[0] - image_id = int(o.path.split('/')[-1]) - return (image_id, host, port) - - def get_default_image_service(): ImageService = utils.import_class(FLAGS.image_service) return ImageService() -# FIXME(sirp): perhaps this should be moved to nova/images/glance so that we -# keep Glance specific code together for the most part -def get_glance_client(image_href): - """Get the correct glance client and id for the given image_href. - - The image_href param can be an href of the form - http://myglanceserver:9292/images/42, or just an int such as 42. If the - image_href is an int, then flags are used to create the default - glance client. - - :param image_href: image ref/id for an image - :returns: a tuple of the form (glance_client, image_id) - - """ - image_href = image_href or 0 - if str(image_href).isdigit(): - glance_host, glance_port = \ - glance_image_service.pick_glance_api_server() - glance_client = GlanceClient(glance_host, glance_port) - return (glance_client, int(image_href)) - - try: - (image_id, host, port) = _parse_image_ref(image_href) - except ValueError: - raise exception.InvalidImageRef(image_href=image_href) - glance_client = GlanceClient(host, port) - return (glance_client, image_id) - - -def get_image_service(image_href): +def get_image_service(context, image_href): """Get the proper image_service and id for the given image_href. The image_href param can be an href of the form @@ -94,6 +44,6 @@ def get_image_service(image_href): if str(image_href).isdigit(): return (get_default_image_service(), int(image_href)) - (glance_client, image_id) = get_glance_client(image_href) + (glance_client, image_id) = glance.get_glance_client(context, image_href) image_service = nova.image.glance.GlanceImageService(glance_client) return (image_service, image_id) diff --git a/nova/image/fake.py b/nova/image/fake.py index 97af81711..4eceabc11 100644 --- a/nova/image/fake.py +++ b/nova/image/fake.py @@ -24,7 +24,6 @@ import random from nova import exception from nova import flags from nova import log as logging -from nova.image import service LOG = logging.getLogger('nova.image.fake') @@ -33,7 +32,7 @@ LOG = logging.getLogger('nova.image.fake') FLAGS = flags.FLAGS -class _FakeImageService(service.BaseImageService): +class _FakeImageService(object): """Mock (fake) image service for unit testing.""" def __init__(self): diff --git a/nova/image/glance.py b/nova/image/glance.py index 80abc7384..5ee1d2b8a 100644 --- a/nova/image/glance.py +++ b/nova/image/glance.py @@ -23,6 +23,7 @@ import copy import datetime import json import random +from urlparse import urlparse from glance.common import exception as glance_exception @@ -30,7 +31,6 @@ from nova import exception from nova import flags from nova import log as logging from nova import utils -from nova.image import service LOG = logging.getLogger('nova.image.glance') @@ -42,6 +42,35 @@ FLAGS = flags.FLAGS GlanceClient = utils.import_class('glance.client.Client') +def _parse_image_ref(image_href): + """Parse an image href into composite parts. + + :param image_href: href of an image + :returns: a tuple of the form (image_id, host, port) + :raises ValueError + + """ + o = urlparse(image_href) + port = o.port or 80 + host = o.netloc.split(':', 1)[0] + image_id = int(o.path.split('/')[-1]) + return (image_id, host, port) + + +def _create_glance_client(context, host, port): + if context.strategy == 'keystone': + # NOTE(dprince): Glance client just needs auth_tok right? Should we + # add username and tenant to the creds below? + creds = {'strategy': 'keystone', + 'username': context.user_id, + 'tenant': context.project_id} + glance_client = GlanceClient(host, port, auth_tok=context.auth_token, + creds=creds) + else: + glance_client = GlanceClient(host, port) + return glance_client + + def pick_glance_api_server(): """Return which Glance API server to use for the request @@ -57,37 +86,47 @@ def pick_glance_api_server(): return host, port -class GlanceImageService(service.BaseImageService): - """Provides storage and retrieval of disk image objects within Glance.""" +def get_glance_client(context, image_href): + """Get the correct glance client and id for the given image_href. + + The image_href param can be an href of the form + http://myglanceserver:9292/images/42, or just an int such as 42. If the + image_href is an int, then flags are used to create the default + glance client. + + :param image_href: image ref/id for an image + :returns: a tuple of the form (glance_client, image_id) + + """ + image_href = image_href or 0 + if str(image_href).isdigit(): + glance_host, glance_port = pick_glance_api_server() + glance_client = _create_glance_client(context, glance_host, + glance_port) + return (glance_client, int(image_href)) + + try: + (image_id, host, port) = _parse_image_ref(image_href) + except ValueError: + raise exception.InvalidImageRef(image_href=image_href) + glance_client = _create_glance_client(context, glance_host, glance_port) + return (glance_client, image_id) - GLANCE_ONLY_ATTRS = ['size', 'location', 'disk_format', - 'container_format', 'checksum'] - # NOTE(sirp): Overriding to use _translate_to_service provided by - # BaseImageService - SERVICE_IMAGE_ATTRS = service.BaseImageService.BASE_IMAGE_ATTRS +\ - GLANCE_ONLY_ATTRS +class GlanceImageService(object): + """Provides storage and retrieval of disk image objects within Glance.""" def __init__(self, client=None): self._client = client - def _get_client(self): + def _get_client(self, context): # NOTE(sirp): we want to load balance each request across glance # servers. Since GlanceImageService is a long-lived object, `client` # is made to choose a new server each time via this property. if self._client is not None: return self._client glance_host, glance_port = pick_glance_api_server() - return GlanceClient(glance_host, glance_port) - - def _set_client(self, client): - self._client = client - - client = property(_get_client, _set_client) - - def _set_client_context(self, context): - """Sets the client's auth token.""" - self.client.set_auth_token(context.auth_token) + return _create_glance_client(context, glance_host, glance_port) def index(self, context, **kwargs): """Calls out to Glance for a list of images available.""" @@ -112,7 +151,7 @@ class GlanceImageService(service.BaseImageService): images = [] for image_meta in image_metas: if self._is_image_available(context, image_meta): - base_image_meta = self._translate_to_base(image_meta) + base_image_meta = self._translate_from_glance(image_meta) images.append(base_image_meta) return images @@ -128,14 +167,14 @@ class GlanceImageService(service.BaseImageService): def _get_images(self, context, **kwargs): """Get image entitites from images service""" - self._set_client_context(context) # ensure filters is a dict kwargs['filters'] = kwargs.get('filters') or {} # NOTE(vish): don't filter out private images kwargs['filters'].setdefault('is_public', 'none') - return self._fetch_images(self.client.get_images_detailed, **kwargs) + client = self._get_client(context) + return self._fetch_images(client.get_images_detailed, **kwargs) def _fetch_images(self, fetch_func, **kwargs): """Paginate through results from glance server""" @@ -168,16 +207,15 @@ class GlanceImageService(service.BaseImageService): def show(self, context, image_id): """Returns a dict with image data for the given opaque image id.""" - self._set_client_context(context) try: - image_meta = self.client.get_image_meta(image_id) + image_meta = self._get_client(context).get_image_meta(image_id) except glance_exception.NotFound: raise exception.ImageNotFound(image_id=image_id) if not self._is_image_available(context, image_meta): raise exception.ImageNotFound(image_id=image_id) - base_image_meta = self._translate_to_base(image_meta) + base_image_meta = self._translate_from_glance(image_meta) return base_image_meta def show_by_name(self, context, name): @@ -192,16 +230,16 @@ class GlanceImageService(service.BaseImageService): def get(self, context, image_id, data): """Calls out to Glance for metadata and data and writes data.""" - self._set_client_context(context) try: - image_meta, image_chunks = self.client.get_image(image_id) + client = self._get_client(context) + image_meta, image_chunks = client.get_image(image_id) except glance_exception.NotFound: raise exception.ImageNotFound(image_id=image_id) for chunk in image_chunks: data.write(chunk) - base_image_meta = self._translate_to_base(image_meta) + base_image_meta = self._translate_from_glance(image_meta) return base_image_meta def create(self, context, image_meta, data=None): @@ -210,19 +248,18 @@ class GlanceImageService(service.BaseImageService): :raises: AlreadyExists if the image already exist. """ - self._set_client_context(context) # Translate Base -> Service LOG.debug(_('Creating image in Glance. Metadata passed in %s'), image_meta) - sent_service_image_meta = self._translate_to_service(image_meta) + sent_service_image_meta = self._translate_to_glance(image_meta) LOG.debug(_('Metadata after formatting for Glance %s'), sent_service_image_meta) - recv_service_image_meta = self.client.add_image( + recv_service_image_meta = self._get_client(context).add_image( sent_service_image_meta, data) # Translate Service -> Base - base_image_meta = self._translate_to_base(recv_service_image_meta) + base_image_meta = self._translate_from_glance(recv_service_image_meta) LOG.debug(_('Metadata returned from Glance formatted for Base %s'), base_image_meta) return base_image_meta @@ -233,16 +270,16 @@ class GlanceImageService(service.BaseImageService): :raises: ImageNotFound if the image does not exist. """ - self._set_client_context(context) # NOTE(vish): show is to check if image is available self.show(context, image_id) - image_meta = _convert_to_string(image_meta) + image_meta = self._translate_to_glance(image_meta) try: - image_meta = self.client.update_image(image_id, image_meta, data) + client = self._get_client(context) + image_meta = client.update_image(image_id, image_meta, data) except glance_exception.NotFound: raise exception.ImageNotFound(image_id=image_id) - base_image_meta = self._translate_to_base(image_meta) + base_image_meta = self._translate_from_glance(image_meta) return base_image_meta def delete(self, context, image_id): @@ -251,11 +288,10 @@ class GlanceImageService(service.BaseImageService): :raises: ImageNotFound if the image does not exist. """ - self._set_client_context(context) # NOTE(vish): show is to check if image is available self.show(context, image_id) try: - result = self.client.delete_image(image_id) + result = self._get_client(context).delete_image(image_id) except glance_exception.NotFound: raise exception.ImageNotFound(image_id=image_id) return result @@ -265,17 +301,14 @@ class GlanceImageService(service.BaseImageService): pass @classmethod - def _translate_to_service(cls, image_meta): - image_meta = super(GlanceImageService, - cls)._translate_to_service(image_meta) + def _translate_to_glance(cls, image_meta): image_meta = _convert_to_string(image_meta) + image_meta = _remove_read_only(image_meta) return image_meta @classmethod - def _translate_to_base(cls, image_meta): - """Override translation to handle conversion to datetime objects.""" - image_meta = service.BaseImageService._propertify_metadata( - image_meta, cls.SERVICE_IMAGE_ATTRS) + def _translate_from_glance(cls, image_meta): + image_meta = _limit_attributes(image_meta) image_meta = _convert_timestamps_to_datetimes(image_meta) image_meta = _convert_from_string(image_meta) return image_meta @@ -285,14 +318,26 @@ class GlanceImageService(service.BaseImageService): """Check image availability. Under Glance, images are always available if the context has - an auth_token. Otherwise, we fall back to the superclass - method. + an auth_token. """ if hasattr(context, 'auth_token') and context.auth_token: return True - return service.BaseImageService._is_image_available(context, - image_meta) + + if image_meta['is_public'] or context.is_admin: + return True + + properties = image_meta['properties'] + + if context.project_id and ('project_id' in properties): + return str(properties['project_id']) == str(context.project_id) + + try: + user_id = properties['user_id'] + except KeyError: + return False + + return str(user_id) == str(context.user_id) # utility functions @@ -352,3 +397,27 @@ def _convert_from_string(metadata): def _convert_to_string(metadata): return _convert(_json_dumps, metadata) + + +def _limit_attributes(image_meta): + IMAGE_ATTRIBUTES = ['size', 'location', 'disk_format', + 'container_format', 'checksum', 'id', + 'name', 'created_at', 'updated_at', + 'deleted_at', 'deleted', 'status', + 'is_public'] + output = {} + for attr in IMAGE_ATTRIBUTES: + output[attr] = image_meta.get(attr) + + output['properties'] = image_meta.get('properties', {}) + + return output + + +def _remove_read_only(image_meta): + IMAGE_ATTRIBUTES = ['updated_at', 'created_at', 'deleted_at'] + output = copy.deepcopy(image_meta) + for attr in IMAGE_ATTRIBUTES: + if attr in output: + del output[attr] + return output diff --git a/nova/image/s3.py b/nova/image/s3.py index abf01a942..343555887 100644 --- a/nova/image/s3.py +++ b/nova/image/s3.py @@ -34,7 +34,6 @@ from nova import flags from nova import image from nova import log as logging from nova import utils -from nova.image import service from nova.api.ec2 import ec2utils @@ -48,7 +47,7 @@ flags.DEFINE_string('s3_secret_key', 'notchecked', 'secret key to use for s3 server for images') -class S3ImageService(service.BaseImageService): +class S3ImageService(object): """Wraps an existing image service to support s3 based register.""" def __init__(self, service=None, *args, **kwargs): diff --git a/nova/image/service.py b/nova/image/service.py deleted file mode 100644 index 5361cfc89..000000000 --- a/nova/image/service.py +++ /dev/null @@ -1,200 +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. - - -from nova import utils - - -class BaseImageService(object): - """Base class for providing image search and retrieval services. - - ImageService exposes two concepts of metadata: - - 1. First-class attributes: This is metadata that is common to all - ImageService subclasses and is shared across all hypervisors. These - attributes are defined by IMAGE_ATTRS. - - 2. Properties: This is metdata that is specific to an ImageService, - and Image, or a particular hypervisor. Any attribute not present in - BASE_IMAGE_ATTRS should be considered an image property. - - This means that ImageServices will return BASE_IMAGE_ATTRS as keys in the - metadata dict, all other attributes will be returned as keys in the nested - 'properties' dict. - - """ - - BASE_IMAGE_ATTRS = ['id', 'name', 'created_at', 'updated_at', - 'deleted_at', 'deleted', 'status', 'is_public'] - - # NOTE(sirp): ImageService subclasses may override this to aid translation - # between BaseImageService attributes and additional metadata stored by - # the ImageService subclass - SERVICE_IMAGE_ATTRS = [] - - def index(self, context, *args, **kwargs): - """List images. - - :returns: a sequence of mappings with the following signature - {'id': opaque id of image, 'name': name of image} - - """ - raise NotImplementedError - - def detail(self, context, *args, **kwargs): - """Detailed information about an images. - - :returns: a sequence of mappings with the following signature - {'id': opaque id of image, - 'name': name of image, - 'created_at': creation datetime object, - 'updated_at': modification datetime object, - 'deleted_at': deletion datetime object or None, - 'deleted': boolean indicating if image has been deleted, - 'status': string description of image status, - 'is_public': boolean indicating if image is public - } - - If the service does not implement a method that provides a detailed - set of information about images, then the method should raise - NotImplementedError, in which case Nova will emulate this method - with repeated calls to show() for each image received from the - index() method. - - """ - raise NotImplementedError - - def show(self, context, image_id): - """Detailed information about an image. - - :returns: a mapping with the following signature: - {'id': opaque id of image, - 'name': name of image, - 'created_at': creation datetime object, - 'updated_at': modification datetime object, - 'deleted_at': deletion datetime object or None, - 'deleted': boolean indicating if image has been deleted, - 'status': string description of image status, - 'is_public': boolean indicating if image is public - }, ... - - :raises: NotFound if the image does not exist - - """ - raise NotImplementedError - - def get(self, context, data): - """Get an image. - - :param data: a file-like object to hold binary image data - :returns: a dict containing image metadata, writes image data to data. - :raises: NotFound if the image does not exist - - """ - raise NotImplementedError - - def create(self, context, metadata, data=None): - """Store the image metadata and data. - - :returns: the new image metadata. - :raises: AlreadyExists if the image already exist. - - """ - raise NotImplementedError - - def update(self, context, image_id, metadata, data=None): - """Update the given image metadata and data and return the metadata. - - :raises: NotFound if the image does not exist. - - """ - raise NotImplementedError - - def delete(self, context, image_id): - """Delete the given image. - - :raises: NotFound if the image does not exist. - - """ - raise NotImplementedError - - @staticmethod - def _is_image_available(context, image_meta): - """Check image availability. - - Images are always available if they are public or if the user is an - admin. - - Otherwise, we filter by project_id (if present) and then fall-back to - images owned by user. - - """ - # FIXME(sirp): We should be filtering by user_id on the Glance side - # for security; however, we can't do that until we get authn/authz - # sorted out. Until then, filtering in Nova. - if image_meta['is_public'] or context.is_admin: - return True - - properties = image_meta['properties'] - - if context.project_id and ('project_id' in properties): - return str(properties['project_id']) == str(context.project_id) - - try: - user_id = properties['user_id'] - except KeyError: - return False - - return str(user_id) == str(context.user_id) - - @classmethod - def _translate_to_base(cls, metadata): - """Return a metadata dictionary that is BaseImageService compliant. - - This is used by subclasses to expose only a metadata dictionary that - is the same across ImageService implementations. - - """ - return cls._propertify_metadata(metadata, cls.BASE_IMAGE_ATTRS) - - @classmethod - def _translate_to_service(cls, metadata): - """Return a metadata dict that is usable by the ImageService subclass. - - As an example, Glance has additional attributes (like 'location'); the - BaseImageService considers these properties, but we need to translate - these back to first-class attrs for sending to Glance. This method - handles this by allowing you to specify the attributes an ImageService - considers first-class. - - """ - if not cls.SERVICE_IMAGE_ATTRS: - raise NotImplementedError(_('Cannot use this without specifying ' - 'SERVICE_IMAGE_ATTRS for subclass')) - return cls._propertify_metadata(metadata, cls.SERVICE_IMAGE_ATTRS) - - @staticmethod - def _propertify_metadata(metadata, keys): - """Move unknown keys to a nested 'properties' dict. - - :returns: a new dict with the keys moved. - - """ - flattened = utils.flatten_dict(metadata) - attributes, properties = utils.partition_dict(flattened, keys) - attributes['properties'] = properties - return attributes diff --git a/nova/tests/public_key/dummy.fingerprint b/nova/tests/api/ec2/public_key/dummy.fingerprint index 715bca27a..715bca27a 100644 --- a/nova/tests/public_key/dummy.fingerprint +++ b/nova/tests/api/ec2/public_key/dummy.fingerprint diff --git a/nova/tests/public_key/dummy.pub b/nova/tests/api/ec2/public_key/dummy.pub index d4cf2bc0d..d4cf2bc0d 100644 --- a/nova/tests/public_key/dummy.pub +++ b/nova/tests/api/ec2/public_key/dummy.pub diff --git a/nova/tests/test_cloud.py b/nova/tests/api/ec2/test_cloud.py index 7fe353b3d..7fe353b3d 100644 --- a/nova/tests/test_cloud.py +++ b/nova/tests/api/ec2/test_cloud.py diff --git a/nova/tests/api/openstack/fakes.py b/nova/tests/api/openstack/fakes.py index 44681d395..3a567f0cc 100644 --- a/nova/tests/api/openstack/fakes.py +++ b/nova/tests/api/openstack/fakes.py @@ -40,8 +40,8 @@ from nova.api.openstack import limits from nova.auth.manager import User, Project import nova.image.fake from nova.image import glance -from nova.image import service from nova.tests import fake_flags +from nova.tests.glance import stubs as glance_stubs class Context(object): @@ -83,7 +83,7 @@ def wsgi_app(inner_app10=None, inner_app11=None, fake_auth=True, if fake_auth_context is not None: ctxt = fake_auth_context else: - ctxt = context.RequestContext('fake', 'fake') + ctxt = context.RequestContext('fake', 'fake', auth_token=True) api10 = openstack.FaultWrapper(api_auth.InjectContext(ctxt, limits.RateLimitingMiddleware(inner_app10))) api11 = openstack.FaultWrapper(api_auth.InjectContext(ctxt, @@ -124,7 +124,7 @@ def stub_out_key_pair_funcs(stubs, have_key_pair=True): def stub_out_image_service(stubs): - def fake_get_image_service(image_href): + def fake_get_image_service(context, image_href): return (nova.image.fake.FakeImageService(), image_href) stubs.Set(nova.image, 'get_image_service', fake_get_image_service) stubs.Set(nova.image, 'get_default_image_service', @@ -177,6 +177,39 @@ def stub_out_compute_api_backup(stubs): stubs.Set(nova.compute.API, 'backup', backup) +def _make_image_fixtures(): + NOW_GLANCE_FORMAT = "2010-10-11T10:30:22" + + image_id = 123 + base_attrs = {'deleted': False} + + fixtures = [] + + def add_fixture(**kwargs): + kwargs.update(base_attrs) + fixtures.append(kwargs) + + # Public image + add_fixture(id=image_id, name='public image', is_public=True, + status='active', properties={'key1': 'value1'}) + image_id += 1 + + # Snapshot for User 1 + server_ref = 'http://localhost/v1.1/servers/42' + snapshot_properties = {'instance_ref': server_ref, 'user_id': 'fake'} + for status in ('queued', 'saving', 'active', 'killed', + 'deleted', 'pending_delete'): + add_fixture(id=image_id, name='%s snapshot' % status, + is_public=False, status=status, + properties=snapshot_properties) + image_id += 1 + + # Image without a name + add_fixture(id=image_id, is_public=True, status='active', properties={}) + + return fixtures + + def stub_out_glance_add_image(stubs, sent_to_glance): """ We return the metadata sent to glance by modifying the sent_to_glance dict @@ -192,91 +225,11 @@ def stub_out_glance_add_image(stubs, sent_to_glance): stubs.Set(glance_client.Client, 'add_image', fake_add_image) -def stub_out_glance(stubs, initial_fixtures=None): - - class FakeGlanceClient: - - def __init__(self, initial_fixtures): - self.fixtures = initial_fixtures or [] - - def _filter_images(self, filters=None, marker=None, limit=None): - found = True - if marker: - found = False - if limit == 0: - limit = None - - fixtures = [] - count = 0 - for f in self.fixtures: - if limit and count >= limit: - break - if found: - fixtures.append(f) - count = count + 1 - if f['id'] == marker: - found = True - - return fixtures - - def fake_get_images(self, filters=None, marker=None, limit=None): - fixtures = self._filter_images(filters, marker, limit) - return [dict(id=f['id'], name=f['name']) - for f in fixtures] - - def fake_get_images_detailed(self, filters=None, - marker=None, limit=None): - return self._filter_images(filters, marker, limit) - - def fake_get_image_meta(self, image_id): - image = self._find_image(image_id) - if image: - return copy.deepcopy(image) - raise glance_exc.NotFound - - def fake_add_image(self, image_meta, data=None): - image_meta = copy.deepcopy(image_meta) - image_id = ''.join(random.choice(string.letters) - for _ in range(20)) - image_meta['id'] = image_id - self.fixtures.append(image_meta) - return copy.deepcopy(image_meta) - - def fake_update_image(self, image_id, image_meta, data=None): - for attr in ('created_at', 'updated_at', 'deleted_at', 'deleted'): - if attr in image_meta: - del image_meta[attr] - - f = self._find_image(image_id) - if not f: - raise glance_exc.NotFound - - f.update(image_meta) - return copy.deepcopy(f) - - def fake_delete_image(self, image_id): - f = self._find_image(image_id) - if not f: - raise glance_exc.NotFound - - self.fixtures.remove(f) - - def _find_image(self, image_id): - for f in self.fixtures: - if str(f['id']) == str(image_id): - return f - return None - - GlanceClient = glance_client.Client - fake = FakeGlanceClient(initial_fixtures) - - stubs.Set(GlanceClient, 'get_images', fake.fake_get_images) - stubs.Set(GlanceClient, 'get_images_detailed', - fake.fake_get_images_detailed) - stubs.Set(GlanceClient, 'get_image_meta', fake.fake_get_image_meta) - stubs.Set(GlanceClient, 'add_image', fake.fake_add_image) - stubs.Set(GlanceClient, 'update_image', fake.fake_update_image) - stubs.Set(GlanceClient, 'delete_image', fake.fake_delete_image) +def stub_out_glance(stubs): + def fake_get_image_service(): + client = glance_stubs.StubGlanceClient(_make_image_fixtures()) + return nova.image.glance.GlanceImageService(client) + stubs.Set(nova.image, 'get_default_image_service', fake_get_image_service) class FakeToken(object): diff --git a/nova/tests/api/openstack/test_image_metadata.py b/nova/tests/api/openstack/test_image_metadata.py index fe42e35e5..314c3c38e 100644 --- a/nova/tests/api/openstack/test_image_metadata.py +++ b/nova/tests/api/openstack/test_image_metadata.py @@ -23,7 +23,6 @@ from nova import flags from nova.api import openstack from nova import test from nova.tests.api.openstack import fakes -import nova.wsgi FLAGS = flags.FLAGS @@ -31,76 +30,20 @@ FLAGS = flags.FLAGS class ImageMetaDataTest(test.TestCase): - IMAGE_FIXTURES = [ - {'status': 'active', - 'name': 'image1', - 'deleted': False, - 'container_format': None, - 'checksum': None, - 'created_at': '2011-03-22T17:40:15', - 'disk_format': None, - 'updated_at': '2011-03-22T17:40:15', - 'id': '1', - 'location': 'file:///var/lib/glance/images/1', - 'is_public': True, - 'deleted_at': None, - 'properties': { - 'key1': 'value1', - 'key2': 'value2'}, - 'size': 5882349}, - {'status': 'active', - 'name': 'image2', - 'deleted': False, - 'container_format': None, - 'checksum': None, - 'created_at': '2011-03-22T17:40:15', - 'disk_format': None, - 'updated_at': '2011-03-22T17:40:15', - 'id': '2', - 'location': 'file:///var/lib/glance/images/2', - 'is_public': True, - 'deleted_at': None, - 'properties': { - 'key1': 'value1', - 'key2': 'value2'}, - 'size': 5882349}, - {'status': 'active', - 'name': 'image3', - 'deleted': False, - 'container_format': None, - 'checksum': None, - 'created_at': '2011-03-22T17:40:15', - 'disk_format': None, - 'updated_at': '2011-03-22T17:40:15', - 'id': '3', - 'location': 'file:///var/lib/glance/images/2', - 'is_public': True, - 'deleted_at': None, - 'properties': {}, - 'size': 5882349}, - ] - def setUp(self): super(ImageMetaDataTest, self).setUp() - self.flags(image_service='nova.image.glance.GlanceImageService') - # NOTE(dprince) max out properties/metadata in image 3 for testing - img3 = self.IMAGE_FIXTURES[2] - for num in range(FLAGS.quota_metadata_items): - img3['properties']['key%i' % num] = "blah" - fakes.stub_out_glance(self.stubs, self.IMAGE_FIXTURES) + fakes.stub_out_glance(self.stubs) def test_index(self): - req = webob.Request.blank('/v1.1/123/images/1/metadata') + req = webob.Request.blank('/v1.1/123/images/123/metadata') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) - expected = self.IMAGE_FIXTURES[0]['properties'] - self.assertEqual(len(expected), len(res_dict['metadata'])) - for (key, value) in res_dict['metadata'].items(): - self.assertEqual(value, res_dict['metadata'][key]) + expected = {'metadata': {'key1': 'value1'}} + self.assertEqual(res_dict, expected) def test_show(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') res = req.get_response(fakes.wsgi_app()) res_dict = json.loads(res.body) self.assertEqual(200, res.status_int) @@ -109,32 +52,38 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual('value1', res_dict['meta']['key1']) def test_show_not_found(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key9') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key9') + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(404, res.status_int) + + def test_show_image_not_found(self): + req = webob.Request.blank('/v1.1/fake/images/100/metadata/key1') res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) def test_create(self): - req = webob.Request.blank('/v1.1/fake/images/2/metadata') + req = webob.Request.blank('/v1.1/fake/images/123/metadata') req.method = 'POST' - req.body = '{"metadata": {"key9": "value9"}}' + req.body = '{"metadata": {"key7": "value7"}}' req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(200, res.status_int) actual_output = json.loads(res.body) + expected_output = {'metadata': {'key1': 'value1', 'key7': 'value7'}} + self.assertEqual(expected_output, actual_output) - expected_output = { - 'metadata': { - 'key1': 'value1', - 'key2': 'value2', - 'key9': 'value9', - }, - } + def test_create_image_not_found(self): + req = webob.Request.blank('/v1.1/fake/images/100/metadata') + req.method = 'POST' + req.body = '{"metadata": {"key7": "value7"}}' + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) - self.assertEqual(expected_output, actual_output) + self.assertEqual(404, res.status_int) def test_update_all(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata') + req = webob.Request.blank('/v1.1/fake/images/123/metadata') req.method = 'PUT' req.body = '{"metadata": {"key9": "value9"}}' req.headers["content-type"] = "application/json" @@ -142,17 +91,20 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(200, res.status_int) actual_output = json.loads(res.body) + expected_output = {'metadata': {'key9': 'value9'}} + self.assertEqual(expected_output, actual_output) - expected_output = { - 'metadata': { - 'key9': 'value9', - }, - } + def test_update_all_image_not_found(self): + req = webob.Request.blank('/v1.1/fake/images/100/metadata') + req.method = 'PUT' + req.body = '{"metadata": {"key9": "value9"}}' + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) - self.assertEqual(expected_output, actual_output) + self.assertEqual(404, res.status_int) def test_update_item(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') req.method = 'PUT' req.body = '{"meta": {"key1": "zz"}}' req.headers["content-type"] = "application/json" @@ -160,15 +112,20 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(200, res.status_int) actual_output = json.loads(res.body) - expected_output = { - 'meta': { - 'key1': 'zz', - }, - } + expected_output = {'meta': {'key1': 'zz'}} self.assertEqual(actual_output, expected_output) + def test_update_item_image_not_found(self): + req = webob.Request.blank('/v1.1/fake/images/100/metadata/key1') + req.method = 'PUT' + req.body = '{"meta": {"key1": "zz"}}' + req.headers["content-type"] = "application/json" + res = req.get_response(fakes.wsgi_app()) + + self.assertEqual(404, res.status_int) + def test_update_item_bad_body(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') req.method = 'PUT' req.body = '{"key1": "zz"}' req.headers["content-type"] = "application/json" @@ -176,15 +133,18 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_update_item_too_many_keys(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') req.method = 'PUT' - req.body = '{"meta": {"key1": "value1", "key2": "value2"}}' + overload = {} + for num in range(FLAGS.quota_metadata_items + 1): + overload['key%s' % num] = 'value%s' % num + req.body = json.dumps({'meta': overload}) req.headers["content-type"] = "application/json" res = req.get_response(fakes.wsgi_app()) self.assertEqual(400, res.status_int) def test_update_item_body_uri_mismatch(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/bad') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/bad') req.method = 'PUT' req.body = '{"meta": {"key1": "value1"}}' req.headers["content-type"] = "application/json" @@ -192,7 +152,7 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(400, res.status_int) def test_update_item_xml(self): - req = webob.Request.blank('/v1.1/fake/images/1/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') req.method = 'PUT' req.body = '<meta key="key1">five</meta>' req.headers["content-type"] = "application/xml" @@ -200,22 +160,24 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(200, res.status_int) actual_output = json.loads(res.body) - expected_output = { - 'meta': { - 'key1': 'five', - }, - } + expected_output = {'meta': {'key1': 'five'}} self.assertEqual(actual_output, expected_output) def test_delete(self): - req = webob.Request.blank('/v1.1/fake/images/2/metadata/key1') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/key1') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(204, res.status_int) self.assertEqual('', res.body) def test_delete_not_found(self): - req = webob.Request.blank('/v1.1/fake/images/2/metadata/blah') + req = webob.Request.blank('/v1.1/fake/images/123/metadata/blah') + req.method = 'DELETE' + res = req.get_response(fakes.wsgi_app()) + self.assertEqual(404, res.status_int) + + def test_delete_image_not_found(self): + req = webob.Request.blank('/v1.1/fake/images/100/metadata/key1') req.method = 'DELETE' res = req.get_response(fakes.wsgi_app()) self.assertEqual(404, res.status_int) @@ -225,7 +187,7 @@ class ImageMetaDataTest(test.TestCase): for num in range(FLAGS.quota_metadata_items + 1): data['metadata']['key%i' % num] = "blah" json_string = str(data).replace("\'", "\"") - req = webob.Request.blank('/v1.1/fake/images/2/metadata') + req = webob.Request.blank('/v1.1/fake/images/123/metadata') req.method = 'POST' req.body = json_string req.headers["content-type"] = "application/json" @@ -233,7 +195,8 @@ class ImageMetaDataTest(test.TestCase): self.assertEqual(413, res.status_int) def test_too_many_metadata_items_on_put(self): - req = webob.Request.blank('/v1.1/fake/images/3/metadata/blah') + FLAGS.quota_metadata_items = 1 + req = webob.Request.blank('/v1.1/fake/images/123/metadata/blah') req.method = 'PUT' req.body = '{"meta": {"blah": "blah"}}' req.headers["content-type"] = "application/json" diff --git a/nova/tests/api/openstack/test_images.py b/nova/tests/api/openstack/test_images.py index 97e940974..2aee1bc14 100644 --- a/nova/tests/api/openstack/test_images.py +++ b/nova/tests/api/openstack/test_images.py @@ -22,362 +22,76 @@ and as a WSGI layer import copy import json -import os -from lxml import etree -import shutil -import tempfile import xml.dom.minidom as minidom +from lxml import etree import mox import stubout import webob -from glance import client as glance_client from nova import context -from nova import exception -from nova import test -from nova import utils import nova.api.openstack from nova.api.openstack import images from nova.api.openstack import xmlutil +from nova import test from nova.tests.api.openstack import fakes NS = "{http://docs.openstack.org/compute/api/v1.1}" ATOMNS = "{http://www.w3.org/2005/Atom}" +NOW_API_FORMAT = "2010-10-11T10:30:22Z" -class _BaseImageServiceTests(test.TestCase): - """Tasks to test for all image services""" - - def __init__(self, *args, **kwargs): - super(_BaseImageServiceTests, self).__init__(*args, **kwargs) - self.service = None - self.context = None - - def test_create(self): - fixture = self._make_fixture('test image') - num_images = len(self.service.index(self.context)) - - image_id = self.service.create(self.context, fixture)['id'] - - self.assertNotEquals(None, image_id) - self.assertEquals(num_images + 1, - len(self.service.index(self.context))) - - def test_create_and_show_non_existing_image(self): - fixture = self._make_fixture('test image') - num_images = len(self.service.index(self.context)) - - image_id = self.service.create(self.context, fixture)['id'] - - self.assertNotEquals(None, image_id) - self.assertRaises(exception.NotFound, - self.service.show, - self.context, - 'bad image id') - - def test_create_and_show_non_existing_image_by_name(self): - fixture = self._make_fixture('test image') - num_images = len(self.service.index(self.context)) - - image_id = self.service.create(self.context, fixture)['id'] - - self.assertNotEquals(None, image_id) - self.assertRaises(exception.ImageNotFound, - self.service.show_by_name, - self.context, - 'bad image id') - - def test_update(self): - fixture = self._make_fixture('test image') - image_id = self.service.create(self.context, fixture)['id'] - fixture['status'] = 'in progress' - - self.service.update(self.context, image_id, fixture) - - new_image_data = self.service.show(self.context, image_id) - self.assertEquals('in progress', new_image_data['status']) - - def test_delete(self): - fixture1 = self._make_fixture('test image 1') - fixture2 = self._make_fixture('test image 2') - fixtures = [fixture1, fixture2] - - 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(self.context, fixture)['id'] - ids.append(new_id) - - num_images = len(self.service.index(self.context)) - self.assertEquals(2, num_images, str(self.service.index(self.context))) - - self.service.delete(self.context, ids[0]) - - num_images = len(self.service.index(self.context)) - self.assertEquals(1, num_images) - - def test_index(self): - fixture = self._make_fixture('test image') - image_id = self.service.create(self.context, fixture)['id'] - image_metas = self.service.index(self.context) - expected = [{'id': 'DONTCARE', 'name': 'test image'}] - self.assertDictListMatch(image_metas, expected) - - @staticmethod - def _make_fixture(name): - fixture = {'name': name, - 'updated': None, - 'created': None, - 'status': None, - 'is_public': True} - return fixture - - -class GlanceImageServiceTest(_BaseImageServiceTests): - - """Tests the Glance image service, in particular that metadata translation - works properly. - - At a high level, the translations involved are: - - 1. Glance -> ImageService - This is needed so we can support - multple ImageServices (Glance, Local, etc) - - 2. ImageService -> API - This is needed so we can support multple - APIs (OpenStack, EC2) - """ - def setUp(self): - super(GlanceImageServiceTest, self).setUp() - self.stubs = stubout.StubOutForTesting() - fakes.stub_out_glance(self.stubs) - fakes.stub_out_compute_api_snapshot(self.stubs) - service_class = 'nova.image.glance.GlanceImageService' - self.service = utils.import_object(service_class) - self.context = context.RequestContext('fake', 'fake') - self.service.delete_all() - self.sent_to_glance = {} - fakes.stub_out_glance_add_image(self.stubs, self.sent_to_glance) - - def tearDown(self): - self.stubs.UnsetAll() - super(GlanceImageServiceTest, self).tearDown() - - def test_create_with_instance_id(self): - """Ensure instance_id is persisted as an image-property""" - fixture = {'name': 'test image', - 'is_public': False, - 'properties': {'instance_id': '42', 'user_id': 'fake'}} - - image_id = self.service.create(self.context, fixture)['id'] - expected = fixture - self.assertDictMatch(self.sent_to_glance['metadata'], expected) - - image_meta = self.service.show(self.context, image_id) - expected = {'id': image_id, - 'name': 'test image', - 'is_public': False, - 'properties': {'instance_id': '42', 'user_id': 'fake'}} - self.assertDictMatch(image_meta, expected) - - image_metas = self.service.detail(self.context) - self.assertDictMatch(image_metas[0], expected) - - def test_create_without_instance_id(self): - """ - Ensure we can create an image without having to specify an - instance_id. Public images are an example of an image not tied to an - instance. - """ - fixture = {'name': 'test image'} - image_id = self.service.create(self.context, fixture)['id'] - - expected = {'name': 'test image', 'properties': {}} - self.assertDictMatch(self.sent_to_glance['metadata'], expected) - - def test_index_default_limit(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.index(self.context) - i = 0 - for meta in image_metas: - expected = {'id': 'DONTCARE', - 'name': 'TestImage %d' % (i)} - self.assertDictMatch(meta, expected) - i = i + 1 - - def test_index_marker(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.index(self.context, marker=ids[1]) - self.assertEquals(len(image_metas), 8) - i = 2 - for meta in image_metas: - expected = {'id': 'DONTCARE', - 'name': 'TestImage %d' % (i)} - self.assertDictMatch(meta, expected) - i = i + 1 - - def test_index_limit(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.index(self.context, limit=3) - self.assertEquals(len(image_metas), 3) - - def test_index_marker_and_limit(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.index(self.context, marker=ids[3], limit=1) - self.assertEquals(len(image_metas), 1) - i = 4 - for meta in image_metas: - expected = {'id': 'DONTCARE', - 'name': 'TestImage %d' % (i)} - self.assertDictMatch(meta, expected) - i = i + 1 - - def test_detail_marker(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.detail(self.context, marker=ids[1]) - self.assertEquals(len(image_metas), 8) - i = 2 - for meta in image_metas: - expected = { - 'id': 'DONTCARE', - 'status': None, - 'is_public': True, - 'name': 'TestImage %d' % (i), - 'properties': { - 'updated': None, - 'created': None, - }, - } - - self.assertDictMatch(meta, expected) - i = i + 1 - - def test_detail_limit(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.detail(self.context, limit=3) - self.assertEquals(len(image_metas), 3) - - def test_detail_marker_and_limit(self): - fixtures = [] - ids = [] - for i in range(10): - fixture = self._make_fixture('TestImage %d' % (i)) - fixtures.append(fixture) - ids.append(self.service.create(self.context, fixture)['id']) - - image_metas = self.service.detail(self.context, marker=ids[3], limit=3) - self.assertEquals(len(image_metas), 3) - i = 4 - for meta in image_metas: - expected = { - 'id': 'DONTCARE', - 'status': None, - 'is_public': True, - 'name': 'TestImage %d' % (i), - 'properties': { - 'updated': None, 'created': None}, - } - self.assertDictMatch(meta, expected) - i = i + 1 - - -class ImageControllerWithGlanceServiceTest(test.TestCase): +class ImagesTest(test.TestCase): """ Test of the OpenStack API /images application controller w/Glance. """ - NOW_GLANCE_FORMAT = "2010-10-11T10:30:22" - NOW_API_FORMAT = "2010-10-11T10:30:22Z" def setUp(self): """Run before each test.""" - super(ImageControllerWithGlanceServiceTest, self).setUp() - self.flags(image_service='nova.image.glance.GlanceImageService') + super(ImagesTest, self).setUp() self.stubs = stubout.StubOutForTesting() fakes.stub_out_networking(self.stubs) fakes.stub_out_rate_limiting(self.stubs) fakes.stub_out_key_pair_funcs(self.stubs) - self.fixtures = self._make_image_fixtures() - fakes.stub_out_glance(self.stubs, initial_fixtures=self.fixtures) fakes.stub_out_compute_api_snapshot(self.stubs) fakes.stub_out_compute_api_backup(self.stubs) + fakes.stub_out_glance(self.stubs) def tearDown(self): """Run after each test.""" self.stubs.UnsetAll() - super(ImageControllerWithGlanceServiceTest, self).tearDown() + super(ImagesTest, self).tearDown() def _get_fake_context(self): class Context(object): project_id = 'fake' + auth_token = True return Context() - def _applicable_fixture(self, fixture, user_id): - """Determine if this fixture is applicable for given user id.""" - is_public = fixture["is_public"] - try: - uid = fixture["properties"]["user_id"] - except KeyError: - uid = None - return uid == user_id or is_public - def test_get_image_index(self): request = webob.Request.blank('/v1.0/images') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) response_dict = json.loads(response.body) response_list = response_dict["images"] - expected = [{'id': 123, 'name': 'public image'}, - {'id': 124, 'name': 'queued snapshot'}, - {'id': 125, 'name': 'saving snapshot'}, - {'id': 126, 'name': 'active snapshot'}, - {'id': 127, 'name': 'killed snapshot'}, - {'id': 129, 'name': None}] + expected = [{'id': '123', 'name': 'public image'}, + {'id': '124', 'name': 'queued snapshot'}, + {'id': '125', 'name': 'saving snapshot'}, + {'id': '126', 'name': 'active snapshot'}, + {'id': '127', 'name': 'killed snapshot'}, + {'id': '128', 'name': 'deleted snapshot'}, + {'id': '129', 'name': 'pending_delete snapshot'}, + {'id': '130', 'name': None}] self.assertDictListMatch(response_list, expected) def test_get_image(self): request = webob.Request.blank('/v1.0/images/123') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) self.assertEqual(200, response.status_int) @@ -385,20 +99,21 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): expected_image = { "image": { - "id": 123, + "id": "123", "name": "public image", - "updated": self.NOW_API_FORMAT, - "created": self.NOW_API_FORMAT, + "updated": NOW_API_FORMAT, + "created": NOW_API_FORMAT, "status": "ACTIVE", "progress": 100, }, } - self.assertEqual(expected_image, actual_image) + self.assertDictMatch(expected_image, actual_image) def test_get_image_v1_1(self): request = webob.Request.blank('/v1.1/fake/images/124') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) actual_image = json.loads(response.body) @@ -409,11 +124,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): expected_image = { "image": { - "id": 124, + "id": "124", "name": "queued snapshot", - "updated": self.NOW_API_FORMAT, - "created": self.NOW_API_FORMAT, - "status": "QUEUED", + "updated": NOW_API_FORMAT, + "created": NOW_API_FORMAT, + "status": "SAVING", "progress": 0, 'server': { 'id': 42, @@ -446,11 +161,12 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_get_image_xml(self): request = webob.Request.blank('/v1.0/images/123') request.accept = "application/xml" - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) actual_image = minidom.parseString(response.body.replace(" ", "")) - expected_now = self.NOW_API_FORMAT + expected_now = NOW_API_FORMAT expected_image = minidom.parseString(""" <image id="123" name="public image" @@ -464,15 +180,16 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): self.assertEqual(expected_image.toxml(), actual_image.toxml()) def test_get_image_xml_no_name(self): - request = webob.Request.blank('/v1.0/images/129') + request = webob.Request.blank('/v1.0/images/130') request.accept = "application/xml" - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) actual_image = minidom.parseString(response.body.replace(" ", "")) - expected_now = self.NOW_API_FORMAT + expected_now = NOW_API_FORMAT expected_image = minidom.parseString(""" - <image id="129" + <image id="130" name="None" updated="%(expected_now)s" created="%(expected_now)s" @@ -557,90 +274,198 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_get_image_index_v1_1(self): request = webob.Request.blank('/v1.1/fake/images') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) response_dict = json.loads(response.body) response_list = response_dict["images"] - fixtures = copy.copy(self.fixtures) - - for image in fixtures: - if not self._applicable_fixture(image, "fake"): - fixtures.remove(image) - continue - - href = "http://localhost/v1.1/fake/images/%s" % image["id"] - bookmark = "http://localhost/fake/images/%s" % image["id"] - test_image = { - "id": image["id"], - "name": image["name"], + expected = [ + { + "id": "123", + "name": "public image", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/123", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/123", + }, + ], + }, + { + "id": "124", + "name": "queued snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/124", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/124", + }, + ], + }, + { + "id": "125", + "name": "saving snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/125", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/125", + }, + ], + }, + { + "id": "126", + "name": "active snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/126", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/126", + }, + ], + }, + { + "id": "127", + "name": "killed snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/127", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/127", + }, + ], + }, + { + "id": "128", + "name": "deleted snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/128", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/128", + }, + ], + }, + { + "id": "129", + "name": "pending_delete snapshot", + "links": [ + { + "rel": "self", + "href": "http://localhost/v1.1/fake/images/129", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/129", + }, + ], + }, + { + "id": "130", + "name": None, "links": [ { "rel": "self", - "href": href, + "href": "http://localhost/v1.1/fake/images/130", }, { "rel": "bookmark", - "href": bookmark, + "href": "http://localhost/fake/images/130", }, ], - } - self.assertTrue(test_image in response_list) + }, + ] - self.assertEqual(len(response_list), len(fixtures)) + self.assertDictListMatch(response_list, expected) def test_get_image_details(self): request = webob.Request.blank('/v1.0/images/detail') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) response_dict = json.loads(response.body) response_list = response_dict["images"] expected = [{ - 'id': 123, + 'id': '123', 'name': 'public image', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, }, { - 'id': 124, + 'id': '124', 'name': 'queued snapshot', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, - 'status': 'QUEUED', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', 'progress': 0, }, { - 'id': 125, + 'id': '125', 'name': 'saving snapshot', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'SAVING', 'progress': 0, }, { - 'id': 126, + 'id': '126', 'name': 'active snapshot', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, }, { - 'id': 127, + 'id': '127', 'name': 'killed snapshot', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, - 'status': 'FAILED', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ERROR', + 'progress': 0, + }, + { + 'id': '128', + 'name': 'deleted snapshot', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', 'progress': 0, }, { - 'id': 129, + 'id': '129', + 'name': 'pending_delete snapshot', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', + 'progress': 0, + }, + { + 'id': '130', 'name': None, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, }] @@ -649,7 +474,8 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_get_image_details_v1_1(self): request = webob.Request.blank('/v1.1/fake/images/detail') - response = request.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + response = request.get_response(app) response_dict = json.loads(response.body) response_list = response_dict["images"] @@ -657,11 +483,11 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): server_bookmark = "http://localhost/servers/42" expected = [{ - 'id': 123, + 'id': '123', 'name': 'public image', - 'metadata': {}, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'metadata': {'key1': 'value1'}, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, "links": [{ @@ -674,15 +500,15 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }], }, { - 'id': 124, + 'id': '124', 'name': 'queued snapshot', 'metadata': { u'instance_ref': u'http://localhost/v1.1/servers/42', u'user_id': u'fake', }, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, - 'status': 'QUEUED', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'SAVING', 'progress': 0, 'server': { 'id': 42, @@ -705,14 +531,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }], }, { - 'id': 125, + 'id': '125', 'name': 'saving snapshot', 'metadata': { u'instance_ref': u'http://localhost/v1.1/servers/42', u'user_id': u'fake', }, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'SAVING', 'progress': 0, 'server': { @@ -736,14 +562,14 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }], }, { - 'id': 126, + 'id': '126', 'name': 'active snapshot', 'metadata': { u'instance_ref': u'http://localhost/v1.1/servers/42', u'user_id': u'fake', }, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, 'server': { @@ -767,15 +593,15 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }], }, { - 'id': 127, + 'id': '127', 'name': 'killed snapshot', 'metadata': { u'instance_ref': u'http://localhost/v1.1/servers/42', u'user_id': u'fake', }, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, - 'status': 'FAILED', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'ERROR', 'progress': 0, 'server': { 'id': 42, @@ -798,20 +624,82 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): }], }, { - 'id': 129, + 'id': '128', + 'name': 'deleted snapshot', + 'metadata': { + u'instance_ref': u'http://localhost/v1.1/servers/42', + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', + 'progress': 0, + 'server': { + 'id': 42, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v1.1/fake/images/128", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/128", + }], + }, + { + 'id': '129', + 'name': 'pending_delete snapshot', + 'metadata': { + u'instance_ref': u'http://localhost/v1.1/servers/42', + u'user_id': u'fake', + }, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, + 'status': 'DELETED', + 'progress': 0, + 'server': { + 'id': 42, + "links": [{ + "rel": "self", + "href": server_href, + }, + { + "rel": "bookmark", + "href": server_bookmark, + }], + }, + "links": [{ + "rel": "self", + "href": "http://localhost/v1.1/fake/images/129", + }, + { + "rel": "bookmark", + "href": "http://localhost/fake/images/129", + }], + }, + { + 'id': '130', 'name': None, 'metadata': {}, - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100, "links": [{ "rel": "self", - "href": "http://localhost/v1.1/fake/images/129", + "href": "http://localhost/v1.1/fake/images/130", }, { "rel": "bookmark", - "href": "http://localhost/fake/images/129", + "href": "http://localhost/fake/images/130", }], }, ] @@ -1023,11 +911,12 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): def test_get_image_found(self): req = webob.Request.blank('/v1.0/images/123') - res = req.get_response(fakes.wsgi_app()) + app = fakes.wsgi_app(fake_auth_context=self._get_fake_context()) + res = req.get_response(app) image_meta = json.loads(res.body)['image'] - expected = {'id': 123, 'name': 'public image', - 'updated': self.NOW_API_FORMAT, - 'created': self.NOW_API_FORMAT, 'status': 'ACTIVE', + expected = {'id': '123', 'name': 'public image', + 'updated': NOW_API_FORMAT, + 'created': NOW_API_FORMAT, 'status': 'ACTIVE', 'progress': 100} self.assertDictMatch(image_meta, expected) @@ -1036,14 +925,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): res = req.get_response(fakes.wsgi_app()) self.assertEqual(res.status_int, 404) - def test_get_image_not_owned(self): - """We should return a 404 if we request an image that doesn't belong - to us - """ - req = webob.Request.blank('/v1.0/images/128') - res = req.get_response(fakes.wsgi_app()) - self.assertEqual(res.status_int, 404) - def test_create_image(self): body = dict(image=dict(serverId='123', name='Snapshot 1')) req = webob.Request.blank('/v1.0/images') @@ -1086,49 +967,6 @@ class ImageControllerWithGlanceServiceTest(test.TestCase): response = req.get_response(fakes.wsgi_app()) self.assertEqual(400, response.status_int) - @classmethod - def _make_image_fixtures(cls): - image_id = 123 - base_attrs = {'created_at': cls.NOW_GLANCE_FORMAT, - 'updated_at': cls.NOW_GLANCE_FORMAT, - 'deleted_at': None, - 'deleted': False} - - fixtures = [] - - def add_fixture(**kwargs): - kwargs.update(base_attrs) - fixtures.append(kwargs) - - # Public image - add_fixture(id=image_id, name='public image', is_public=True, - status='active', properties={}) - image_id += 1 - - # Snapshot for User 1 - server_ref = 'http://localhost/v1.1/servers/42' - snapshot_properties = {'instance_ref': server_ref, 'user_id': 'fake'} - for status in ('queued', 'saving', 'active', 'killed'): - add_fixture(id=image_id, name='%s snapshot' % status, - is_public=False, status=status, - properties=snapshot_properties) - image_id += 1 - - # Snapshot for User 2 - other_snapshot_properties = {'instance_id': '43', 'user_id': 'other'} - add_fixture(id=image_id, name='someone elses snapshot', - is_public=False, status='active', - properties=other_snapshot_properties) - - image_id += 1 - - # Image without a name - add_fixture(id=image_id, is_public=True, status='active', - properties={}) - image_id += 1 - - return fixtures - class ImageXMLSerializationTest(test.TestCase): diff --git a/nova/tests/glance/stubs.py b/nova/tests/glance/stubs.py index f2a19f22d..1567393e3 100644 --- a/nova/tests/glance/stubs.py +++ b/nova/tests/glance/stubs.py @@ -16,14 +16,15 @@ import StringIO -import nova.image +from nova import exception +from nova.image import glance def stubout_glance_client(stubs): - def fake_get_glance_client(image_href): + def fake_get_glance_client(context, image_href): image_id = int(str(image_href).split('/')[-1]) return (FakeGlance('foo'), image_id) - stubs.Set(nova.image, 'get_glance_client', fake_get_glance_client) + stubs.Set(glance, 'get_glance_client', fake_get_glance_client) class FakeGlance(object): @@ -78,3 +79,70 @@ class FakeGlance(object): def get_image(self, image_id): image = self.IMAGE_FIXTURES[int(image_id)] return image['image_meta'], image['image_data'] + + +NOW_GLANCE_FORMAT = "2010-10-11T10:30:22" + + +class StubGlanceClient(object): + + def __init__(self, images=None): + self.images = [] + _images = images or [] + map(lambda image: self.add_image(image, None), _images) + + def set_auth_token(self, auth_tok): + pass + + def get_image_meta(self, image_id): + for image in self.images: + if image['id'] == str(image_id): + return image + raise exception.ImageNotFound(image_id=image_id) + + #TODO(bcwaldon): implement filters + def get_images_detailed(self, filters=None, marker=None, limit=3): + if marker is None: + index = 0 + else: + for index, image in enumerate(self.images): + if image['id'] == str(marker): + index += 1 + break + + return self.images[index:index + limit] + + def get_image(self, image_id): + return self.get_image_meta(image_id), [] + + def add_image(self, metadata, data): + metadata['created_at'] = NOW_GLANCE_FORMAT + metadata['updated_at'] = NOW_GLANCE_FORMAT + + self.images.append(metadata) + + try: + image_id = str(metadata['id']) + except KeyError: + # auto-generate an id if one wasn't provided + image_id = str(len(self.images)) + + self.images[-1]['id'] = image_id + + return self.images[-1] + + def update_image(self, image_id, metadata, data): + for i, image in enumerate(self.images): + if image['id'] == str(image_id): + if 'id' in metadata: + metadata['id'] = str(metadata['id']) + self.images[i].update(metadata) + return self.images[i] + raise exception.ImageNotFound(image_id=image_id) + + def delete_image(self, image_id): + for i, image in enumerate(self.images): + if image['id'] == image_id: + del self.images[i] + return + raise exception.ImageNotFound(image_id=image_id) diff --git a/nova/tests/image/test_glance.py b/nova/tests/image/test_glance.py index b1ebd8436..290c9a04a 100644 --- a/nova/tests/image/test_glance.py +++ b/nova/tests/image/test_glance.py @@ -17,47 +17,14 @@ import datetime -import unittest +import stubout +from nova.tests.api.openstack import fakes from nova import context from nova import exception -from nova import test from nova.image import glance - - -class StubGlanceClient(object): - - def __init__(self, images, add_response=None, update_response=None): - self.images = images - self.add_response = add_response - self.update_response = update_response - - def set_auth_token(self, auth_tok): - pass - - def get_image_meta(self, image_id): - return self.images[image_id] - - def get_images_detailed(self, filters=None, marker=None, limit=None): - images = self.images.values() - if marker is None: - index = 0 - else: - for index, image in enumerate(images): - if image['id'] == marker: - index += 1 - break - # default to a page size of 3 to ensure we flex the pagination code - return images[index:index + 3] - - def get_image(self, image_id): - return self.images[image_id], [] - - def add_image(self, metadata, data): - return self.add_response - - def update_image(self, image_id, metadata, data): - return self.update_response +from nova import test +from nova.tests.glance import stubs as glance_stubs class NullWriter(object): @@ -67,218 +34,7 @@ class NullWriter(object): pass -class BaseGlanceTest(unittest.TestCase): - NOW_GLANCE_OLD_FORMAT = "2010-10-11T10:30:22" - NOW_GLANCE_FORMAT = "2010-10-11T10:30:22.000000" - NOW_DATETIME = datetime.datetime(2010, 10, 11, 10, 30, 22) - - def setUp(self): - self.client = StubGlanceClient(None) - self.service = glance.GlanceImageService(client=self.client) - self.context = context.RequestContext(None, None) - - def assertDateTimesFilled(self, image_meta): - self.assertEqual(image_meta['created_at'], self.NOW_DATETIME) - self.assertEqual(image_meta['updated_at'], self.NOW_DATETIME) - self.assertEqual(image_meta['deleted_at'], self.NOW_DATETIME) - - def assertDateTimesEmpty(self, image_meta): - self.assertEqual(image_meta['updated_at'], None) - self.assertEqual(image_meta['deleted_at'], None) - - def assertDateTimesBlank(self, image_meta): - self.assertEqual(image_meta['updated_at'], '') - self.assertEqual(image_meta['deleted_at'], '') - - -class TestGlanceImageServiceProperties(BaseGlanceTest): - def test_show_passes_through_to_client(self): - """Ensure attributes which aren't BASE_IMAGE_ATTRS are stored in the - properties dict - """ - fixtures = {'image1': {'id': '1', 'name': 'image1', 'is_public': True, - 'foo': 'bar', - 'properties': {'prop1': 'propvalue1'}}} - self.client.images = fixtures - image_meta = self.service.show(self.context, 'image1') - - expected = {'id': '1', 'name': 'image1', 'is_public': True, - 'properties': {'prop1': 'propvalue1', 'foo': 'bar'}} - self.assertEqual(image_meta, expected) - - def test_show_raises_when_no_authtoken_in_the_context(self): - fixtures = {'image1': {'name': 'image1', 'is_public': False, - 'foo': 'bar', - 'properties': {'prop1': 'propvalue1'}}} - self.client.images = fixtures - self.context.auth_token = False - - expected = {'name': 'image1', 'is_public': True, - 'properties': {'prop1': 'propvalue1', 'foo': 'bar'}} - self.assertRaises(exception.ImageNotFound, - self.service.show, self.context, 'image1') - - def test_show_passes_through_to_client_with_authtoken_in_context(self): - fixtures = {'image1': {'name': 'image1', 'is_public': False, - 'foo': 'bar', - 'properties': {'prop1': 'propvalue1'}}} - self.client.images = fixtures - self.context.auth_token = True - - expected = {'name': 'image1', 'is_public': False, - 'properties': {'prop1': 'propvalue1', 'foo': 'bar'}} - - image_meta = self.service.show(self.context, 'image1') - self.assertEqual(image_meta, expected) - - def test_detail_passes_through_to_client(self): - fixtures = {'image1': {'id': '1', 'name': 'image1', 'is_public': True, - 'foo': 'bar', - 'properties': {'prop1': 'propvalue1'}}} - self.client.images = fixtures - image_meta = self.service.detail(self.context) - expected = [{'id': '1', 'name': 'image1', 'is_public': True, - 'properties': {'prop1': 'propvalue1', 'foo': 'bar'}}] - self.assertEqual(image_meta, expected) - - -class TestGetterDateTimeNoneTests(BaseGlanceTest): - - def test_show_handles_none_datetimes(self): - self.client.images = self._make_none_datetime_fixtures() - image_meta = self.service.show(self.context, 'image1') - self.assertDateTimesEmpty(image_meta) - - def test_show_handles_blank_datetimes(self): - self.client.images = self._make_blank_datetime_fixtures() - image_meta = self.service.show(self.context, 'image1') - self.assertDateTimesBlank(image_meta) - - def test_detail_handles_none_datetimes(self): - self.client.images = self._make_none_datetime_fixtures() - image_meta = self.service.detail(self.context)[0] - self.assertDateTimesEmpty(image_meta) - - def test_detail_handles_blank_datetimes(self): - self.client.images = self._make_blank_datetime_fixtures() - image_meta = self.service.detail(self.context)[0] - self.assertDateTimesBlank(image_meta) - - def test_get_handles_none_datetimes(self): - self.client.images = self._make_none_datetime_fixtures() - writer = NullWriter() - image_meta = self.service.get(self.context, 'image1', writer) - self.assertDateTimesEmpty(image_meta) - - def test_get_handles_blank_datetimes(self): - self.client.images = self._make_blank_datetime_fixtures() - writer = NullWriter() - image_meta = self.service.get(self.context, 'image1', writer) - self.assertDateTimesBlank(image_meta) - - def test_show_makes_datetimes(self): - self.client.images = self._make_datetime_fixtures() - image_meta = self.service.show(self.context, 'image1') - self.assertDateTimesFilled(image_meta) - image_meta = self.service.show(self.context, 'image2') - self.assertDateTimesFilled(image_meta) - - def test_detail_makes_datetimes(self): - self.client.images = self._make_datetime_fixtures() - image_meta = self.service.detail(self.context)[0] - self.assertDateTimesFilled(image_meta) - image_meta = self.service.detail(self.context)[1] - self.assertDateTimesFilled(image_meta) - - def test_get_makes_datetimes(self): - self.client.images = self._make_datetime_fixtures() - writer = NullWriter() - image_meta = self.service.get(self.context, 'image1', writer) - self.assertDateTimesFilled(image_meta) - image_meta = self.service.get(self.context, 'image2', writer) - self.assertDateTimesFilled(image_meta) - - def _make_datetime_fixtures(self): - fixtures = { - 'image1': { - 'id': '1', - 'name': 'image1', - 'is_public': True, - 'created_at': self.NOW_GLANCE_FORMAT, - 'updated_at': self.NOW_GLANCE_FORMAT, - 'deleted_at': self.NOW_GLANCE_FORMAT, - }, - 'image2': { - 'id': '2', - 'name': 'image2', - 'is_public': True, - 'created_at': self.NOW_GLANCE_OLD_FORMAT, - 'updated_at': self.NOW_GLANCE_OLD_FORMAT, - 'deleted_at': self.NOW_GLANCE_OLD_FORMAT, - }, - } - return fixtures - - def _make_none_datetime_fixtures(self): - fixtures = {'image1': {'id': '1', - 'name': 'image1', - 'is_public': True, - 'updated_at': None, - 'deleted_at': None}} - return fixtures - - def _make_blank_datetime_fixtures(self): - fixtures = {'image1': {'id': '1', - 'name': 'image1', - 'is_public': True, - 'updated_at': '', - 'deleted_at': ''}} - return fixtures - - -class TestMutatorDateTimeTests(BaseGlanceTest): - """Tests create(), update()""" - - def test_create_handles_datetimes(self): - self.client.add_response = self._make_datetime_fixture() - image_meta = self.service.create(self.context, {}) - self.assertDateTimesFilled(image_meta) - - def test_create_handles_none_datetimes(self): - self.client.add_response = self._make_none_datetime_fixture() - dummy_meta = {} - image_meta = self.service.create(self.context, dummy_meta) - self.assertDateTimesEmpty(image_meta) - - def test_update_handles_datetimes(self): - self.client.images = {'image1': self._make_datetime_fixture()} - self.client.update_response = self._make_datetime_fixture() - dummy_meta = {} - image_meta = self.service.update(self.context, 'image1', dummy_meta) - self.assertDateTimesFilled(image_meta) - - def test_update_handles_none_datetimes(self): - self.client.images = {'image1': self._make_datetime_fixture()} - self.client.update_response = self._make_none_datetime_fixture() - dummy_meta = {} - image_meta = self.service.update(self.context, 'image1', dummy_meta) - self.assertDateTimesEmpty(image_meta) - - def _make_datetime_fixture(self): - fixture = {'id': 'image1', 'name': 'image1', 'is_public': True, - 'created_at': self.NOW_GLANCE_FORMAT, - 'updated_at': self.NOW_GLANCE_FORMAT, - 'deleted_at': self.NOW_GLANCE_FORMAT} - return fixture - - def _make_none_datetime_fixture(self): - fixture = {'id': 'image1', 'name': 'image1', 'is_public': True, - 'updated_at': None, - 'deleted_at': None} - return fixture - - -class TestGlanceSerializer(unittest.TestCase): +class TestGlanceSerializer(test.TestCase): def test_serialize(self): metadata = {'name': 'image1', 'is_public': True, @@ -312,3 +68,386 @@ class TestGlanceSerializer(unittest.TestCase): converted = glance._convert_to_string(metadata) self.assertEqual(converted, converted_expected) self.assertEqual(glance._convert_from_string(converted), metadata) + + +class TestGlanceImageService(test.TestCase): + """ + Tests the Glance image service. + + At a high level, the translations involved are: + + 1. Glance -> ImageService - This is needed so we can support + multple ImageServices (Glance, Local, etc) + + 2. ImageService -> API - This is needed so we can support multple + APIs (OpenStack, EC2) + + """ + NOW_GLANCE_OLD_FORMAT = "2010-10-11T10:30:22" + NOW_GLANCE_FORMAT = "2010-10-11T10:30:22.000000" + NOW_DATETIME = datetime.datetime(2010, 10, 11, 10, 30, 22) + + def setUp(self): + super(TestGlanceImageService, self).setUp() + self.stubs = stubout.StubOutForTesting() + fakes.stub_out_compute_api_snapshot(self.stubs) + client = glance_stubs.StubGlanceClient() + self.service = glance.GlanceImageService(client=client) + self.context = context.RequestContext('fake', 'fake', auth_token=True) + self.service.delete_all() + + def tearDown(self): + self.stubs.UnsetAll() + super(TestGlanceImageService, self).tearDown() + + @staticmethod + def _make_fixture(**kwargs): + fixture = {'name': None, + 'properties': {}, + 'status': None, + 'is_public': None} + fixture.update(kwargs) + return fixture + + def _make_datetime_fixture(self): + return self._make_fixture(created_at=self.NOW_GLANCE_FORMAT, + updated_at=self.NOW_GLANCE_FORMAT, + deleted_at=self.NOW_GLANCE_FORMAT) + + def test_create_with_instance_id(self): + """Ensure instance_id is persisted as an image-property""" + fixture = {'name': 'test image', + 'is_public': False, + 'properties': {'instance_id': '42', 'user_id': 'fake'}} + + image_id = self.service.create(self.context, fixture)['id'] + image_meta = self.service.show(self.context, image_id) + expected = { + 'id': image_id, + 'name': 'test image', + 'is_public': False, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None, + 'status': None, + 'properties': {'instance_id': '42', 'user_id': 'fake'}, + } + self.assertDictMatch(image_meta, expected) + + image_metas = self.service.detail(self.context) + self.assertDictMatch(image_metas[0], expected) + + def test_create_without_instance_id(self): + """ + Ensure we can create an image without having to specify an + instance_id. Public images are an example of an image not tied to an + instance. + """ + fixture = {'name': 'test image', 'is_public': False} + image_id = self.service.create(self.context, fixture)['id'] + + expected = { + 'id': image_id, + 'name': 'test image', + 'is_public': False, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None, + 'status': None, + 'properties': {}, + } + actual = self.service.show(self.context, image_id) + self.assertDictMatch(actual, expected) + + def test_create(self): + fixture = self._make_fixture(name='test image') + num_images = len(self.service.index(self.context)) + image_id = self.service.create(self.context, fixture)['id'] + + self.assertNotEquals(None, image_id) + self.assertEquals(num_images + 1, + len(self.service.index(self.context))) + + def test_create_and_show_non_existing_image(self): + fixture = self._make_fixture(name='test image') + image_id = self.service.create(self.context, fixture)['id'] + + self.assertNotEquals(None, image_id) + self.assertRaises(exception.NotFound, + self.service.show, + self.context, + 'bad image id') + + def test_create_and_show_non_existing_image_by_name(self): + fixture = self._make_fixture(name='test image') + image_id = self.service.create(self.context, fixture)['id'] + + self.assertNotEquals(None, image_id) + self.assertRaises(exception.ImageNotFound, + self.service.show_by_name, + self.context, + 'bad image id') + + def test_index(self): + fixture = self._make_fixture(name='test image') + image_id = self.service.create(self.context, fixture)['id'] + image_metas = self.service.index(self.context) + expected = [{'id': image_id, 'name': 'test image'}] + self.assertDictListMatch(image_metas, expected) + + def test_index_default_limit(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.index(self.context) + i = 0 + for meta in image_metas: + expected = {'id': 'DONTCARE', + 'name': 'TestImage %d' % (i)} + self.assertDictMatch(meta, expected) + i = i + 1 + + def test_index_marker(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.index(self.context, marker=ids[1]) + self.assertEquals(len(image_metas), 8) + i = 2 + for meta in image_metas: + expected = {'id': 'DONTCARE', + 'name': 'TestImage %d' % (i)} + self.assertDictMatch(meta, expected) + i = i + 1 + + def test_index_limit(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.index(self.context, limit=5) + self.assertEquals(len(image_metas), 5) + + def test_index_marker_and_limit(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.index(self.context, marker=ids[3], limit=1) + self.assertEquals(len(image_metas), 1) + i = 4 + for meta in image_metas: + expected = {'id': ids[i], + 'name': 'TestImage %d' % (i)} + self.assertDictMatch(meta, expected) + i = i + 1 + + def test_detail_marker(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.detail(self.context, marker=ids[1]) + self.assertEquals(len(image_metas), 8) + i = 2 + for meta in image_metas: + expected = { + 'id': ids[i], + 'status': None, + 'is_public': None, + 'name': 'TestImage %d' % (i), + 'properties': {}, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None + } + + self.assertDictMatch(meta, expected) + i = i + 1 + + def test_detail_limit(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.detail(self.context, limit=5) + self.assertEquals(len(image_metas), 5) + + def test_detail_marker_and_limit(self): + fixtures = [] + ids = [] + for i in range(10): + fixture = self._make_fixture(name='TestImage %d' % (i)) + fixtures.append(fixture) + ids.append(self.service.create(self.context, fixture)['id']) + + image_metas = self.service.detail(self.context, marker=ids[3], limit=5) + self.assertEquals(len(image_metas), 5) + i = 4 + for meta in image_metas: + expected = { + 'id': ids[i], + 'status': None, + 'is_public': None, + 'name': 'TestImage %d' % (i), + 'properties': {}, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None + } + self.assertDictMatch(meta, expected) + i = i + 1 + + def test_update(self): + fixture = self._make_fixture(name='test image') + image_id = self.service.create(self.context, fixture)['id'] + fixture['name'] = 'new image name' + self.service.update(self.context, image_id, fixture) + + new_image_data = self.service.show(self.context, image_id) + self.assertEquals('new image name', new_image_data['name']) + + def test_delete(self): + fixture1 = self._make_fixture(name='test image 1') + fixture2 = self._make_fixture(name='test image 2') + fixtures = [fixture1, fixture2] + + 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(self.context, fixture)['id'] + ids.append(new_id) + + num_images = len(self.service.index(self.context)) + self.assertEquals(2, num_images, str(self.service.index(self.context))) + + self.service.delete(self.context, ids[0]) + + num_images = len(self.service.index(self.context)) + self.assertEquals(1, num_images) + + def test_show_passes_through_to_client(self): + fixture = self._make_fixture(name='image1', is_public=True) + image_id = self.service.create(self.context, fixture)['id'] + + image_meta = self.service.show(self.context, image_id) + expected = { + 'id': image_id, + 'name': 'image1', + 'is_public': True, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None, + 'status': None, + 'properties': {}, + } + self.assertEqual(image_meta, expected) + + def test_show_raises_when_no_authtoken_in_the_context(self): + fixture = self._make_fixture(name='image1', + is_public=False, + properties={'one': 'two'}) + image_id = self.service.create(self.context, fixture)['id'] + self.context.auth_token = False + self.assertRaises(exception.ImageNotFound, + self.service.show, + self.context, + image_id) + + def test_detail_passes_through_to_client(self): + fixture = self._make_fixture(name='image10', is_public=True) + image_id = self.service.create(self.context, fixture)['id'] + image_metas = self.service.detail(self.context) + expected = [ + { + 'id': image_id, + 'name': 'image10', + 'is_public': True, + 'size': None, + 'location': None, + 'disk_format': None, + 'container_format': None, + 'checksum': None, + 'created_at': self.NOW_DATETIME, + 'updated_at': self.NOW_DATETIME, + 'deleted_at': None, + 'deleted': None, + 'status': None, + 'properties': {}, + }, + ] + self.assertEqual(image_metas, expected) + + def test_show_makes_datetimes(self): + fixture = self._make_datetime_fixture() + image_id = self.service.create(self.context, fixture)['id'] + image_meta = self.service.show(self.context, image_id) + self.assertEqual(image_meta['created_at'], self.NOW_DATETIME) + self.assertEqual(image_meta['updated_at'], self.NOW_DATETIME) + + def test_detail_makes_datetimes(self): + fixture = self._make_datetime_fixture() + self.service.create(self.context, fixture) + image_meta = self.service.detail(self.context)[0] + self.assertEqual(image_meta['created_at'], self.NOW_DATETIME) + self.assertEqual(image_meta['updated_at'], self.NOW_DATETIME) + + def test_get_makes_datetimes(self): + fixture = self._make_datetime_fixture() + image_id = self.service.create(self.context, fixture)['id'] + writer = NullWriter() + image_meta = self.service.get(self.context, image_id, writer) + self.assertEqual(image_meta['created_at'], self.NOW_DATETIME) + self.assertEqual(image_meta['updated_at'], self.NOW_DATETIME) diff --git a/nova/tests/integrated/integrated_helpers.py b/nova/tests/integrated/integrated_helpers.py index 343190427..49de9c854 100644 --- a/nova/tests/integrated/integrated_helpers.py +++ b/nova/tests/integrated/integrated_helpers.py @@ -64,7 +64,7 @@ class _IntegratedTestBase(test.TestCase): self.flags(**f) self.flags(verbose=True) - def fake_get_image_service(image_href): + def fake_get_image_service(context, image_href): image_id = int(str(image_href).split('/')[-1]) return (nova.image.fake.FakeImageService(), image_id) self.stubs.Set(nova.image, 'get_image_service', fake_get_image_service) diff --git a/nova/tests/test_direct.py b/nova/tests/test_direct.py index 4ed0c2aa5..8d856dc4b 100644 --- a/nova/tests/test_direct.py +++ b/nova/tests/test_direct.py @@ -30,7 +30,7 @@ from nova import test from nova import volume from nova import utils from nova.api import direct -from nova.tests import test_cloud +from nova.tests.api.ec2 import test_cloud class ArbitraryObject(object): diff --git a/nova/tests/test_libvirt.py b/nova/tests/test_libvirt.py index 93967ceec..233ee14de 100644 --- a/nova/tests/test_libvirt.py +++ b/nova/tests/test_libvirt.py @@ -700,7 +700,7 @@ class LibvirtConnTestCase(test.TestCase): # qemu-img should be mockd since test environment might not have # large disk space. self.mox.StubOutWithMock(utils, "execute") - utils.execute('sudo', 'qemu-img', 'create', '-f', 'raw', + utils.execute('qemu-img', 'create', '-f', 'raw', '%s/%s/disk' % (tmpdir, instance_ref.name), '10G') self.mox.ReplayAll() @@ -752,7 +752,7 @@ class LibvirtConnTestCase(test.TestCase): os.path.getsize("/test/disk").AndReturn(10 * 1024 * 1024 * 1024) # another is qcow image, so qemu-img should be mocked. self.mox.StubOutWithMock(utils, "execute") - utils.execute('sudo', 'qemu-img', 'info', '/test/disk.local').\ + utils.execute('qemu-img', 'info', '/test/disk.local').\ AndReturn((ret, '')) self.mox.ReplayAll() diff --git a/nova/tests/test_xenapi.py b/nova/tests/test_xenapi.py index 91b4161b0..4a83d139e 100644 --- a/nova/tests/test_xenapi.py +++ b/nova/tests/test_xenapi.py @@ -932,8 +932,9 @@ class XenAPIDetermineDiskImageTestCase(test.TestCase): self.fake_instance.architecture = 'x86-64' def assert_disk_type(self, disk_type): + ctx = context.RequestContext('fake', 'fake') dt = vm_utils.VMHelper.determine_disk_image_type( - self.fake_instance) + self.fake_instance, ctx) self.assertEqual(disk_type, dt) def test_instance_disk(self): diff --git a/nova/virt/disk.py b/nova/virt/disk.py index 12a2cea45..cd3422829 100644 --- a/nova/virt/disk.py +++ b/nova/virt/disk.py @@ -160,6 +160,7 @@ def destroy_container(target, instance, nbd=False): except Exception, exn: LOG.exception(_('Failed to remove container: %s'), exn) + def _link_device(image, nbd): """Link image to device using loopback or nbd""" @@ -229,8 +230,8 @@ def _inject_metadata_into_fs(metadata, fs, execute=None): metadata_path = os.path.join(fs, "meta.js") metadata = dict([(m.key, m.value) for m in metadata]) - utils.execute('sudo', 'tee', metadata_path, - process_input=json.dumps(metadata)) + utils.execute('tee', metadata_path, + process_input=json.dumps(metadata), run_as_root=True) def _inject_key_into_fs(key, fs, execute=None): diff --git a/nova/virt/images.py b/nova/virt/images.py index 54c691a40..810b359d9 100644 --- a/nova/virt/images.py +++ b/nova/virt/images.py @@ -37,7 +37,8 @@ def fetch(context, image_href, path, _user_id, _project_id): # when it is added to glance. Right now there is no # auth checking in glance, so we assume that access was # checked before we got here. - (image_service, image_id) = nova.image.get_image_service(image_href) + (image_service, image_id) = nova.image.get_image_service(context, + image_href) with open(path, "wb") as image_file: metadata = image_service.get(context, image_id, image_file) return metadata diff --git a/nova/virt/libvirt/connection.py b/nova/virt/libvirt/connection.py index 19cef5ad7..f591ce02c 100644 --- a/nova/virt/libvirt/connection.py +++ b/nova/virt/libvirt/connection.py @@ -196,7 +196,7 @@ class LibvirtConnection(driver.ComputeDriver): def _test_connection(self): try: - self._wrapped_conn.getInfo() + self._wrapped_conn.getCapabilities() return True except libvirt.libvirtError as e: if e.get_error_code() == libvirt.VIR_ERR_SYSTEM_ERROR and \ @@ -398,10 +398,10 @@ class LibvirtConnection(driver.ComputeDriver): virt_dom = self._lookup_by_name(instance['name']) (image_service, image_id) = nova.image.get_image_service( - instance['image_ref']) + context, instance['image_ref']) base = image_service.show(context, image_id) (snapshot_image_service, snapshot_image_id) = \ - nova.image.get_image_service(image_href) + nova.image.get_image_service(context, image_href) snapshot = snapshot_image_service.show(context, snapshot_image_id) metadata = {'is_public': False, diff --git a/nova/virt/vmwareapi/fake.py b/nova/virt/vmwareapi/fake.py index 4c62d18bb..0dea13aba 100644 --- a/nova/virt/vmwareapi/fake.py +++ b/nova/virt/vmwareapi/fake.py @@ -412,7 +412,7 @@ def fake_get_network(*args, **kwargs): return [{'type': 'fake'}]
-def fake_fetch_image(image, instance, **kwargs):
+def fake_fetch_image(context, image, instance, **kwargs):
"""Fakes fetch image call. Just adds a reference to the db for the file."""
ds_name = kwargs.get("datastore_name")
file_path = kwargs.get("file_path")
@@ -420,12 +420,12 @@ def fake_fetch_image(image, instance, **kwargs): _add_file(ds_file_path)
-def fake_upload_image(image, instance, **kwargs):
+def fake_upload_image(context, image, instance, **kwargs):
"""Fakes the upload of an image."""
pass
-def fake_get_vmdk_size_and_properties(image_id, instance):
+def fake_get_vmdk_size_and_properties(context, image_id, instance):
"""Fakes the file size and properties fetch for the image file."""
props = {"vmware_ostype": "otherGuest",
"vmware_adaptertype": "lsiLogic"}
diff --git a/nova/virt/vmwareapi/vmops.py b/nova/virt/vmwareapi/vmops.py index 07a6ba6ab..6bdc2f23a 100644 --- a/nova/virt/vmwareapi/vmops.py +++ b/nova/virt/vmwareapi/vmops.py @@ -157,7 +157,7 @@ class VMWareVMOps(object): repository.
"""
image_size, image_properties = \
- vmware_images.get_vmdk_size_and_properties(
+ vmware_images.get_vmdk_size_and_properties(context,
instance.image_ref, instance)
vmdk_file_size_in_kb = int(image_size) / 1024
os_type = image_properties.get("vmware_ostype", "otherGuest")
@@ -282,6 +282,7 @@ class VMWareVMOps(object): # Upload the -flat.vmdk file whose meta-data file we just created
# above
vmware_images.fetch_image(
+ context,
instance.image_ref,
instance,
host=self._session._host_ip,
@@ -448,6 +449,7 @@ class VMWareVMOps(object): # Upload the contents of -flat.vmdk file which has the disk data.
LOG.debug(_("Uploading image %s") % snapshot_name)
vmware_images.upload_image(
+ context,
snapshot_name,
instance,
os_type=os_type,
diff --git a/nova/virt/vmwareapi/vmware_images.py b/nova/virt/vmwareapi/vmware_images.py index f5f75dae2..53f2d372e 100644 --- a/nova/virt/vmwareapi/vmware_images.py +++ b/nova/virt/vmwareapi/vmware_images.py @@ -20,15 +20,13 @@ Utility functions for Image transfer. from nova import exception
from nova import flags
-import nova.image
+from nova.image import glance
from nova import log as logging
from nova.virt.vmwareapi import io_util
from nova.virt.vmwareapi import read_write_util
LOG = logging.getLogger("nova.virt.vmwareapi.vmware_images")
-FLAGS = flags.FLAGS
-
QUEUE_BUFFER_SIZE = 10
@@ -87,36 +85,10 @@ def start_transfer(read_file_handle, data_size, write_file_handle=None, write_file_handle.close()
-def fetch_image(image, instance, **kwargs):
- """Fetch an image for attaching to the newly created VM."""
- # Depending upon the image service, make appropriate image service call
- if FLAGS.image_service == "nova.image.glance.GlanceImageService":
- func = _get_glance_image
- elif FLAGS.image_service == "nova.image.s3.S3ImageService":
- func = _get_s3_image
- else:
- raise NotImplementedError(_("The Image Service %s is not implemented")
- % FLAGS.image_service)
- return func(image, instance, **kwargs)
-
-
-def upload_image(image, instance, **kwargs):
- """Upload the newly snapshotted VM disk file."""
- # Depending upon the image service, make appropriate image service call
- if FLAGS.image_service == "nova.image.glance.GlanceImageService":
- func = _put_glance_image
- elif FLAGS.image_service == "nova.image.s3.S3ImageService":
- func = _put_s3_image
- else:
- raise NotImplementedError(_("The Image Service %s is not implemented")
- % FLAGS.image_service)
- return func(image, instance, **kwargs)
-
-
-def _get_glance_image(image, instance, **kwargs):
+def fetch_image(context, image, instance, **kwargs):
"""Download image from the glance image server."""
LOG.debug(_("Downloading image %s from glance image server") % image)
- (glance_client, image_id) = nova.image.get_glance_client(image)
+ (glance_client, image_id) = glance.get_glance_client(context, image)
metadata, read_iter = glance_client.get_image(image_id)
read_file_handle = read_write_util.GlanceFileRead(read_iter)
file_size = int(metadata['size'])
@@ -132,17 +104,7 @@ def _get_glance_image(image, instance, **kwargs): LOG.debug(_("Downloaded image %s from glance image server") % image)
-def _get_s3_image(image, instance, **kwargs):
- """Download image from the S3 image server."""
- raise NotImplementedError
-
-
-def _get_local_image(image, instance, **kwargs):
- """Download image from the local nova compute node."""
- raise NotImplementedError
-
-
-def _put_glance_image(image, instance, **kwargs):
+def upload_image(context, image, instance, **kwargs):
"""Upload the snapshotted vm disk file to Glance image server."""
LOG.debug(_("Uploading image %s to the Glance image server") % image)
read_file_handle = read_write_util.VmWareHTTPReadFile(
@@ -152,7 +114,7 @@ def _put_glance_image(image, instance, **kwargs): kwargs.get("cookies"),
kwargs.get("file_path"))
file_size = read_file_handle.get_size()
- (glance_client, image_id) = nova.image.get_glance_client(image)
+ (glance_client, image_id) = glance.get_glance_client(context, image)
# The properties and other fields that we need to set for the image.
image_metadata = {"is_public": True,
"disk_format": "vmdk",
@@ -168,17 +130,7 @@ def _put_glance_image(image, instance, **kwargs): LOG.debug(_("Uploaded image %s to the Glance image server") % image)
-def _put_local_image(image, instance, **kwargs):
- """Upload the snapshotted vm disk file to the local nova compute node."""
- raise NotImplementedError
-
-
-def _put_s3_image(image, instance, **kwargs):
- """Upload the snapshotted vm disk file to S3 image server."""
- raise NotImplementedError
-
-
-def get_vmdk_size_and_properties(image, instance):
+def get_vmdk_size_and_properties(context, image, instance):
"""
Get size of the vmdk file that is to be downloaded for attach in spawn.
Need this to create the dummy virtual disk for the meta-data file. The
@@ -186,12 +138,9 @@ def get_vmdk_size_and_properties(image, instance): """
LOG.debug(_("Getting image size for the image %s") % image)
- if FLAGS.image_service == "nova.image.glance.GlanceImageService":
- (glance_client, image_id) = nova.image.get_glance_client(image)
- meta_data = glance_client.get_image_meta(image_id)
- size, properties = meta_data["size"], meta_data["properties"]
- elif FLAGS.image_service == "nova.image.s3.S3ImageService":
- raise NotImplementedError
+ (glance_client, image_id) = glance.get_glance_client(context, image)
+ meta_data = glance_client.get_image_meta(image_id)
+ size, properties = meta_data["size"], meta_data["properties"]
LOG.debug(_("Got image size of %(size)s for the image %(image)s") %
locals())
return size, properties
diff --git a/nova/virt/xenapi/vm_utils.py b/nova/virt/xenapi/vm_utils.py index efbea7076..302238c98 100644 --- a/nova/virt/xenapi/vm_utils.py +++ b/nova/virt/xenapi/vm_utils.py @@ -31,12 +31,10 @@ import urllib import uuid from xml.dom import minidom -import glance.client from nova import db from nova import exception from nova import flags -import nova.image -from nova.image import glance as glance_image_service +from nova.image import glance from nova import log as logging from nova import utils from nova.compute import instance_types @@ -383,8 +381,7 @@ class VMHelper(HelperBase): os_type = instance.os_type or FLAGS.default_os_type - glance_host, glance_port = \ - glance_image_service.pick_glance_api_server() + glance_host, glance_port = glance.pick_glance_api_server() params = {'vdi_uuids': vdi_uuids, 'image_id': image_id, 'glance_host': glance_host, @@ -447,8 +444,7 @@ class VMHelper(HelperBase): # pass them as arguments uuid_stack = [str(uuid.uuid4()) for i in xrange(2)] - glance_host, glance_port = \ - glance_image_service.pick_glance_api_server() + glance_host, glance_port = glance.pick_glance_api_server() params = {'image_id': image, 'glance_host': glance_host, 'glance_port': glance_port, @@ -546,7 +542,7 @@ class VMHelper(HelperBase): else: sr_ref = safe_find_sr(session) - glance_client, image_id = nova.image.get_glance_client(image) + glance_client, image_id = glance.get_glance_client(context, image) glance_client.set_auth_token(getattr(context, 'auth_token', None)) meta, image_file = glance_client.get_image(image_id) virtual_size = int(meta['size']) @@ -606,7 +602,7 @@ class VMHelper(HelperBase): raise e @classmethod - def determine_disk_image_type(cls, instance): + def determine_disk_image_type(cls, instance, context): """Disk Image Types are used to determine where the kernel will reside within an image. To figure out which type we're dealing with, we use the following rules: @@ -639,7 +635,8 @@ class VMHelper(HelperBase): 'vhd': ImageType.DISK_VHD, 'iso': ImageType.DISK_ISO} image_ref = instance.image_ref - glance_client, image_id = nova.image.get_glance_client(image_ref) + glance_client, image_id = glance.get_glance_client(context, + image_ref) meta = glance_client.get_image_meta(image_id) disk_format = meta['disk_format'] try: diff --git a/nova/virt/xenapi/vmops.py b/nova/virt/xenapi/vmops.py index 996bdb209..fb9c602d9 100644 --- a/nova/virt/xenapi/vmops.py +++ b/nova/virt/xenapi/vmops.py @@ -135,7 +135,7 @@ class VMOps(object): self._session.call_xenapi('VM.start', vm_ref, False, False) def _create_disks(self, context, instance): - disk_image_type = VMHelper.determine_disk_image_type(instance) + disk_image_type = VMHelper.determine_disk_image_type(instance, context) vdis = VMHelper.fetch_image(context, self._session, instance, instance.image_ref, instance.user_id, instance.project_id, @@ -176,7 +176,7 @@ class VMOps(object): power_state.SHUTDOWN) return - disk_image_type = VMHelper.determine_disk_image_type(instance) + disk_image_type = VMHelper.determine_disk_image_type(instance, context) kernel = None ramdisk = None try: diff --git a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance index a06312890..1a9ac37e9 100755 --- a/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance +++ b/plugins/xenserver/xenapi/etc/xapi.d/plugins/glance @@ -252,7 +252,11 @@ def _upload_tarball(staging_path, image_id, glance_host, glance_port, os_type, # NOTE(dprince): We need to resend any existing Glance meta/property # headers so they are preserved in Glance. We obtain them here with a # HEAD request. - conn.request('HEAD', '/v1/images/%s' % image_id) + conn.putrequest('HEAD', '/v1/images/%s' % image_id) + if auth_token: + conn.putheader('x-auth-token', auth_token) + conn.endheaders() + resp = conn.getresponse() if resp.status != httplib.OK: raise Exception("Unexpected response from Glance %i" % resp.status) |
