Support copy-from for queued images.

Fixes lp 939481

An image may be added referencing an external source via either
the x-image-meta-location or x-glance-api-copy-from headers.

Similarly, a queued image may be associated with an external source
via the x-image-meta-location header.

However previously a queued image could not have image content copied
from an external source via the x-glance-api-copy-from header.

This patch addresses this asymmetry in respect of POST versus PUT.

Change-Id: I81848ba3a5d41e7e5b69daec491e83307023f020
This commit is contained in:
Eoghan Glynn 2012-02-23 13:25:12 +00:00
parent d26f66bce5
commit 0613daa931
4 changed files with 98 additions and 9 deletions

View File

@ -182,6 +182,12 @@ location Optional. When specified, should be a readable location
filesystem at /usr/share/images/some.image.tar.gz
you would specify:
location=file:///usr/share/images/some.image.tar.gz
copy_from Optional. An external location (HTTP, S3 or Swift URI) to
copy image content from. For example, if the image data is
stored as an object called fedora16 in an S3 bucket named
images, you would specify (with the approriate access and
secret keys):
copy_from=s3://akey:skey@s3.amazonaws.com/images/fedora16
Any other field names are considered to be custom properties so be careful
to spell field names correctly. :)
@ -317,7 +323,8 @@ Field names that can be specified:
name A name for the image.
location An external location to serve out from.
copy_from An external location (HTTP, S3 or Swift URI) to copy from.
copy_from An external location (HTTP, S3 or Swift URI) to copy image
content from.
is_public If specified, interpreted as a boolean value
and sets or unsets the image's availability to the public.
protected If specified, interpreted as a boolean value
@ -351,6 +358,11 @@ to spell field names correctly. :)"""
print 'Found non-modifiable field %s. Removing.' % field
fields.pop(field)
features = {}
if 'location' not in fields and 'copy_from' in fields:
source = fields.pop('copy_from')
features['x-glance-api-copy-from'] = source
base_image_fields = ['disk_format', 'container_format', 'name',
'min_disk', 'min_ram', 'location', 'owner']
for field in base_image_fields:
@ -371,7 +383,8 @@ to spell field names correctly. :)"""
if not options.dry_run:
try:
image_meta = c.update_image(image_id, image_meta=image_meta)
image_meta = c.update_image(image_id, image_meta=image_meta,
features=features)
print "Updated image %s" % image_id
if options.verbose:
@ -387,10 +400,18 @@ to spell field names correctly. :)"""
print piece
return FAILURE
else:
def _dump(dict):
for k, v in sorted(dict.items()):
print " %(k)30s => %(v)s" % locals()
print "Dry run. We would have done the following:"
print "Update existing image with metadata:"
for k, v in sorted(image_meta.items()):
print " %(k)30s => %(v)s" % locals()
_dump(image_meta)
if features:
print "with features enabled:"
_dump(features)
return SUCCESS

View File

@ -636,7 +636,8 @@ class Controller(controller.BaseController):
image_meta = registry.update_image_metadata(req.context, id,
image_meta,
purge_props)
if image_data is not None:
if self._copy_from(req) or image_data is not None:
image_meta = self._upload_and_activate(req, image_meta)
except exception.Invalid, e:
msg = (_("Failed to update image metadata. Got error: %(e)s")

View File

@ -140,7 +140,7 @@ class V1Client(base_client.BaseClient):
:param image_data: Optional string of raw image data
or file-like object that can be
used to read the image data
:param features: Optional map of features
:param features: Optional map of features
:retval The newly-stored image's metadata.
"""
@ -162,9 +162,18 @@ class V1Client(base_client.BaseClient):
data = json.loads(res.read())
return data['image']
def update_image(self, image_id, image_meta=None, image_data=None):
def update_image(self, image_id, image_meta=None, image_data=None,
features=None):
"""
Updates Glance's information about an image
:param image_id: Required image ID
:param image_meta: Optional Mapping of information about the
image
:param image_data: Optional string of raw image data
or file-like object that can be
used to read the image data
:param features: Optional map of features
"""
if image_meta is None:
image_meta = {}
@ -181,6 +190,8 @@ class V1Client(base_client.BaseClient):
else:
body = None
utils.add_features_to_http_headers(features, headers)
res = self.do_request("PUT", "/images/%s" % image_id, body, headers)
data = json.loads(res.read())
return data['image']

View File

@ -18,6 +18,8 @@
"""Functional test case that utilizes the bin/glance CLI tool"""
import datetime
import httplib2
import json
import os
import tempfile
@ -122,8 +124,62 @@ class TestBinGlance(functional.FunctionalTest):
[c.strip() for c in line.split()]
self.assertEqual('MyImage', name)
self.assertEqual('5120', size, "Expected image to be 0 bytes in size,"
" but got %s. " % size)
self.assertEqual('5120', size, "Expected image to be 5120 bytes "
" in size, but got %s. " % size)
def _do_test_update_external_source(self, source):
self.cleanup()
self.start_servers(**self.__dict__.copy())
api_port = self.api_port
registry_port = self.registry_port
# 1. Add public image with no image content
headers = {'X-Image-Meta-Name': 'MyImage',
'X-Image-Meta-disk_format': 'raw',
'X-Image-Meta-container_format': 'ovf',
'X-Image-Meta-Is-Public': 'True'}
path = "http://%s:%d/v1/images" % ("0.0.0.0", api_port)
http = httplib2.Http()
response, content = http.request(path, 'POST', headers=headers)
self.assertEqual(response.status, 201)
data = json.loads(content)
self.assertEqual(data['image']['name'], 'MyImage')
image_id = data['image']['id']
# 2. Update image with external source
source = '%s=%s' % (source, get_http_uri(self, 'foobar'))
cmd = "bin/glance update %s %s -p %d" % (image_id, source, api_port)
exitcode, out, err = execute(cmd, raise_error=False)
self.assertEqual(0, exitcode)
self.assertTrue(out.strip().endswith('Updated image %s' % image_id))
# 3. Verify image added as public image
cmd = "bin/glance --port=%d index" % api_port
exitcode, out, err = execute(cmd)
self.assertEqual(0, exitcode)
lines = out.split("\n")[2:-1]
self.assertEqual(1, len(lines))
line = lines[0]
image_id, name, disk_format, container_format, size = \
[c.strip() for c in line.split()]
self.assertEqual('MyImage', name)
self.assertEqual('5120', size, "Expected image to be 5120 bytes "
" in size, but got %s. " % size)
@requires(setup_http, teardown_http)
def test_update_copying_from(self):
"""
Tests creating an queued image then subsequently updating
with a copy-from source
"""
self._do_test_update_external_source('copy_from')
def test_add_with_location_and_stdin(self):
self.cleanup()