Add the os-extend_volume_completion volume action
Split off the finalization part of the volume manager's extend_volume method and make it externally callable as the new os-extend_volume_completion admin volume action. This is the first part of a feature that will allow volume drivers to rely on feedback from Nova when extending attached volumes, allowing e.g. NFS-based drivers to support online extend. See the linked blueprint for details. Implements: bp extend-volume-completion-action Change-Id: I4aaa5da1ad67a948102c498483de318bd245d86b
This commit is contained in:
parent
015262b954
commit
2a1a0bc3e2
@ -1400,6 +1400,12 @@ event_id:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
|
extend_completion_error:
|
||||||
|
description: |
|
||||||
|
Used to indicate that the extend operation has failed outside of cinder.
|
||||||
|
in: body
|
||||||
|
required: false
|
||||||
|
type: boolean
|
||||||
extra_info:
|
extra_info:
|
||||||
description: |
|
description: |
|
||||||
More information about the resource.
|
More information about the resource.
|
||||||
@ -2358,6 +2364,12 @@ os-extend:
|
|||||||
in: body
|
in: body
|
||||||
required: true
|
required: true
|
||||||
type: object
|
type: object
|
||||||
|
os-extend_volume_completion:
|
||||||
|
description: |
|
||||||
|
The ``os-extend_volume_completion`` action.
|
||||||
|
in: body
|
||||||
|
required: true
|
||||||
|
type: object
|
||||||
os-force_delete:
|
os-force_delete:
|
||||||
description: |
|
description: |
|
||||||
The ``os-force_delete`` action.
|
The ``os-force_delete`` action.
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
],
|
],
|
||||||
"min_version": "3.0",
|
"min_version": "3.0",
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"updated": "2022-08-31T00:00:00Z",
|
"updated": "2023-08-31T00:00:00Z",
|
||||||
"version": "3.70"
|
"version": "3.71"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
"min_version": "3.0",
|
"min_version": "3.0",
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"updated": "2022-08-31T00:00:00Z",
|
"updated": "2022-08-31T00:00:00Z",
|
||||||
"version": "3.70"
|
"version": "3.71"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"os-extend_volume_completion": {
|
||||||
|
"error": false
|
||||||
|
}
|
||||||
|
}
|
@ -77,6 +77,55 @@ Request Example
|
|||||||
:language: javascript
|
:language: javascript
|
||||||
|
|
||||||
|
|
||||||
|
Complete extending a volume
|
||||||
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. rest_method:: POST /v3/{project_id}/volumes/{volume_id}/action
|
||||||
|
|
||||||
|
Specify the ``os-extend_volume_completion`` action in the request body.
|
||||||
|
|
||||||
|
Complete extending an attached volume that has been left in status
|
||||||
|
``extending`` after notifying the compute agent.
|
||||||
|
Depending on the value of the ``error`` parameter, the extend operation
|
||||||
|
will be either rolled back or finalized.
|
||||||
|
|
||||||
|
**Preconditions**
|
||||||
|
|
||||||
|
* The volume must have the status ``extending``.
|
||||||
|
* The volume's admin metadata must contain a set of keys indicating that
|
||||||
|
Cinder was waiting for external feedback on the success of the operation.
|
||||||
|
|
||||||
|
**Asynchronous Postconditions**
|
||||||
|
|
||||||
|
If the ``error`` parameter is ``false`` or missing, and the extend operation
|
||||||
|
was successfully finalized, the volume status will be ``in-use``.
|
||||||
|
Otherwise, the volume status will be ``error_extending``.
|
||||||
|
|
||||||
|
Response codes
|
||||||
|
--------------
|
||||||
|
|
||||||
|
.. rest_status_code:: success ../status.yaml
|
||||||
|
|
||||||
|
- 202
|
||||||
|
|
||||||
|
|
||||||
|
Request
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. rest_parameters:: parameters.yaml
|
||||||
|
|
||||||
|
- volume_id: volume_id_path
|
||||||
|
- project_id: project_id_path
|
||||||
|
- os-extend_volume_completion: os-extend_volume_completion
|
||||||
|
- error: extend_completion_error
|
||||||
|
|
||||||
|
Request Example
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. literalinclude:: ./samples/volume-os-extend_volume_completion-request.json
|
||||||
|
:language: javascript
|
||||||
|
|
||||||
|
|
||||||
Reset a volume's statuses
|
Reset a volume's statuses
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
@ -283,6 +283,19 @@ class VolumeAdminController(AdminController):
|
|||||||
new_volume, error)
|
new_volume, error)
|
||||||
return {'save_volume_id': ret}
|
return {'save_volume_id': ret}
|
||||||
|
|
||||||
|
@wsgi.response(HTTPStatus.ACCEPTED)
|
||||||
|
@wsgi.action('os-extend_volume_completion')
|
||||||
|
@validation.schema(admin_actions.extend_volume_completion)
|
||||||
|
def _extend_volume_completion(self, req, id, body):
|
||||||
|
"""Complete an in-progress extend operation."""
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
# Not found exception will be handled at the wsgi level
|
||||||
|
volume = self._get(context, id)
|
||||||
|
self.authorize(context, 'extend_volume_completion', target_obj=volume)
|
||||||
|
params = body['os-extend_volume_completion']
|
||||||
|
error = params.get('error', False)
|
||||||
|
self.volume_api.extend_volume_completion(context, volume, error)
|
||||||
|
|
||||||
|
|
||||||
class SnapshotAdminController(AdminController):
|
class SnapshotAdminController(AdminController):
|
||||||
"""AdminController for Snapshots."""
|
"""AdminController for Snapshots."""
|
||||||
|
@ -179,6 +179,8 @@ SHARED_TARGETS_TRISTATE = '3.69'
|
|||||||
|
|
||||||
TRANSFER_ENCRYPTED_VOLUME = '3.70'
|
TRANSFER_ENCRYPTED_VOLUME = '3.70'
|
||||||
|
|
||||||
|
EXTEND_VOLUME_COMPLETION = '3.71'
|
||||||
|
|
||||||
|
|
||||||
def get_mv_header(version):
|
def get_mv_header(version):
|
||||||
"""Gets a formatted HTTP microversion header.
|
"""Gets a formatted HTTP microversion header.
|
||||||
|
@ -156,14 +156,15 @@ REST_API_VERSION_HISTORY = """
|
|||||||
* 3.68 - Support re-image volume
|
* 3.68 - Support re-image volume
|
||||||
* 3.69 - Allow null value for shared_targets
|
* 3.69 - Allow null value for shared_targets
|
||||||
* 3.70 - Support encrypted volume transfers
|
* 3.70 - Support encrypted volume transfers
|
||||||
|
* 3.71 - Support 'os-extend_volume_completion' volume action
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 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.70"
|
_MAX_API_VERSION = "3.71"
|
||||||
UPDATED = "2022-08-31T00:00:00Z"
|
UPDATED = "2023-08-31T00:00:00Z"
|
||||||
|
|
||||||
|
|
||||||
# NOTE(cyeoh): min and max versions declared as functions so we can
|
# NOTE(cyeoh): min and max versions declared as functions so we can
|
||||||
|
@ -535,3 +535,9 @@ following meanings:
|
|||||||
Add the ability to transfer encrypted volumes and their snapshots. The feature
|
Add the ability to transfer encrypted volumes and their snapshots. The feature
|
||||||
removes a prior restriction on transferring encrypted volumes. Otherwise, the
|
removes a prior restriction on transferring encrypted volumes. Otherwise, the
|
||||||
API request and response schema are unchanged.
|
API request and response schema are unchanged.
|
||||||
|
|
||||||
|
3.71
|
||||||
|
----
|
||||||
|
Add the ``os-extend_volume_completion`` volume action, which Nova can use
|
||||||
|
to notify Cinder of success and error when handling a ``volume-extended``
|
||||||
|
external server event.
|
||||||
|
@ -119,6 +119,22 @@ migrate_volume_completion = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extend_volume_completion = {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'os-extend_volume_completion': {
|
||||||
|
'type': 'object',
|
||||||
|
'properties': {
|
||||||
|
'error': {'type': ['string', 'null', 'boolean']},
|
||||||
|
},
|
||||||
|
'additionalProperties': False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
'required': ['os-extend_volume_completion'],
|
||||||
|
'additionalProperties': False,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
reset_status_backup = {
|
reset_status_backup = {
|
||||||
'type': 'object',
|
'type': 'object',
|
||||||
'properties': {
|
'properties': {
|
||||||
|
@ -20,6 +20,8 @@ from cinder.policies import base
|
|||||||
|
|
||||||
EXTEND_POLICY = "volume:extend"
|
EXTEND_POLICY = "volume:extend"
|
||||||
EXTEND_ATTACHED_POLICY = "volume:extend_attached_volume"
|
EXTEND_ATTACHED_POLICY = "volume:extend_attached_volume"
|
||||||
|
EXTEND_COMPLETE_POLICY = \
|
||||||
|
"volume_extension:volume_admin_actions:extend_volume_completion"
|
||||||
REVERT_POLICY = "volume:revert_to_snapshot"
|
REVERT_POLICY = "volume:revert_to_snapshot"
|
||||||
RESET_STATUS = "volume_extension:volume_admin_actions:reset_status"
|
RESET_STATUS = "volume_extension:volume_admin_actions:reset_status"
|
||||||
RETYPE_POLICY = "volume:retype"
|
RETYPE_POLICY = "volume:retype"
|
||||||
@ -124,6 +126,16 @@ volume_action_policies = [
|
|||||||
],
|
],
|
||||||
deprecated_rule=deprecated_extend_attached_policy,
|
deprecated_rule=deprecated_extend_attached_policy,
|
||||||
),
|
),
|
||||||
|
policy.DocumentedRuleDefault(
|
||||||
|
name=EXTEND_COMPLETE_POLICY,
|
||||||
|
check_str=base.RULE_ADMIN_API,
|
||||||
|
description="Complete a volume extend operation.",
|
||||||
|
operations=[{
|
||||||
|
'method': 'POST',
|
||||||
|
'path':
|
||||||
|
'/volumes/{volume_id}/action (os-extend_volume_completion)'}
|
||||||
|
],
|
||||||
|
),
|
||||||
policy.DocumentedRuleDefault(
|
policy.DocumentedRuleDefault(
|
||||||
name=REVERT_POLICY,
|
name=REVERT_POLICY,
|
||||||
check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER,
|
check_str=base.SYSTEM_ADMIN_OR_PROJECT_MEMBER,
|
||||||
|
@ -1031,6 +1031,88 @@ class AdminActionsTest(BaseAdminTest):
|
|||||||
res = req.get_response(app())
|
res = req.get_response(app())
|
||||||
self.assertEqual(HTTPStatus.METHOD_NOT_ALLOWED, res.status_int)
|
self.assertEqual(HTTPStatus.METHOD_NOT_ALLOWED, res.status_int)
|
||||||
|
|
||||||
|
def _extend_volume_comp_exec(self, ctx, volume, error, expected_status):
|
||||||
|
req = webob.Request.blank('/v3/%s/volumes/%s/action' % (
|
||||||
|
fake.PROJECT_ID, volume['id']))
|
||||||
|
req.method = 'POST'
|
||||||
|
req.headers['content-type'] = 'application/json'
|
||||||
|
body = {'os-extend_volume_completion': {'error': error}}
|
||||||
|
req.body = jsonutils.dump_as_bytes(body)
|
||||||
|
req.environ['cinder.context'] = ctx
|
||||||
|
resp = req.get_response(app())
|
||||||
|
# verify status
|
||||||
|
self.assertEqual(expected_status, resp.status_int)
|
||||||
|
|
||||||
|
def test_extend_volume_comp_accepted_success(self):
|
||||||
|
volume = self._create_volume(
|
||||||
|
self.ctx,
|
||||||
|
{'size': 1,
|
||||||
|
'status': 'extending',
|
||||||
|
'admin_metadata': {
|
||||||
|
'extend_new_size': '2',
|
||||||
|
'extend_reservations':
|
||||||
|
'["563e9e70-5f46-4265-9a92-f7bca28d896c"]'
|
||||||
|
}})
|
||||||
|
|
||||||
|
expected_status = HTTPStatus.ACCEPTED
|
||||||
|
self._extend_volume_comp_exec(self.ctx, volume, False,
|
||||||
|
expected_status)
|
||||||
|
|
||||||
|
def test_extend_volume_comp_accepted_failure(self):
|
||||||
|
volume = self._create_volume(
|
||||||
|
self.ctx,
|
||||||
|
{'size': 1,
|
||||||
|
'status': 'extending',
|
||||||
|
'admin_metadata': {
|
||||||
|
'extend_new_size': '2',
|
||||||
|
'extend_reservations':
|
||||||
|
'["563e9e70-5f46-4265-9a92-f7bca28d896c"]'
|
||||||
|
}})
|
||||||
|
|
||||||
|
expected_status = HTTPStatus.ACCEPTED
|
||||||
|
self._extend_volume_comp_exec(self.ctx, volume, True,
|
||||||
|
expected_status)
|
||||||
|
|
||||||
|
def test_extend_volume_comp_wrong_status(self):
|
||||||
|
volume = self._create_volume(
|
||||||
|
self.ctx,
|
||||||
|
{'size': 1,
|
||||||
|
'status': 'in-use',
|
||||||
|
'admin_metadata': {
|
||||||
|
'extend_new_size': '2',
|
||||||
|
'extend_reservations':
|
||||||
|
'["563e9e70-5f46-4265-9a92-f7bca28d896c"]'
|
||||||
|
}})
|
||||||
|
|
||||||
|
expected_status = HTTPStatus.BAD_REQUEST
|
||||||
|
self._extend_volume_comp_exec(self.ctx, volume, False,
|
||||||
|
expected_status)
|
||||||
|
|
||||||
|
def test_extend_volume_comp_missing_metadata(self):
|
||||||
|
volume = self._create_volume(
|
||||||
|
self.ctx,
|
||||||
|
{'size': 1,
|
||||||
|
'status': 'extending'})
|
||||||
|
|
||||||
|
expected_status = HTTPStatus.BAD_REQUEST
|
||||||
|
self._extend_volume_comp_exec(self.ctx, volume, False,
|
||||||
|
expected_status)
|
||||||
|
|
||||||
|
def test_extend_volume_comp_wrong_size(self):
|
||||||
|
volume = self._create_volume(
|
||||||
|
self.ctx,
|
||||||
|
{'size': 2,
|
||||||
|
'status': 'extending',
|
||||||
|
'admin_metadata': {
|
||||||
|
'extend_new_size': '1',
|
||||||
|
'extend_reservations':
|
||||||
|
'["563e9e70-5f46-4265-9a92-f7bca28d896c"]'
|
||||||
|
}})
|
||||||
|
|
||||||
|
expected_status = HTTPStatus.BAD_REQUEST
|
||||||
|
self._extend_volume_comp_exec(self.ctx, volume, False,
|
||||||
|
expected_status)
|
||||||
|
|
||||||
|
|
||||||
class AdminActionsAttachDetachTest(BaseAdminTest):
|
class AdminActionsAttachDetachTest(BaseAdminTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
@ -29,6 +29,7 @@ import eventlet
|
|||||||
import os_brick.initiator.connectors.iscsi
|
import os_brick.initiator.connectors.iscsi
|
||||||
from oslo_concurrency import processutils
|
from oslo_concurrency import processutils
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
|
from oslo_utils.fixture import uuidsentinel as uuids
|
||||||
from oslo_utils import imageutils
|
from oslo_utils import imageutils
|
||||||
from taskflow.engines.action_engine import engine
|
from taskflow.engines.action_engine import engine
|
||||||
|
|
||||||
@ -2804,7 +2805,7 @@ class VolumeTestCase(base.BaseVolumeTestCase):
|
|||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
self.volume.message_api, 'create') as mock_create:
|
self.volume.message_api, 'create') as mock_create:
|
||||||
volume['status'] = 'extending'
|
volume['status'] = 'extending'
|
||||||
self.volume.extend_volume(self.context, volume, '4',
|
self.volume.extend_volume(self.context, volume, 4,
|
||||||
fake_reservations)
|
fake_reservations)
|
||||||
volume.refresh()
|
volume.refresh()
|
||||||
self.assertEqual(2, volume.size)
|
self.assertEqual(2, volume.size)
|
||||||
@ -2832,7 +2833,7 @@ class VolumeTestCase(base.BaseVolumeTestCase):
|
|||||||
with mock.patch.object(QUOTAS, 'commit') as quotas_commit:
|
with mock.patch.object(QUOTAS, 'commit') as quotas_commit:
|
||||||
extend_volume.return_value = fake_extend
|
extend_volume.return_value = fake_extend
|
||||||
volume.status = 'extending'
|
volume.status = 'extending'
|
||||||
self.volume.extend_volume(self.context, volume, '4',
|
self.volume.extend_volume(self.context, volume, 4,
|
||||||
fake_reservations)
|
fake_reservations)
|
||||||
volume.refresh()
|
volume.refresh()
|
||||||
self.assertEqual(4, volume.size)
|
self.assertEqual(4, volume.size)
|
||||||
@ -2946,6 +2947,77 @@ class VolumeTestCase(base.BaseVolumeTestCase):
|
|||||||
|
|
||||||
self.assertEqual(100, volumes_reserved)
|
self.assertEqual(100, volumes_reserved)
|
||||||
|
|
||||||
|
@mock.patch('cinder.compute.nova.API.extend_volume')
|
||||||
|
@mock.patch('cinder.volume.manager.VolumeManager.'
|
||||||
|
'extend_volume_completion')
|
||||||
|
def test_extend_volume_no_wait_for_nova_available(self,
|
||||||
|
extend_completion,
|
||||||
|
nova_extend):
|
||||||
|
volume = tests_utils.create_volume(self.context, size=2,
|
||||||
|
status='extending')
|
||||||
|
|
||||||
|
with mock.patch.object(self.volume.driver, 'extend_volume'):
|
||||||
|
self.volume.extend_volume(self.context, volume, 4,
|
||||||
|
[uuids.reservation])
|
||||||
|
|
||||||
|
extend_completion.assert_called_once_with(self.context,
|
||||||
|
volume,
|
||||||
|
4,
|
||||||
|
[uuids.reservation],
|
||||||
|
error=False)
|
||||||
|
nova_extend.assert_not_called()
|
||||||
|
self.assertNotIn('extend_new_size', volume.admin_metadata)
|
||||||
|
self.assertNotIn('extend_reservations', volume.admin_metadata)
|
||||||
|
|
||||||
|
@mock.patch('cinder.compute.nova.API.extend_volume')
|
||||||
|
@mock.patch('cinder.volume.manager.VolumeManager.'
|
||||||
|
'extend_volume_completion')
|
||||||
|
def test_extend_volume_no_wait_for_nova_attached(self,
|
||||||
|
extend_completion,
|
||||||
|
nova_extend):
|
||||||
|
volume = tests_utils.create_volume(self.context, size=2)
|
||||||
|
tests_utils.attach_volume(self.context, volume.id, uuids.instance,
|
||||||
|
'fake-host', '/dev/vda')
|
||||||
|
db.volume_update(self.context, volume.id, {'status': 'extending'})
|
||||||
|
volume.refresh()
|
||||||
|
|
||||||
|
with mock.patch.object(self.volume.driver, 'extend_volume'):
|
||||||
|
self.volume.extend_volume(self.context, volume, 4,
|
||||||
|
[uuids.reservation])
|
||||||
|
|
||||||
|
extend_completion.assert_called_once_with(self.context,
|
||||||
|
volume,
|
||||||
|
4,
|
||||||
|
[uuids.reservation],
|
||||||
|
error=False)
|
||||||
|
nova_extend.assert_called_once_with(self.context,
|
||||||
|
[uuids.instance],
|
||||||
|
volume.id)
|
||||||
|
self.assertNotIn('extend_new_size', volume.admin_metadata)
|
||||||
|
self.assertNotIn('extend_reservations', volume.admin_metadata)
|
||||||
|
|
||||||
|
@mock.patch('cinder.compute.nova.API.extend_volume', return_value=False)
|
||||||
|
@mock.patch('cinder.volume.manager.VolumeManager.'
|
||||||
|
'extend_volume_completion')
|
||||||
|
def test_extend_volume_no_wait_for_nova_fail_to_send(self,
|
||||||
|
extend_completion,
|
||||||
|
nova_extend):
|
||||||
|
volume = tests_utils.create_volume(self.context, size=2)
|
||||||
|
tests_utils.attach_volume(self.context, volume.id, uuids.instance,
|
||||||
|
'fake-host', '/dev/vda')
|
||||||
|
db.volume_update(self.context, volume.id, {'status': 'extending'})
|
||||||
|
volume.refresh()
|
||||||
|
|
||||||
|
with mock.patch.object(self.volume.driver, 'extend_volume'):
|
||||||
|
self.volume.extend_volume(self.context, volume, 4,
|
||||||
|
[uuids.reservation])
|
||||||
|
|
||||||
|
extend_completion.assert_called_once_with(self.context,
|
||||||
|
volume,
|
||||||
|
4,
|
||||||
|
[uuids.reservation],
|
||||||
|
error=False)
|
||||||
|
|
||||||
def test_create_volume_from_sourcevol(self):
|
def test_create_volume_from_sourcevol(self):
|
||||||
"""Test volume can be created from a source volume."""
|
"""Test volume can be created from a source volume."""
|
||||||
def fake_create_cloned_volume(volume, src_vref):
|
def fake_create_cloned_volume(volume, src_vref):
|
||||||
|
@ -26,6 +26,7 @@ from typing import (Any, DefaultDict, Iterable, Optional, Union)
|
|||||||
from castellan import key_manager
|
from castellan import key_manager
|
||||||
from oslo_config import cfg
|
from oslo_config import cfg
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_serialization import jsonutils
|
||||||
from oslo_utils import excutils
|
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
|
||||||
@ -1675,6 +1676,48 @@ class API(base.Base):
|
|||||||
target_obj=volume)
|
target_obj=volume)
|
||||||
self._extend(context, volume, new_size, attached=True)
|
self._extend(context, volume, new_size, attached=True)
|
||||||
|
|
||||||
|
def extend_volume_completion(self,
|
||||||
|
context: context.RequestContext,
|
||||||
|
volume: objects.Volume,
|
||||||
|
error: bool):
|
||||||
|
context.authorize(vol_action_policy.EXTEND_COMPLETE_POLICY,
|
||||||
|
target_obj=volume)
|
||||||
|
|
||||||
|
if volume.status != 'extending':
|
||||||
|
msg = _('Volume is not being extended.')
|
||||||
|
raise exception.InvalidVolume(reason=msg)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with volume.obj_as_admin():
|
||||||
|
new_size = int(volume.admin_metadata['extend_new_size'])
|
||||||
|
reservations = jsonutils.loads(
|
||||||
|
volume.admin_metadata['extend_reservations'])
|
||||||
|
except (KeyError, ValueError, jsonutils.json.decoder.JSONDecodeError):
|
||||||
|
msg = _('Required volume admin metadata is malformed or missing.')
|
||||||
|
raise exception.InvalidVolume(reason=msg)
|
||||||
|
|
||||||
|
if new_size <= volume.size:
|
||||||
|
msg = _('The target volume size provided in volume admin metadata '
|
||||||
|
'%(size)s is smaller or equal to the current volume size.'
|
||||||
|
% volume.admin_metadata["extend_new_size"])
|
||||||
|
raise exception.InvalidVolume(reason=msg)
|
||||||
|
|
||||||
|
if type(reservations) is not list:
|
||||||
|
msg = _('The stored quota reservations for extending the volume '
|
||||||
|
'must be in a list format.')
|
||||||
|
raise exception.InvalidVolume(reason=msg)
|
||||||
|
|
||||||
|
with volume.obj_as_admin():
|
||||||
|
del volume.admin_metadata['extend_new_size']
|
||||||
|
del volume.admin_metadata['extend_reservations']
|
||||||
|
volume.save()
|
||||||
|
|
||||||
|
self.volume_rpcapi.extend_volume_completion(context, volume, new_size,
|
||||||
|
reservations, error)
|
||||||
|
|
||||||
|
LOG.info("Extend volume completion issued successfully.",
|
||||||
|
resource=volume)
|
||||||
|
|
||||||
def migrate_volume(self,
|
def migrate_volume(self,
|
||||||
context: context.RequestContext,
|
context: context.RequestContext,
|
||||||
volume: objects.Volume,
|
volume: objects.Volume,
|
||||||
|
@ -2910,8 +2910,6 @@ class VolumeManager(manager.CleanableManager,
|
|||||||
volume.status = 'error_extending'
|
volume.status = 'error_extending'
|
||||||
volume.save()
|
volume.save()
|
||||||
|
|
||||||
project_id = volume.project_id
|
|
||||||
size_increase = (int(new_size)) - volume.size
|
|
||||||
self._notify_about_volume_usage(context, volume, "resize.start")
|
self._notify_about_volume_usage(context, volume, "resize.start")
|
||||||
try:
|
try:
|
||||||
self.driver.extend_volume(volume, new_size)
|
self.driver.extend_volume(volume, new_size)
|
||||||
@ -2921,6 +2919,38 @@ class VolumeManager(manager.CleanableManager,
|
|||||||
except Exception:
|
except Exception:
|
||||||
LOG.exception("Extend volume failed.",
|
LOG.exception("Extend volume failed.",
|
||||||
resource=volume)
|
resource=volume)
|
||||||
|
self.extend_volume_completion(context, volume, new_size,
|
||||||
|
reservations, error=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.extend_volume_completion(context, volume, new_size,
|
||||||
|
reservations, error=False)
|
||||||
|
|
||||||
|
attachments = volume.volume_attachment or []
|
||||||
|
# If instance_uuid field is None on attachment, it means that the
|
||||||
|
# volume is used by Glance Cinder store
|
||||||
|
instance_uuids = [attachment.instance_uuid
|
||||||
|
for attachment in attachments
|
||||||
|
if attachment.instance_uuid]
|
||||||
|
|
||||||
|
# If the volume is not attached to any instances, we should not send
|
||||||
|
# external events to Nova
|
||||||
|
if instance_uuids:
|
||||||
|
nova_api = compute.API()
|
||||||
|
nova_api.extend_volume(context, instance_uuids, volume.id)
|
||||||
|
|
||||||
|
def extend_volume_completion(self,
|
||||||
|
context: context.RequestContext,
|
||||||
|
volume: objects.Volume,
|
||||||
|
new_size: int,
|
||||||
|
reservations: list[str],
|
||||||
|
error: bool) -> None:
|
||||||
|
|
||||||
|
project_id = volume.project_id
|
||||||
|
size_increase = new_size - volume.size
|
||||||
|
|
||||||
|
if error:
|
||||||
|
LOG.error("Failed to extend volume.", resource=volume)
|
||||||
self.message_api.create(
|
self.message_api.create(
|
||||||
context,
|
context,
|
||||||
message_field.Action.EXTEND_VOLUME,
|
message_field.Action.EXTEND_VOLUME,
|
||||||
@ -2938,8 +2968,7 @@ class VolumeManager(manager.CleanableManager,
|
|||||||
|
|
||||||
QUOTAS.commit(context, reservations, project_id=project_id)
|
QUOTAS.commit(context, reservations, project_id=project_id)
|
||||||
|
|
||||||
attachments = volume.volume_attachment
|
if not volume.volume_attachment:
|
||||||
if not attachments:
|
|
||||||
orig_volume_status = 'available'
|
orig_volume_status = 'available'
|
||||||
else:
|
else:
|
||||||
orig_volume_status = 'in-use'
|
orig_volume_status = 'in-use'
|
||||||
@ -2947,18 +2976,6 @@ class VolumeManager(manager.CleanableManager,
|
|||||||
volume.update({'size': int(new_size), 'status': orig_volume_status})
|
volume.update({'size': int(new_size), 'status': orig_volume_status})
|
||||||
volume.save()
|
volume.save()
|
||||||
|
|
||||||
if orig_volume_status == 'in-use':
|
|
||||||
nova_api = compute.API()
|
|
||||||
# If instance_uuid field is None on attachment, it means the
|
|
||||||
# request is coming from glance and not nova
|
|
||||||
instance_uuids = [attachment.instance_uuid
|
|
||||||
for attachment in attachments
|
|
||||||
if attachment.instance_uuid]
|
|
||||||
# If we are using glance cinder store, we should not send any
|
|
||||||
# external events to nova
|
|
||||||
if instance_uuids:
|
|
||||||
nova_api.extend_volume(context, instance_uuids, volume.id)
|
|
||||||
|
|
||||||
pool = volume_utils.extract_host(volume.host, 'pool')
|
pool = volume_utils.extract_host(volume.host, 'pool')
|
||||||
if pool is None:
|
if pool is None:
|
||||||
# Legacy volume, put them into default pool
|
# Legacy volume, put them into default pool
|
||||||
|
@ -140,9 +140,10 @@ class VolumeAPI(rpc.RPCAPI):
|
|||||||
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
|
3.18 - Add reimage method
|
||||||
|
3.19 - Add extend_volume_completion method
|
||||||
"""
|
"""
|
||||||
|
|
||||||
RPC_API_VERSION = '3.18'
|
RPC_API_VERSION = '3.19'
|
||||||
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
|
||||||
@ -279,6 +280,13 @@ class VolumeAPI(rpc.RPCAPI):
|
|||||||
cctxt.cast(ctxt, 'extend_volume', volume=volume, new_size=new_size,
|
cctxt.cast(ctxt, 'extend_volume', volume=volume, new_size=new_size,
|
||||||
reservations=reservations)
|
reservations=reservations)
|
||||||
|
|
||||||
|
@rpc.assert_min_rpc_version('3.19')
|
||||||
|
def extend_volume_completion(self, ctxt, volume, new_size, reservations,
|
||||||
|
error):
|
||||||
|
cctxt = self._get_cctxt(volume.service_topic_queue, version='3.19')
|
||||||
|
cctxt.cast(ctxt, 'extend_volume_completion', volume=volume,
|
||||||
|
new_size=new_size, reservations=reservations, error=error)
|
||||||
|
|
||||||
def migrate_volume(self, ctxt, volume, dest_backend, force_host_copy):
|
def migrate_volume(self, ctxt, volume, dest_backend, force_host_copy):
|
||||||
backend_p = {'host': dest_backend.host,
|
backend_p = {'host': dest_backend.host,
|
||||||
'cluster_name': dest_backend.cluster_name,
|
'cluster_name': dest_backend.cluster_name,
|
||||||
|
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
Add the new ``os-extend_volume_completion`` volume action, which the Nova
|
||||||
|
compute agent can use to notify Cinder that it has finished handling the
|
||||||
|
``volume-extended`` external server event.
|
Loading…
Reference in New Issue
Block a user