Enable client V2 to download images
Added the CLI option image-download to download an image via API V2. Added utility function to save an image. Added common iterator to validate the checksum. Related to bp glance-client-v2 Change-Id: I0247f5a3462142dc5e9f3dc16cbe00c8e3d42f42
This commit is contained in:
@@ -13,6 +13,8 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import hashlib
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
@@ -130,3 +132,39 @@ def exit(msg=''):
|
|||||||
if msg:
|
if msg:
|
||||||
print >> sys.stderr, msg
|
print >> sys.stderr, msg
|
||||||
sys.exit(1)
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def save_image(data, path):
|
||||||
|
"""
|
||||||
|
Save an image to the specified path.
|
||||||
|
|
||||||
|
:param data: binary data of the image
|
||||||
|
:param path: path to save the image to
|
||||||
|
"""
|
||||||
|
if path is None:
|
||||||
|
image = sys.stdout
|
||||||
|
else:
|
||||||
|
image = open(path, 'wb')
|
||||||
|
try:
|
||||||
|
for chunk in data:
|
||||||
|
image.write(chunk)
|
||||||
|
finally:
|
||||||
|
if path is not None:
|
||||||
|
image.close()
|
||||||
|
|
||||||
|
|
||||||
|
def integrity_iter(iter, checksum):
|
||||||
|
"""
|
||||||
|
Check image data integrity.
|
||||||
|
|
||||||
|
:raises: IOError
|
||||||
|
"""
|
||||||
|
md5sum = hashlib.md5()
|
||||||
|
for chunk in iter:
|
||||||
|
yield chunk
|
||||||
|
md5sum.update(chunk)
|
||||||
|
md5sum = md5sum.hexdigest()
|
||||||
|
if md5sum != checksum:
|
||||||
|
raise IOError(errno.EPIPE,
|
||||||
|
'Corrupt image download. Checksum was %s expected %s' %
|
||||||
|
(md5sum, checksum))
|
||||||
|
@@ -20,6 +20,7 @@ import os
|
|||||||
import urllib
|
import urllib
|
||||||
|
|
||||||
from glanceclient.common import base
|
from glanceclient.common import base
|
||||||
|
from glanceclient.common import utils
|
||||||
|
|
||||||
UPDATE_PARAMS = ('name', 'disk_format', 'container_format', 'min_disk',
|
UPDATE_PARAMS = ('name', 'disk_format', 'container_format', 'min_disk',
|
||||||
'min_ram', 'owner', 'size', 'is_public', 'protected',
|
'min_ram', 'owner', 'size', 'is_public', 'protected',
|
||||||
@@ -40,8 +41,8 @@ class Image(base.Resource):
|
|||||||
def delete(self):
|
def delete(self):
|
||||||
return self.manager.delete(self)
|
return self.manager.delete(self)
|
||||||
|
|
||||||
def data(self):
|
def data(self, **kwargs):
|
||||||
return self.manager.data(self)
|
return self.manager.data(self, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ImageManager(base.Manager):
|
class ImageManager(base.Manager):
|
||||||
@@ -77,15 +78,20 @@ class ImageManager(base.Manager):
|
|||||||
meta = self._image_meta_from_headers(dict(resp.getheaders()))
|
meta = self._image_meta_from_headers(dict(resp.getheaders()))
|
||||||
return Image(self, meta)
|
return Image(self, meta)
|
||||||
|
|
||||||
def data(self, image):
|
def data(self, image, do_checksum=True):
|
||||||
"""Get the raw data for a specific image.
|
"""Get the raw data for a specific image.
|
||||||
|
|
||||||
:param image: image object or id to look up
|
:param image: image object or id to look up
|
||||||
|
:param do_checksum: Enable/disable checksum validation
|
||||||
:rtype: iterable containing image data
|
:rtype: iterable containing image data
|
||||||
"""
|
"""
|
||||||
image_id = base.getid(image)
|
image_id = base.getid(image)
|
||||||
_, body = self.api.raw_request('GET', '/v1/images/%s' % image_id)
|
resp, body = self.api.raw_request('GET', '/v1/images/%s' % image_id)
|
||||||
return body
|
checksum = resp.getheader('x-image-meta-checksum', None)
|
||||||
|
if do_checksum and checksum is not None:
|
||||||
|
return utils.integrity_iter(body, checksum)
|
||||||
|
else:
|
||||||
|
return body
|
||||||
|
|
||||||
def list(self, **kwargs):
|
def list(self, **kwargs):
|
||||||
"""Get a list of images.
|
"""Get a list of images.
|
||||||
|
@@ -13,6 +13,8 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
from glanceclient.common import utils
|
||||||
|
|
||||||
DEFAULT_PAGE_SIZE = 20
|
DEFAULT_PAGE_SIZE = 20
|
||||||
|
|
||||||
|
|
||||||
@@ -54,3 +56,18 @@ class Controller(object):
|
|||||||
# way to pass it into the model constructor without conflict
|
# way to pass it into the model constructor without conflict
|
||||||
body.pop('self', None)
|
body.pop('self', None)
|
||||||
return self.model(**body)
|
return self.model(**body)
|
||||||
|
|
||||||
|
def data(self, image_id, do_checksum=True):
|
||||||
|
"""
|
||||||
|
Retrieve data of an image.
|
||||||
|
|
||||||
|
:param image_id: ID of the image to download.
|
||||||
|
:param do_checksum: Enable/disable checksum validation.
|
||||||
|
"""
|
||||||
|
url = '/v2/images/%s/file' % image_id
|
||||||
|
resp, body = self.http_client.raw_request('GET', url)
|
||||||
|
checksum = resp.getheader('content-md5', None)
|
||||||
|
if do_checksum and checksum is not None:
|
||||||
|
return utils.integrity_iter(body, checksum)
|
||||||
|
else:
|
||||||
|
return body
|
||||||
|
@@ -49,3 +49,14 @@ def do_explain(gc, args):
|
|||||||
formatters = {'Attribute': lambda m: m.name}
|
formatters = {'Attribute': lambda m: m.name}
|
||||||
columns = ['Attribute', 'Description']
|
columns = ['Attribute', 'Description']
|
||||||
utils.print_list(schema.properties, columns, formatters)
|
utils.print_list(schema.properties, columns, formatters)
|
||||||
|
|
||||||
|
|
||||||
|
@utils.arg('--file', metavar='<FILE>',
|
||||||
|
help='Local file to save downloaded image data to. '
|
||||||
|
'If this is not specified the image data will be '
|
||||||
|
'written to stdout.')
|
||||||
|
@utils.arg('id', metavar='<IMAGE_ID>', help='ID of image to download.')
|
||||||
|
def do_image_download(gc, args):
|
||||||
|
"""Download a specific image."""
|
||||||
|
body = gc.images.data(args.id)
|
||||||
|
utils.save_image(body, args.file)
|
||||||
|
45
tests/test_utils.py
Normal file
45
tests/test_utils.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
# Copyright 2012 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.
|
||||||
|
|
||||||
|
import errno
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from glanceclient.common import utils
|
||||||
|
|
||||||
|
|
||||||
|
class TestUtils(unittest.TestCase):
|
||||||
|
|
||||||
|
def test_integrity_iter_without_checksum(self):
|
||||||
|
try:
|
||||||
|
data = ''.join([f for f in utils.integrity_iter('A', None)])
|
||||||
|
self.fail('integrity checked passed without checksum.')
|
||||||
|
except IOError, e:
|
||||||
|
self.assertEqual(errno.EPIPE, e.errno)
|
||||||
|
msg = 'was 7fc56270e7a70fa81a5935b72eacbe29 expected None'
|
||||||
|
self.assertTrue(msg in str(e))
|
||||||
|
|
||||||
|
def test_integrity_iter_with_wrong_checksum(self):
|
||||||
|
try:
|
||||||
|
data = ''.join([f for f in utils.integrity_iter('BB', 'wrong')])
|
||||||
|
self.fail('integrity checked passed with wrong checksum')
|
||||||
|
except IOError, e:
|
||||||
|
self.assertEqual(errno.EPIPE, e.errno)
|
||||||
|
msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong'
|
||||||
|
self.assertTrue('expected wrong' in str(e))
|
||||||
|
|
||||||
|
def test_integrity_iter_with_checksum(self):
|
||||||
|
fixture = 'CCC'
|
||||||
|
checksum = 'defb99e69a9f1f6e06f15006b1f166ae'
|
||||||
|
data = ''.join([f for f in utils.integrity_iter(fixture, checksum)])
|
@@ -45,3 +45,6 @@ class FakeResponse(object):
|
|||||||
|
|
||||||
def getheaders(self):
|
def getheaders(self):
|
||||||
return copy.deepcopy(self.headers).items()
|
return copy.deepcopy(self.headers).items()
|
||||||
|
|
||||||
|
def getheader(self, key, default):
|
||||||
|
return self.headers.get(key, default)
|
||||||
|
@@ -13,6 +13,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import errno
|
||||||
import json
|
import json
|
||||||
import StringIO
|
import StringIO
|
||||||
import unittest
|
import unittest
|
||||||
@@ -164,7 +165,34 @@ fixtures = {
|
|||||||
),
|
),
|
||||||
'DELETE': ({}, None),
|
'DELETE': ({}, None),
|
||||||
},
|
},
|
||||||
|
'/v1/images/2': {
|
||||||
|
'HEAD': (
|
||||||
|
{
|
||||||
|
'x-image-meta-id': '2'
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
'GET': (
|
||||||
|
{
|
||||||
|
'x-image-meta-checksum': 'wrong'
|
||||||
|
},
|
||||||
|
'YYY',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'/v1/images/3': {
|
||||||
|
'HEAD': (
|
||||||
|
{
|
||||||
|
'x-image-meta-id': '3'
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
),
|
||||||
|
'GET': (
|
||||||
|
{
|
||||||
|
'x-image-meta-checksum': '0745064918b49693cca64d6b6a13d28a'
|
||||||
|
},
|
||||||
|
'ZZZ',
|
||||||
|
),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -220,11 +248,44 @@ class ImageManagerTest(unittest.TestCase):
|
|||||||
self.assertEqual(image.name, 'image-1')
|
self.assertEqual(image.name, 'image-1')
|
||||||
|
|
||||||
def test_data(self):
|
def test_data(self):
|
||||||
data = ''.join([b for b in self.mgr.data('1')])
|
data = ''.join([b for b in self.mgr.data('1', do_checksum=False)])
|
||||||
expect = [('GET', '/v1/images/1', {}, None)]
|
expect = [('GET', '/v1/images/1', {}, None)]
|
||||||
self.assertEqual(self.api.calls, expect)
|
self.assertEqual(self.api.calls, expect)
|
||||||
self.assertEqual(data, 'XXX')
|
self.assertEqual(data, 'XXX')
|
||||||
|
|
||||||
|
expect += [('GET', '/v1/images/1', {}, None)]
|
||||||
|
data = ''.join([b for b in self.mgr.data('1')])
|
||||||
|
self.assertEqual(self.api.calls, expect)
|
||||||
|
self.assertEqual(data, 'XXX')
|
||||||
|
|
||||||
|
def test_data_with_wrong_checksum(self):
|
||||||
|
data = ''.join([b for b in self.mgr.data('2', do_checksum=False)])
|
||||||
|
expect = [('GET', '/v1/images/2', {}, None)]
|
||||||
|
self.assertEqual(self.api.calls, expect)
|
||||||
|
self.assertEqual(data, 'YYY')
|
||||||
|
|
||||||
|
expect += [('GET', '/v1/images/2', {}, None)]
|
||||||
|
data = self.mgr.data('2')
|
||||||
|
self.assertEqual(self.api.calls, expect)
|
||||||
|
try:
|
||||||
|
data = ''.join([b for b in data])
|
||||||
|
self.fail('data did not raise an error.')
|
||||||
|
except IOError, e:
|
||||||
|
self.assertEqual(errno.EPIPE, e.errno)
|
||||||
|
msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong'
|
||||||
|
self.assertTrue(msg in str(e))
|
||||||
|
|
||||||
|
def test_data_with_checksum(self):
|
||||||
|
data = ''.join([b for b in self.mgr.data('3', do_checksum=False)])
|
||||||
|
expect = [('GET', '/v1/images/3', {}, None)]
|
||||||
|
self.assertEqual(self.api.calls, expect)
|
||||||
|
self.assertEqual(data, 'ZZZ')
|
||||||
|
|
||||||
|
expect += [('GET', '/v1/images/3', {}, None)]
|
||||||
|
data = ''.join([b for b in self.mgr.data('3')])
|
||||||
|
self.assertEqual(self.api.calls, expect)
|
||||||
|
self.assertEqual(data, 'ZZZ')
|
||||||
|
|
||||||
def test_delete(self):
|
def test_delete(self):
|
||||||
self.mgr.delete('1')
|
self.mgr.delete('1')
|
||||||
expect = [('DELETE', '/v1/images/1', {}, None)]
|
expect = [('DELETE', '/v1/images/1', {}, None)]
|
||||||
@@ -352,3 +413,44 @@ class ImageTest(unittest.TestCase):
|
|||||||
]
|
]
|
||||||
self.assertEqual(self.api.calls, expect)
|
self.assertEqual(self.api.calls, expect)
|
||||||
self.assertEqual(data, 'XXX')
|
self.assertEqual(data, 'XXX')
|
||||||
|
|
||||||
|
data = ''.join([b for b in image.data(do_checksum=False)])
|
||||||
|
expect += [('GET', '/v1/images/1', {}, None)]
|
||||||
|
self.assertEqual(self.api.calls, expect)
|
||||||
|
self.assertEqual(data, 'XXX')
|
||||||
|
|
||||||
|
def test_data_with_wrong_checksum(self):
|
||||||
|
image = self.mgr.get('2')
|
||||||
|
data = ''.join([b for b in image.data(do_checksum=False)])
|
||||||
|
expect = [
|
||||||
|
('HEAD', '/v1/images/2', {}, None),
|
||||||
|
('GET', '/v1/images/2', {}, None),
|
||||||
|
]
|
||||||
|
self.assertEqual(self.api.calls, expect)
|
||||||
|
self.assertEqual(data, 'YYY')
|
||||||
|
|
||||||
|
data = image.data()
|
||||||
|
expect += [('GET', '/v1/images/2', {}, None)]
|
||||||
|
self.assertEqual(self.api.calls, expect)
|
||||||
|
try:
|
||||||
|
data = ''.join([b for b in image.data()])
|
||||||
|
self.fail('data did not raise an error.')
|
||||||
|
except IOError, e:
|
||||||
|
self.assertEqual(errno.EPIPE, e.errno)
|
||||||
|
msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong'
|
||||||
|
self.assertTrue(msg in str(e))
|
||||||
|
|
||||||
|
def test_data_with_checksum(self):
|
||||||
|
image = self.mgr.get('3')
|
||||||
|
data = ''.join([b for b in image.data(do_checksum=False)])
|
||||||
|
expect = [
|
||||||
|
('HEAD', '/v1/images/3', {}, None),
|
||||||
|
('GET', '/v1/images/3', {}, None),
|
||||||
|
]
|
||||||
|
self.assertEqual(self.api.calls, expect)
|
||||||
|
self.assertEqual(data, 'ZZZ')
|
||||||
|
|
||||||
|
data = ''.join([b for b in image.data()])
|
||||||
|
expect += [('GET', '/v1/images/3', {}, None)]
|
||||||
|
self.assertEqual(self.api.calls, expect)
|
||||||
|
self.assertEqual(data, 'ZZZ')
|
||||||
|
@@ -13,6 +13,7 @@
|
|||||||
# License for the specific language governing permissions and limitations
|
# License for the specific language governing permissions and limitations
|
||||||
# under the License.
|
# under the License.
|
||||||
|
|
||||||
|
import errno
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import warlock
|
import warlock
|
||||||
@@ -72,6 +73,28 @@ fixtures = {
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
'/v2/images/5cc4bebc-db27-11e1-a1eb-080027cbe205/file': {
|
||||||
|
'GET': (
|
||||||
|
{},
|
||||||
|
'A',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'/v2/images/66fb18d6-db27-11e1-a1eb-080027cbe205/file': {
|
||||||
|
'GET': (
|
||||||
|
{
|
||||||
|
'content-md5': 'wrong'
|
||||||
|
},
|
||||||
|
'BB',
|
||||||
|
),
|
||||||
|
},
|
||||||
|
'/v2/images/1b1c6366-dd57-11e1-af0f-02163e68b1d8/file': {
|
||||||
|
'GET': (
|
||||||
|
{
|
||||||
|
'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae'
|
||||||
|
},
|
||||||
|
'CCC',
|
||||||
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -105,3 +128,38 @@ class TestController(unittest.TestCase):
|
|||||||
image = self.controller.get('3a4560a1-e585-443e-9b39-553b46ec92d1')
|
image = self.controller.get('3a4560a1-e585-443e-9b39-553b46ec92d1')
|
||||||
self.assertEqual(image.id, '3a4560a1-e585-443e-9b39-553b46ec92d1')
|
self.assertEqual(image.id, '3a4560a1-e585-443e-9b39-553b46ec92d1')
|
||||||
self.assertEqual(image.name, 'image-1')
|
self.assertEqual(image.name, 'image-1')
|
||||||
|
|
||||||
|
def test_data_without_checksum(self):
|
||||||
|
body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205',
|
||||||
|
do_checksum=False)
|
||||||
|
body = ''.join([b for b in body])
|
||||||
|
self.assertEqual(body, 'A')
|
||||||
|
|
||||||
|
body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205')
|
||||||
|
body = ''.join([b for b in body])
|
||||||
|
self.assertEqual(body, 'A')
|
||||||
|
|
||||||
|
def test_data_with_wrong_checksum(self):
|
||||||
|
body = self.controller.data('66fb18d6-db27-11e1-a1eb-080027cbe205',
|
||||||
|
do_checksum=False)
|
||||||
|
body = ''.join([b for b in body])
|
||||||
|
self.assertEqual(body, 'BB')
|
||||||
|
|
||||||
|
body = self.controller.data('66fb18d6-db27-11e1-a1eb-080027cbe205')
|
||||||
|
try:
|
||||||
|
body = ''.join([b for b in body])
|
||||||
|
self.fail('data did not raise an error.')
|
||||||
|
except IOError, e:
|
||||||
|
self.assertEqual(errno.EPIPE, e.errno)
|
||||||
|
msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong'
|
||||||
|
self.assertTrue(msg in str(e))
|
||||||
|
|
||||||
|
def test_data_with_checksum(self):
|
||||||
|
body = self.controller.data('1b1c6366-dd57-11e1-af0f-02163e68b1d8',
|
||||||
|
do_checksum=False)
|
||||||
|
body = ''.join([b for b in body])
|
||||||
|
self.assertEqual(body, 'CCC')
|
||||||
|
|
||||||
|
body = self.controller.data('1b1c6366-dd57-11e1-af0f-02163e68b1d8')
|
||||||
|
body = ''.join([b for b in body])
|
||||||
|
self.assertEqual(body, 'CCC')
|
||||||
|
Reference in New Issue
Block a user