Merge trunk and resolve conflicts from utils and client patch from Vishy

This commit is contained in:
jaypipes@gmail.com 2011-03-06 12:02:44 -05:00
commit 226687e48b
10 changed files with 127 additions and 76 deletions

@ -48,9 +48,19 @@ Usage
# available
import argparse
import pprint
import os
import sys
# If ../glance/__init__.py exists, add ../ to Python search path, so that
# it will override what happens to be installed in /usr/(local/)lib/python...
possible_topdir = os.path.normpath(os.path.join(os.path.abspath(sys.argv[0]),
os.pardir,
os.pardir))
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):
@ -69,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',
@ -80,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:
@ -89,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)

@ -17,7 +17,7 @@
Disk and Container Formats
==========================
When adding an image to Glance, you are required to specify what the virtual
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.

@ -260,14 +260,14 @@ The list of metadata headers that Glance accepts are listed below.
* ``x-image-meta-disk-format``
This header is required. Valid values are one of ``aki``, ``ari``, ``ami``,
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 required. Valid values are one of ``aki``, ``ari``, ``ami``,
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>`

@ -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 "
@ -262,10 +264,17 @@ class Client(BaseClient):
"""
Updates Glance's information about an image
"""
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']

@ -161,25 +161,20 @@ def validate_image(values):
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 not disk_format:
msg = "Image disk format is required."
raise exception.Invalid(msg)
if not container_format:
msg = "Image container format is required."
raise exception.Invalid(msg)
if status not in STATUSES:
msg = "Invalid image status '%s' for image." % status
raise exception.Invalid(msg)
if disk_format not in DISK_FORMATS:
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 not in CONTAINER_FORMATS:
if container_format and container_format not in CONTAINER_FORMATS:
msg = "Invalid container format '%s' for image." % container_format
raise exception.Invalid(msg)
@ -204,6 +199,12 @@ def _image_update(context, values, image_id):
session = get_session()
with session.begin():
# 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:

@ -98,8 +98,8 @@ class Image(BASE, ModelBase):
id = Column(Integer, primary_key=True)
name = Column(String(255))
disk_format = Column(String(20), nullable=False)
container_format = Column(String(20), nullable=False)
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)

@ -31,6 +31,7 @@ Configuration Options
import json
import logging
import sys
import routes
from webob import Response
@ -306,13 +307,18 @@ class Controller(wsgi.Controller):
try:
location = self._upload(req, image_meta)
self._activate(req, image_meta, location)
except Exception, e:
except: # unqualified b/c we're re-raising it
exc_type, exc_value, exc_traceback = sys.exc_info()
self._safe_kill(req, image_meta)
# NOTE(sirp): _safe_kill uses httplib which, in turn, uses
# Eventlet's GreenSocket. Eventlet subsequently clears exceptions
# by calling `sys.exc_clear()`. This is why we have to `raise e`
# instead of `raise`
self._safe_kill(req, image_meta)
raise e
# by calling `sys.exc_clear()`.
#
# This is why we can't use a raise with no arguments here: our
# exception context was destroyed by Eventlet. To work around
# this, we need to 'memorize' the exception context, and then
# re-raise using 3-arg form after Eventlet has run
raise exc_type, exc_value, exc_traceback
def create(self, req):
"""

@ -33,10 +33,9 @@ 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
% pk.lower()] = unicode(pv)
else:
headers["x-image-meta-%s" % k.lower()] = v
headers["x-image-meta-%s" % k.lower()] = unicode(v)
return headers
@ -83,6 +82,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

@ -350,20 +350,6 @@ class TestGlanceAPI(unittest.TestCase):
stubs.clean_out_fake_filesystem_backend()
self.stubs.UnsetAll()
def test_missing_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'}
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('Image disk format is required' in res.body)
def test_bad_disk_format(self):
fixture_headers = {'x-image-meta-store': 'bad',
'x-image-meta-name': 'bogus',
@ -380,21 +366,6 @@ class TestGlanceAPI(unittest.TestCase):
self.assertEquals(res.status_int, webob.exc.HTTPBadRequest.code)
self.assertTrue('Invalid disk format' in res.body, res.body)
def test_missing_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'}
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('Image container format is required' in res.body)
def test_bad_container_format(self):
fixture_headers = {'x-image-meta-store': 'bad',
'x-image-meta-name': 'bogus',
@ -474,7 +445,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'

@ -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
@ -146,30 +188,15 @@ sql_idle_timeout = 3600
self.assertEquals('{"images": []}', out.strip())
cmd = "curl -X POST -H 'Content-Type: application/octet-stream' "\
"-dinvalid http://0.0.0.0:%d/images" % api_port
"-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)
self.assertTrue('Image disk format is required' in out,
"Could not find 'Image disk format is required' "
self.assertTrue('Invalid disk format' in out,
"Could not find 'Invalid disk format' "
"in output: %s" % out)
cmd = "./bin/glance-upload --port=%(api_port)d "\
"--type=invalid %(conf_file_name)s "\
"'my image'" % locals()
# 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('Image disk format is required' in str(e),
"Could not find 'Image disk format is "
"required' in output: %s" % out)
self.assertTrue(hit_exception)
# Spin down the API and default registry server
cmd = "./bin/glance-control api stop "\
"%s --pid-file=glance-api.pid" % conf_file_name