Merge trunk and resolve conflicts from utils and client patch from Vishy
This commit is contained in:
commit
226687e48b
bin
doc/source
glance
tests/unit
@ -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
|
||||
|
Loading…
x
Reference in New Issue
Block a user