Merge "Snapshot in-use volumes without force flag"
This commit is contained in:
commit
6258cc99f9
@ -21,8 +21,8 @@
|
|||||||
],
|
],
|
||||||
"min_version": "3.0",
|
"min_version": "3.0",
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"updated": "2021-08-25T00:00:00Z",
|
"updated": "2021-09-16T00:00:00Z",
|
||||||
"version": "3.65"
|
"version": "3.66"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -21,8 +21,8 @@
|
|||||||
],
|
],
|
||||||
"min_version": "3.0",
|
"min_version": "3.0",
|
||||||
"status": "CURRENT",
|
"status": "CURRENT",
|
||||||
"updated": "2021-08-25T00:00:00Z",
|
"updated": "2021-09-16T00:00:00Z",
|
||||||
"version": "3.65"
|
"version": "3.66"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -107,6 +107,12 @@ Create a snapshot
|
|||||||
Creates a volume snapshot, which is a point-in-time, complete copy of a volume.
|
Creates a volume snapshot, which is a point-in-time, complete copy of a volume.
|
||||||
You can create a volume from a snapshot.
|
You can create a volume from a snapshot.
|
||||||
|
|
||||||
|
Prior to API version 3.66, a 'force' flag was required to create a snapshot of
|
||||||
|
an in-use volume, but this is no longer the case. From API version 3.66, the
|
||||||
|
'force' flag is invalid when passed in a volume snapshot request. (For
|
||||||
|
backward compatibility, however, a 'force' flag with a value evaluating to
|
||||||
|
True is silently ignored.)
|
||||||
|
|
||||||
Response codes
|
Response codes
|
||||||
--------------
|
--------------
|
||||||
|
|
||||||
|
@ -169,6 +169,8 @@ ENCRYPTION_KEY_ID_IN_DETAILS = '3.64'
|
|||||||
|
|
||||||
USE_QUOTA = '3.65'
|
USE_QUOTA = '3.65'
|
||||||
|
|
||||||
|
SNAPSHOT_IN_USE = '3.66'
|
||||||
|
|
||||||
|
|
||||||
def get_mv_header(version):
|
def get_mv_header(version):
|
||||||
"""Gets a formatted HTTP microversion header.
|
"""Gets a formatted HTTP microversion header.
|
||||||
|
@ -151,14 +151,15 @@ REST_API_VERSION_HISTORY = """
|
|||||||
* 3.65 - Include 'consumes_quota' in volume and snapshot details
|
* 3.65 - Include 'consumes_quota' in volume and snapshot details
|
||||||
- Accept 'consumes_quota' filter in volume and snapshot list
|
- Accept 'consumes_quota' filter in volume and snapshot list
|
||||||
operation.
|
operation.
|
||||||
|
* 3.66 - Allow snapshotting in-use volumes without force flag.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# 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.65"
|
_MAX_API_VERSION = "3.66"
|
||||||
UPDATED = "2021-08-25T00:00:00Z"
|
UPDATED = "2021-09-16T00: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
|
||||||
|
@ -498,3 +498,10 @@ whether the resource is consuming quota or not. Also, accept a
|
|||||||
``consumes_quota`` filter, which takes a boolean value, in the volume and
|
``consumes_quota`` filter, which takes a boolean value, in the volume and
|
||||||
snapshot list requests. (The default listing behavior is not to use this
|
snapshot list requests. (The default listing behavior is not to use this
|
||||||
filter.)
|
filter.)
|
||||||
|
|
||||||
|
3.66 (Maximum in Xena)
|
||||||
|
----------------------
|
||||||
|
Volume snapshots of in-use volumes can be created without the 'force' flag.
|
||||||
|
Although the 'force' flag is now considered invalid when passed in a volume
|
||||||
|
snapshot request, for backward compatibility, the 'force' flag with a value
|
||||||
|
evaluating to True is silently ignored.
|
||||||
|
@ -16,19 +16,30 @@
|
|||||||
"""The volumes snapshots V3 API."""
|
"""The volumes snapshots V3 API."""
|
||||||
|
|
||||||
import ast
|
import ast
|
||||||
|
from http import HTTPStatus
|
||||||
|
|
||||||
from oslo_log import log as logging
|
from oslo_log import log as logging
|
||||||
|
from oslo_utils import strutils
|
||||||
|
from webob import exc
|
||||||
|
|
||||||
from cinder.api import api_utils
|
from cinder.api import api_utils
|
||||||
from cinder.api import common
|
from cinder.api import common
|
||||||
from cinder.api import microversions as mv
|
from cinder.api import microversions as mv
|
||||||
from cinder.api.openstack import wsgi
|
from cinder.api.openstack import wsgi
|
||||||
|
from cinder.api.schemas import snapshots as snapshot
|
||||||
from cinder.api.v2 import snapshots as snapshots_v2
|
from cinder.api.v2 import snapshots as snapshots_v2
|
||||||
from cinder.api.v3.views import snapshots as snapshot_views
|
from cinder.api.v3.views import snapshots as snapshot_views
|
||||||
|
from cinder.api import validation
|
||||||
from cinder import utils
|
from cinder import utils
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SNAPSHOT_IN_USE_FLAG_MSG = (
|
||||||
|
f"Since microversion {mv.SNAPSHOT_IN_USE} the 'force' flag is "
|
||||||
|
"invalid for this request. For backward compatability, however, when "
|
||||||
|
"the 'force' flag is passed with a value evaluating to True, it is "
|
||||||
|
"silently ignored.")
|
||||||
|
|
||||||
|
|
||||||
class SnapshotsController(snapshots_v2.SnapshotsController):
|
class SnapshotsController(snapshots_v2.SnapshotsController):
|
||||||
"""The Snapshots API controller for the OpenStack API."""
|
"""The Snapshots API controller for the OpenStack API."""
|
||||||
@ -131,6 +142,58 @@ class SnapshotsController(snapshots_v2.SnapshotsController):
|
|||||||
total_count)
|
total_count)
|
||||||
return snapshots
|
return snapshots
|
||||||
|
|
||||||
|
@wsgi.response(HTTPStatus.ACCEPTED)
|
||||||
|
@validation.schema(snapshot.create)
|
||||||
|
def create(self, req, body):
|
||||||
|
"""Creates a new snapshot."""
|
||||||
|
kwargs = {}
|
||||||
|
context = req.environ['cinder.context']
|
||||||
|
snapshot = body['snapshot']
|
||||||
|
kwargs['metadata'] = snapshot.get('metadata', None)
|
||||||
|
volume_id = snapshot['volume_id']
|
||||||
|
volume = self.volume_api.get(context, volume_id)
|
||||||
|
req_version = req.api_version_request
|
||||||
|
force_flag = snapshot.get('force')
|
||||||
|
force = False
|
||||||
|
if force_flag is not None:
|
||||||
|
# note: this won't raise because it passed schema validation
|
||||||
|
force = strutils.bool_from_string(force_flag, strict=True)
|
||||||
|
|
||||||
|
if req_version.matches(mv.SNAPSHOT_IN_USE):
|
||||||
|
# strictly speaking, the 'force' flag is invalid for
|
||||||
|
# mv.SNAPSHOT_IN_USE, but we silently ignore a True
|
||||||
|
# value for backward compatibility
|
||||||
|
if force is False:
|
||||||
|
raise exc.HTTPBadRequest(
|
||||||
|
explanation=SNAPSHOT_IN_USE_FLAG_MSG)
|
||||||
|
|
||||||
|
LOG.info("Create snapshot from volume %s", volume_id)
|
||||||
|
|
||||||
|
self.validate_name_and_description(snapshot, check_length=False)
|
||||||
|
if 'name' in snapshot:
|
||||||
|
snapshot['display_name'] = snapshot.pop('name')
|
||||||
|
|
||||||
|
if force:
|
||||||
|
new_snapshot = self.volume_api.create_snapshot_force(
|
||||||
|
context,
|
||||||
|
volume,
|
||||||
|
snapshot.get('display_name'),
|
||||||
|
snapshot.get('description'),
|
||||||
|
**kwargs)
|
||||||
|
else:
|
||||||
|
if req_version.matches(mv.SNAPSHOT_IN_USE):
|
||||||
|
kwargs['allow_in_use'] = True
|
||||||
|
|
||||||
|
new_snapshot = self.volume_api.create_snapshot(
|
||||||
|
context,
|
||||||
|
volume,
|
||||||
|
snapshot.get('display_name'),
|
||||||
|
snapshot.get('description'),
|
||||||
|
**kwargs)
|
||||||
|
req.cache_db_snapshot(new_snapshot)
|
||||||
|
|
||||||
|
return self._view_builder.detail(req, new_snapshot)
|
||||||
|
|
||||||
|
|
||||||
def create_resource(ext_mgr):
|
def create_resource(ext_mgr):
|
||||||
return wsgi.Resource(SnapshotsController(ext_mgr))
|
return wsgi.Resource(SnapshotsController(ext_mgr))
|
||||||
|
@ -17,6 +17,7 @@ from unittest import mock
|
|||||||
|
|
||||||
import ddt
|
import ddt
|
||||||
from oslo_utils import strutils
|
from oslo_utils import strutils
|
||||||
|
from webob import exc
|
||||||
|
|
||||||
from cinder.api import microversions as mv
|
from cinder.api import microversions as mv
|
||||||
from cinder.api.v3 import snapshots
|
from cinder.api.v3 import snapshots
|
||||||
@ -400,3 +401,56 @@ class SnapshotApiTest(test.TestCase):
|
|||||||
|
|
||||||
# verify some snapshot is returned
|
# verify some snapshot is returned
|
||||||
self.assertNotEqual(0, len(res_dict['snapshots']))
|
self.assertNotEqual(0, len(res_dict['snapshots']))
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.api.API.create_snapshot')
|
||||||
|
def test_snapshot_create_allow_in_use(self, mock_create):
|
||||||
|
req = create_snapshot_query_with_metadata(
|
||||||
|
'{"key2": "val2"}',
|
||||||
|
mv.SNAPSHOT_IN_USE)
|
||||||
|
|
||||||
|
body = {'snapshot': {'volume_id': fake.VOLUME_ID}}
|
||||||
|
|
||||||
|
self.controller.create(req, body=body)
|
||||||
|
self.assertIn('allow_in_use', mock_create.call_args_list[0][1])
|
||||||
|
self.assertTrue(mock_create.call_args_list[0][1]['allow_in_use'])
|
||||||
|
|
||||||
|
@mock.patch('cinder.volume.api.API.create_snapshot')
|
||||||
|
def test_snapshot_create_allow_in_use_negative(self, mock_create):
|
||||||
|
req = create_snapshot_query_with_metadata(
|
||||||
|
'{"key2": "val2"}',
|
||||||
|
mv.get_prior_version(mv.SNAPSHOT_IN_USE))
|
||||||
|
|
||||||
|
body = {'snapshot': {'volume_id': fake.VOLUME_ID}}
|
||||||
|
|
||||||
|
self.controller.create(req, body=body)
|
||||||
|
self.assertNotIn('allow_in_use', mock_create.call_args_list[0][1])
|
||||||
|
|
||||||
|
@ddt.data(False, 'false', 'f', 'no', 'n', '0', 'off')
|
||||||
|
@mock.patch('cinder.volume.api.API.create_snapshot')
|
||||||
|
def test_snapshot_create_force_false(self, force_flag, mock_create):
|
||||||
|
snapshot_name = 'Snapshot Test Name'
|
||||||
|
snapshot_description = 'Snapshot Test Desc'
|
||||||
|
snapshot = {
|
||||||
|
"volume_id": fake.VOLUME_ID,
|
||||||
|
"force": force_flag,
|
||||||
|
"name": snapshot_name,
|
||||||
|
"description": snapshot_description
|
||||||
|
}
|
||||||
|
|
||||||
|
body = dict(snapshot=snapshot)
|
||||||
|
req = create_snapshot_query_with_metadata(
|
||||||
|
'{"key2": "val2"}',
|
||||||
|
mv.SNAPSHOT_IN_USE)
|
||||||
|
self.assertRaises(exc.HTTPBadRequest,
|
||||||
|
self.controller.create,
|
||||||
|
req,
|
||||||
|
body=body)
|
||||||
|
mock_create.assert_not_called()
|
||||||
|
|
||||||
|
# prevent regression -- shouldn't raise for pre-mv-3.66
|
||||||
|
req = create_snapshot_query_with_metadata(
|
||||||
|
'{"key2": "val2"}',
|
||||||
|
mv.get_prior_version(mv.SNAPSHOT_IN_USE))
|
||||||
|
self.controller.create(req, body=body)
|
||||||
|
# ... but also shouldn't allow an in-use snapshot
|
||||||
|
self.assertNotIn('allow_in_use', mock_create.call_args_list[0][1])
|
||||||
|
@ -335,7 +335,7 @@ class SnapshotTestCase(base.BaseVolumeTestCase):
|
|||||||
def test_create_snapshot_force(self):
|
def test_create_snapshot_force(self):
|
||||||
"""Test snapshot in use can be created forcibly."""
|
"""Test snapshot in use can be created forcibly."""
|
||||||
|
|
||||||
instance_uuid = '12345678-1234-5678-1234-567812345678'
|
instance_uuid = '12345678-1234-4678-1234-567812345678'
|
||||||
# create volume and attach to the instance
|
# create volume and attach to the instance
|
||||||
volume = tests_utils.create_volume(self.context, **self.volume_params)
|
volume = tests_utils.create_volume(self.context, **self.volume_params)
|
||||||
self.volume.create_volume(self.context, volume)
|
self.volume.create_volume(self.context, volume)
|
||||||
@ -359,6 +359,7 @@ class SnapshotTestCase(base.BaseVolumeTestCase):
|
|||||||
snapshot_ref.destroy()
|
snapshot_ref.destroy()
|
||||||
db.volume_destroy(self.context, volume['id'])
|
db.volume_destroy(self.context, volume['id'])
|
||||||
|
|
||||||
|
def test_create_snapshot_force_host(self):
|
||||||
# create volume and attach to the host
|
# create volume and attach to the host
|
||||||
volume = tests_utils.create_volume(self.context, **self.volume_params)
|
volume = tests_utils.create_volume(self.context, **self.volume_params)
|
||||||
self.volume.create_volume(self.context, volume)
|
self.volume.create_volume(self.context, volume)
|
||||||
@ -382,6 +383,58 @@ class SnapshotTestCase(base.BaseVolumeTestCase):
|
|||||||
snapshot_ref.destroy()
|
snapshot_ref.destroy()
|
||||||
db.volume_destroy(self.context, volume['id'])
|
db.volume_destroy(self.context, volume['id'])
|
||||||
|
|
||||||
|
def test_create_snapshot_in_use(self):
|
||||||
|
"""Test snapshot in use can be created forcibly."""
|
||||||
|
|
||||||
|
instance_uuid = 'a14dc210-d43b-4792-a608-09fe0824de54'
|
||||||
|
# create volume and attach to the instance
|
||||||
|
volume = tests_utils.create_volume(self.context, **self.volume_params)
|
||||||
|
self.volume.create_volume(self.context, volume)
|
||||||
|
values = {'volume_id': volume['id'],
|
||||||
|
'instance_uuid': instance_uuid,
|
||||||
|
'attach_status': fields.VolumeAttachStatus.ATTACHING, }
|
||||||
|
attachment = db.volume_attach(self.context, values)
|
||||||
|
db.volume_attached(self.context, attachment['id'], instance_uuid,
|
||||||
|
None, '/dev/sda1')
|
||||||
|
|
||||||
|
volume_api = cinder.volume.api.API()
|
||||||
|
volume = volume_api.get(self.context, volume['id'])
|
||||||
|
self.assertRaises(exception.InvalidVolume,
|
||||||
|
volume_api.create_snapshot,
|
||||||
|
self.context, volume,
|
||||||
|
'fake_name', 'fake_description')
|
||||||
|
snapshot_ref = volume_api.create_snapshot(self.context,
|
||||||
|
volume,
|
||||||
|
'fake_name',
|
||||||
|
'fake_description',
|
||||||
|
allow_in_use=True)
|
||||||
|
snapshot_ref.destroy()
|
||||||
|
db.volume_destroy(self.context, volume['id'])
|
||||||
|
|
||||||
|
# create volume and attach to the host
|
||||||
|
volume = tests_utils.create_volume(self.context, **self.volume_params)
|
||||||
|
self.volume.create_volume(self.context, volume)
|
||||||
|
values = {'volume_id': volume['id'],
|
||||||
|
'attached_host': 'fake_host',
|
||||||
|
'attach_status': fields.VolumeAttachStatus.ATTACHING, }
|
||||||
|
attachment = db.volume_attach(self.context, values)
|
||||||
|
db.volume_attached(self.context, attachment['id'], None,
|
||||||
|
'fake_host', '/dev/sda1')
|
||||||
|
|
||||||
|
volume_api = cinder.volume.api.API()
|
||||||
|
volume = volume_api.get(self.context, volume['id'])
|
||||||
|
self.assertRaises(exception.InvalidVolume,
|
||||||
|
volume_api.create_snapshot,
|
||||||
|
self.context, volume,
|
||||||
|
'fake_name', 'fake_description')
|
||||||
|
snapshot_ref = volume_api.create_snapshot(self.context,
|
||||||
|
volume,
|
||||||
|
'fake_name',
|
||||||
|
'fake_description',
|
||||||
|
allow_in_use=True)
|
||||||
|
snapshot_ref.destroy()
|
||||||
|
db.volume_destroy(self.context, volume['id'])
|
||||||
|
|
||||||
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
@mock.patch('cinder.image.image_utils.qemu_img_info')
|
||||||
def test_create_snapshot_from_bootable_volume(self, mock_qemu_info):
|
def test_create_snapshot_from_bootable_volume(self, mock_qemu_info):
|
||||||
"""Test create snapshot from bootable volume."""
|
"""Test create snapshot from bootable volume."""
|
||||||
|
@ -850,12 +850,13 @@ class API(base.Base):
|
|||||||
volume, name, description,
|
volume, name, description,
|
||||||
force=False, metadata=None,
|
force=False, metadata=None,
|
||||||
cgsnapshot_id=None,
|
cgsnapshot_id=None,
|
||||||
group_snapshot_id=None):
|
group_snapshot_id=None,
|
||||||
|
allow_in_use=False):
|
||||||
volume.assert_not_frozen()
|
volume.assert_not_frozen()
|
||||||
snapshot = self.create_snapshot_in_db(
|
snapshot = self.create_snapshot_in_db(
|
||||||
context, volume, name,
|
context, volume, name,
|
||||||
description, force, metadata, cgsnapshot_id,
|
description, force, metadata, cgsnapshot_id,
|
||||||
True, group_snapshot_id)
|
True, group_snapshot_id, allow_in_use)
|
||||||
# NOTE(tommylikehu): We only wrap the 'size' attribute here
|
# NOTE(tommylikehu): We only wrap the 'size' attribute here
|
||||||
# because only the volume's host is passed and only capacity is
|
# because only the volume's host is passed and only capacity is
|
||||||
# validated in the scheduler now.
|
# validated in the scheduler now.
|
||||||
@ -872,12 +873,15 @@ class API(base.Base):
|
|||||||
force, metadata,
|
force, metadata,
|
||||||
cgsnapshot_id,
|
cgsnapshot_id,
|
||||||
commit_quota=True,
|
commit_quota=True,
|
||||||
group_snapshot_id=None):
|
group_snapshot_id=None,
|
||||||
|
allow_in_use=False):
|
||||||
self._create_snapshot_in_db_validate(context, volume)
|
self._create_snapshot_in_db_validate(context, volume)
|
||||||
|
|
||||||
utils.check_metadata_properties(metadata)
|
utils.check_metadata_properties(metadata)
|
||||||
|
|
||||||
valid_status = ["available", "in-use"] if force else ["available"]
|
valid_status = ('available',)
|
||||||
|
if force or allow_in_use:
|
||||||
|
valid_status = ('available', 'in-use')
|
||||||
|
|
||||||
if volume['status'] not in valid_status:
|
if volume['status'] not in valid_status:
|
||||||
msg = _("Volume %(vol_id)s status must be %(status)s, "
|
msg = _("Volume %(vol_id)s status must be %(status)s, "
|
||||||
@ -1063,10 +1067,11 @@ class API(base.Base):
|
|||||||
def create_snapshot(self, context,
|
def create_snapshot(self, context,
|
||||||
volume, name, description,
|
volume, name, description,
|
||||||
metadata=None, cgsnapshot_id=None,
|
metadata=None, cgsnapshot_id=None,
|
||||||
group_snapshot_id=None):
|
group_snapshot_id=None,
|
||||||
|
allow_in_use=False):
|
||||||
result = self._create_snapshot(context, volume, name, description,
|
result = self._create_snapshot(context, volume, name, description,
|
||||||
False, metadata, cgsnapshot_id,
|
False, metadata, cgsnapshot_id,
|
||||||
group_snapshot_id)
|
group_snapshot_id, allow_in_use)
|
||||||
LOG.info("Snapshot create request issued successfully.",
|
LOG.info("Snapshot create request issued successfully.",
|
||||||
resource=result)
|
resource=result)
|
||||||
return result
|
return result
|
||||||
|
@ -0,0 +1,8 @@
|
|||||||
|
---
|
||||||
|
features:
|
||||||
|
- |
|
||||||
|
As of API version 3.66, volume snapshots of in-use volumes can be created
|
||||||
|
without passing the 'force' flag, and the 'force' flag is considered
|
||||||
|
invalid for this request. For backward compatability, however, when the
|
||||||
|
'force' flag is passed with a value evaluating to True, it is silently
|
||||||
|
ignored.
|
Loading…
Reference in New Issue
Block a user