diff --git a/api-ref/source/v3/parameters.yaml b/api-ref/source/v3/parameters.yaml index f887e4b3b0e..50bd3a1714b 100644 --- a/api-ref/source/v3/parameters.yaml +++ b/api-ref/source/v3/parameters.yaml @@ -2134,6 +2134,13 @@ os-migrate_volume_completion: in: body required: true type: object +os-reimage: + description: | + The ``os-reimage`` action. + in: body + required: true + type: object + min_version: 3.68 os-reserve: description: | The ``os-reserve`` action. @@ -2479,6 +2486,15 @@ reference: in: body required: true 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: description: | Removes volume type access from a project. diff --git a/api-ref/source/v3/samples/versions/version-show-response.json b/api-ref/source/v3/samples/versions/version-show-response.json index dc4d062341e..6551cf8bc5e 100644 --- a/api-ref/source/v3/samples/versions/version-show-response.json +++ b/api-ref/source/v3/samples/versions/version-show-response.json @@ -21,8 +21,8 @@ ], "min_version": "3.0", "status": "CURRENT", - "updated": "2021-12-16T00:00:00Z", - "version": "3.67" + "updated": "2022-03-30T00:00:00Z", + "version": "3.68" } ] } diff --git a/api-ref/source/v3/samples/versions/versions-response.json b/api-ref/source/v3/samples/versions/versions-response.json index 59c049bfa00..2569c9018d0 100644 --- a/api-ref/source/v3/samples/versions/versions-response.json +++ b/api-ref/source/v3/samples/versions/versions-response.json @@ -21,8 +21,8 @@ ], "min_version": "3.0", "status": "CURRENT", - "updated": "2021-12-16T00:00:00Z", - "version": "3.67" + "updated": "2022-03-30T00:00:00Z", + "version": "3.68" } ] } diff --git a/api-ref/source/v3/samples/volume-os-reimage-request.json b/api-ref/source/v3/samples/volume-os-reimage-request.json new file mode 100644 index 00000000000..176c91af9fc --- /dev/null +++ b/api-ref/source/v3/samples/volume-os-reimage-request.json @@ -0,0 +1,6 @@ +{ + "os-reimage": { + "image_id": "71543ced-a8af-45b6-a5c4-a46282108a90", + "reimage_reserved": false + } +} diff --git a/api-ref/source/v3/volumes-v3-volumes-actions.inc b/api-ref/source/v3/volumes-v3-volumes-actions.inc index 597d623563d..808dcda8d87 100644 --- a/api-ref/source/v3/volumes-v3-volumes-actions.inc +++ b/api-ref/source/v3/volumes-v3-volumes-actions.inc @@ -973,3 +973,44 @@ Request Example .. literalinclude:: ./samples/volume-readonly-update-request.json :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 diff --git a/cinder/api/contrib/volume_actions.py b/cinder/api/contrib/volume_actions.py index da09d07f3eb..8b80a5edc02 100644 --- a/cinder/api/contrib/volume_actions.py +++ b/cinder/api/contrib/volume_actions.py @@ -326,6 +326,26 @@ class VolumeActionsController(wsgi.Controller): 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): """Enable volume actions.""" diff --git a/cinder/api/microversions.py b/cinder/api/microversions.py index e405e7fecd7..c5900e60cac 100644 --- a/cinder/api/microversions.py +++ b/cinder/api/microversions.py @@ -173,6 +173,8 @@ SNAPSHOT_IN_USE = '3.66' PROJECT_ID_OPTIONAL_IN_URL = '3.67' +SUPPORT_REIMAGE_VOLUME = '3.68' + def get_mv_header(version): """Gets a formatted HTTP microversion header. diff --git a/cinder/api/openstack/api_version_request.py b/cinder/api/openstack/api_version_request.py index d7395b0e43e..65d9697efdf 100644 --- a/cinder/api/openstack/api_version_request.py +++ b/cinder/api/openstack/api_version_request.py @@ -153,13 +153,14 @@ REST_API_VERSION_HISTORY = """ operation. * 3.66 - Allow snapshotting in-use volumes without force flag. * 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 default api version request is defined to be the # minimum version of the API supported. _MIN_API_VERSION = "3.0" -_MAX_API_VERSION = "3.67" +_MAX_API_VERSION = "3.68" UPDATED = "2021-11-02T00:00:00Z" diff --git a/cinder/api/openstack/rest_api_version_history.rst b/cinder/api/openstack/rest_api_version_history.rst index bfba94e79b4..63aa5668090 100644 --- a/cinder/api/openstack/rest_api_version_history.rst +++ b/cinder/api/openstack/rest_api_version_history.rst @@ -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 with the cinder service as system or domain scoped users, a project_id should 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. diff --git a/cinder/api/schemas/volume_actions.py b/cinder/api/schemas/volume_actions.py index a7605249a77..598428e0132 100644 --- a/cinder/api/schemas/volume_actions.py +++ b/cinder/api/schemas/volume_actions.py @@ -202,3 +202,20 @@ volume_readonly_update = { 'required': ['os-update_readonly_flag'], '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, +} diff --git a/cinder/compute/nova.py b/cinder/compute/nova.py index b13c7be38f5..aff75d6c00a 100644 --- a/cinder/compute/nova.py +++ b/cinder/compute/nova.py @@ -143,6 +143,11 @@ class API(base.Base): 'server_uuid': server_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): nova = novaclient(context, privileged_user=True, api_version=api_version) @@ -219,3 +224,16 @@ class API(base.Base): resource_uuid=volume_id, detail=message_field.Detail.NOTIFY_COMPUTE_SERVICE_FAILED) 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 diff --git a/cinder/policies/volume_actions.py b/cinder/policies/volume_actions.py index a643e2f1999..ceece819623 100644 --- a/cinder/policies/volume_actions.py +++ b/cinder/policies/volume_actions.py @@ -39,6 +39,8 @@ RESERVE_POLICY = "volume_extension:volume_actions:reserve" ROLL_DETACHING_POLICY = "volume_extension:volume_actions:roll_detaching" TERMINATE_POLICY = "volume_extension:volume_actions:terminate_connection" INITIALIZE_POLICY = "volume_extension:volume_actions:initialize_connection" +REIMAGE_POLICY = "volume:reimage" +REIMAGE_RESERVED_POLICY = "volume:reimage_reserved" deprecated_extend_policy = base.CinderDeprecatedRule( name=EXTEND_POLICY, @@ -323,6 +325,26 @@ volume_action_policies = [ ], 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)' + } + ]), ] diff --git a/cinder/tests/unit/api/contrib/test_volume_actions.py b/cinder/tests/unit/api/contrib/test_volume_actions.py index 6573f2a1ff2..db7d4382568 100644 --- a/cinder/tests/unit/api/contrib/test_volume_actions.py +++ b/cinder/tests/unit/api/contrib/test_volume_actions.py @@ -1552,3 +1552,70 @@ class VolumeImageActionsTest(test.TestCase): vol_db = objects.Volume.get_by_id(self.context, volume.id) self.assertEqual('uploading', vol_db.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) diff --git a/cinder/tests/unit/volume/test_rpcapi.py b/cinder/tests/unit/volume/test_rpcapi.py index 32169e59655..9840738cbb0 100644 --- a/cinder/tests/unit/volume/test_rpcapi.py +++ b/cinder/tests/unit/volume/test_rpcapi.py @@ -676,3 +676,12 @@ class VolumeRPCAPITestCase(test.RPCAPITestCase): server=self.fake_group.host, group=self.fake_group, 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') diff --git a/cinder/tests/unit/volume/test_volume_reimage.py b/cinder/tests/unit/volume/test_volume_reimage.py new file mode 100644 index 00000000000..b37380cc070 --- /dev/null +++ b/cinder/tests/unit/volume/test_volume_reimage.py @@ -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)) diff --git a/cinder/volume/api.py b/cinder/volume/api.py index f2e6429188d..b65816b0750 100644 --- a/cinder/volume/api.py +++ b/cinder/volume/api.py @@ -29,6 +29,7 @@ from oslo_utils import excutils from oslo_utils import strutils from oslo_utils import timeutils from oslo_utils import versionutils +import webob from cinder.api import common from cinder.common import constants @@ -2529,6 +2530,38 @@ class API(base.Base): volume_utils.notify_about_volume_usage(ctxt, volume, "detach.end") 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): """Sub-set of the Volume Manager API for managing host operations.""" diff --git a/cinder/volume/manager.py b/cinder/volume/manager.py index 78e29e7234d..ee582750c60 100644 --- a/cinder/volume/manager.py +++ b/cinder/volume/manager.py @@ -5305,3 +5305,51 @@ class VolumeManager(manager.CleanableManager, raise exception.VolumeBackendAPIException(data=err_msg) 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() diff --git a/cinder/volume/rpcapi.py b/cinder/volume/rpcapi.py index 03493f9dfcd..a7d0d12845a 100644 --- a/cinder/volume/rpcapi.py +++ b/cinder/volume/rpcapi.py @@ -137,9 +137,10 @@ class VolumeAPI(rpc.RPCAPI): 3.15 - Add revert_to_snapshot method 3.16 - Add no_snapshots to accept_transfer method 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' TOPIC = constants.VOLUME_TOPIC BINARY = constants.VOLUME_BINARY @@ -533,3 +534,8 @@ class VolumeAPI(rpc.RPCAPI): cctxt = self._get_cctxt(group.service_topic_queue, version='3.14') return cctxt.call(ctxt, 'list_replication_targets', 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) diff --git a/releasenotes/notes/add-volume-re-image-api-6f02dcefd4975a66.yaml b/releasenotes/notes/add-volume-re-image-api-6f02dcefd4975a66.yaml new file mode 100644 index 00000000000..802ef269962 --- /dev/null +++ b/releasenotes/notes/add-volume-re-image-api-6f02dcefd4975a66.yaml @@ -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 + `_ + 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 + `_ + documentation in the `Cinder Service Configuration` guide for details.