From dcc9aad9c0c374f62c6152bb77d6db59dea59ea5 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 10 Feb 2016 19:48:48 -0500 Subject: [PATCH] Add a method to download an image from glance This commit adds the missing function to download image data from glance. The get_image() call returns the metadata about an image but there was no method to get the actual data. Change-Id: I8797f90ea4152dfed90b3311ceca098b2807ef7e --- shade/openstackcloud.py | 40 +++++++++++ shade/tests/functional/test_image.py | 22 +++++++ shade/tests/unit/test_image.py | 99 ++++++++++++++++++++++++++++ 3 files changed, 161 insertions(+) create mode 100644 shade/tests/unit/test_image.py diff --git a/shade/openstackcloud.py b/shade/openstackcloud.py index bc3fec3b8..9a791ee95 100644 --- a/shade/openstackcloud.py +++ b/shade/openstackcloud.py @@ -1564,6 +1564,46 @@ class OpenStackCloud(object): """ return _utils._get_entity(self.search_images, name_or_id, filters) + def download_image(self, name_or_id, output_path=None, output_file=None): + """Download an image from glance by name or ID + + :param str name_or_id: Name or ID of the image. + :param output_path: the output path to write the image to. Either this + or output_file must be specified + :param output_file: a file object (or file-like object) to write the + image data to. Only write() will be called on this object. Either + this or output_path must be specified + + :raises: OpenStackCloudException in the event download_image is called + without exactly one of either output_path or output_file + :raises: OpenStackCloudResourceNotFound if no images are found matching + the name or id provided + """ + if output_path is None and output_file is None: + raise OpenStackCloudException('No output specified, an output path' + ' or file object is necessary to ' + 'write the image data to') + elif output_path is not None and output_file is not None: + raise OpenStackCloudException('Both an output path and file object' + ' were provided, however only one ' + 'can be used at once') + + image = self.search_images(name_or_id) + if len(image) == 0: + raise OpenStackCloudResourceNotFound( + "No images with name or id %s were found" % name_or_id) + image_contents = self.glance_client.images.data(image[0]['id']) + with _utils.shade_exceptions("Unable to download image"): + if output_path: + with open(output_path, 'wb') as fd: + for chunk in image_contents: + fd.write(chunk) + return + elif output_file: + for chunk in image_contents: + output_file.write(chunk) + return + def get_floating_ip(self, id, filters=None): """Get a floating IP by ID diff --git a/shade/tests/functional/test_image.py b/shade/tests/functional/test_image.py index e603acee9..1dc241694 100644 --- a/shade/tests/functional/test_image.py +++ b/shade/tests/functional/test_image.py @@ -19,6 +19,8 @@ test_compute Functional tests for `shade` image methods. """ +import filecmp +import os import tempfile from shade import openstack_cloud @@ -47,3 +49,23 @@ class TestImage(base.TestCase): wait=True) finally: self.cloud.delete_image(image_name, wait=True) + + def test_download_image(self): + test_image = tempfile.NamedTemporaryFile(delete=False) + self.addCleanup(os.remove, test_image.name) + test_image.write('\0' * 1024 * 1024) + test_image.close() + image_name = self.getUniqueString('image') + self.cloud.create_image(name=image_name, + filename=test_image.name, + disk_format='raw', + container_format='bare', + min_disk=10, + min_ram=1024, + wait=True) + self.addCleanup(self.cloud.delete_image, image_name, wait=True) + output = os.path.join(tempfile.gettempdir(), self.getUniqueString()) + self.cloud.download_image(image_name, output) + self.addCleanup(os.remove, output) + self.assertTrue(filecmp.cmp(test_image.name, output), + "Downloaded contents don't match created image") diff --git a/shade/tests/unit/test_image.py b/shade/tests/unit/test_image.py new file mode 100644 index 000000000..6d35070d8 --- /dev/null +++ b/shade/tests/unit/test_image.py @@ -0,0 +1,99 @@ +# Copyright 2016 Hewlett-Packard Development Company, L.P. +# +# 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 tempfile +import uuid + +import mock +import six + +import shade +from shade import exc +from shade.tests import base + + +class TestImage(base.TestCase): + + def setUp(self): + super(TestImage, self).setUp() + self.cloud = shade.openstack_cloud(validate=False) + self.image_id = str(uuid.uuid4()) + self.fake_search_return = [{ + u'image_state': u'available', + u'container_format': u'bare', + u'min_ram': 0, + u'ramdisk_id': None, + u'updated_at': u'2016-02-10T05:05:02Z', + u'file': '/v2/images/' + self.image_id + '/file', + u'size': 3402170368, + u'image_type': u'snapshot', + u'disk_format': u'qcow2', + u'id': self.image_id, + u'schema': u'/v2/schemas/image', + u'status': u'active', + u'tags': [], + u'visibility': u'private', + u'locations': [{ + u'url': u'http://127.0.0.1/images/' + self.image_id, + u'metadata': {}}], + u'min_disk': 40, + u'virtual_size': None, + u'name': u'fake_image', + u'checksum': u'ee36e35a297980dee1b514de9803ec6d', + u'created_at': u'2016-02-10T05:03:11Z', + u'protected': False}] + self.output = six.BytesIO() + self.output.write(uuid.uuid4().bytes) + self.output.seek(0) + + def test_download_image_no_output(self): + self.assertRaises(exc.OpenStackCloudException, + self.cloud.download_image, 'fake_image') + + def test_download_image_two_outputs(self): + fake_fd = six.BytesIO() + self.assertRaises(exc.OpenStackCloudException, + self.cloud.download_image, 'fake_image', + output_path='fake_path', output_file=fake_fd) + + @mock.patch.object(shade.OpenStackCloud, 'search_images', return_value=[]) + def test_download_image_no_images_found(self, mock_search): + self.assertRaises(exc.OpenStackCloudResourceNotFound, + self.cloud.download_image, 'fake_image', + output_path='fake_path') + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + @mock.patch.object(shade.OpenStackCloud, 'search_images') + def test_download_image_with_fd(self, mock_search, mock_glance): + output_file = six.BytesIO() + mock_glance.images.data.return_value = self.output + mock_search.return_value = self.fake_search_return + self.cloud.download_image('fake_image', output_file=output_file) + mock_glance.images.data.assert_called_once_with(self.image_id) + output_file.seek(0) + self.output.seek(0) + self.assertEqual(output_file.read(), self.output.read()) + + @mock.patch.object(shade.OpenStackCloud, 'glance_client') + @mock.patch.object(shade.OpenStackCloud, 'search_images') + def test_download_image_with_path(self, mock_search, mock_glance): + output_file = tempfile.NamedTemporaryFile() + mock_glance.images.data.return_value = self.output + mock_search.return_value = self.fake_search_return + self.cloud.download_image('fake_image', + output_path=output_file.name) + mock_glance.images.data.assert_called_once_with(self.image_id) + output_file.seek(0) + self.output.seek(0) + self.assertEqual(output_file.read(), self.output.read())