From de8a882ce79ff10ef1f0fb1e5ffe6be87923709a Mon Sep 17 00:00:00 2001 From: Victor Coutellier Date: Mon, 25 Feb 2019 15:53:00 +0100 Subject: [PATCH] Add glance image import support Interoperable image import process are introduced in the Image API v2.6. It mainly allow image importing from an external url and let Image Service download it by itself without sending binary data at image creation. This commit will add a method import_image in both Image resource and proxy. I also add a simple create_image proxy function for creating an image without uploading it's data, as it is not required in glance API. Change-Id: Idee9bea3f1f5db412e0ecd2105adff316aef4c4b Story: 2005085 Task: 29671 --- doc/source/user/guides/image.rst | 16 ++++++++ doc/source/user/proxies/image_v2.rst | 2 + examples/image/import.py | 38 +++++++++++++++++++ openstack/image/v2/_proxy.py | 31 ++++++++++++++- openstack/image/v2/image.py | 12 ++++++ openstack/tests/unit/image/v2/test_image.py | 17 +++++++++ openstack/tests/unit/image/v2/test_proxy.py | 19 +++++++++- ...image_import_support-6cea2e7d7a781071.yaml | 7 ++++ 8 files changed, 139 insertions(+), 3 deletions(-) create mode 100644 examples/image/import.py create mode 100644 releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml diff --git a/doc/source/user/guides/image.rst b/doc/source/user/guides/image.rst index 154f9d076..42e32118c 100644 --- a/doc/source/user/guides/image.rst +++ b/doc/source/user/guides/image.rst @@ -32,6 +32,20 @@ Create an image by uploading its data and setting its attributes. Full example: `image resource create`_ +Create Image via interoperable image import process +--------------------------------------------------- + +Create an image then use interoperable image import process to download data +from a web URL. + +For more information about the image import process, please check +`interoperable image import`_ + +.. literalinclude:: ../examples/image/import.py + :pyobject: import_image + +Full example: `image resource import`_ + .. _download_image-stream-true: Downloading an Image with stream=True @@ -76,6 +90,8 @@ Delete an image. Full example: `image resource delete`_ .. _image resource create: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/create.py +.. _image resource import: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/import.py .. _image resource delete: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/delete.py .. _image resource list: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/list.py .. _image resource download: http://git.openstack.org/cgit/openstack/openstacksdk/tree/examples/image/download.py +.. _interoperable image import: https://docs.openstack.org/glance/latest/admin/interoperable-image-import.html diff --git a/doc/source/user/proxies/image_v2.rst b/doc/source/user/proxies/image_v2.rst index 4e108948d..2cf235daf 100644 --- a/doc/source/user/proxies/image_v2.rst +++ b/doc/source/user/proxies/image_v2.rst @@ -17,6 +17,8 @@ Image Operations .. autoclass:: openstack.image.v2._proxy.Proxy + .. automethod:: openstack.image.v2._proxy.Proxy.create_image + .. automethod:: openstack.image.v2._proxy.Proxy.import_image .. automethod:: openstack.image.v2._proxy.Proxy.upload_image .. automethod:: openstack.image.v2._proxy.Proxy.download_image .. automethod:: openstack.image.v2._proxy.Proxy.update_image diff --git a/examples/image/import.py b/examples/image/import.py new file mode 100644 index 000000000..ec9c6c67e --- /dev/null +++ b/examples/image/import.py @@ -0,0 +1,38 @@ +# 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. + +from examples.connect import EXAMPLE_IMAGE_NAME + +""" +Create resources with the Image service. + +For a full guide see +http://developer.openstack.org/sdks/python/openstacksdk/user/guides/image.html +""" + + +def import_image(conn): + print("Import Image:") + + # Url where glance can download the image + uri = 'https://download.cirros-cloud.net/0.4.0/' \ + 'cirros-0.4.0-x86_64-disk.img' + + # Build the image attributes and import the image. + image_attrs = { + 'name': EXAMPLE_IMAGE_NAME, + 'disk_format': 'qcow2', + 'container_format': 'bare', + 'visibility': 'public', + } + image = conn.image.create_image(**image_attrs) + conn.image.import_image(image, method="web-download", uri=uri) diff --git a/openstack/image/v2/_proxy.py b/openstack/image/v2/_proxy.py index e64af6387..9f77ec821 100644 --- a/openstack/image/v2/_proxy.py +++ b/openstack/image/v2/_proxy.py @@ -33,9 +33,38 @@ _INT_PROPERTIES = ('min_disk', 'min_ram', 'size', 'virtual_size') class Proxy(_base_proxy.BaseImageProxy): + def import_image(self, image, method='glance-direct', uri=None): + """Import data to an existing image + + Interoperable image import process are introduced in the Image API + v2.6. It mainly allow image importing from an external url and let + Image Service download it by itself without sending binary data at + image creation. + + :param image: The value can be the ID of a image or a + :class:`~openstack.image.v2.image.Image` instance. + :param method: Method to use for importing the image. + A valid value is glance-direct or web-download. + :param uri: Required only if using the web-download import method. + This url is where the data is made available to the Image + service. + + :returns: None + """ + image = self._get_resource(_image.Image, image) + + # as for the standard image upload function, container_format and + # disk_format are required for using image import process + if not all([image.container_format, image.disk_format]): + raise exceptions.InvalidRequest( + "Both container_format and disk_format are required for" + " importing an image") + + image.import_image(self, method=method, uri=uri) + def upload_image(self, container_format=None, disk_format=None, data=None, **attrs): - """Upload a new image from attributes + """Create and upload a new image from attributes .. warning: This method is deprecated - and also doesn't work very well. diff --git a/openstack/image/v2/image.py b/openstack/image/v2/image.py index e58a7b33f..1c7b5845c 100644 --- a/openstack/image/v2/image.py +++ b/openstack/image/v2/image.py @@ -249,6 +249,18 @@ class Image(resource.Resource, resource.TagMixin): headers={"Content-Type": "application/octet-stream", "Accept": ""}) + def import_image(self, session, method='glance-direct', uri=None): + """Import Image via interoperable image import process""" + url = utils.urljoin(self.base_path, self.id, 'import') + json = {'method': {'name': method}} + if uri: + if method == 'web-download': + json['method']['uri'] = uri + else: + raise exceptions.InvalidRequest('URI is only supported with ' + 'method: "web-download"') + session.post(url, json=json) + def download(self, session, stream=False): """Download the data contained in an image""" # TODO(briancurtin): This method should probably offload the get diff --git a/openstack/tests/unit/image/v2/test_image.py b/openstack/tests/unit/image/v2/test_image.py index 7f506a1fe..5028e4f28 100644 --- a/openstack/tests/unit/image/v2/test_image.py +++ b/openstack/tests/unit/image/v2/test_image.py @@ -239,6 +239,23 @@ class TestImage(base.TestCase): 'images/IDENTIFIER/tags/%s' % tag, ) + def test_import_image(self): + sot = image.Image(**EXAMPLE) + json = {"method": {"name": "web-download", "uri": "such-a-good-uri"}} + sot.import_image(self.sess, "web-download", "such-a-good-uri") + self.sess.post.assert_called_with( + 'images/IDENTIFIER/import', + json=json + ) + + def test_import_image_with_uri_not_web_download(self): + sot = image.Image(**EXAMPLE) + self.assertRaises(exceptions.InvalidRequest, + sot.import_image, + self.sess, + "glance-direct", + "such-a-good-uri") + def test_upload(self): sot = image.Image(**EXAMPLE) diff --git a/openstack/tests/unit/image/v2/test_proxy.py b/openstack/tests/unit/image/v2/test_proxy.py index 5af325436..1d097a7cb 100644 --- a/openstack/tests/unit/image/v2/test_proxy.py +++ b/openstack/tests/unit/image/v2/test_proxy.py @@ -29,11 +29,26 @@ class TestImageProxy(test_proxy_base.TestProxyBase): super(TestImageProxy, self).setUp() self.proxy = _proxy.Proxy(self.session) - def test_image_create_no_args(self): + def test_image_import_no_required_attrs(self): + # container_format and disk_format are required attrs of the image + existing_image = image.Image(id="id") + self.assertRaises(exceptions.InvalidRequest, + self.proxy.import_image, + existing_image) + + def test_image_import(self): + original_image = image.Image(**EXAMPLE) + self._verify("openstack.image.v2.image.Image.import_image", + self.proxy.import_image, + method_args=[original_image, "method", "uri"], + expected_kwargs={"method": "method", + "uri": "uri"}) + + def test_image_upload_no_args(self): # container_format and disk_format are required args self.assertRaises(exceptions.InvalidRequest, self.proxy.upload_image) - def test_image_create(self): + def test_image_upload(self): # NOTE: This doesn't use any of the base class verify methods # because it ends up making two separate calls to complete the # operation. diff --git a/releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml b/releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml new file mode 100644 index 000000000..22b35ce6b --- /dev/null +++ b/releasenotes/notes/add_image_import_support-6cea2e7d7a781071.yaml @@ -0,0 +1,7 @@ +--- +features: + - Add ability to create image without upload data at the same time + - Add support for interoperable image import process as introduced in the + Image API v2.6 at [1] + + [1]https://developer.openstack.org/api-ref/image/v2/index.html#interoperable-image-import