Add image.stage methods
Add support for staging (and completing the import) image data. Change-Id: Id865c7ffe5fff5d723074c22d0fd01d817ae932d
This commit is contained in:
parent
1e810595c6
commit
cc51e34cf1
doc/source/user/proxies
openstack
releasenotes/notes
@ -28,6 +28,7 @@ Image Operations
|
|||||||
.. automethod:: openstack.image.v2._proxy.Proxy.images
|
.. automethod:: openstack.image.v2._proxy.Proxy.images
|
||||||
.. automethod:: openstack.image.v2._proxy.Proxy.deactivate_image
|
.. automethod:: openstack.image.v2._proxy.Proxy.deactivate_image
|
||||||
.. automethod:: openstack.image.v2._proxy.Proxy.reactivate_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.add_tag
|
||||||
.. automethod:: openstack.image.v2._proxy.Proxy.remove_tag
|
.. automethod:: openstack.image.v2._proxy.Proxy.remove_tag
|
||||||
|
|
||||||
|
@ -64,6 +64,36 @@ class Proxy(_base_proxy.BaseImageProxy):
|
|||||||
|
|
||||||
image.import_image(self, method=method, uri=uri)
|
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,
|
def upload_image(self, container_format=None, disk_format=None,
|
||||||
data=None, **attrs):
|
data=None, **attrs):
|
||||||
"""Create and upload a new image from attributes
|
"""Create and upload a new image from attributes
|
||||||
|
@ -262,6 +262,16 @@ class Image(resource.Resource, resource.TagMixin):
|
|||||||
headers={"Content-Type": "application/octet-stream",
|
headers={"Content-Type": "application/octet-stream",
|
||||||
"Accept": ""})
|
"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):
|
def import_image(self, session, method='glance-direct', uri=None):
|
||||||
"""Import Image via interoperable image import process"""
|
"""Import Image via interoperable image import process"""
|
||||||
url = utils.urljoin(self.base_path, self.id, 'import')
|
url = utils.urljoin(self.base_path, self.id, 'import')
|
||||||
@ -269,6 +279,8 @@ class Image(resource.Resource, resource.TagMixin):
|
|||||||
if uri:
|
if uri:
|
||||||
if method == 'web-download':
|
if method == 'web-download':
|
||||||
json['method']['uri'] = uri
|
json['method']['uri'] = uri
|
||||||
|
elif method == 'glance-direct':
|
||||||
|
json['method']['uri'] = uri
|
||||||
else:
|
else:
|
||||||
raise exceptions.InvalidRequest('URI is only supported with '
|
raise exceptions.InvalidRequest('URI is only supported with '
|
||||||
'method: "web-download"')
|
'method: "web-download"')
|
||||||
|
@ -92,6 +92,7 @@ class FakeResponse(object):
|
|||||||
def __init__(self, response, status_code=200, headers=None, reason=None):
|
def __init__(self, response, status_code=200, headers=None, reason=None):
|
||||||
self.body = response
|
self.body = response
|
||||||
self.content = response
|
self.content = response
|
||||||
|
self.text = response
|
||||||
self.status_code = status_code
|
self.status_code = status_code
|
||||||
headers = headers if headers else {'content-type': 'application/json'}
|
headers = headers if headers else {'content-type': 'application/json'}
|
||||||
self.headers = requests.structures.CaseInsensitiveDict(headers)
|
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.post = mock.Mock(return_value=self.resp)
|
||||||
self.sess.put = mock.Mock(return_value=FakeResponse({}))
|
self.sess.put = mock.Mock(return_value=FakeResponse({}))
|
||||||
self.sess.delete = 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.default_microversion = None
|
||||||
self.sess.retriable_status_codes = None
|
self.sess.retriable_status_codes = None
|
||||||
self.sess.log = _log.setup_logging('openstack')
|
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):
|
def test_import_image_with_uri_not_web_download(self):
|
||||||
sot = image.Image(**EXAMPLE)
|
sot = image.Image(**EXAMPLE)
|
||||||
self.assertRaises(exceptions.InvalidRequest,
|
|
||||||
sot.import_image,
|
sot.import_image(self.sess, "glance-direct")
|
||||||
self.sess,
|
self.sess.post.assert_called_with(
|
||||||
"glance-direct",
|
'images/IDENTIFIER/import',
|
||||||
"such-a-good-uri")
|
json={"method": {"name": "glance-direct"}}
|
||||||
|
)
|
||||||
|
|
||||||
def test_upload(self):
|
def test_upload(self):
|
||||||
sot = image.Image(**EXAMPLE)
|
sot = image.Image(**EXAMPLE)
|
||||||
@ -275,6 +277,22 @@ class TestImage(base.TestCase):
|
|||||||
"application/octet-stream",
|
"application/octet-stream",
|
||||||
"Accept": ""})
|
"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):
|
def test_download_checksum_match(self):
|
||||||
sot = image.Image(**EXAMPLE)
|
sot = image.Image(**EXAMPLE)
|
||||||
|
|
||||||
|
@ -94,6 +94,38 @@ class TestImageProxy(test_proxy_base.TestProxyBase):
|
|||||||
'chunk_size': 1,
|
'chunk_size': 1,
|
||||||
'stream': True})
|
'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):
|
def test_image_delete(self):
|
||||||
self.verify_delete(self.proxy.delete_image, image.Image, False)
|
self.verify_delete(self.proxy.delete_image, image.Image, False)
|
||||||
|
|
||||||
|
4
releasenotes/notes/add-image-stage-1dbc3844a042fd26.yaml
Normal file
4
releasenotes/notes/add-image-stage-1dbc3844a042fd26.yaml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Add support for staging image data.
|
Loading…
x
Reference in New Issue
Block a user