Support volume re-image
This patch adds volume re-image API to enable the ability to re-image a specific volume. Implements: blueprint add-volume-re-image-api Co-Authored-by: Rajat Dhasmana <rajatdhasmana@gmail.com> Change-Id: I031aae50ee82198648f46c503bba04c6e231bbe5
This commit is contained in:
parent
55ea01c1d0
commit
d69e89ea3b
@ -2134,6 +2134,13 @@ os-migrate_volume_completion:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: object
|
type: object
|
||||||
|
os-reimage:
|
||||||
|
description: |
|
||||||
|
The ``os-reimage`` action.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: object
|
||||||
|
min_version: 3.68
|
||||||
os-reserve:
|
os-reserve:
|
||||||
description: |
|
description: |
|
||||||
The ``os-reserve`` action.
|
The ``os-reserve`` action.
|
||||||
@ -2479,6 +2486,15 @@ reference:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: object
|
type: object
|
||||||
|
reimage_reserved:
|
||||||
|
description: |
|
||||||
|
Normally, volumes to be re-imaged are in ``available`` or ``error`` status.
|
||||||
|
When ``true``, this parameter will allow a volume in the ``reserved`` status
|
||||||
|
to be re-imaged. The ability to re-image a volume in ``reserved`` status
|
||||||
|
may be restricted to administrators in some clouds. Default value is ``false``.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
remove_project_access:
|
remove_project_access:
|
||||||
description: |
|
description: |
|
||||||
Removes volume type access from a project.
|
Removes volume type access from a project.
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
],
|
],
|
||||||
"min_version": "3.0",
|
"min_version": "3.0",
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"updated": "2021-12-16T00:00:00Z",
|
"updated": "2022-03-30T00:00:00Z",
|
||||||
"version": "3.67"
|
"version": "3.68"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
],
|
],
|
||||||
"min_version": "3.0",
|
"min_version": "3.0",
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"updated": "2021-12-16T00:00:00Z",
|
"updated": "2022-03-30T00:00:00Z",
|
||||||
"version": "3.67"
|
"version": "3.68"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
6
api-ref/source/v3/samples/volume-os-reimage-request.json
Normal file
6
api-ref/source/v3/samples/volume-os-reimage-request.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"os-reimage": {
|
||||||
|
"image_id": "71543ced-a8af-45b6-a5c4-a46282108a90",
|
||||||
|
"reimage_reserved": false
|
||||||
|
}
|
||||||
|
}
|
@ -973,3 +973,44 @@ Request Example
|
|||||||
|
|
||||||
.. literalinclude:: ./samples/volume-readonly-update-request.json
|
.. literalinclude:: ./samples/volume-readonly-update-request.json
|
||||||
:language: javascript
|
:language: javascript
|
||||||
|
|
||||||
|
|
||||||
|
Reimage a volume
|
||||||
|
~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. rest_method:: POST /v3/{project_id}/volumes/{volume_id}/action
|
||||||
|
|
||||||
|
Re-image a volume with a specific image. Specify the ``os-reimage`` action
|
||||||
|
in the request body.
|
||||||
|
|
||||||
|
A volume in ``available`` or ``error`` status can be re-imaged directly. To
|
||||||
|
re-image a volume in ``reserved`` status, you must include the
|
||||||
|
``reimage_reserved`` parameter set to ``true``.
|
||||||
|
|
||||||
|
.. note:: Image signature verification is currently unsupported when
|
||||||
|
re-imaging a volume.
|
||||||
|
|
||||||
|
Response codes
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. rest_status_code:: success ../status.yaml
|
||||||
|
|
||||||
|
- 202
|
||||||
|
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- project_id: project_id_path
|
||||||
|
- volume_id: volume_id_path
|
||||||
|
- image_id: image_id
|
||||||
|
- reimage_reserved: reimage_reserved
|
||||||
|
- os-reimage: os-reimage
|
||||||
|
|
||||||
|
Request Example
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. literalinclude:: ./samples/volume-os-reimage-request.json
|
||||||
|
:language: javascript
|
||||||
|
@ -326,6 +326,26 @@ class VolumeActionsController(wsgi.Controller):
|
|||||||
|
|
||||||
self.volume_api.update(context, volume, update_dict)
|
self.volume_api.update(context, volume, update_dict)
|
||||||
|
|
||||||
|
@wsgi.Controller.api_version(mv.SUPPORT_REIMAGE_VOLUME)
|
||||||
|
@wsgi.response(HTTPStatus.ACCEPTED)
|
||||||
|
@wsgi.action('os-reimage')
|
||||||
|
@validation.schema(volume_action.reimage, mv.SUPPORT_REIMAGE_VOLUME)
|
||||||
|
def _reimage(self, req, id, body):
|
||||||
|
"""Re-image a volume with specific image."""
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
# Not found exception will be handled at the wsgi level
|
||||||
|
volume = self.volume_api.get(context, id)
|
||||||
|
params = body['os-reimage']
|
||||||
|
reimage_reserved = params.get('reimage_reserved', 'False')
|
||||||
|
reimage_reserved = strutils.bool_from_string(reimage_reserved,
|
||||||
|
strict=True)
|
||||||
|
image_id = params['image_id']
|
||||||
|
try:
|
||||||
|
self.volume_api.reimage(context, volume, image_id,
|
||||||
|
reimage_reserved)
|
||||||
|
except exception.InvalidVolume as error:
|
||||||
|
raise webob.exc.HTTPBadRequest(explanation=error.msg)
|
||||||
|
|
||||||
|
|
||||||
class Volume_actions(extensions.ExtensionDescriptor):
|
class Volume_actions(extensions.ExtensionDescriptor):
|
||||||
"""Enable volume actions."""
|
"""Enable volume actions."""
|
||||||
|
@ -173,6 +173,8 @@ SNAPSHOT_IN_USE = '3.66'
|
|||||||
|
|
||||||
PROJECT_ID_OPTIONAL_IN_URL = '3.67'
|
PROJECT_ID_OPTIONAL_IN_URL = '3.67'
|
||||||
|
|
||||||
|
SUPPORT_REIMAGE_VOLUME = '3.68'
|
||||||
|
|
||||||
|
|
||||||
def get_mv_header(version):
|
def get_mv_header(version):
|
||||||
"""Gets a formatted HTTP microversion header.
|
"""Gets a formatted HTTP microversion header.
|
||||||
|
@ -153,13 +153,14 @@ REST_API_VERSION_HISTORY = """
|
|||||||
operation.
|
operation.
|
||||||
* 3.66 - Allow snapshotting in-use volumes without force flag.
|
* 3.66 - Allow snapshotting in-use volumes without force flag.
|
||||||
* 3.67 - API URLs no longer need to include a project_id parameter.
|
* 3.67 - API URLs no longer need to include a project_id parameter.
|
||||||
|
* 3.68 - Support re-image volume
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# The minimum and maximum versions of the API supported
|
# The minimum and maximum versions of the API supported
|
||||||
# The default api version request is defined to be the
|
# The default api version request is defined to be the
|
||||||
# minimum version of the API supported.
|
# minimum version of the API supported.
|
||||||
_MIN_API_VERSION = "3.0"
|
_MIN_API_VERSION = "3.0"
|
||||||
_MAX_API_VERSION = "3.67"
|
_MAX_API_VERSION = "3.68"
|
||||||
UPDATED = "2021-11-02T00:00:00Z"
|
UPDATED = "2021-11-02T00:00:00Z"
|
||||||
|
|
||||||
|
|
||||||
|
@ -513,3 +513,8 @@ route: ``https://$(controller)s/volume/v3/$(project_id)s/volumes`` is
|
|||||||
equivalent to ``https://$(controller)s/volume/v3/volumes``. When interacting
|
equivalent to ``https://$(controller)s/volume/v3/volumes``. When interacting
|
||||||
with the cinder service as system or domain scoped users, a project_id should
|
with the cinder service as system or domain scoped users, a project_id should
|
||||||
not be specified in the API path.
|
not be specified in the API path.
|
||||||
|
|
||||||
|
3.68
|
||||||
|
----
|
||||||
|
Support ability to re-image a volume with a specific image. Specify the
|
||||||
|
``os-reimage`` action in the request body.
|
||||||
|
@ -202,3 +202,20 @@ volume_readonly_update = {
|
|||||||
'required': ['os-update_readonly_flag'],
|
'required': ['os-update_readonly_flag'],
|
||||||
'additionalProperties': False,
|
'additionalProperties': False,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reimage = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'os-reimage': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'image_id': parameter_types.uuid,
|
||||||
|
'reimage_reserved': parameter_types.boolean,
|
||||||
|
},
|
||||||
|
'required': ['image_id'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['os-reimage'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
@ -143,6 +143,11 @@ class API(base.Base):
|
|||||||
'server_uuid': server_id,
|
'server_uuid': server_id,
|
||||||
'tag': volume_id}
|
'tag': volume_id}
|
||||||
|
|
||||||
|
def _get_volume_reimaged_event(self, server_id, volume_id):
|
||||||
|
return {'name': 'volume-reimaged',
|
||||||
|
'server_uuid': server_id,
|
||||||
|
'tag': volume_id}
|
||||||
|
|
||||||
def _send_events(self, context, events, api_version=None):
|
def _send_events(self, context, events, api_version=None):
|
||||||
nova = novaclient(context, privileged_user=True,
|
nova = novaclient(context, privileged_user=True,
|
||||||
api_version=api_version)
|
api_version=api_version)
|
||||||
@ -219,3 +224,16 @@ class API(base.Base):
|
|||||||
resource_uuid=volume_id,
|
resource_uuid=volume_id,
|
||||||
detail=message_field.Detail.NOTIFY_COMPUTE_SERVICE_FAILED)
|
detail=message_field.Detail.NOTIFY_COMPUTE_SERVICE_FAILED)
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def reimage_volume(self, context, server_ids, volume_id):
|
||||||
|
api_version = '2.91'
|
||||||
|
events = [self._get_volume_reimaged_event(server_id, volume_id)
|
||||||
|
for server_id in server_ids]
|
||||||
|
result = self._send_events(context, events, api_version=api_version)
|
||||||
|
if not result:
|
||||||
|
self.message_api.create(
|
||||||
|
context,
|
||||||
|
message_field.Action.REIMAGE_VOLUME,
|
||||||
|
resource_uuid=volume_id,
|
||||||
|
detail=message_field.Detail.NOTIFY_COMPUTE_SERVICE_FAILED)
|
||||||
|
return result
|
||||||
|
@ -39,6 +39,8 @@ RESERVE_POLICY = "volume_extension:volume_actions:reserve"
|
|||||||
ROLL_DETACHING_POLICY = "volume_extension:volume_actions:roll_detaching"
|
ROLL_DETACHING_POLICY = "volume_extension:volume_actions:roll_detaching"
|
||||||
TERMINATE_POLICY = "volume_extension:volume_actions:terminate_connection"
|
TERMINATE_POLICY = "volume_extension:volume_actions:terminate_connection"
|
||||||
INITIALIZE_POLICY = "volume_extension:volume_actions:initialize_connection"
|
INITIALIZE_POLICY = "volume_extension:volume_actions:initialize_connection"
|
||||||
|
REIMAGE_POLICY = "volume:reimage"
|
||||||
|
REIMAGE_RESERVED_POLICY = "volume:reimage_reserved"
|
||||||
|
|
||||||
deprecated_extend_policy = base.CinderDeprecatedRule(
|
deprecated_extend_policy = base.CinderDeprecatedRule(
|
||||||
name=EXTEND_POLICY,
|
name=EXTEND_POLICY,
|
||||||
@ -323,6 +325,26 @@ volume_action_policies = [
|
|||||||
],
|
],
|
||||||
deprecated_rule=deprecated_detach_policy,
|
deprecated_rule=deprecated_detach_policy,
|
||||||
),
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=REIMAGE_POLICY,
|
||||||
|
check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER,
|
||||||
|
description="Reimage a volume in 'available' or 'error' status.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'POST',
|
||||||
|
'path': '/volumes/{volume_id}/action (os-reimage)'
|
||||||
|
}
|
||||||
|
]),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=REIMAGE_RESERVED_POLICY,
|
||||||
|
check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER,
|
||||||
|
description="Reimage a volume in 'reserved' status.",
|
||||||
|
operations=[
|
||||||
|
{
|
||||||
|
'method': 'POST',
|
||||||
|
'path': '/volumes/{volume_id}/action (os-reimage)'
|
||||||
|
}
|
||||||
|
]),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -1552,3 +1552,70 @@ class VolumeImageActionsTest(test.TestCase):
|
|||||||
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
vol_db = objects.Volume.get_by_id(self.context, volume.id)
|
||||||
self.assertEqual('uploading', vol_db.status)
|
self.assertEqual('uploading', vol_db.status)
|
||||||
self.assertEqual('available', vol_db.previous_status)
|
self.assertEqual('available', vol_db.previous_status)
|
||||||
|
|
||||||
|
def _build_reimage_req(self, body, vol_id,
|
||||||
|
version=mv.SUPPORT_REIMAGE_VOLUME):
|
||||||
|
req = fakes.HTTPRequest.blank(
|
||||||
|
'/v3/%s/volumes/%s/action' % (fake.PROJECT_ID, id))
|
||||||
|
req.method = "POST"
|
||||||
|
req.body = jsonutils.dump_as_bytes(body)
|
||||||
|
req.environ['cinder.context'] = self.context
|
||||||
|
req.api_version_request = mv.get_api_version(version)
|
||||||
|
req.headers["content-type"] = "application/json"
|
||||||
|
return req
|
||||||
|
|
||||||
|
@ddt.data(None, False, True)
|
||||||
|
@mock.patch.object(volume_api.API, "reimage")
|
||||||
|
def test_volume_reimage(self, reimage_reserved, mock_image):
|
||||||
|
vol = utils.create_volume(self.context)
|
||||||
|
body = {"os-reimage": {"image_id": fake.IMAGE_ID}}
|
||||||
|
if reimage_reserved is not None:
|
||||||
|
body["os-reimage"]["reimage_reserved"] = reimage_reserved
|
||||||
|
req = self._build_reimage_req(body, vol.id)
|
||||||
|
self.controller._reimage(req, vol.id, body=body)
|
||||||
|
|
||||||
|
@mock.patch.object(volume_api.API, "reimage")
|
||||||
|
def test_volume_reimage_invaild_params(self, mock_image):
|
||||||
|
vol = utils.create_volume(self.context)
|
||||||
|
body = {"os-reimage": {"image_id": fake.IMAGE_ID,
|
||||||
|
"reimage_reserved": 'wrong'}}
|
||||||
|
req = self._build_reimage_req(body, vol)
|
||||||
|
self.assertRaises(exception.ValidationError,
|
||||||
|
self.controller._reimage, req,
|
||||||
|
vol.id, body=body)
|
||||||
|
|
||||||
|
def test_volume_reimage_before_3_68(self):
|
||||||
|
vol = utils.create_volume(self.context)
|
||||||
|
body = {"os-reimage": {"image_id": fake.IMAGE_ID}}
|
||||||
|
|
||||||
|
req = self._build_reimage_req(body, vol.id, version="3.67")
|
||||||
|
self.assertRaises(exception.VersionNotFoundForAPIMethod,
|
||||||
|
self.controller._reimage, req, vol.id, body=body)
|
||||||
|
|
||||||
|
def test_reimage_volume_invalid_status(self):
|
||||||
|
def fake_reimage_volume(*args, **kwargs):
|
||||||
|
msg = "Volume status must be available."
|
||||||
|
raise exception.InvalidVolume(reason=msg)
|
||||||
|
self.mock_object(volume.api.API, 'reimage',
|
||||||
|
fake_reimage_volume)
|
||||||
|
|
||||||
|
vol = utils.create_volume(self.context)
|
||||||
|
body = {"os-reimage": {"image_id": fake.IMAGE_ID}}
|
||||||
|
req = self._build_reimage_req(body, vol)
|
||||||
|
self.assertRaises(webob.exc.HTTPBadRequest,
|
||||||
|
self.controller._reimage, req,
|
||||||
|
vol.id, body=body)
|
||||||
|
|
||||||
|
@mock.patch('cinder.context.RequestContext.authorize')
|
||||||
|
def test_reimage_volume_attach_more_than_one_server(self, mock_authorize):
|
||||||
|
vol = utils.create_volume(self.context)
|
||||||
|
va_objs = [objects.VolumeAttachment(context=self.context, id=i)
|
||||||
|
for i in [fake.OBJECT_ID, fake.OBJECT2_ID, fake.OBJECT3_ID]]
|
||||||
|
va_list = objects.VolumeAttachmentList(context=self.context,
|
||||||
|
objects=va_objs)
|
||||||
|
vol.volume_attachment = va_list
|
||||||
|
self.mock_object(volume_api.API, 'get', return_value=vol)
|
||||||
|
body = {"os-reimage": {"image_id": fake.IMAGE_ID}}
|
||||||
|
req = self._build_reimage_req(body, vol)
|
||||||
|
self.assertRaises(webob.exc.HTTPConflict,
|
||||||
|
self.controller._reimage, req, vol.id, body=body)
|
||||||
|
@ -676,3 +676,12 @@ class VolumeRPCAPITestCase(test.RPCAPITestCase):
|
|||||||
server=self.fake_group.host,
|
server=self.fake_group.host,
|
||||||
group=self.fake_group,
|
group=self.fake_group,
|
||||||
version='3.14')
|
version='3.14')
|
||||||
|
|
||||||
|
def test_reimage(self):
|
||||||
|
self._test_rpc_api('reimage', rpc_method='cast',
|
||||||
|
server=self.fake_volume_obj.host,
|
||||||
|
volume=self.fake_volume_obj,
|
||||||
|
image_meta={'id': fake.IMAGE_ID,
|
||||||
|
'container_format': 'fake_type',
|
||||||
|
'disk_format': 'fake_format'},
|
||||||
|
version='3.18')
|
||||||
|
136
cinder/tests/unit/volume/test_volume_reimage.py
Normal file
136
cinder/tests/unit/volume/test_volume_reimage.py
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
# 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.
|
||||||
|
"""Tests for Volume reimage Code."""
|
||||||
|
|
||||||
|
from unittest import mock
|
||||||
|
|
||||||
|
import ddt
|
||||||
|
from oslo_concurrency import processutils
|
||||||
|
|
||||||
|
from cinder import exception
|
||||||
|
from cinder.tests.unit import fake_constants
|
||||||
|
from cinder.tests.unit.image import fake as fake_image
|
||||||
|
from cinder.tests.unit import utils as tests_utils
|
||||||
|
from cinder.tests.unit import volume as base
|
||||||
|
|
||||||
|
|
||||||
|
@ddt.ddt
|
||||||
|
class VolumeReimageTestCase(base.BaseVolumeTestCase):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(VolumeReimageTestCase, self).setUp()
|
||||||
|
self.patch('cinder.volume.volume_utils.clear_volume', autospec=True)
|
||||||
|
fake_image.mock_image_service(self)
|
||||||
|
self.image_meta = fake_image.FakeImageService().show(
|
||||||
|
self.context, fake_constants.IMAGE_ID)
|
||||||
|
|
||||||
|
def test_volume_reimage(self):
|
||||||
|
volume = tests_utils.create_volume(self.context, status='downloading',
|
||||||
|
previous_status='available')
|
||||||
|
self.assertEqual(volume.status, 'downloading')
|
||||||
|
self.assertEqual(volume.previous_status, 'available')
|
||||||
|
self.volume.create_volume(self.context, volume)
|
||||||
|
|
||||||
|
with mock.patch.object(self.volume.driver, 'copy_image_to_volume'
|
||||||
|
) as mock_cp_img:
|
||||||
|
self.volume.reimage(self.context, volume, self.image_meta)
|
||||||
|
mock_cp_img.assert_called_once_with(self.context, volume,
|
||||||
|
fake_image.FakeImageService(),
|
||||||
|
self.image_meta['id'])
|
||||||
|
self.assertEqual(volume.status, 'available')
|
||||||
|
|
||||||
|
def test_volume_reimage_raise_exception(self):
|
||||||
|
volume = tests_utils.create_volume(self.context)
|
||||||
|
self.volume.create_volume(self.context, volume)
|
||||||
|
|
||||||
|
with mock.patch.object(self.volume.driver, 'copy_image_to_volume'
|
||||||
|
) as mock_cp_img:
|
||||||
|
mock_cp_img.side_effect = processutils.ProcessExecutionError
|
||||||
|
self.assertRaises(exception.ImageCopyFailure, self.volume.reimage,
|
||||||
|
self.context, volume, self.image_meta)
|
||||||
|
self.assertEqual(volume.previous_status, 'available')
|
||||||
|
self.assertEqual(volume.status, 'error')
|
||||||
|
|
||||||
|
mock_cp_img.side_effect = exception.ImageUnacceptable(
|
||||||
|
image_id=self.image_meta['id'], reason='')
|
||||||
|
self.assertRaises(exception.ImageUnacceptable, self.volume.reimage,
|
||||||
|
self.context, volume, self.image_meta)
|
||||||
|
|
||||||
|
mock_cp_img.side_effect = exception.ImageTooBig(
|
||||||
|
image_id=self.image_meta['id'], reason='')
|
||||||
|
self.assertRaises(exception.ImageTooBig, self.volume.reimage,
|
||||||
|
self.context, volume, self.image_meta)
|
||||||
|
|
||||||
|
mock_cp_img.side_effect = Exception
|
||||||
|
self.assertRaises(exception.ImageCopyFailure, self.volume.reimage,
|
||||||
|
self.context, volume, self.image_meta)
|
||||||
|
|
||||||
|
mock_cp_img.side_effect = exception.ImageCopyFailure(reason='')
|
||||||
|
self.assertRaises(exception.ImageCopyFailure, self.volume.reimage,
|
||||||
|
self.context, volume, self.image_meta)
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.volume_utils.check_image_metadata')
|
||||||
|
@mock.patch('cinder.volume.rpcapi.VolumeAPI.reimage')
|
||||||
|
@ddt.data('available', 'error')
|
||||||
|
def test_volume_reimage_api(self, status, mock_reimage, mock_check):
|
||||||
|
volume = tests_utils.create_volume(self.context)
|
||||||
|
volume.status = status
|
||||||
|
volume.save()
|
||||||
|
self.assertEqual(volume.status, status)
|
||||||
|
# The available or error volume can be reimage directly
|
||||||
|
self.volume_api.reimage(self.context, volume, self.image_meta['id'])
|
||||||
|
mock_check.assert_called_once_with(self.image_meta, volume.size)
|
||||||
|
mock_reimage.assert_called_once_with(self.context, volume,
|
||||||
|
self.image_meta)
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.volume_utils.check_image_metadata')
|
||||||
|
@mock.patch('cinder.volume.rpcapi.VolumeAPI.reimage')
|
||||||
|
def test_volume_reimage_api_with_reimage_reserved(self, mock_reimage,
|
||||||
|
mock_check):
|
||||||
|
volume = tests_utils.create_volume(self.context)
|
||||||
|
# The reserved volume can not be reimage directly, and only can
|
||||||
|
# be reimaged with reimage_reserved flag
|
||||||
|
volume.status = 'reserved'
|
||||||
|
volume.save()
|
||||||
|
self.assertEqual(volume.status, 'reserved')
|
||||||
|
self.volume_api.reimage(self.context, volume, self.image_meta['id'],
|
||||||
|
reimage_reserved=True)
|
||||||
|
mock_check.assert_called_once_with(self.image_meta, volume.size)
|
||||||
|
mock_reimage.assert_called_once_with(self.context, volume,
|
||||||
|
self.image_meta)
|
||||||
|
|
||||||
|
def test_volume_reimage_api_with_invaild_status(self):
|
||||||
|
volume = tests_utils.create_volume(self.context)
|
||||||
|
# The reserved volume can not be reimage directly, and only can
|
||||||
|
# be reimaged with reimage_reserved flag
|
||||||
|
|
||||||
|
volume.status = 'reserved'
|
||||||
|
volume.save()
|
||||||
|
self.assertEqual(volume.status, 'reserved')
|
||||||
|
ex = self.assertRaises(exception.InvalidVolume,
|
||||||
|
self.volume_api.reimage,
|
||||||
|
self.context, volume,
|
||||||
|
self.image_meta['id'],
|
||||||
|
reimage_reserved=False)
|
||||||
|
self.assertIn("status must be available or error",
|
||||||
|
str(ex))
|
||||||
|
# The other status volume can not be reimage
|
||||||
|
volume.status = 'in-use'
|
||||||
|
volume.save()
|
||||||
|
self.assertEqual(volume.status, 'in-use')
|
||||||
|
ex = self.assertRaises(exception.InvalidVolume,
|
||||||
|
self.volume_api.reimage,
|
||||||
|
self.context, volume, self.image_meta['id'],
|
||||||
|
reimage_reserved=True)
|
||||||
|
self.assertIn("status must be "
|
||||||
|
"available or error or reserved",
|
||||||
|
str(ex))
|
@ -29,6 +29,7 @@ from oslo_utils import excutils
|
|||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
from oslo_utils import timeutils
|
from oslo_utils import timeutils
|
||||||
from oslo_utils import versionutils
|
from oslo_utils import versionutils
|
||||||
|
import webob
|
||||||
|
|
||||||
from cinder.api import common
|
from cinder.api import common
|
||||||
from cinder.common import constants
|
from cinder.common import constants
|
||||||
@ -2529,6 +2530,38 @@ class API(base.Base):
|
|||||||
volume_utils.notify_about_volume_usage(ctxt, volume, "detach.end")
|
volume_utils.notify_about_volume_usage(ctxt, volume, "detach.end")
|
||||||
return volume.volume_attachment
|
return volume.volume_attachment
|
||||||
|
|
||||||
|
def reimage(self, context, volume, image_id, reimage_reserved=False):
|
||||||
|
if volume.status in ['reserved']:
|
||||||
|
context.authorize(vol_action_policy.REIMAGE_RESERVED_POLICY,
|
||||||
|
target_obj=volume)
|
||||||
|
else:
|
||||||
|
context.authorize(vol_action_policy.REIMAGE_POLICY,
|
||||||
|
target_obj=volume)
|
||||||
|
if len(volume.volume_attachment) > 1:
|
||||||
|
msg = _("Cannot re-image a volume which is attached to more than "
|
||||||
|
"one server.")
|
||||||
|
raise webob.exc.HTTPConflict(explanation=msg)
|
||||||
|
# Build required conditions for conditional update
|
||||||
|
expected = {'status': ('available', 'error', 'reserved'
|
||||||
|
) if reimage_reserved else ('available',
|
||||||
|
'error')}
|
||||||
|
values = {'status': 'downloading',
|
||||||
|
'previous_status': volume.model.status}
|
||||||
|
|
||||||
|
result = volume.conditional_update(values, expected)
|
||||||
|
if not result:
|
||||||
|
msg = (_('Volume %(vol_id)s status must be %(statuses)s, but '
|
||||||
|
'current status is %(status)s.') %
|
||||||
|
{'vol_id': volume.id,
|
||||||
|
'statuses': utils.build_or_str(expected['status']),
|
||||||
|
'status': volume.status})
|
||||||
|
raise exception.InvalidVolume(reason=msg)
|
||||||
|
image_meta = self.image_service.show(context, image_id)
|
||||||
|
volume_utils.check_image_metadata(image_meta, volume['size'])
|
||||||
|
self.volume_rpcapi.reimage(context,
|
||||||
|
volume,
|
||||||
|
image_meta)
|
||||||
|
|
||||||
|
|
||||||
class HostAPI(base.Base):
|
class HostAPI(base.Base):
|
||||||
"""Sub-set of the Volume Manager API for managing host operations."""
|
"""Sub-set of the Volume Manager API for managing host operations."""
|
||||||
|
@ -5305,3 +5305,51 @@ class VolumeManager(manager.CleanableManager,
|
|||||||
raise exception.VolumeBackendAPIException(data=err_msg)
|
raise exception.VolumeBackendAPIException(data=err_msg)
|
||||||
|
|
||||||
return {'replication_targets': replication_targets}
|
return {'replication_targets': replication_targets}
|
||||||
|
|
||||||
|
def _refresh_volume_glance_meta(self, context, volume, image_meta):
|
||||||
|
volume_utils.enable_bootable_flag(volume)
|
||||||
|
volume_meta = volume_utils.get_volume_image_metadata(
|
||||||
|
image_meta['id'], image_meta)
|
||||||
|
LOG.debug("Creating volume glance metadata for volume %(volume_id)s"
|
||||||
|
" backed by image %(image_id)s with: %(vol_metadata)s.",
|
||||||
|
{'volume_id': volume.id, 'image_id': image_meta['id'],
|
||||||
|
'vol_metadata': volume_meta})
|
||||||
|
self.db.volume_glance_metadata_delete_by_volume(context, volume.id)
|
||||||
|
self.db.volume_glance_metadata_bulk_create(context, volume.id,
|
||||||
|
volume_meta)
|
||||||
|
|
||||||
|
def reimage(self, context, volume, image_meta):
|
||||||
|
"""Reimage a volume with specific image."""
|
||||||
|
image_id = None
|
||||||
|
try:
|
||||||
|
image_id = image_meta['id']
|
||||||
|
image_service, _ = glance.get_remote_image_service(
|
||||||
|
context, image_meta['id'])
|
||||||
|
image_location = image_service.get_location(context, image_id)
|
||||||
|
|
||||||
|
volume_utils.copy_image_to_volume(self.driver, context, volume,
|
||||||
|
image_meta, image_location,
|
||||||
|
image_service)
|
||||||
|
|
||||||
|
self._refresh_volume_glance_meta(context, volume, image_meta)
|
||||||
|
volume.status = volume.previous_status
|
||||||
|
volume.save()
|
||||||
|
|
||||||
|
if volume.status in ['reserved']:
|
||||||
|
nova_api = compute.API()
|
||||||
|
attachments = volume.volume_attachment
|
||||||
|
instance_uuids = [attachment.instance_uuid
|
||||||
|
for attachment in attachments]
|
||||||
|
nova_api.reimage_volume(context, instance_uuids, volume.id)
|
||||||
|
|
||||||
|
LOG.debug("Re-image %(image_id)s"
|
||||||
|
" to volume %(volume_id)s successfully.",
|
||||||
|
{'image_id': image_id, 'volume_id': volume.id})
|
||||||
|
except Exception:
|
||||||
|
with excutils.save_and_reraise_exception():
|
||||||
|
LOG.error('Failed to re-image volume %(volume_id)s with '
|
||||||
|
'image %(image_id)s.',
|
||||||
|
{'image_id': image_id, 'volume_id': volume.id})
|
||||||
|
volume.previous_status = volume.status
|
||||||
|
volume.status = 'error'
|
||||||
|
volume.save()
|
||||||
|
@ -137,9 +137,10 @@ class VolumeAPI(rpc.RPCAPI):
|
|||||||
3.15 - Add revert_to_snapshot method
|
3.15 - Add revert_to_snapshot method
|
||||||
3.16 - Add no_snapshots to accept_transfer method
|
3.16 - Add no_snapshots to accept_transfer method
|
||||||
3.17 - Make get_backup_device a cast (async)
|
3.17 - Make get_backup_device a cast (async)
|
||||||
|
3.18 - Add reimage method
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RPC_API_VERSION = '3.17'
|
RPC_API_VERSION = '3.18'
|
||||||
RPC_DEFAULT_VERSION = '3.0'
|
RPC_DEFAULT_VERSION = '3.0'
|
||||||
TOPIC = constants.VOLUME_TOPIC
|
TOPIC = constants.VOLUME_TOPIC
|
||||||
BINARY = constants.VOLUME_BINARY
|
BINARY = constants.VOLUME_BINARY
|
||||||
@ -533,3 +534,8 @@ class VolumeAPI(rpc.RPCAPI):
|
|||||||
cctxt = self._get_cctxt(group.service_topic_queue, version='3.14')
|
cctxt = self._get_cctxt(group.service_topic_queue, version='3.14')
|
||||||
return cctxt.call(ctxt, 'list_replication_targets',
|
return cctxt.call(ctxt, 'list_replication_targets',
|
||||||
group=group)
|
group=group)
|
||||||
|
|
||||||
|
@rpc.assert_min_rpc_version('3.18')
|
||||||
|
def reimage(self, ctxt, volume, image_meta):
|
||||||
|
cctxt = self._get_cctxt(volume.service_topic_queue, version='3.18')
|
||||||
|
cctxt.cast(ctxt, 'reimage', volume=volume, image_meta=image_meta)
|
||||||
|
@ -0,0 +1,40 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Add microversion 3.68 to support ability to re-image a volume with a
|
||||||
|
specific image. Specify the ``os-reimage`` action in the request body.
|
||||||
|
|
||||||
|
The 'available' and 'error' volume can be re-imaged directly, and the
|
||||||
|
'reserved' volume can only be re-imaged when the `reimage_reserved`
|
||||||
|
parameter is set to 'true'. When reimaging a volume, the volume state
|
||||||
|
will be changed to ``downloading`` first.
|
||||||
|
|
||||||
|
Note that this is a destructive action, that is, all data currently
|
||||||
|
contained in a volume is destroyed when the volume is re-imaged.
|
||||||
|
|
||||||
|
Two new policies are introduced to govern this functionality:
|
||||||
|
|
||||||
|
* ``REIMAGE_POLICY`` - users who satisfy this policy may re-image a volume
|
||||||
|
in status ``available`` or ``error``
|
||||||
|
* ``REIMAGE_RESERVED_POLICY`` - users who satisfy this policy may re-image
|
||||||
|
a volume in status ``reserved``
|
||||||
|
|
||||||
|
The default setting for both policies allow an administrator or the volume
|
||||||
|
owner to perform the associated action. See the `Policy configuration
|
||||||
|
<https://docs.openstack.org/cinder/yoga/configuration/block-storage/policy.html>`_
|
||||||
|
documentation in the `Cinder Service Configuration` guide for details.
|
||||||
|
|
||||||
|
upgrade:
|
||||||
|
- |
|
||||||
|
Two new policies are introduced to govern the volume reimage functionality
|
||||||
|
introduced with microversion 3.68:
|
||||||
|
|
||||||
|
* ``REIMAGE_POLICY`` - users who satisfy this policy may re-image a volume
|
||||||
|
in status ``available`` or ``error``
|
||||||
|
* ``REIMAGE_RESERVED_POLICY`` - users who satisfy this policy may re-image
|
||||||
|
a volume in status ``reserved``
|
||||||
|
|
||||||
|
The default setting for both policies allow an administrator or the volume
|
||||||
|
owner to perform the associated action. See the `Policy configuration
|
||||||
|
<https://docs.openstack.org/cinder/yoga/configuration/block-storage/policy.html>`_
|
||||||
|
documentation in the `Cinder Service Configuration` guide for details.
|
Loading…
x
Reference in New Issue
Block a user