Add image.stage methods

Add support for staging (and completing the import) image data.

Change-Id: Id865c7ffe5fff5d723074c22d0fd01d817ae932d
This commit is contained in:
Artem Goncharov 2019-04-16 17:42:50 +02:00
parent 1e810595c6
commit cc51e34cf1
6 changed files with 103 additions and 6 deletions

View File

@ -28,6 +28,7 @@ Image Operations
.. automethod:: openstack.image.v2._proxy.Proxy.images
.. automethod:: openstack.image.v2._proxy.Proxy.deactivate_image
.. automethod:: openstack.image.v2._proxy.Proxy.reactivate_image
.. automethod:: openstack.image.v2._proxy.Proxy.stage_image
.. automethod:: openstack.image.v2._proxy.Proxy.add_tag
.. automethod:: openstack.image.v2._proxy.Proxy.remove_tag

View File

@ -64,6 +64,36 @@ class Proxy(_base_proxy.BaseImageProxy):
image.import_image(self, method=method, uri=uri)
def stage_image(self, image, filename=None, data=None):
"""Stage binary image data
:param image: The value can be the ID of a image or a
:class:`~openstack.image.v2.image.Image` instance.
:param filename: Optional name of the file to read data from.
:param data: Optional data to be uploaded as an image.
:returns: The results of image creation
:rtype: :class:`~openstack.image.v2.image.Image`
"""
image = self._get_resource(_image.Image, image)
if 'queued' != image.status:
raise exceptions.SDKException('Image stage is only possible for '
'images in the queued state.'
' Current state is {status}'
.format(status=image.status))
if filename:
image.data = open(filename, 'rb')
elif data:
image.data = data
image.stage(self)
# Stage does not return content, but updates the object
image.fetch(self)
return image
def upload_image(self, container_format=None, disk_format=None,
data=None, **attrs):
"""Create and upload a new image from attributes

View File

@ -262,6 +262,16 @@ class Image(resource.Resource, resource.TagMixin):
headers={"Content-Type": "application/octet-stream",
"Accept": ""})
def stage(self, session):
"""Stage binary image data into an existing image"""
url = utils.urljoin(self.base_path, self.id, 'stage')
response = session.put(
url, data=self.data,
headers={"Content-Type": "application/octet-stream",
"Accept": ""})
self._translate_response(response, has_body=False)
return self
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')
@ -269,6 +279,8 @@ class Image(resource.Resource, resource.TagMixin):
if uri:
if method == 'web-download':
json['method']['uri'] = uri
elif method == 'glance-direct':
json['method']['uri'] = uri
else:
raise exceptions.InvalidRequest('URI is only supported with '
'method: "web-download"')

View File

@ -92,6 +92,7 @@ class FakeResponse(object):
def __init__(self, response, status_code=200, headers=None, reason=None):
self.body = response
self.content = response
self.text = response
self.status_code = status_code
headers = headers if headers else {'content-type': 'application/json'}
self.headers = requests.structures.CaseInsensitiveDict(headers)
@ -115,7 +116,7 @@ class TestImage(base.TestCase):
self.sess.post = mock.Mock(return_value=self.resp)
self.sess.put = mock.Mock(return_value=FakeResponse({}))
self.sess.delete = mock.Mock(return_value=FakeResponse({}))
self.sess.fetch = mock.Mock(return_value=FakeResponse({}))
self.sess.get = mock.Mock(return_value=FakeResponse({}))
self.sess.default_microversion = None
self.sess.retriable_status_codes = None
self.sess.log = _log.setup_logging('openstack')
@ -259,11 +260,12 @@ class TestImage(base.TestCase):
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")
sot.import_image(self.sess, "glance-direct")
self.sess.post.assert_called_with(
'images/IDENTIFIER/import',
json={"method": {"name": "glance-direct"}}
)
def test_upload(self):
sot = image.Image(**EXAMPLE)
@ -275,6 +277,22 @@ class TestImage(base.TestCase):
"application/octet-stream",
"Accept": ""})
def test_stage(self):
sot = image.Image(**EXAMPLE)
self.assertIsNotNone(sot.stage(self.sess))
self.sess.put.assert_called_with('images/IDENTIFIER/stage',
data=sot.data,
headers={"Content-Type":
"application/octet-stream",
"Accept": ""})
def test_stage_error(self):
sot = image.Image(**EXAMPLE)
self.sess.put.return_value = FakeResponse("dummy", status_code=400)
self.assertRaises(exceptions.SDKException, sot.stage, self.sess)
def test_download_checksum_match(self):
sot = image.Image(**EXAMPLE)

View File

@ -94,6 +94,38 @@ class TestImageProxy(test_proxy_base.TestProxyBase):
'chunk_size': 1,
'stream': True})
@mock.patch("openstack.image.v2.image.Image.fetch")
def test_image_stage(self, mock_fetch):
img = image.Image(id="id", status="queued")
img.stage = mock.Mock()
self.proxy.stage_image(image=img)
mock_fetch.assert_called()
img.stage.assert_called_with(self.proxy)
@mock.patch("openstack.image.v2.image.Image.fetch")
def test_image_stage_with_data(self, mock_fetch):
img = image.Image(id="id", status="queued")
img.stage = mock.Mock()
mock_fetch.return_value = img
rv = self.proxy.stage_image(image=img, data="data")
img.stage.assert_called_with(self.proxy)
mock_fetch.assert_called()
self.assertEqual(rv.data, "data")
def test_image_stage_wrong_status(self):
img = image.Image(id="id", status="active")
img.stage = mock.Mock()
self.assertRaises(
exceptions.SDKException,
self.proxy.stage_image,
img,
"data"
)
def test_image_delete(self):
self.verify_delete(self.proxy.delete_image, image.Image, False)

View File

@ -0,0 +1,4 @@
---
features:
- |
Add support for staging image data.