Merge trunk

This commit is contained in:
jaypipes@gmail.com 2011-03-06 10:41:29 -05:00
commit a7007ae849
19 changed files with 565 additions and 220 deletions

View File

@ -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)

99
doc/source/formats.rst Normal file
View File

@ -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

View File

@ -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 <architecture>`
For more details on Glance's architecture see :doc:`here <architecture>`. For
more information on what a Glance registry server is, see
:doc:`here <registries>`.

View File

@ -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 <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 <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.

View File

@ -50,6 +50,7 @@ Concepts
identifiers
registries
statuses
formats
Using Glance
============

View File

@ -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 <http://www.sqlalchemy.org/docs/05/reference/sqlalchemy/connections.html?highlight=engine#sqlalchemy.create_engine>`_
* ``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 <controllingservers>`
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/<ID> Update metadata about an existing image
DELETE /images/<ID> 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': <ID>|None,
'name': <NAME>,
'status': <STATUS>,
'disk_format': <DISK_FORMAT>,
'container_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
********

View File

@ -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']

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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,

View File

@ -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': <ID>,
'name': <NAME>,
'size': <SIZE>,
'type': <TYPE>}, ...
'size': <SIZE>}, ...
]}
"""
images = registry.get_images_list(self.options)
@ -109,7 +107,8 @@ class Controller(wsgi.Controller):
{'id': <ID>,
'name': <NAME>,
'size': <SIZE>,
'type': <TYPE>,
'disk_format': <DISK_FORMAT>,
'container_format': <CONTAINER_FORMAT>,
'store': <STORE>,
'status': <STATUS>,
'created_at': <TIMESTAMP>,

View File

@ -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

View File

@ -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]

View File

@ -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/<ID>"""
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'

View File

@ -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',
}

View File

@ -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)

View File

@ -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 "\