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:
parent
d26f66bce5
commit
0613daa931
29
bin/glance
29
bin/glance
@ -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
|
||||
|
||||
|
||||
|
@ -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")
|
||||
|
@ -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']
|
||||
|
@ -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()
|
||||
|
Loading…
Reference in New Issue
Block a user