diff --git a/bin/glance-upload b/bin/glance-upload index dfe3a86c01..f1e0e8650a 100755 --- a/bin/glance-upload +++ b/bin/glance-upload @@ -60,6 +60,7 @@ if os.path.exists(os.path.join(possible_topdir, 'glance', '__init__.py')): sys.path.insert(0, possible_topdir) from glance.client import Client +from glance.registry.db.api import DISK_FORMATS, CONTAINER_FORMATS def die(msg): @@ -78,6 +79,14 @@ def parse_args(): parser.add_argument('--type', metavar='TYPE', default='raw', help='Type of Image [kernel, ramdisk, machine, raw] ' '(default: %default)') + parser.add_argument('--disk-format', metavar='DISK_FORMAT', default=None, + choices=DISK_FORMATS, + help='Disk format of Image [%s] ' + '(default: %%default)' % ','.join(DISK_FORMATS)) + parser.add_argument('--container-format', metavar='CONTAINER_FORMAT', + default=None, choices=CONTAINER_FORMATS, + help='Disk format of Image [%s] ' + '(default: %%default)' % ','.join(CONTAINER_FORMATS)) parser.add_argument('--kernel', metavar='KERNEL', help='ID of kernel associated with this machine image') parser.add_argument('--ramdisk', metavar='RAMDISK', @@ -89,7 +98,8 @@ def parse_args(): def main(): args = parse_args() - meta = {'name': args.name, 'type': args.type, 'is_public': True, + meta = {'name': args.name, + 'is_public': True, 'properties': {}} if args.kernel: @@ -98,9 +108,22 @@ def main(): if args.ramdisk: meta['properties']['ramdisk_id'] = args.ramdisk + if args.type: + meta['properties']['type'] = args.type + + if args.disk_format: + meta['disk_format'] = args.disk_format + + if args.container_format: + meta['container_format'] = args.container_format + client = Client(args.host, args.port) with open(args.filename) as f: - new_meta = client.add_image(meta, f) + try: + new_meta = client.add_image(meta, f) + except Exception, e: + print "Failed to add new image. Got error: ", str(e) + return 1 print 'Stored image. Got identifier: %s' % pprint.pformat(new_meta) diff --git a/doc/source/formats.rst b/doc/source/formats.rst new file mode 100644 index 0000000000..927bab586f --- /dev/null +++ b/doc/source/formats.rst @@ -0,0 +1,99 @@ +.. + Copyright 2011 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. + +Disk and Container Formats +========================== + +When adding an image to Glance, you are may specify what the virtual +machine image's *disk format* and *container format* are. + +This document explains exactly what these formats are. + +Disk Format +----------- + +The disk format of a virtual machine image is the format of the underlying +disk image. Virtual appliance vendors have different formats for laying out +the information contained in a virtual machine disk image. + +You can set your image's container format to one of the following: + +* **raw** + + This is an unstructured disk image format + +* **vhd** + + This is the VHD disk format, a common disk format used by virtual machine + monitors from VMWare, Xen, Microsoft, VirtualBox, and others + +* **vmdk** + + Another common disk format supported by many common virtual machine monitors + +* **vdi** + + A disk format supported by VirtualBox virtual machine monitor and the QEMU + emulator + +* **qcow2** + + A disk format supported by the QEMU emulator that can expand dynamically and + supports Copy on Write + +* **aki** + + This indicates what is stored in Glance is an Amazon kernel image + +* **ari** + + This indicates what is stored in Glance is an Amazon ramdisk image + +* **ami** + + This indicates what is stored in Glance is an Amazon machine image + +Container Format +---------------- + +The container format refers to whether the virtual machine image is in a +file format that also contains metadata about the actual virtual machine. + +There are two main types of container formats: OVF and Amazon's AMI. In +addition, a virtual machine image may have no container format at all -- +basically, it's just a blob of unstructured data... + +You can set your image's container format to one of the following: + +* **ovf** + + This is the OVF container format + +* **bare** + + This indicates there is no container or metadata envelope for the image + +* **aki** + + This indicates what is stored in Glance is an Amazon kernel image + +* **ari** + + This indicates what is stored in Glance is an Amazon ramdisk image + +* **ami** + + This indicates what is stored in Glance is an Amazon machine image diff --git a/doc/source/gettingstarted.rst b/doc/source/gettingstarted.rst index d5b66fc1c6..9e8a985aff 100644 --- a/doc/source/gettingstarted.rst +++ b/doc/source/gettingstarted.rst @@ -78,4 +78,6 @@ Glance registry servers are servers that conform to the Glance Registry API. Glance ships with a reference implementation of a registry server that complies with this API (``glance-registry``). -For more details on Glance's architecture see :doc:`here ` +For more details on Glance's architecture see :doc:`here `. For +more information on what a Glance registry server is, see +:doc:`here `. diff --git a/doc/source/glanceapi.rst b/doc/source/glanceapi.rst index d09782261d..f3ca125803 100644 --- a/doc/source/glanceapi.rst +++ b/doc/source/glanceapi.rst @@ -41,7 +41,8 @@ mapping in the following format:: {'images': [ {'uri': 'http://glance.example.com/images/1', 'name': 'Ubuntu 10.04 Plain', - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'size': '5368709120'} ...]} @@ -63,9 +64,10 @@ JSON-encoded mapping in the following format:: {'images': [ {'uri': 'http://glance.example.com/images/1', 'name': 'Ubuntu 10.04 Plain 5GB', - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'size': '5368709120', - 'store': 'swift', + 'location': 'swift://account:key/container/image.tar.gz.0', 'created_at': '2010-02-03 09:34:01', 'updated_at': '2010-02-03 09:34:01', 'deleted_at': '', @@ -111,14 +113,15 @@ following shows an example of the HTTP headers returned from the above x-image-meta-uri http://glance.example.com/images/1 x-image-meta-name Ubuntu 10.04 Plain 5GB - x-image-meta-type kernel + x-image-meta-disk-format vhd + x-image-meta-container-format ovf x-image-meta-size 5368709120 - x-image-meta-store swift + x-image-meta-location swift://account:key/container/image.tar.gz.0 x-image-meta-created_at 2010-02-03 09:34:01 x-image-meta-updated_at 2010-02-03 09:34:01 x-image-meta-deleted_at x-image-meta-status available - x-image-meta-is_public True + x-image-meta-is-public True x-image-meta-property-distro Ubuntu 10.04 LTS .. note:: @@ -160,14 +163,15 @@ returned from the above ``GET`` request:: x-image-meta-uri http://glance.example.com/images/1 x-image-meta-name Ubuntu 10.04 Plain 5GB - x-image-meta-type kernel + x-image-meta-disk-format vhd + x-image-meta-container-format ovf x-image-meta-size 5368709120 - x-image-meta-store swift + x-image-meta-location swift://account:key/container/image.tar.gz.0 x-image-meta-created_at 2010-02-03 09:34:01 x-image-meta-updated_at 2010-02-03 09:34:01 x-image-meta-deleted_at x-image-meta-status available - x-image-meta-is_public True + x-image-meta-is-public True x-image-meta-property-distro Ubuntu 10.04 LTS .. note:: @@ -254,10 +258,19 @@ The list of metadata headers that Glance accepts are listed below. store that is marked default. See the configuration option ``default_store`` for more information. -* ``x-image-meta-type`` +* ``x-image-meta-disk-format`` - This header is required. Valid values are one of ``kernel``, ``machine``, - ``raw``, or ``ramdisk``. + This header is optional. Valid values are one of ``aki``, ``ari``, ``ami``, + ``raw``, ``vhd``, ``vdi``, ``qcow2``, or ``vmdk``. + + For more information, see :doc:`About Disk and Container Formats ` + +* ``x-image-meta-container-format`` + + This header is optional. Valid values are one of ``aki``, ``ari``, ``ami``, + ``bare``, or ``ovf``. + + For more information, see :doc:`About Disk and Container Formats ` * ``x-image-meta-size`` @@ -271,7 +284,7 @@ The list of metadata headers that Glance accepts are listed below. When not present, Glance will calculate the image's size based on the size of the request body. -* ``x-image-meta-is_public`` +* ``x-image-meta-is-public`` This header is optional. diff --git a/doc/source/index.rst b/doc/source/index.rst index acaa9f5889..8b27e006e0 100644 --- a/doc/source/index.rst +++ b/doc/source/index.rst @@ -50,6 +50,7 @@ Concepts identifiers registries statuses + formats Using Glance ============ diff --git a/doc/source/registries.rst b/doc/source/registries.rst index 911562d4e0..6aafcbc6cb 100644 --- a/doc/source/registries.rst +++ b/doc/source/registries.rst @@ -24,44 +24,9 @@ Glance REST-like API for image metadata. Glance comes with a server program ``glance-registry`` that acts as a reference implementation of a Glance Registry. -Using ``glance-registry``, the Glance Registry reference implementation ------------------------------------------------------------------------ - -As mentioned above, ``glance-registry`` is the reference registry -server implementation that ships with Glance. It uses a SQL database -to store information about an image, and publishes this information -via an HTTP/REST-like interface. - -Starting the server -******************* - -Starting the Glance registry server is trivial. Simply call the program -from the command line, as the following example shows:: - - $> glance-registry - (5588) wsgi starting up on http://0.0.0.0:9191/ - -Configuring the server -********************** - -There are a few options that can be supplied to the registry server when -starting it up: - -* ``verbose`` - - Show more verbose/debugging output - -* ``sql_connection`` - - A proper SQLAlchemy connection string as described `here `_ - -* ``registry_host`` - - Address of the host the registry runs on. Defaults to 0.0.0.0. - -* ``registry_port`` - - Port the registry server listens on. Defaults to 9191. +Please see the document :doc:`on Controlling Servers ` +for more information on starting up the Glance registry server that ships +with Glance. Glance Registry API ------------------- @@ -81,6 +46,38 @@ The following is a brief description of the Glance API:: PUT /images/ Update metadata about an existing image DELETE /images/ Remove an image's metadata from the registry +``POST /images`` +---------------- + +The body of the request will be a JSON-encoded set of data about +the image to add to the registry. It will be in the following format:: + + {'image': + {'id': |None, + 'name': , + 'status': , + 'disk_format': , + 'container_format': , + 'properties': [ ... ] + } + } + +The request shall validate the following conditions and return a +``400 Bad request`` when any of the conditions are not met: + +* ``status`` must be non-empty, and must be one of **active**, **saving**, + **queued**, or **killed** + +* ``disk_format`` must be non-empty, and must be one of **ari**, **aki**, + **ami**, **raw**, **vhd**, **vdi**, **qcow2**, or **vmdk** + +* ``container_format`` must be non-empty, and must be on of **ari**, + **aki**, **ami**, **bare**, or **ovf** + +* If ``disk_format`` *or* ``container_format`` is **ari**, **aki**, + **ami**, then *both* ``disk_format`` and ``container_format`` must be + the same. + Examples ******** diff --git a/glance/client.py b/glance/client.py index 810f831a47..03c5b97ba7 100644 --- a/glance/client.py +++ b/glance/client.py @@ -154,8 +154,10 @@ class BaseClient(object): raise exception.Duplicate(res.read()) elif status_code == httplib.BAD_REQUEST: raise exception.Invalid(res.read()) + elif status_code == httplib.INTERNAL_SERVER_ERROR: + raise Exception("Internal Server error: %s" % res.read()) else: - raise Exception("Unknown error occurred! %s" % res.__dict__) + raise Exception("Unknown error occurred! %s" % res.read()) except (socket.error, IOError), e: raise ClientConnectionError("Unable to connect to " @@ -264,13 +266,17 @@ class Client(BaseClient): """ Updates Glance's information about an image """ - if image_meta: - if 'image' not in image_meta: - image_meta = dict(image=image_meta) + if image_meta is None: + image_meta = {} - headers = utils.image_meta_to_http_headers(image_meta or {}) + headers = utils.image_meta_to_http_headers(image_meta) + + if image_data: + body = image_data + headers['content-type'] = 'application/octet-stream' + else: + body = None - body = image_data res = self.do_request("PUT", "/images/%s" % image_id, body, headers) data = json.loads(res.read()) return data['image'] diff --git a/glance/registry/db/api.py b/glance/registry/db/api.py index 6fee83ee5e..8d8cc45af6 100644 --- a/glance/registry/db/api.py +++ b/glance/registry/db/api.py @@ -40,9 +40,14 @@ BASE = models.BASE BASE_MODEL_ATTRS = set(['id', 'created_at', 'updated_at', 'deleted_at', 'deleted']) -IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'type', 'status', 'size', +IMAGE_ATTRS = BASE_MODEL_ATTRS | set(['name', 'status', 'size', + 'disk_format', 'container_format', 'is_public', 'location']) +CONTAINER_FORMATS = ['ami', 'ari', 'aki', 'bare', 'ovf'] +DISK_FORMATS = ['ami', 'ari', 'aki', 'vhd', 'vmdk', 'raw', 'qcow2', 'vdi'] +STATUSES = ['active', 'saving', 'queued', 'killed'] + def configure_db(options): """ @@ -152,19 +157,36 @@ def validate_image(values): :param values: Mapping of image metadata to check """ - image_type = values.get('type', None) - if image_type and image_type not in ( - 'machine', 'kernel', 'ramdisk', 'raw', 'vhd'): - msg = "Invalid image type '%s' for image." % image_type - raise exception.Invalid(msg) + status = values.get('status') + disk_format = values.get('disk_format') + container_format = values.get('container_format') + status = values.get('status', None) if not status: msg = "Image status is required." raise exception.Invalid(msg) - if status not in ('active', 'queued', 'killed', 'saving'): + + if status not in STATUSES: msg = "Invalid image status '%s' for image." % status raise exception.Invalid(msg) + if disk_format and disk_format not in DISK_FORMATS: + msg = "Invalid disk format '%s' for image." % disk_format + raise exception.Invalid(msg) + + if container_format and container_format not in CONTAINER_FORMATS: + msg = "Invalid container format '%s' for image." % container_format + raise exception.Invalid(msg) + + if disk_format in ('aki', 'ari', 'ami') or\ + container_format in ('aki', 'ari', 'ami'): + if container_format != disk_format: + msg = ("Invalid mix of disk and container formats. " + "When setting a disk or container format to " + "one of 'ami', 'ari', or 'ami', the container " + "and disk formats must match.") + raise exception.Invalid(msg) + def _image_update(context, values, image_id): """Used internally by image_create and image_update @@ -174,27 +196,35 @@ def _image_update(context, values, image_id): :param image_id: If None, create the image, otherwise, find and update it """ - # Validate the attributes before we go any further. From my investigation, - # the @validates decorator does not validate on new records, only on - # existing records, which is, well, idiotic. - validate_image(values) - session = get_session() with session.begin(): - _drop_protected_attrs(models.Image, values) - if 'size' in values: - values['size'] = int(values['size']) - - values['is_public'] = bool(values.get('is_public', False)) + # Remove the properties passed in the values mapping. We + # handle properties separately from base image attributes, + # and leaving properties in the values mapping will cause + # a SQLAlchemy model error because SQLAlchemy expects the + # properties attribute of an Image model to be a list and + # not a dict. properties = values.pop('properties', {}) if image_id: image_ref = image_get(context, image_id, session=session) else: + if 'size' in values: + values['size'] = int(values['size']) + + values['is_public'] = bool(values.get('is_public', False)) image_ref = models.Image() + _drop_protected_attrs(models.Image, values) image_ref.update(values) + + # Validate the attributes before we go any further. From my + # investigation, the @validates decorator does not validate + # on new records, only on existing records, which is, well, + # idiotic. + validate_image(image_ref.to_dict()) + image_ref.save(session=session) _set_properties_for_image(context, image_ref, properties, session) diff --git a/glance/registry/db/migrate_repo/versions/001_add_images_table.py b/glance/registry/db/migrate_repo/versions/001_add_images_table.py index b258ed2794..8f64c749a6 100644 --- a/glance/registry/db/migrate_repo/versions/001_add_images_table.py +++ b/glance/registry/db/migrate_repo/versions/001_add_images_table.py @@ -36,7 +36,8 @@ def define_images_table(meta): Column('deleted_at', DateTime()), Column('deleted', Boolean(), nullable=False, default=False, index=True), - mysql_engine='InnoDB') + mysql_engine='InnoDB', + useexisting=True) return images diff --git a/glance/registry/db/migrate_repo/versions/002_add_image_properties_table.py b/glance/registry/db/migrate_repo/versions/002_add_image_properties_table.py index 72bf9ed622..893eee2ba2 100644 --- a/glance/registry/db/migrate_repo/versions/002_add_image_properties_table.py +++ b/glance/registry/db/migrate_repo/versions/002_add_image_properties_table.py @@ -41,7 +41,8 @@ def define_image_properties_table(meta): Column('deleted', Boolean(), nullable=False, default=False, index=True), UniqueConstraint('image_id', 'key'), - mysql_engine='InnoDB') + mysql_engine='InnoDB', + useexisting=True) Index('ix_image_properties_image_id_key', image_properties.c.image_id, image_properties.c.key) diff --git a/glance/registry/db/models.py b/glance/registry/db/models.py index 82e7d9b3b4..398c4a365d 100644 --- a/glance/registry/db/models.py +++ b/glance/registry/db/models.py @@ -88,6 +88,9 @@ class ModelBase(object): def items(self): return self.__dict__.items() + def to_dict(self): + return self.__dict__.copy() + class Image(BASE, ModelBase): """Represents an image in the datastore""" @@ -95,7 +98,8 @@ class Image(BASE, ModelBase): id = Column(Integer, primary_key=True) name = Column(String(255)) - type = Column(String(30)) + disk_format = Column(String(20)) + container_format = Column(String(20)) size = Column(Integer) status = Column(String(30), nullable=False) is_public = Column(Boolean, nullable=False, default=False) diff --git a/glance/registry/server.py b/glance/registry/server.py index 33ea85581b..fd50ac4a3c 100644 --- a/glance/registry/server.py +++ b/glance/registry/server.py @@ -55,7 +55,6 @@ class Controller(wsgi.Controller): images = db_api.image_get_all_public(None) image_dicts = [dict(id=i['id'], name=i['name'], - type=i['type'], size=i['size']) for i in images] return dict(images=image_dicts) @@ -147,6 +146,11 @@ class Controller(wsgi.Controller): try: updated_image = db_api.image_update(context, id, image_data) return dict(image=make_image_dict(updated_image)) + except exception.Invalid, e: + msg = ("Failed to update image metadata. " + "Got error: %(e)s" % locals()) + logger.error(msg) + return exc.HTTPBadRequest(msg) except exception.NotFound: raise exc.HTTPNotFound(body='Image not found', request=req, diff --git a/glance/server.py b/glance/server.py index 7931172ee6..8ec7ac05e9 100644 --- a/glance/server.py +++ b/glance/server.py @@ -83,7 +83,6 @@ class Controller(wsgi.Controller): * id -- The opaque image identifier * name -- The name of the image * size -- Size of image data in bytes - * type -- One of 'kernel', 'ramdisk', 'raw', or 'machine' :param request: The WSGI/Webob Request object :retval The response body is a mapping of the following form:: @@ -91,8 +90,7 @@ class Controller(wsgi.Controller): {'images': [ {'id': , 'name': , - 'size': , - 'type': }, ... + 'size': }, ... ]} """ images = registry.get_images_list(self.options) @@ -109,7 +107,8 @@ class Controller(wsgi.Controller): {'id': , 'name': , 'size': , - 'type': , + 'disk_format': , + 'container_format': , 'store': , 'status': , 'created_at': , diff --git a/glance/utils.py b/glance/utils.py index e8de275980..0569ed7c07 100644 --- a/glance/utils.py +++ b/glance/utils.py @@ -33,9 +33,8 @@ def image_meta_to_http_headers(image_meta): if k == 'properties': for pk, pv in v.items(): headers["x-image-meta-property-%s" - % pk.lower()] = pv - - headers["x-image-meta-%s" % k.lower()] = v + % pk.lower()] = unicode(pv) + headers["x-image-meta-%s" % k.lower()] = unicode(v) return headers @@ -82,6 +81,12 @@ def get_image_meta_from_headers(response): field_name = key[len('x-image-meta-'):].replace('-', '_') result[field_name] = value result['properties'] = properties + if 'id' in result: + result['id'] = int(result['id']) + if 'size' in result: + result['size'] = int(result['size']) + if 'is_public' in result: + result['is_public'] = (result['is_public'] == 'True') return result diff --git a/tests/stubs.py b/tests/stubs.py index 97ae9d527d..6b09c18e0e 100644 --- a/tests/stubs.py +++ b/tests/stubs.py @@ -270,7 +270,8 @@ def stub_out_registry_db_image_api(stubs): {'id': 1, 'name': 'fake image #1', 'status': 'active', - 'type': 'kernel', + 'disk_format': 'ami', + 'container_format': 'ami', 'is_public': False, 'created_at': datetime.datetime.utcnow(), 'updated_at': datetime.datetime.utcnow(), @@ -278,11 +279,14 @@ def stub_out_registry_db_image_api(stubs): 'deleted': False, 'size': 13, 'location': "swift://user:passwd@acct/container/obj.tar.0", - 'properties': []}, + 'properties': [{'key': 'type', + 'value': 'kernel', + 'deleted': False}]}, {'id': 2, 'name': 'fake image #2', 'status': 'active', - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'is_public': True, 'created_at': datetime.datetime.utcnow(), 'updated_at': datetime.datetime.utcnow(), @@ -292,8 +296,6 @@ def stub_out_registry_db_image_api(stubs): 'location': "file:///tmp/glance-tests/2", 'properties': []}] - VALID_STATUSES = ('active', 'killed', 'queued', 'saving') - def __init__(self): self.images = FakeDatastore.FIXTURES self.next_id = 3 @@ -306,12 +308,7 @@ def stub_out_registry_db_image_api(stubs): raise exception.Duplicate("Duplicate image id: %s" % values['id']) - if 'status' not in values.keys(): - values['status'] = 'active' - else: - if not values['status'] in self.VALID_STATUSES: - raise exception.Invalid("Invalid status '%s' for image" % - values['status']) + glance.registry.db.api.validate_image(values) values['size'] = values.get('size', 0) values['deleted'] = False @@ -323,7 +320,7 @@ def stub_out_registry_db_image_api(stubs): props = [] if 'properties' in values.keys(): - for k, v in values['properties'].iteritems(): + for k, v in values['properties'].items(): p = {} p['key'] = k p['value'] = v @@ -341,10 +338,14 @@ def stub_out_registry_db_image_api(stubs): def image_update(self, _context, image_id, values): + image = self.image_get(_context, image_id) + copy_image = image.copy() + copy_image.update(values) + glance.registry.db.api.validate_image(copy_image) props = [] if 'properties' in values.keys(): - for k, v in values['properties'].iteritems(): + for k, v in values['properties'].items(): p = {} p['key'] = k p['value'] = v @@ -356,7 +357,6 @@ def stub_out_registry_db_image_api(stubs): values['properties'] = props - image = self.image_get(_context, image_id) image.update(values) return image @@ -369,9 +369,8 @@ def stub_out_registry_db_image_api(stubs): images = [i for i in self.images if str(i['id']) == str(image_id)] if len(images) != 1 or images[0]['deleted']: - new_exc = exception.NotFound("No model for id %s %s" % - (image_id, str(self.images))) - raise new_exc.__class__, new_exc, sys.exc_info()[2] + raise exception.NotFound("No model for id %s %s" % + (image_id, str(self.images))) else: return images[0] diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 51c7309fdd..00800350ed 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -85,7 +85,8 @@ class TestRegistryAPI(unittest.TestCase): fixture = {'id': 2, 'name': 'fake image #2', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'status': 'active'} req = webob.Request.blank('/images/detail') @@ -103,7 +104,8 @@ class TestRegistryAPI(unittest.TestCase): """Tests that the /images POST registry API creates the image""" fixture = {'name': 'fake public image', 'is_public': True, - 'type': 'kernel'} + 'disk_format': 'vhd', + 'container_format': 'ovf'} req = webob.Request.blank('/images') @@ -125,12 +127,64 @@ class TestRegistryAPI(unittest.TestCase): # Test status was updated properly self.assertEquals('active', res_dict['image']['status']) + def test_create_image_with_bad_container_format(self): + """Tests proper exception is raised if a bad disk_format is set""" + fixture = {'id': 3, + 'name': 'fake public image', + 'is_public': True, + 'disk_format': 'vhd', + 'container_format': 'invalid'} + + req = webob.Request.blank('/images') + + req.method = 'POST' + req.body = json.dumps(dict(image=fixture)) + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) + self.assertTrue('Invalid container format' in res.body) + + def test_create_image_with_bad_disk_format(self): + """Tests proper exception is raised if a bad disk_format is set""" + fixture = {'id': 3, + 'name': 'fake public image', + 'is_public': True, + 'disk_format': 'invalid', + 'container_format': 'ovf'} + + req = webob.Request.blank('/images') + + req.method = 'POST' + req.body = json.dumps(dict(image=fixture)) + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) + self.assertTrue('Invalid disk format' in res.body) + + def test_create_image_with_mismatched_formats(self): + """Tests that exception raised for bad matching disk and container + formats""" + fixture = {'name': 'fake public image #3', + 'container_format': 'aki', + 'disk_format': 'ari'} + + req = webob.Request.blank('/images') + + req.method = 'POST' + req.body = json.dumps(dict(image=fixture)) + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) + self.assertTrue('Invalid mix of disk and container formats' + in res.body) + def test_create_image_with_bad_status(self): """Tests proper exception is raised if a bad status is set""" fixture = {'id': 3, 'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'status': 'bad status'} req = webob.Request.blank('/images') @@ -140,11 +194,12 @@ class TestRegistryAPI(unittest.TestCase): res = req.get_response(self.api) self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) + self.assertTrue('Invalid image status' in res.body) def test_update_image(self): """Tests that the /images PUT registry API updates the image""" fixture = {'name': 'fake public image #2', - 'type': 'ramdisk'} + 'disk_format': 'raw'} req = webob.Request.blank('/images/2') @@ -163,11 +218,7 @@ class TestRegistryAPI(unittest.TestCase): def test_update_image_not_existing(self): """Tests proper exception is raised if attempt to update non-existing image""" - fixture = {'id': 3, - 'name': 'fake public image', - 'is_public': True, - 'type': 'kernel', - 'status': 'bad status'} + fixture = {'status': 'killed'} req = webob.Request.blank('/images/3') @@ -178,6 +229,60 @@ class TestRegistryAPI(unittest.TestCase): self.assertEquals(res.status_int, webob.exc.HTTPNotFound.code) + def test_update_image_with_bad_status(self): + """Tests that exception raised trying to set a bad status""" + fixture = {'status': 'invalid'} + + req = webob.Request.blank('/images/2') + + req.method = 'PUT' + req.body = json.dumps(dict(image=fixture)) + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) + self.assertTrue('Invalid image status' in res.body) + + def test_update_image_with_bad_disk_format(self): + """Tests that exception raised trying to set a bad disk_format""" + fixture = {'disk_format': 'invalid'} + + req = webob.Request.blank('/images/2') + + req.method = 'PUT' + req.body = json.dumps(dict(image=fixture)) + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) + self.assertTrue('Invalid disk format' in res.body) + + def test_update_image_with_bad_container_format(self): + """Tests that exception raised trying to set a bad container_format""" + fixture = {'container_format': 'invalid'} + + req = webob.Request.blank('/images/2') + + req.method = 'PUT' + req.body = json.dumps(dict(image=fixture)) + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) + self.assertTrue('Invalid container format' in res.body) + + def test_update_image_with_mismatched_formats(self): + """Tests that exception raised for bad matching disk and container + formats""" + fixture = {'container_format': 'ari'} + + req = webob.Request.blank('/images/2') # Image 2 has disk format 'vhd' + + req.method = 'PUT' + req.body = json.dumps(dict(image=fixture)) + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) + self.assertTrue('Invalid mix of disk and container formats' + in res.body) + def test_delete_image(self): """Tests that the /images DELETE registry API deletes the image""" @@ -239,10 +344,44 @@ class TestGlanceAPI(unittest.TestCase): stubs.clean_out_fake_filesystem_backend() self.stubs.UnsetAll() + def test_bad_disk_format(self): + fixture_headers = {'x-image-meta-store': 'bad', + 'x-image-meta-name': 'bogus', + 'x-image-meta-location': 'http://example.com/image.tar.gz', + 'x-image-meta-disk-format': 'invalid', + 'x-image-meta-container-format': 'ami'} + + req = webob.Request.blank("/images") + req.method = 'POST' + for k, v in fixture_headers.iteritems(): + req.headers[k] = v + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) + self.assertTrue('Invalid disk format' in res.body, res.body) + + def test_bad_container_format(self): + fixture_headers = {'x-image-meta-store': 'bad', + 'x-image-meta-name': 'bogus', + 'x-image-meta-location': 'http://example.com/image.tar.gz', + 'x-image-meta-disk-format': 'vhd', + 'x-image-meta-container-format': 'invalid'} + + req = webob.Request.blank("/images") + req.method = 'POST' + for k, v in fixture_headers.iteritems(): + req.headers[k] = v + + res = req.get_response(self.api) + self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) + self.assertTrue('Invalid container format' in res.body) + def test_add_image_no_location_no_image_as_body(self): """Tests creates a queued image for no body and no loc header""" fixture_headers = {'x-image-meta-store': 'file', - 'x-image-meta-name': 'fake image #3'} + 'x-image-meta-disk-format': 'vhd', + 'x-image-meta-container-format': 'ovf', + 'x-image-meta-name': 'fake image #3'} req = webob.Request.blank("/images") req.method = 'POST' @@ -270,8 +409,10 @@ class TestGlanceAPI(unittest.TestCase): self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code) def test_add_image_basic_file_store(self): - """Tests raises BadRequest for invalid store header""" + """Tests to add a basic image in the file store""" fixture_headers = {'x-image-meta-store': 'file', + 'x-image-meta-disk-format': 'vhd', + 'x-image-meta-container-format': 'ovf', 'x-image-meta-name': 'fake image #3'} req = webob.Request.blank("/images") @@ -298,7 +439,7 @@ class TestGlanceAPI(unittest.TestCase): def test_image_meta(self): """Test for HEAD /images/""" - expected_headers = {'x-image-meta-id': 2, + expected_headers = {'x-image-meta-id': '2', 'x-image-meta-name': 'fake image #2'} req = webob.Request.blank("/images/2") req.method = 'HEAD' diff --git a/tests/unit/test_clients.py b/tests/unit/test_clients.py index 68969a7b40..a3a75c6bc2 100644 --- a/tests/unit/test_clients.py +++ b/tests/unit/test_clients.py @@ -66,7 +66,7 @@ class TestRegistryClient(unittest.TestCase): images = self.client.get_images() self.assertEquals(len(images), 1) - for k, v in fixture.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, images[0][k]) def test_get_image_details(self): @@ -74,16 +74,8 @@ class TestRegistryClient(unittest.TestCase): fixture = {'id': 2, 'name': 'fake image #2', 'is_public': True, - 'type': 'kernel', - 'status': 'active', - 'size': 19, - 'location': "file:///tmp/glance-tests/2", - 'properties': {}} - - expected = {'id': 2, - 'name': 'fake image #2', - 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'status': 'active', 'size': 19, 'location': "file:///tmp/glance-tests/2", @@ -92,33 +84,28 @@ class TestRegistryClient(unittest.TestCase): images = self.client.get_images_detailed() self.assertEquals(len(images), 1) - for k, v in expected.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, images[0][k]) def test_get_image(self): """Tests that the detailed info about an image returned""" - fixture = {'id': 2, - 'name': 'fake image #2', - 'is_public': True, - 'type': 'kernel', + fixture = {'id': 1, + 'name': 'fake image #1', + 'is_public': False, + 'disk_format': 'ami', + 'container_format': 'ami', 'status': 'active', - 'size': 19, - 'location': "file:///tmp/glance-tests/2", - 'properties': {}} + 'size': 13, + 'location': "swift://user:passwd@acct/container/obj.tar.0", + 'properties': {'type': 'kernel'}} - expected = {'id': 2, - 'name': 'fake image #2', - 'is_public': True, - 'type': 'kernel', - 'status': 'active', - 'size': 19, - 'location': "file:///tmp/glance-tests/2", - 'properties': {}} + data = self.client.get_image(1) - data = self.client.get_image(2) - - for k, v in expected.iteritems(): - self.assertEquals(v, data[k]) + for k, v in fixture.items(): + el = data[k] + self.assertEquals(v, data[k], + "Failed v != data[k] where v = %(v)s and " + "k = %(k)s and data[k] = %(el)s" % locals()) def test_get_image_non_existing(self): """Tests that NotFound is raised when getting a non-existing image""" @@ -131,7 +118,8 @@ class TestRegistryClient(unittest.TestCase): """Tests that we can add image metadata and returns the new id""" fixture = {'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vmdk', + 'container_format': 'ovf', 'size': 19, 'location': "file:///tmp/glance-tests/acct/3.gz.0", } @@ -144,7 +132,7 @@ class TestRegistryClient(unittest.TestCase): # Test all other attributes set data = self.client.get_image(3) - for k, v in fixture.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, data[k]) # Test status was updated properly @@ -155,23 +143,18 @@ class TestRegistryClient(unittest.TestCase): """Tests that we can add image metadata with properties""" fixture = {'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vmdk', + 'container_format': 'ovf', 'size': 19, 'location': "file:///tmp/glance-tests/2", 'properties': {'distro': 'Ubuntu 10.04 LTS'}} - expected = {'name': 'fake public image', - 'is_public': True, - 'type': 'kernel', - 'size': 19, - 'location': "file:///tmp/glance-tests/2", - 'properties': {'distro': 'Ubuntu 10.04 LTS'}} new_image = self.client.add_image(fixture) # Test ID auto-assigned properly self.assertEquals(3, new_image['id']) - for k, v in expected.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, new_image[k]) # Test status was updated properly @@ -183,7 +166,8 @@ class TestRegistryClient(unittest.TestCase): fixture = {'id': 2, 'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vmdk', + 'container_format': 'ovf', 'status': 'bad status', 'size': 19, 'location': "file:///tmp/glance-tests/2", @@ -198,7 +182,8 @@ class TestRegistryClient(unittest.TestCase): fixture = {'id': 3, 'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vmdk', + 'container_format': 'ovf', 'status': 'bad status', 'size': 19, 'location': "file:///tmp/glance-tests/2", @@ -211,14 +196,14 @@ class TestRegistryClient(unittest.TestCase): def test_update_image(self): """Tests that the /images PUT registry API updates the image""" fixture = {'name': 'fake public image #2', - 'type': 'ramdisk'} + 'disk_format': 'vmdk'} self.assertTrue(self.client.update_image(2, fixture)) # Test all other attributes set data = self.client.get_image(2) - for k, v in fixture.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, data[k]) def test_update_image_not_existing(self): @@ -226,7 +211,8 @@ class TestRegistryClient(unittest.TestCase): fixture = {'id': 3, 'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vmdk', + 'container_format': 'ovf', 'status': 'bad status', } @@ -283,7 +269,8 @@ class TestClient(unittest.TestCase): expected_meta = {'id': 2, 'name': 'fake image #2', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'status': 'active', 'size': 19, 'location': "file:///tmp/glance-tests/2", @@ -295,7 +282,7 @@ class TestClient(unittest.TestCase): image_data += image_chunk self.assertEquals(expected_image, image_data) - for k, v in expected_meta.iteritems(): + for k, v in expected_meta.items(): self.assertEquals(v, meta[k]) def test_get_image_not_existing(self): @@ -312,7 +299,7 @@ class TestClient(unittest.TestCase): images = self.client.get_images() self.assertEquals(len(images), 1) - for k, v in fixture.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, images[0][k]) def test_get_image_details(self): @@ -320,7 +307,8 @@ class TestClient(unittest.TestCase): fixture = {'id': 2, 'name': 'fake image #2', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'status': 'active', 'size': 19, 'location': "file:///tmp/glance-tests/2", @@ -329,7 +317,8 @@ class TestClient(unittest.TestCase): expected = {'id': 2, 'name': 'fake image #2', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'status': 'active', 'size': 19, 'location': "file:///tmp/glance-tests/2", @@ -338,7 +327,7 @@ class TestClient(unittest.TestCase): images = self.client.get_images_detailed() self.assertEquals(len(images), 1) - for k, v in expected.iteritems(): + for k, v in expected.items(): self.assertEquals(v, images[0][k]) def test_get_image_meta(self): @@ -346,16 +335,8 @@ class TestClient(unittest.TestCase): fixture = {'id': 2, 'name': 'fake image #2', 'is_public': True, - 'type': 'kernel', - 'status': 'active', - 'size': 19, - 'location': "file:///tmp/glance-tests/2", - 'properties': {}} - - expected = {'id': 2, - 'name': 'fake image #2', - 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'status': 'active', 'size': 19, 'location': "file:///tmp/glance-tests/2", @@ -363,7 +344,7 @@ class TestClient(unittest.TestCase): data = self.client.get_image_meta(2) - for k, v in expected.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, data[k]) def test_get_image_non_existing(self): @@ -377,7 +358,8 @@ class TestClient(unittest.TestCase): """Tests client returns image as queued""" fixture = {'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', } image_meta = self.client.add_image(fixture) self.assertEquals('queued', image_meta['status']) @@ -387,7 +369,8 @@ class TestClient(unittest.TestCase): """Tests that we can add image metadata and returns the new id""" fixture = {'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'size': 19, 'location': "file:///tmp/glance-tests/2", } @@ -400,7 +383,7 @@ class TestClient(unittest.TestCase): # Test all other attributes set data = self.client.get_image_meta(3) - for k, v in fixture.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, data[k]) # Test status was updated properly @@ -411,18 +394,12 @@ class TestClient(unittest.TestCase): """Tests that we can add image metadata with properties""" fixture = {'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'size': 19, 'location': "file:///tmp/glance-tests/2", 'properties': {'distro': 'Ubuntu 10.04 LTS'}, } - expected = {'name': 'fake public image', - 'is_public': True, - 'type': 'kernel', - 'size': 19, - 'location': "file:///tmp/glance-tests/2", - 'properties': {'distro': 'Ubuntu 10.04 LTS'}, - } new_image = self.client.add_image(fixture) new_image_id = new_image['id'] @@ -432,7 +409,7 @@ class TestClient(unittest.TestCase): # Test all other attributes set data = self.client.get_image_meta(3) - for k, v in expected.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, data[k]) # Test status was updated properly @@ -444,7 +421,8 @@ class TestClient(unittest.TestCase): fixture = {'id': 2, 'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'status': 'bad status', 'size': 19, 'location': "file:///tmp/glance-tests/2", @@ -458,7 +436,8 @@ class TestClient(unittest.TestCase): """Tests a bad status is set to a proper one by server""" fixture = {'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'status': 'bad status', 'size': 19, 'location': "file:///tmp/glance-tests/2", @@ -471,7 +450,8 @@ class TestClient(unittest.TestCase): """Tests can add image by passing image data as string""" fixture = {'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'size': 19, 'properties': {'distro': 'Ubuntu 10.04 LTS'}, } @@ -489,14 +469,15 @@ class TestClient(unittest.TestCase): new_image_data += image_chunk self.assertEquals(image_data_fixture, new_image_data) - for k, v in fixture.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, new_meta[k]) def test_add_image_with_image_data_as_file(self): """Tests can add image by passing image data as file""" fixture = {'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'size': 19, 'properties': {'distro': 'Ubuntu 10.04 LTS'}, } @@ -526,14 +507,15 @@ class TestClient(unittest.TestCase): new_image_data += image_chunk self.assertEquals(image_data_fixture, new_image_data) - for k, v in fixture.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, new_meta[k]) def test_add_image_with_image_data_as_string_and_no_size(self): """Tests add image by passing image data as string w/ no size attr""" fixture = {'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'properties': {'distro': 'Ubuntu 10.04 LTS'}, } @@ -550,7 +532,7 @@ class TestClient(unittest.TestCase): new_image_data += image_chunk self.assertEquals(image_data_fixture, new_image_data) - for k, v in fixture.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, new_meta[k]) self.assertEquals(19, new_meta['size']) @@ -559,7 +541,8 @@ class TestClient(unittest.TestCase): """Tests BadRequest raised when supplying bad store name in meta""" fixture = {'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'size': 19, 'store': 'bad', 'properties': {'distro': 'Ubuntu 10.04 LTS'}, @@ -575,15 +558,14 @@ class TestClient(unittest.TestCase): def test_update_image(self): """Tests that the /images PUT registry API updates the image""" fixture = {'name': 'fake public image #2', - 'type': 'ramdisk', - } + 'disk_format': 'vmdk'} self.assertTrue(self.client.update_image(2, fixture)) # Test all other attributes set data = self.client.get_image_meta(2) - for k, v in fixture.iteritems(): + for k, v in fixture.items(): self.assertEquals(v, data[k]) def test_update_image_not_existing(self): @@ -591,7 +573,8 @@ class TestClient(unittest.TestCase): fixture = {'id': 3, 'name': 'fake public image', 'is_public': True, - 'type': 'kernel', + 'disk_format': 'vhd', + 'container_format': 'ovf', 'status': 'bad status', } diff --git a/tests/unit/test_migrations.py b/tests/unit/test_migrations.py index dcef297ca8..c06e8e2d43 100644 --- a/tests/unit/test_migrations.py +++ b/tests/unit/test_migrations.py @@ -27,6 +27,8 @@ class TestMigrations(unittest.TestCase): def setUp(self): self.db_path = "glance_test_migration.sqlite" + if os.path.exists(self.db_path): + os.unlink(self.db_path) self.options = dict(sql_connection="sqlite:///%s" % self.db_path, verbose=False) config.setup_logging(self.options) diff --git a/tests/unit/test_misc.py b/tests/unit/test_misc.py index 686bf99c07..9768c7dd07 100644 --- a/tests/unit/test_misc.py +++ b/tests/unit/test_misc.py @@ -23,6 +23,8 @@ import tempfile import time import unittest +from glance import utils + def execute(cmd): env = os.environ.copy() @@ -66,6 +68,46 @@ class TestMiscellaneous(unittest.TestCase): pass # Ignore if the process group is dead os.unlink(pid_file) + def test_headers_are_unicode(self): + """ + Verifies that the headers returned by conversion code are unicode. + + Headers are passed via http in non-testing mode, which automatically + converts them to unicode. Verifying that the method does the + conversion proves that we aren't passing data that works in tests + but will fail in production. + """ + fixture = {'name': 'fake public image', + 'is_public': True, + 'type': 'kernel', + 'size': 19, + 'location': "file:///tmp/glance-tests/2", + 'properties': {'distro': 'Ubuntu 10.04 LTS'}} + headers = utils.image_meta_to_http_headers(fixture) + for k, v in headers.iteritems(): + self.assert_(isinstance(v, unicode), "%s is not unicode" % v) + + def test_data_passed_properly_through_headers(self): + """ + Verifies that data is the same after being passed through headers + """ + fixture = {'name': 'fake public image', + 'is_public': True, + 'type': 'kernel', + 'size': 19, + 'location': "file:///tmp/glance-tests/2", + 'properties': {'distro': 'Ubuntu 10.04 LTS'}} + headers = utils.image_meta_to_http_headers(fixture) + + class FakeResponse(): + pass + + response = FakeResponse() + response.headers = headers + result = utils.get_image_meta_from_headers(response) + for k, v in fixture.iteritems(): + self.assertEqual(v, result[k]) + def test_exception_not_eaten_from_registry_to_api(self): """ A test for LP bug #704854 -- Exception thrown by registry @@ -145,22 +187,15 @@ sql_idle_timeout = 3600 self.assertEquals(0, exitcode) self.assertEquals('{"images": []}', out.strip()) - cmd = "./bin/glance-upload --port=%(api_port)d "\ - "--type=invalid %(conf_file_name)s "\ - "'my image'" % locals() + cmd = "curl -X POST -H 'Content-Type: application/octet-stream' "\ + "-H 'X-Image-Meta-Name: ImageName' "\ + "-H 'X-Image-Meta-Disk-Format: Invalid' "\ + "http://0.0.0.0:%d/images" % api_port + ignored, out, err = execute(cmd) - # Normally, we would simply self.assertRaises() here, but - # we also want to check that the Invalid image type is in - # the exception text... - hit_exception = False - try: - ignored, out, err = execute(cmd) - except RuntimeError, e: - hit_exception = True - self.assertTrue('Invalid image type' in str(e), - "Could not find 'Invalid image type' in " - "result from glance-upload:\n%(e)s" % locals()) - self.assertTrue(hit_exception) + self.assertTrue('Invalid disk format' in out, + "Could not find 'Invalid disk format' " + "in output: %s" % out) # Spin down the API and default registry server cmd = "./bin/glance-control api stop "\