Volume status management during migration

This patch proposes a new implementation for the status and
the migration_status for volumes.

* The initial migration_status is None, meaning no migration has been
done; Migration_status 'error' means the previous migration failed.
Migration_status 'success' means the previous migration succeeded.

* If the key 'lock_volume' is set to True from the request, the volume
status should be set to 'maintenance' during migration and goes
back to its original status after migration. Otherwise, if the
key 'lock_volume' is set to False, the volume status will remain the
same as its original status. The default value for lock_volume is
False and it applies to the available volume.

* From the REST's perspectives, all the create, update and delete
actions are not allowed if the volume is in 'maintenance', because
it means the volume is out of service. If it is not in maintenance
mode, the migration can be interrupted if other requests are
issued, e.g. attach. For the termination of migration, another
patch will target to resolve it.

DocImpact
APIImpact The key 'lock_volume' has been added into the API,
telling the volume to change the status to 'maintenance' or not.
The migration_status has been added into results returned
from volume list command, if the request is from an admin.

Change-Id: Ia86421f2d6fce61dcfeb073f8e7b9c9dde517373
Partial-implements: blueprint migration-improvement
This commit is contained in:
Vincent Hou 2015-05-28 11:18:09 +08:00
parent e7c6d6d0cc
commit 21bc0537e0
13 changed files with 910 additions and 355 deletions

View File

@ -14,7 +14,6 @@
from oslo_log import log as logging from oslo_log import log as logging
import oslo_messaging as messaging import oslo_messaging as messaging
from oslo_utils import strutils
import webob import webob
from webob import exc from webob import exc
@ -26,6 +25,7 @@ from cinder import exception
from cinder.i18n import _ from cinder.i18n import _
from cinder import objects from cinder import objects
from cinder import rpc from cinder import rpc
from cinder import utils
from cinder import volume from cinder import volume
@ -127,12 +127,12 @@ class VolumeAdminController(AdminController):
# Perhaps we don't even want any definitions in the abstract # Perhaps we don't even want any definitions in the abstract
# parent class? # parent class?
valid_status = AdminController.valid_status.union( valid_status = AdminController.valid_status.union(
set(['attaching', 'in-use', 'detaching'])) ('attaching', 'in-use', 'detaching', 'maintenance'))
valid_attach_status = set(['detached', 'attached', ]) valid_attach_status = ('detached', 'attached',)
valid_migration_status = set(['migrating', 'error', valid_migration_status = ('migrating', 'error',
'completing', 'none', 'success', 'completing',
'starting', ]) 'none', 'starting',)
def _update(self, *args, **kwargs): def _update(self, *args, **kwargs):
db.volume_update(*args, **kwargs) db.volume_update(*args, **kwargs)
@ -220,15 +220,11 @@ class VolumeAdminController(AdminController):
try: try:
host = params['host'] host = params['host']
except KeyError: except KeyError:
raise exc.HTTPBadRequest(explanation=_("Must specify 'host'")) raise exc.HTTPBadRequest(explanation=_("Must specify 'host'."))
force_host_copy = params.get('force_host_copy', 'False') force_host_copy = utils.get_bool_param('force_host_copy', params)
try: lock_volume = utils.get_bool_param('lock_volume', params)
force_host_copy = strutils.bool_from_string(force_host_copy, self.volume_api.migrate_volume(context, volume, host, force_host_copy,
strict=True) lock_volume)
except ValueError as e:
msg = (_("Invalid value for force_host_copy: '%s'") % e.message)
raise exc.HTTPBadRequest(explanation=msg)
self.volume_api.migrate_volume(context, volume, host, force_host_copy)
return webob.Response(status_int=202) return webob.Response(status_int=202)
@wsgi.action('os-migrate_volume_completion') @wsgi.action('os-migrate_volume_completion')

View File

@ -39,7 +39,7 @@ class ViewBuilder(common.ViewBuilder):
"""Detailed view of a list of volumes.""" """Detailed view of a list of volumes."""
return self._list_view(self.detail, request, volumes, return self._list_view(self.detail, request, volumes,
volume_count, volume_count,
coll_name=self._collection_name + '/detail') self._collection_name + '/detail')
def summary(self, request, volume): def summary(self, request, volume):
"""Generic, non-detailed view of an volume.""" """Generic, non-detailed view of an volume."""
@ -54,7 +54,7 @@ class ViewBuilder(common.ViewBuilder):
def detail(self, request, volume): def detail(self, request, volume):
"""Detailed view of a single volume.""" """Detailed view of a single volume."""
return { volume_ref = {
'volume': { 'volume': {
'id': volume.get('id'), 'id': volume.get('id'),
'status': volume.get('status'), 'status': volume.get('status'),
@ -74,9 +74,13 @@ class ViewBuilder(common.ViewBuilder):
'encrypted': self._is_volume_encrypted(volume), 'encrypted': self._is_volume_encrypted(volume),
'replication_status': volume.get('replication_status'), 'replication_status': volume.get('replication_status'),
'consistencygroup_id': volume.get('consistencygroup_id'), 'consistencygroup_id': volume.get('consistencygroup_id'),
'multiattach': volume.get('multiattach') 'multiattach': volume.get('multiattach'),
} }
} }
if request.environ['cinder.context'].is_admin:
volume_ref['volume']['migration_status'] = (
volume.get('migration_status'))
return volume_ref
def _is_volume_encrypted(self, volume): def _is_volume_encrypted(self, volume):
"""Determine if volume is encrypted.""" """Determine if volume is encrypted."""

View File

@ -30,12 +30,17 @@ class Controller(wsgi.Controller):
super(Controller, self).__init__() super(Controller, self).__init__()
def _get_metadata(self, context, volume_id): def _get_metadata(self, context, volume_id):
# The metadata is at the second position of the tuple returned
# from _get_volume_and_metadata
return self._get_volume_and_metadata(context, volume_id)[1]
def _get_volume_and_metadata(self, context, volume_id):
try: try:
volume = self.volume_api.get(context, volume_id) volume = self.volume_api.get(context, volume_id)
meta = self.volume_api.get_volume_metadata(context, volume) meta = self.volume_api.get_volume_metadata(context, volume)
except exception.VolumeNotFound as error: except exception.VolumeNotFound as error:
raise webob.exc.HTTPNotFound(explanation=error.msg) raise webob.exc.HTTPNotFound(explanation=error.msg)
return meta return (volume, meta)
@wsgi.serializers(xml=common.MetadataTemplate) @wsgi.serializers(xml=common.MetadataTemplate)
def index(self, req, volume_id): def index(self, req, volume_id):
@ -133,14 +138,13 @@ class Controller(wsgi.Controller):
"""Deletes an existing metadata.""" """Deletes an existing metadata."""
context = req.environ['cinder.context'] context = req.environ['cinder.context']
metadata = self._get_metadata(context, volume_id) volume, metadata = self._get_volume_and_metadata(context, volume_id)
if id not in metadata: if id not in metadata:
msg = _("Metadata item was not found") msg = _("Metadata item was not found")
raise webob.exc.HTTPNotFound(explanation=msg) raise webob.exc.HTTPNotFound(explanation=msg)
try: try:
volume = self.volume_api.get(context, volume_id)
self.volume_api.delete_volume_metadata( self.volume_api.delete_volume_metadata(
context, context,
volume, volume,

View File

@ -1285,8 +1285,9 @@ def volume_detached(context, volume_id, attachment_id):
if not remain_attachment: if not remain_attachment:
# Hide status update from user if we're performing volume migration # Hide status update from user if we're performing volume migration
# or uploading it to image # or uploading it to image
if (not volume_ref['migration_status'] and if ((not volume_ref['migration_status'] and
not (volume_ref['status'] == 'uploading')): not (volume_ref['status'] == 'uploading')) or
volume_ref['migration_status'] in ('success', 'error')):
volume_ref['status'] = 'available' volume_ref['status'] = 'available'
volume_ref['attach_status'] = 'detached' volume_ref['attach_status'] = 'detached'

View File

@ -146,7 +146,14 @@ class SchedulerManager(manager.Manager):
self._wait_for_scheduler() self._wait_for_scheduler()
def _migrate_volume_set_error(self, context, ex, request_spec): def _migrate_volume_set_error(self, context, ex, request_spec):
volume_state = {'volume_state': {'migration_status': None}} volume = db.volume_get(context, request_spec['volume_id'])
if volume.status == 'maintenance':
previous_status = (
volume.previous_status or 'maintenance')
volume_state = {'volume_state': {'migration_status': 'error',
'status': previous_status}}
else:
volume_state = {'volume_state': {'migration_status': 'error'}}
self._set_volume_state_and_notify('migrate_volume_to_host', self._set_volume_state_and_notify('migrate_volume_to_host',
volume_state, volume_state,
context, ex, request_spec) context, ex, request_spec)
@ -183,12 +190,9 @@ class SchedulerManager(manager.Manager):
volume_ref, msg, reservations): volume_ref, msg, reservations):
if reservations: if reservations:
QUOTAS.rollback(context, reservations) QUOTAS.rollback(context, reservations)
if (volume_ref['volume_attachment'] is None or previous_status = (
len(volume_ref['volume_attachment']) == 0): volume_ref.previous_status or volume_ref.status)
orig_status = 'available' volume_state = {'volume_state': {'status': previous_status}}
else:
orig_status = 'in-use'
volume_state = {'volume_state': {'status': orig_status}}
self._set_volume_state_and_notify('retype', volume_state, self._set_volume_state_and_notify('retype', volume_state,
context, ex, request_spec, msg) context, ex, request_spec, msg)

View File

@ -15,6 +15,7 @@
import uuid import uuid
import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
import webob import webob
@ -200,26 +201,42 @@ class volumeMetaDataTest(test.TestCase):
self.assertRaises(webob.exc.HTTPNotFound, self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show, req, self.req_id, 'key6') self.controller.show, req, self.req_id, 'key6')
def test_delete(self): @mock.patch.object(cinder.db, 'volume_metadata_delete')
self.stubs.Set(cinder.db, 'volume_metadata_get', @mock.patch.object(cinder.db, 'volume_metadata_get')
return_volume_metadata) def test_delete(self, metadata_get, metadata_delete):
self.stubs.Set(cinder.db, 'volume_metadata_delete', fake_volume = {'id': self.req_id, 'status': 'available'}
delete_volume_metadata) fake_context = mock.Mock()
metadata_get.side_effect = return_volume_metadata
metadata_delete.side_effect = delete_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key2') req = fakes.HTTPRequest.blank(self.url + '/key2')
req.method = 'DELETE' req.method = 'DELETE'
res = self.controller.delete(req, self.req_id, 'key2') req.environ['cinder.context'] = fake_context
self.assertEqual(200, res.status_int) with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res = self.controller.delete(req, self.req_id, 'key2')
self.assertEqual(200, res.status_int)
get_volume.assert_called_with(fake_context, self.req_id)
def test_delete_nonexistent_volume(self): @mock.patch.object(cinder.db, 'volume_metadata_delete')
self.stubs.Set(cinder.db, 'volume_metadata_get', @mock.patch.object(cinder.db, 'volume_metadata_get')
return_volume_metadata) def test_delete_nonexistent_volume(self, metadata_get, metadata_delete):
self.stubs.Set(cinder.db, 'volume_metadata_delete', fake_volume = {'id': self.req_id, 'status': 'available'}
return_volume_nonexistent) fake_context = mock.Mock()
metadata_get.side_effect = return_volume_metadata
metadata_delete.side_effect = return_volume_nonexistent
req = fakes.HTTPRequest.blank(self.url + '/key1') req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'DELETE' req.method = 'DELETE'
self.assertRaises(webob.exc.HTTPNotFound, req.environ['cinder.context'] = fake_context
self.controller.delete, req, self.req_id, 'key1')
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete, req,
self.req_id, 'key1')
get_volume.assert_called_with(fake_context, self.req_id)
def test_delete_meta_not_found(self): def test_delete_meta_not_found(self):
self.stubs.Set(cinder.db, 'volume_metadata_get', self.stubs.Set(cinder.db, 'volume_metadata_get',
@ -229,31 +246,40 @@ class volumeMetaDataTest(test.TestCase):
self.assertRaises(webob.exc.HTTPNotFound, self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete, req, self.req_id, 'key6') self.controller.delete, req, self.req_id, 'key6')
def test_create(self): @mock.patch.object(cinder.db, 'volume_metadata_update')
self.stubs.Set(cinder.db, 'volume_metadata_get', @mock.patch.object(cinder.db, 'volume_metadata_get')
return_empty_volume_metadata) def test_create(self, metadata_get, metadata_update):
self.stubs.Set(cinder.db, 'volume_metadata_update', fake_volume = {'id': self.req_id, 'status': 'available'}
return_create_volume_metadata) fake_context = mock.Mock()
metadata_get.side_effect = return_empty_volume_metadata
req = fakes.HTTPRequest.blank('/v1/volume_metadata') metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank('/v2/volume_metadata')
req.method = 'POST' req.method = 'POST'
req.content_type = "application/json" req.content_type = "application/json"
body = {"metadata": {"key1": "value1", body = {"metadata": {"key1": "value1",
"key2": "value2", "key2": "value2",
"key3": "value3", }} "key3": "value3", }}
req.body = jsonutils.dumps(body) req.body = jsonutils.dumps(body)
res_dict = self.controller.create(req, self.req_id, body) req.environ['cinder.context'] = fake_context
self.assertEqual(body, res_dict)
def test_create_with_keys_in_uppercase_and_lowercase(self): with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.create(req, self.req_id, body)
self.assertEqual(body, res_dict)
@mock.patch.object(cinder.db, 'volume_metadata_update')
@mock.patch.object(cinder.db, 'volume_metadata_get')
def test_create_with_keys_in_uppercase_and_lowercase(self, metadata_get,
metadata_update):
# if the keys in uppercase_and_lowercase, should return the one # if the keys in uppercase_and_lowercase, should return the one
# which server added # which server added
self.stubs.Set(cinder.db, 'volume_metadata_get', fake_volume = {'id': self.req_id, 'status': 'available'}
return_empty_volume_metadata) fake_context = mock.Mock()
self.stubs.Set(cinder.db, 'volume_metadata_update', metadata_get.side_effect = return_empty_volume_metadata
return_create_volume_metadata_insensitive) metadata_update.side_effect = return_create_volume_metadata_insensitive
req = fakes.HTTPRequest.blank('/v1/volume_metadata') req = fakes.HTTPRequest.blank('/v2/volume_metadata')
req.method = 'POST' req.method = 'POST'
req.content_type = "application/json" req.content_type = "application/json"
body = {"metadata": {"key1": "value1", body = {"metadata": {"key1": "value1",
@ -267,8 +293,13 @@ class volumeMetaDataTest(test.TestCase):
"key3": "value3", "key3": "value3",
"KEY4": "value4"}} "KEY4": "value4"}}
req.body = jsonutils.dumps(body) req.body = jsonutils.dumps(body)
res_dict = self.controller.create(req, self.req_id, body) req.environ['cinder.context'] = fake_context
self.assertEqual(expected, res_dict)
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.create(req, self.req_id, body)
self.assertEqual(expected, res_dict)
def test_create_empty_body(self): def test_create_empty_body(self):
self.stubs.Set(cinder.db, 'volume_metadata_update', self.stubs.Set(cinder.db, 'volume_metadata_update',
@ -321,9 +352,11 @@ class volumeMetaDataTest(test.TestCase):
self.assertRaises(webob.exc.HTTPNotFound, self.assertRaises(webob.exc.HTTPNotFound,
self.controller.create, req, self.req_id, body) self.controller.create, req, self.req_id, body)
def test_update_all(self): @mock.patch.object(cinder.db, 'volume_metadata_update')
self.stubs.Set(cinder.db, 'volume_metadata_update', def test_update_all(self, metadata_update):
return_new_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_new_volume_metadata
req = fakes.HTTPRequest.blank(self.url) req = fakes.HTTPRequest.blank(self.url)
req.method = 'PUT' req.method = 'PUT'
req.content_type = "application/json" req.content_type = "application/json"
@ -335,15 +368,24 @@ class volumeMetaDataTest(test.TestCase):
}, },
} }
req.body = jsonutils.dumps(expected) req.body = jsonutils.dumps(expected)
res_dict = self.controller.update_all(req, self.req_id, expected) req.environ['cinder.context'] = fake_context
self.assertEqual(expected, res_dict) with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.update_all(req, self.req_id, expected)
self.assertEqual(expected, res_dict)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_all_with_keys_in_uppercase_and_lowercase(self): @mock.patch.object(cinder.db, 'volume_metadata_update')
self.stubs.Set(cinder.db, 'volume_metadata_get', @mock.patch.object(cinder.db, 'volume_metadata_get')
return_create_volume_metadata) def test_update_all_with_keys_in_uppercase_and_lowercase(self,
self.stubs.Set(cinder.db, 'volume_metadata_update', metadata_get,
return_new_volume_metadata) metadata_update):
fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_get.side_effect = return_create_volume_metadata
metadata_update.side_effect = return_new_volume_metadata
req = fakes.HTTPRequest.blank(self.url) req = fakes.HTTPRequest.blank(self.url)
req.method = 'PUT' req.method = 'PUT'
req.content_type = "application/json" req.content_type = "application/json"
@ -363,21 +405,54 @@ class volumeMetaDataTest(test.TestCase):
}, },
} }
req.body = jsonutils.dumps(expected) req.body = jsonutils.dumps(expected)
res_dict = self.controller.update_all(req, self.req_id, body) req.environ['cinder.context'] = fake_context
self.assertEqual(expected, res_dict) with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.update_all(req, self.req_id, body)
self.assertEqual(expected, res_dict)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_all_empty_container(self): @mock.patch.object(cinder.db, 'volume_metadata_update')
self.stubs.Set(cinder.db, 'volume_metadata_update', def test_update_all_empty_container(self, metadata_update):
return_empty_container_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_empty_container_metadata
req = fakes.HTTPRequest.blank(self.url) req = fakes.HTTPRequest.blank(self.url)
req.method = 'PUT' req.method = 'PUT'
req.content_type = "application/json" req.content_type = "application/json"
expected = {'metadata': {}} expected = {'metadata': {}}
req.body = jsonutils.dumps(expected) req.body = jsonutils.dumps(expected)
res_dict = self.controller.update_all(req, self.req_id, expected) req.environ['cinder.context'] = fake_context
self.assertEqual(expected, res_dict) with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.update_all(req, self.req_id, expected)
self.assertEqual(expected, res_dict)
get_volume.assert_called_once_with(fake_context, self.req_id)
@mock.patch.object(cinder.db, 'volume_metadata_update')
def test_update_item_value_too_long(self, metadata_update):
fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'PUT'
body = {"meta": {"key1": ("a" * 260)}}
req.body = jsonutils.dumps(body)
req.headers["content-type"] = "application/json"
req.environ['cinder.context'] = fake_context
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
self.controller.update,
req, self.req_id, "key1", body)
self.assertFalse(metadata_update.called)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_all_malformed_container(self): def test_update_all_malformed_container(self):
self.stubs.Set(cinder.db, 'volume_metadata_update', self.stubs.Set(cinder.db, 'volume_metadata_update',
@ -392,18 +467,24 @@ class volumeMetaDataTest(test.TestCase):
self.controller.update_all, req, self.req_id, self.controller.update_all, req, self.req_id,
expected) expected)
def test_update_all_malformed_data(self): @mock.patch.object(cinder.db, 'volume_metadata_update')
self.stubs.Set(cinder.db, 'volume_metadata_update', def test_update_all_malformed_data(self, metadata_update):
return_create_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url) req = fakes.HTTPRequest.blank(self.url)
req.method = 'PUT' req.method = 'PUT'
req.content_type = "application/json" req.content_type = "application/json"
expected = {'metadata': ['asdf']} expected = {'metadata': ['asdf']}
req.body = jsonutils.dumps(expected) req.body = jsonutils.dumps(expected)
req.environ['cinder.context'] = fake_context
self.assertRaises(webob.exc.HTTPBadRequest, with mock.patch.object(self.controller.volume_api,
self.controller.update_all, req, self.req_id, 'get') as get_volume:
expected) get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update_all, req, self.req_id,
expected)
def test_update_all_nonexistent_volume(self): def test_update_all_nonexistent_volume(self):
self.stubs.Set(cinder.db, 'volume_get', return_volume_nonexistent) self.stubs.Set(cinder.db, 'volume_get', return_volume_nonexistent)
@ -416,17 +497,25 @@ class volumeMetaDataTest(test.TestCase):
self.assertRaises(webob.exc.HTTPNotFound, self.assertRaises(webob.exc.HTTPNotFound,
self.controller.update_all, req, '100', body) self.controller.update_all, req, '100', body)
def test_update_item(self): @mock.patch.object(cinder.db, 'volume_metadata_update')
self.stubs.Set(cinder.db, 'volume_metadata_update', def test_update_item(self, metadata_update):
return_create_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key1') req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'PUT' req.method = 'PUT'
body = {"meta": {"key1": "value1"}} body = {"meta": {"key1": "value1"}}
req.body = jsonutils.dumps(body) req.body = jsonutils.dumps(body)
req.headers["content-type"] = "application/json" req.headers["content-type"] = "application/json"
res_dict = self.controller.update(req, self.req_id, 'key1', body) req.environ['cinder.context'] = fake_context
expected = {'meta': {'key1': 'value1'}}
self.assertEqual(expected, res_dict) with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.update(req, self.req_id, 'key1', body)
expected = {'meta': {'key1': 'value1'}}
self.assertEqual(expected, res_dict)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_item_nonexistent_volume(self): def test_update_item_nonexistent_volume(self):
self.stubs.Set(cinder.db, 'volume_get', self.stubs.Set(cinder.db, 'volume_get',
@ -452,43 +541,47 @@ class volumeMetaDataTest(test.TestCase):
self.controller.update, req, self.req_id, 'key1', self.controller.update, req, self.req_id, 'key1',
None) None)
def test_update_item_empty_key(self): @mock.patch.object(cinder.db, 'volume_metadata_update')
self.stubs.Set(cinder.db, 'volume_metadata_update', def test_update_item_empty_key(self, metadata_update):
return_create_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key1') req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'PUT' req.method = 'PUT'
body = {"meta": {"": "value1"}} body = {"meta": {"": "value1"}}
req.body = jsonutils.dumps(body) req.body = jsonutils.dumps(body)
req.headers["content-type"] = "application/json" req.headers["content-type"] = "application/json"
req.environ['cinder.context'] = fake_context
self.assertRaises(webob.exc.HTTPBadRequest, with mock.patch.object(self.controller.volume_api,
self.controller.update, req, self.req_id, '', body) 'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update, req, self.req_id,
'', body)
self.assertFalse(metadata_update.called)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_item_key_too_long(self): @mock.patch.object(cinder.db, 'volume_metadata_update')
self.stubs.Set(cinder.db, 'volume_metadata_update', def test_update_item_key_too_long(self, metadata_update):
return_create_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key1') req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'PUT' req.method = 'PUT'
body = {"meta": {("a" * 260): "value1"}} body = {"meta": {("a" * 260): "value1"}}
req.body = jsonutils.dumps(body) req.body = jsonutils.dumps(body)
req.headers["content-type"] = "application/json" req.headers["content-type"] = "application/json"
req.environ['cinder.context'] = fake_context
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, with mock.patch.object(self.controller.volume_api,
self.controller.update, 'get') as get_volume:
req, self.req_id, ("a" * 260), body) get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
def test_update_item_value_too_long(self): self.controller.update,
self.stubs.Set(cinder.db, 'volume_metadata_update', req, self.req_id, ("a" * 260), body)
return_create_volume_metadata) self.assertFalse(metadata_update.called)
req = fakes.HTTPRequest.blank(self.url + '/key1') get_volume.assert_called_once_with(fake_context, self.req_id)
req.method = 'PUT'
body = {"meta": {"key1": ("a" * 260)}}
req.body = jsonutils.dumps(body)
req.headers["content-type"] = "application/json"
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
self.controller.update,
req, self.req_id, "key1", body)
def test_update_item_too_many_keys(self): def test_update_item_too_many_keys(self):
self.stubs.Set(cinder.db, 'volume_metadata_update', self.stubs.Set(cinder.db, 'volume_metadata_update',
@ -516,9 +609,11 @@ class volumeMetaDataTest(test.TestCase):
self.controller.update, req, self.req_id, 'bad', self.controller.update, req, self.req_id, 'bad',
body) body)
def test_invalid_metadata_items_on_create(self): @mock.patch.object(cinder.db, 'volume_metadata_update')
self.stubs.Set(cinder.db, 'volume_metadata_update', def test_invalid_metadata_items_on_create(self, metadata_update):
return_create_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url) req = fakes.HTTPRequest.blank(self.url)
req.method = 'POST' req.method = 'POST'
req.headers["content-type"] = "application/json" req.headers["content-type"] = "application/json"
@ -526,17 +621,32 @@ class volumeMetaDataTest(test.TestCase):
# test for long key # test for long key
data = {"metadata": {"a" * 260: "value1"}} data = {"metadata": {"a" * 260: "value1"}}
req.body = jsonutils.dumps(data) req.body = jsonutils.dumps(data)
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, req.environ['cinder.context'] = fake_context
self.controller.create, req, self.req_id, data)
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
self.controller.create, req, self.req_id, data)
# test for long value # test for long value
data = {"metadata": {"key": "v" * 260}} data = {"metadata": {"key": "v" * 260}}
req.body = jsonutils.dumps(data) req.body = jsonutils.dumps(data)
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, req.environ['cinder.context'] = fake_context
self.controller.create, req, self.req_id, data)
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
self.controller.create, req, self.req_id, data)
# test for empty key. # test for empty key.
data = {"metadata": {"": "value1"}} data = {"metadata": {"": "value1"}}
req.body = jsonutils.dumps(data) req.body = jsonutils.dumps(data)
self.assertRaises(webob.exc.HTTPBadRequest, req.environ['cinder.context'] = fake_context
self.controller.create, req, self.req_id, data)
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, req, self.req_id, data)

View File

@ -15,6 +15,7 @@
import uuid import uuid
import mock
from oslo_config import cfg from oslo_config import cfg
from oslo_serialization import jsonutils from oslo_serialization import jsonutils
import webob import webob
@ -201,26 +202,61 @@ class volumeMetaDataTest(test.TestCase):
self.assertRaises(webob.exc.HTTPNotFound, self.assertRaises(webob.exc.HTTPNotFound,
self.controller.show, req, self.req_id, 'key6') self.controller.show, req, self.req_id, 'key6')
def test_delete(self): @mock.patch.object(db, 'volume_metadata_delete')
self.stubs.Set(db, 'volume_metadata_get', @mock.patch.object(db, 'volume_metadata_get')
return_volume_metadata) def test_delete(self, metadata_get, metadata_delete):
self.stubs.Set(db, 'volume_metadata_delete', fake_volume = {'id': self.req_id, 'status': 'available'}
delete_volume_metadata) fake_context = mock.Mock()
metadata_get.side_effect = return_volume_metadata
metadata_delete.side_effect = delete_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key2') req = fakes.HTTPRequest.blank(self.url + '/key2')
req.method = 'DELETE' req.method = 'DELETE'
res = self.controller.delete(req, self.req_id, 'key2') req.environ['cinder.context'] = fake_context
self.assertEqual(200, res.status_int) with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res = self.controller.delete(req, self.req_id, 'key2')
self.assertEqual(200, res.status_int)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_delete_nonexistent_volume(self): @mock.patch.object(db, 'volume_metadata_delete')
self.stubs.Set(db, 'volume_metadata_get', @mock.patch.object(db, 'volume_metadata_get')
return_volume_metadata) def test_delete_volume_maintenance(self, metadata_get, metadata_delete):
self.stubs.Set(db, 'volume_metadata_delete', fake_volume = {'id': self.req_id, 'status': 'maintenance'}
return_volume_nonexistent) fake_context = mock.Mock()
metadata_get.side_effect = return_volume_metadata
metadata_delete.side_effect = delete_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key2')
req.method = 'DELETE'
req.environ['cinder.context'] = fake_context
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(exception.InvalidVolume,
self.controller.delete, req,
self.req_id, 'key2')
get_volume.assert_called_once_with(fake_context, self.req_id)
@mock.patch.object(db, 'volume_metadata_delete')
@mock.patch.object(db, 'volume_metadata_get')
def test_delete_nonexistent_volume(self, metadata_get, metadata_delete):
fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_get.side_effect = return_volume_metadata
metadata_delete.side_effect = return_volume_nonexistent
req = fakes.HTTPRequest.blank(self.url + '/key1') req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'DELETE' req.method = 'DELETE'
self.assertRaises(webob.exc.HTTPNotFound, req.environ['cinder.context'] = fake_context
self.controller.delete, req, self.req_id, 'key1')
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete, req,
self.req_id, 'key1')
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_delete_meta_not_found(self): def test_delete_meta_not_found(self):
self.stubs.Set(db, 'volume_metadata_get', self.stubs.Set(db, 'volume_metadata_get',
@ -230,12 +266,13 @@ class volumeMetaDataTest(test.TestCase):
self.assertRaises(webob.exc.HTTPNotFound, self.assertRaises(webob.exc.HTTPNotFound,
self.controller.delete, req, self.req_id, 'key6') self.controller.delete, req, self.req_id, 'key6')
def test_create(self): @mock.patch.object(db, 'volume_metadata_update')
self.stubs.Set(db, 'volume_metadata_get', @mock.patch.object(db, 'volume_metadata_get')
return_empty_volume_metadata) def test_create(self, metadata_get, metadata_update):
self.stubs.Set(db, 'volume_metadata_update', fake_volume = {'id': self.req_id, 'status': 'available'}
return_create_volume_metadata) fake_context = mock.Mock()
metadata_get.side_effect = return_empty_volume_metadata
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank('/v2/volume_metadata') req = fakes.HTTPRequest.blank('/v2/volume_metadata')
req.method = 'POST' req.method = 'POST'
req.content_type = "application/json" req.content_type = "application/json"
@ -243,16 +280,47 @@ class volumeMetaDataTest(test.TestCase):
"key2": "value2", "key2": "value2",
"key3": "value3", }} "key3": "value3", }}
req.body = jsonutils.dumps(body) req.body = jsonutils.dumps(body)
res_dict = self.controller.create(req, self.req_id, body) req.environ['cinder.context'] = fake_context
self.assertEqual(body, res_dict)
def test_create_with_keys_in_uppercase_and_lowercase(self): with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.create(req, self.req_id, body)
self.assertEqual(body, res_dict)
@mock.patch.object(db, 'volume_metadata_update')
@mock.patch.object(db, 'volume_metadata_get')
def test_create_volume_maintenance(self, metadata_get, metadata_update):
fake_volume = {'id': self.req_id, 'status': 'maintenance'}
fake_context = mock.Mock()
metadata_get.side_effect = return_empty_volume_metadata
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank('/v2/volume_metadata')
req.method = 'POST'
req.content_type = "application/json"
body = {"metadata": {"key1": "value1",
"key2": "value2",
"key3": "value3", }}
req.body = jsonutils.dumps(body)
req.environ['cinder.context'] = fake_context
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(exception.InvalidVolume,
self.controller.create,
req, self.req_id, body)
@mock.patch.object(db, 'volume_metadata_update')
@mock.patch.object(db, 'volume_metadata_get')
def test_create_with_keys_in_uppercase_and_lowercase(self, metadata_get,
metadata_update):
# if the keys in uppercase_and_lowercase, should return the one # if the keys in uppercase_and_lowercase, should return the one
# which server added # which server added
self.stubs.Set(db, 'volume_metadata_get', fake_volume = {'id': self.req_id, 'status': 'available'}
return_empty_volume_metadata) fake_context = mock.Mock()
self.stubs.Set(db, 'volume_metadata_update', metadata_get.side_effect = return_empty_volume_metadata
return_create_volume_metadata_insensitive) metadata_update.side_effect = return_create_volume_metadata_insensitive
req = fakes.HTTPRequest.blank('/v2/volume_metadata') req = fakes.HTTPRequest.blank('/v2/volume_metadata')
req.method = 'POST' req.method = 'POST'
@ -268,8 +336,13 @@ class volumeMetaDataTest(test.TestCase):
"key3": "value3", "key3": "value3",
"KEY4": "value4"}} "KEY4": "value4"}}
req.body = jsonutils.dumps(body) req.body = jsonutils.dumps(body)
res_dict = self.controller.create(req, self.req_id, body) req.environ['cinder.context'] = fake_context
self.assertEqual(expected, res_dict)
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.create(req, self.req_id, body)
self.assertEqual(expected, res_dict)
def test_create_empty_body(self): def test_create_empty_body(self):
self.stubs.Set(db, 'volume_metadata_update', self.stubs.Set(db, 'volume_metadata_update',
@ -322,9 +395,11 @@ class volumeMetaDataTest(test.TestCase):
self.assertRaises(webob.exc.HTTPNotFound, self.assertRaises(webob.exc.HTTPNotFound,
self.controller.create, req, self.req_id, body) self.controller.create, req, self.req_id, body)
def test_update_all(self): @mock.patch.object(db, 'volume_metadata_update')
self.stubs.Set(db, 'volume_metadata_update', def test_update_all(self, metadata_update):
return_new_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_new_volume_metadata
req = fakes.HTTPRequest.blank(self.url) req = fakes.HTTPRequest.blank(self.url)
req.method = 'PUT' req.method = 'PUT'
req.content_type = "application/json" req.content_type = "application/json"
@ -336,15 +411,51 @@ class volumeMetaDataTest(test.TestCase):
}, },
} }
req.body = jsonutils.dumps(expected) req.body = jsonutils.dumps(expected)
res_dict = self.controller.update_all(req, self.req_id, expected) req.environ['cinder.context'] = fake_context
self.assertEqual(expected, res_dict) with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.update_all(req, self.req_id, expected)
self.assertEqual(expected, res_dict)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_all_with_keys_in_uppercase_and_lowercase(self): @mock.patch.object(db, 'volume_metadata_update')
self.stubs.Set(db, 'volume_metadata_get', def test_update_all_volume_maintenance(self, metadata_update):
return_create_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'maintenance'}
self.stubs.Set(db, 'volume_metadata_update', fake_context = mock.Mock()
return_new_volume_metadata) metadata_update.side_effect = return_new_volume_metadata
req = fakes.HTTPRequest.blank(self.url)
req.method = 'PUT'
req.content_type = "application/json"
expected = {
'metadata': {
'key10': 'value10',
'key99': 'value99',
'KEY20': 'value20',
},
}
req.body = jsonutils.dumps(expected)
req.environ['cinder.context'] = fake_context
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(exception.InvalidVolume,
self.controller.update_all, req,
self.req_id, expected)
self.assertFalse(metadata_update.called)
get_volume.assert_called_once_with(fake_context, self.req_id)
@mock.patch.object(db, 'volume_metadata_update')
@mock.patch.object(db, 'volume_metadata_get')
def test_update_all_with_keys_in_uppercase_and_lowercase(self,
metadata_get,
metadata_update):
fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_get.side_effect = return_create_volume_metadata
metadata_update.side_effect = return_new_volume_metadata
req = fakes.HTTPRequest.blank(self.url) req = fakes.HTTPRequest.blank(self.url)
req.method = 'PUT' req.method = 'PUT'
req.content_type = "application/json" req.content_type = "application/json"
@ -364,21 +475,33 @@ class volumeMetaDataTest(test.TestCase):
}, },
} }
req.body = jsonutils.dumps(expected) req.body = jsonutils.dumps(expected)
res_dict = self.controller.update_all(req, self.req_id, body) req.environ['cinder.context'] = fake_context
self.assertEqual(expected, res_dict) with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.update_all(req, self.req_id, body)
self.assertEqual(expected, res_dict)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_all_empty_container(self): @mock.patch.object(db, 'volume_metadata_update')
self.stubs.Set(db, 'volume_metadata_update', def test_update_all_empty_container(self, metadata_update):
return_empty_container_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_empty_container_metadata
req = fakes.HTTPRequest.blank(self.url) req = fakes.HTTPRequest.blank(self.url)
req.method = 'PUT' req.method = 'PUT'
req.content_type = "application/json" req.content_type = "application/json"
expected = {'metadata': {}} expected = {'metadata': {}}
req.body = jsonutils.dumps(expected) req.body = jsonutils.dumps(expected)
res_dict = self.controller.update_all(req, self.req_id, expected) req.environ['cinder.context'] = fake_context
self.assertEqual(expected, res_dict) with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.update_all(req, self.req_id, expected)
self.assertEqual(expected, res_dict)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_all_malformed_container(self): def test_update_all_malformed_container(self):
self.stubs.Set(db, 'volume_metadata_update', self.stubs.Set(db, 'volume_metadata_update',
@ -417,17 +540,46 @@ class volumeMetaDataTest(test.TestCase):
self.assertRaises(webob.exc.HTTPNotFound, self.assertRaises(webob.exc.HTTPNotFound,
self.controller.update_all, req, '100', body) self.controller.update_all, req, '100', body)
def test_update_item(self): @mock.patch.object(db, 'volume_metadata_update')
self.stubs.Set(db, 'volume_metadata_update', def test_update_item(self, metadata_update):
return_create_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key1') req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'PUT' req.method = 'PUT'
body = {"meta": {"key1": "value1"}} body = {"meta": {"key1": "value1"}}
req.body = jsonutils.dumps(body) req.body = jsonutils.dumps(body)
req.headers["content-type"] = "application/json" req.headers["content-type"] = "application/json"
res_dict = self.controller.update(req, self.req_id, 'key1', body) req.environ['cinder.context'] = fake_context
expected = {'meta': {'key1': 'value1'}}
self.assertEqual(expected, res_dict) with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
res_dict = self.controller.update(req, self.req_id, 'key1', body)
expected = {'meta': {'key1': 'value1'}}
self.assertEqual(expected, res_dict)
get_volume.assert_called_once_with(fake_context, self.req_id)
@mock.patch.object(db, 'volume_metadata_update')
def test_update_item_volume_maintenance(self, metadata_update):
fake_volume = {'id': self.req_id, 'status': 'maintenance'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'PUT'
body = {"meta": {"key1": "value1"}}
req.body = jsonutils.dumps(body)
req.headers["content-type"] = "application/json"
req.environ['cinder.context'] = fake_context
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(exception.InvalidVolume,
self.controller.update, req,
self.req_id, 'key1', body)
self.assertFalse(metadata_update.called)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_item_nonexistent_volume(self): def test_update_item_nonexistent_volume(self):
self.stubs.Set(db, 'volume_get', self.stubs.Set(db, 'volume_get',
@ -453,43 +605,68 @@ class volumeMetaDataTest(test.TestCase):
self.controller.update, req, self.req_id, 'key1', self.controller.update, req, self.req_id, 'key1',
None) None)
def test_update_item_empty_key(self): @mock.patch.object(db, 'volume_metadata_update')
self.stubs.Set(db, 'volume_metadata_update', def test_update_item_empty_key(self, metadata_update):
return_create_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key1') req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'PUT' req.method = 'PUT'
body = {"meta": {"": "value1"}} body = {"meta": {"": "value1"}}
req.body = jsonutils.dumps(body) req.body = jsonutils.dumps(body)
req.headers["content-type"] = "application/json" req.headers["content-type"] = "application/json"
req.environ['cinder.context'] = fake_context
self.assertRaises(webob.exc.HTTPBadRequest, with mock.patch.object(self.controller.volume_api,
self.controller.update, req, self.req_id, '', body) 'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.update, req, self.req_id,
'', body)
self.assertFalse(metadata_update.called)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_item_key_too_long(self): @mock.patch.object(db, 'volume_metadata_update')
self.stubs.Set(db, 'volume_metadata_update', def test_update_item_key_too_long(self, metadata_update):
return_create_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key1') req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'PUT' req.method = 'PUT'
body = {"meta": {("a" * 260): "value1"}} body = {"meta": {("a" * 260): "value1"}}
req.body = jsonutils.dumps(body) req.body = jsonutils.dumps(body)
req.headers["content-type"] = "application/json" req.headers["content-type"] = "application/json"
req.environ['cinder.context'] = fake_context
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, with mock.patch.object(self.controller.volume_api,
self.controller.update, 'get') as get_volume:
req, self.req_id, ("a" * 260), body) get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
self.controller.update,
req, self.req_id, ("a" * 260), body)
self.assertFalse(metadata_update.called)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_item_value_too_long(self): @mock.patch.object(db, 'volume_metadata_update')
self.stubs.Set(db, 'volume_metadata_update', def test_update_item_value_too_long(self, metadata_update):
return_create_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url + '/key1') req = fakes.HTTPRequest.blank(self.url + '/key1')
req.method = 'PUT' req.method = 'PUT'
body = {"meta": {"key1": ("a" * 260)}} body = {"meta": {"key1": ("a" * 260)}}
req.body = jsonutils.dumps(body) req.body = jsonutils.dumps(body)
req.headers["content-type"] = "application/json" req.headers["content-type"] = "application/json"
req.environ['cinder.context'] = fake_context
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, with mock.patch.object(self.controller.volume_api,
self.controller.update, 'get') as get_volume:
req, self.req_id, "key1", body) get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
self.controller.update,
req, self.req_id, "key1", body)
self.assertFalse(metadata_update.called)
get_volume.assert_called_once_with(fake_context, self.req_id)
def test_update_item_too_many_keys(self): def test_update_item_too_many_keys(self):
self.stubs.Set(db, 'volume_metadata_update', self.stubs.Set(db, 'volume_metadata_update',
@ -517,9 +694,11 @@ class volumeMetaDataTest(test.TestCase):
self.controller.update, req, self.req_id, 'bad', self.controller.update, req, self.req_id, 'bad',
body) body)
def test_invalid_metadata_items_on_create(self): @mock.patch.object(db, 'volume_metadata_update')
self.stubs.Set(db, 'volume_metadata_update', def test_invalid_metadata_items_on_create(self, metadata_update):
return_create_volume_metadata) fake_volume = {'id': self.req_id, 'status': 'available'}
fake_context = mock.Mock()
metadata_update.side_effect = return_create_volume_metadata
req = fakes.HTTPRequest.blank(self.url) req = fakes.HTTPRequest.blank(self.url)
req.method = 'POST' req.method = 'POST'
req.headers["content-type"] = "application/json" req.headers["content-type"] = "application/json"
@ -527,17 +706,32 @@ class volumeMetaDataTest(test.TestCase):
# test for long key # test for long key
data = {"metadata": {"a" * 260: "value1"}} data = {"metadata": {"a" * 260: "value1"}}
req.body = jsonutils.dumps(data) req.body = jsonutils.dumps(data)
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, req.environ['cinder.context'] = fake_context
self.controller.create, req, self.req_id, data)
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
self.controller.create, req, self.req_id, data)
# test for long value # test for long value
data = {"metadata": {"key": "v" * 260}} data = {"metadata": {"key": "v" * 260}}
req.body = jsonutils.dumps(data) req.body = jsonutils.dumps(data)
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge, req.environ['cinder.context'] = fake_context
self.controller.create, req, self.req_id, data)
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPRequestEntityTooLarge,
self.controller.create, req, self.req_id, data)
# test for empty key. # test for empty key.
data = {"metadata": {"": "value1"}} data = {"metadata": {"": "value1"}}
req.body = jsonutils.dumps(data) req.body = jsonutils.dumps(data)
self.assertRaises(webob.exc.HTTPBadRequest, req.environ['cinder.context'] = fake_context
self.controller.create, req, self.req_id, data)
with mock.patch.object(self.controller.volume_api,
'get') as get_volume:
get_volume.return_value = fake_volume
self.assertRaises(webob.exc.HTTPBadRequest,
self.controller.create, req, self.req_id, data)

View File

@ -161,33 +161,39 @@ class VolumeApiTest(test.TestCase):
metadata=None, metadata=None,
attachments=None, attachments=None,
volume_type=stubs.DEFAULT_VOL_TYPE, volume_type=stubs.DEFAULT_VOL_TYPE,
status=stubs.DEFAULT_VOL_STATUS): status=stubs.DEFAULT_VOL_STATUS,
with_migration_status=False):
metadata = metadata or {} metadata = metadata or {}
attachments = attachments or [] attachments = attachments or []
return {'volume': volume = {'volume':
{'attachments': attachments, {'attachments': attachments,
'availability_zone': availability_zone, 'availability_zone': availability_zone,
'bootable': 'false', 'bootable': 'false',
'consistencygroup_id': consistencygroup_id, 'consistencygroup_id': consistencygroup_id,
'created_at': datetime.datetime(1900, 1, 1, 1, 1, 1), 'created_at': datetime.datetime(1900, 1, 1, 1, 1, 1),
'description': description, 'description': description,
'id': stubs.DEFAULT_VOL_ID, 'id': stubs.DEFAULT_VOL_ID,
'links': 'links':
[{'href': 'http://localhost/v2/fakeproject/volumes/1', [{'href': 'http://localhost/v2/fakeproject/volumes/1',
'rel': 'self'}, 'rel': 'self'},
{'href': 'http://localhost/fakeproject/volumes/1', {'href': 'http://localhost/fakeproject/volumes/1',
'rel': 'bookmark'}], 'rel': 'bookmark'}],
'metadata': metadata, 'metadata': metadata,
'name': name, 'name': name,
'replication_status': 'disabled', 'replication_status': 'disabled',
'multiattach': False, 'multiattach': False,
'size': size, 'size': size,
'snapshot_id': snapshot_id, 'snapshot_id': snapshot_id,
'source_volid': source_volid, 'source_volid': source_volid,
'status': status, 'status': status,
'user_id': 'fakeuser', 'user_id': 'fakeuser',
'volume_type': volume_type, 'volume_type': volume_type,
'encrypted': False}} 'encrypted': False}}
if with_migration_status:
volume['volume']['migration_status'] = None
return volume
def _expected_volume_api_create_kwargs(self, snapshot=None, def _expected_volume_api_create_kwargs(self, snapshot=None,
availability_zone=DEFAULT_AZ, availability_zone=DEFAULT_AZ,
@ -652,7 +658,8 @@ class VolumeApiTest(test.TestCase):
'host_name': None, 'host_name': None,
'device': '/', 'device': '/',
}], }],
metadata={'key': 'value', 'readonly': 'True'}) metadata={'key': 'value', 'readonly': 'True'},
with_migration_status=True)
self.assertEqual(expected, res_dict) self.assertEqual(expected, res_dict)
self.assertEqual(2, len(self.notifier.notifications)) self.assertEqual(2, len(self.notifier.notifications))
self.assertTrue(mock_validate.called) self.assertTrue(mock_validate.called)
@ -758,7 +765,8 @@ class VolumeApiTest(test.TestCase):
'host_name': None, 'host_name': None,
'id': '1', 'id': '1',
'volume_id': stubs.DEFAULT_VOL_ID}], 'volume_id': stubs.DEFAULT_VOL_ID}],
metadata={'key': 'value', 'readonly': 'True'}) metadata={'key': 'value', 'readonly': 'True'},
with_migration_status=True)
expected = {'volumes': [exp_vol['volume']]} expected = {'volumes': [exp_vol['volume']]}
self.assertEqual(expected, res_dict) self.assertEqual(expected, res_dict)
@ -1174,7 +1182,8 @@ class VolumeApiTest(test.TestCase):
'server_id': stubs.FAKE_UUID, 'server_id': stubs.FAKE_UUID,
'host_name': None, 'host_name': None,
'device': '/'}], 'device': '/'}],
metadata={'key': 'value', 'readonly': 'True'}) metadata={'key': 'value', 'readonly': 'True'},
with_migration_status=True)
self.assertEqual(expected, res_dict) self.assertEqual(expected, res_dict)
def test_volume_show_with_encrypted_volume(self): def test_volume_show_with_encrypted_volume(self):

View File

@ -28,6 +28,7 @@ from cinder.scheduler import filter_scheduler
from cinder.scheduler import manager from cinder.scheduler import manager
from cinder import test from cinder import test
from cinder.tests.unit import fake_consistencygroup from cinder.tests.unit import fake_consistencygroup
from cinder.tests.unit import utils as tests_utils
CONF = cfg.CONF CONF = cfg.CONF
@ -47,7 +48,7 @@ class SchedulerManagerTestCase(test.TestCase):
self.flags(scheduler_driver=self.driver_cls_name) self.flags(scheduler_driver=self.driver_cls_name)
self.manager = self.manager_cls() self.manager = self.manager_cls()
self.manager._startup_delay = False self.manager._startup_delay = False
self.context = context.RequestContext('fake_user', 'fake_project') self.context = context.get_admin_context()
self.topic = 'fake_topic' self.topic = 'fake_topic'
self.fake_args = (1, 2, 3) self.fake_args = (1, 2, 3)
self.fake_kwargs = {'cat': 'meow', 'dog': 'woof'} self.fake_kwargs = {'cat': 'meow', 'dog': 'woof'}
@ -171,16 +172,42 @@ class SchedulerManagerTestCase(test.TestCase):
{}) {})
self.assertFalse(_mock_sleep.called) self.assertFalse(_mock_sleep.called)
@mock.patch('cinder.db.volume_get')
@mock.patch('cinder.scheduler.driver.Scheduler.host_passes_filters') @mock.patch('cinder.scheduler.driver.Scheduler.host_passes_filters')
@mock.patch('cinder.db.volume_update') @mock.patch('cinder.db.volume_update')
def test_migrate_volume_exception_returns_volume_state( def test_migrate_volume_exception_returns_volume_state(
self, _mock_volume_update, _mock_host_passes): self, _mock_volume_update, _mock_host_passes,
_mock_volume_get):
# Test NoValidHost exception behavior for migrate_volume_to_host. # Test NoValidHost exception behavior for migrate_volume_to_host.
# Puts the volume in 'error_migrating' state and eats the exception. # Puts the volume in 'error_migrating' state and eats the exception.
_mock_host_passes.side_effect = exception.NoValidHost(reason="") fake_updates = {'migration_status': 'error'}
fake_volume_id = 1 self._test_migrate_volume_exception_returns_volume_state(
_mock_volume_update, _mock_host_passes, _mock_volume_get,
'available', fake_updates)
@mock.patch('cinder.db.volume_get')
@mock.patch('cinder.scheduler.driver.Scheduler.host_passes_filters')
@mock.patch('cinder.db.volume_update')
def test_migrate_volume_exception_returns_volume_state_maintenance(
self, _mock_volume_update, _mock_host_passes,
_mock_volume_get):
fake_updates = {'status': 'available',
'migration_status': 'error'}
self._test_migrate_volume_exception_returns_volume_state(
_mock_volume_update, _mock_host_passes, _mock_volume_get,
'maintenance', fake_updates)
def _test_migrate_volume_exception_returns_volume_state(
self, _mock_volume_update, _mock_host_passes,
_mock_volume_get, status, fake_updates):
volume = tests_utils.create_volume(self.context,
status=status,
previous_status='available')
fake_volume_id = volume.id
topic = 'fake_topic' topic = 'fake_topic'
request_spec = {'volume_id': fake_volume_id} request_spec = {'volume_id': fake_volume_id}
_mock_host_passes.side_effect = exception.NoValidHost(reason="")
_mock_volume_get.return_value = volume
self.manager.migrate_volume_to_host(self.context, topic, self.manager.migrate_volume_to_host(self.context, topic,
fake_volume_id, 'host', True, fake_volume_id, 'host', True,
@ -188,7 +215,7 @@ class SchedulerManagerTestCase(test.TestCase):
filter_properties={}) filter_properties={})
_mock_volume_update.assert_called_once_with(self.context, _mock_volume_update.assert_called_once_with(self.context,
fake_volume_id, fake_volume_id,
{'migration_status': None}) fake_updates)
_mock_host_passes.assert_called_once_with(self.context, 'host', _mock_host_passes.assert_called_once_with(self.context, 'host',
request_spec, {}) request_spec, {})
@ -198,24 +225,24 @@ class SchedulerManagerTestCase(test.TestCase):
_mock_vol_update): _mock_vol_update):
# Test NoValidHost exception behavior for retype. # Test NoValidHost exception behavior for retype.
# Puts the volume in original state and eats the exception. # Puts the volume in original state and eats the exception.
fake_volume_id = 1 volume = tests_utils.create_volume(self.context,
status='retyping',
previous_status='in-use')
instance_uuid = '12345678-1234-5678-1234-567812345678'
volume = tests_utils.attach_volume(self.context, volume['id'],
instance_uuid, None, '/dev/fake')
fake_volume_id = volume.id
topic = 'fake_topic' topic = 'fake_topic'
volume_id = fake_volume_id
request_spec = {'volume_id': fake_volume_id, 'volume_type': {'id': 3}, request_spec = {'volume_id': fake_volume_id, 'volume_type': {'id': 3},
'migration_policy': 'on-demand'} 'migration_policy': 'on-demand'}
vol_info = {'id': fake_volume_id, 'status': 'in-use', _mock_vol_get.return_value = volume
'volume_attachment': [{'id': 'fake_id',
'instance_uuid': 'foo',
'attached_host': None}]}
_mock_vol_get.return_value = vol_info
_mock_vol_update.return_value = {'status': 'in-use'} _mock_vol_update.return_value = {'status': 'in-use'}
_mock_find_retype_host = mock.Mock( _mock_find_retype_host = mock.Mock(
side_effect=exception.NoValidHost(reason="")) side_effect=exception.NoValidHost(reason=""))
orig_retype = self.manager.driver.find_retype_host orig_retype = self.manager.driver.find_retype_host
self.manager.driver.find_retype_host = _mock_find_retype_host self.manager.driver.find_retype_host = _mock_find_retype_host
self.manager.retype(self.context, topic, volume_id, self.manager.retype(self.context, topic, fake_volume_id,
request_spec=request_spec, request_spec=request_spec,
filter_properties={}) filter_properties={})

View File

@ -233,6 +233,7 @@ class VolumeTestCase(BaseVolumeTestCase):
self._clear_patch = mock.patch('cinder.volume.utils.clear_volume', self._clear_patch = mock.patch('cinder.volume.utils.clear_volume',
autospec=True) autospec=True)
self._clear_patch.start() self._clear_patch.start()
self.expected_status = 'available'
def tearDown(self): def tearDown(self):
super(VolumeTestCase, self).tearDown() super(VolumeTestCase, self).tearDown()
@ -639,6 +640,22 @@ class VolumeTestCase(BaseVolumeTestCase):
False, False,
FAKE_METADATA_TYPE.fake_type) FAKE_METADATA_TYPE.fake_type)
def test_update_volume_metadata_maintenance(self):
"""Test update volume metadata with different metadata type."""
test_meta1 = {'fake_key1': 'fake_value1'}
FAKE_METADATA_TYPE = enum.Enum('METADATA_TYPES', 'fake_type')
volume = tests_utils.create_volume(self.context, metadata=test_meta1,
**self.volume_params)
volume['status'] = 'maintenance'
volume_api = cinder.volume.api.API()
self.assertRaises(exception.InvalidVolume,
volume_api.update_volume_metadata,
self.context,
volume,
test_meta1,
False,
FAKE_METADATA_TYPE.fake_type)
def test_delete_volume_metadata_with_metatype(self): def test_delete_volume_metadata_with_metatype(self):
"""Test delete volume metadata with different metadata type.""" """Test delete volume metadata with different metadata type."""
test_meta1 = {'fake_key1': 'fake_value1', 'fake_key2': 'fake_value2'} test_meta1 = {'fake_key1': 'fake_value1', 'fake_key2': 'fake_value2'}
@ -693,6 +710,85 @@ class VolumeTestCase(BaseVolumeTestCase):
'fake_key1', 'fake_key1',
FAKE_METADATA_TYPE.fake_type) FAKE_METADATA_TYPE.fake_type)
def test_delete_volume_metadata_maintenance(self):
"""Test delete volume metadata in maintenance."""
FAKE_METADATA_TYPE = enum.Enum('METADATA_TYPES', 'fake_type')
test_meta1 = {'fake_key1': 'fake_value1', 'fake_key2': 'fake_value2'}
volume = tests_utils.create_volume(self.context, metadata=test_meta1,
**self.volume_params)
volume['status'] = 'maintenance'
volume_api = cinder.volume.api.API()
self.assertRaises(exception.InvalidVolume,
volume_api.delete_volume_metadata,
self.context,
volume,
'fake_key1',
FAKE_METADATA_TYPE.fake_type)
def test_volume_attach_in_maintenance(self):
"""Test attach the volume in maintenance."""
test_meta1 = {'fake_key1': 'fake_value1', 'fake_key2': 'fake_value2'}
volume = tests_utils.create_volume(self.context, metadata=test_meta1,
**self.volume_params)
volume['status'] = 'maintenance'
volume_api = cinder.volume.api.API()
self.assertRaises(exception.InvalidVolume,
volume_api.attach,
self.context,
volume, None, None, None, None)
def test_volume_detach_in_maintenance(self):
"""Test detach the volume in maintenance."""
test_meta1 = {'fake_key1': 'fake_value1', 'fake_key2': 'fake_value2'}
volume = tests_utils.create_volume(self.context, metadata=test_meta1,
**self.volume_params)
volume['status'] = 'maintenance'
volume_api = cinder.volume.api.API()
self.assertRaises(exception.InvalidVolume,
volume_api.detach,
self.context,
volume, None)
def test_initialize_connection_maintenance(self):
"""Test initialize connection in maintenance."""
test_meta1 = {'fake_key1': 'fake_value1', 'fake_key2': 'fake_value2'}
volume = tests_utils.create_volume(self.context, metadata=test_meta1,
**self.volume_params)
volume['status'] = 'maintenance'
volume_api = cinder.volume.api.API()
self.assertRaises(exception.InvalidVolume,
volume_api.initialize_connection,
self.context,
volume,
None)
def test_accept_transfer_maintenance(self):
"""Test accept transfer in maintenance."""
test_meta1 = {'fake_key1': 'fake_value1', 'fake_key2': 'fake_value2'}
volume = tests_utils.create_volume(self.context, metadata=test_meta1,
**self.volume_params)
volume['status'] = 'maintenance'
volume_api = cinder.volume.api.API()
self.assertRaises(exception.InvalidVolume,
volume_api.accept_transfer,
self.context,
volume,
None, None)
def test_copy_volume_to_image_maintenance(self):
"""Test copy volume to image in maintenance."""
test_meta1 = {'fake_key1': 'fake_value1', 'fake_key2': 'fake_value2'}
volume = tests_utils.create_volume(self.context, metadata=test_meta1,
**self.volume_params)
volume['status'] = 'maintenance'
volume_api = cinder.volume.api.API()
self.assertRaises(exception.InvalidVolume,
volume_api.copy_volume_to_image,
self.context,
volume,
test_meta1,
force=True)
@mock.patch.object(cinder.volume.api.API, 'list_availability_zones') @mock.patch.object(cinder.volume.api.API, 'list_availability_zones')
def test_create_volume_uses_default_availability_zone(self, mock_list_az): def test_create_volume_uses_default_availability_zone(self, mock_list_az):
"""Test setting availability_zone correctly during volume create.""" """Test setting availability_zone correctly during volume create."""
@ -2815,10 +2911,16 @@ class VolumeTestCase(BaseVolumeTestCase):
self.assertTrue(volume_get.called) self.assertTrue(volume_get.called)
self.assertTrue(volume_update.called) self.assertTrue(volume_update.called)
def test_reserve_volume_bad_status(self): def test_reserve_volume_in_attaching(self):
self._test_reserve_volume_bad_status('attaching')
def test_reserve_volume_in_maintenance(self):
self._test_reserve_volume_bad_status('maintenance')
def _test_reserve_volume_bad_status(self, status):
fake_volume = { fake_volume = {
'id': self.FAKE_UUID, 'id': self.FAKE_UUID,
'status': 'attaching' 'status': status
} }
with mock.patch.object(db, 'volume_get') as mock_volume_get: with mock.patch.object(db, 'volume_get') as mock_volume_get:
@ -3017,6 +3119,21 @@ class VolumeTestCase(BaseVolumeTestCase):
'fake_name', 'fake_name',
'fake_description') 'fake_description')
def test_create_snapshot_failed_maintenance(self):
"""Test exception handling when create snapshot in maintenance."""
test_volume = tests_utils.create_volume(
self.context,
**self.volume_params)
self.volume.create_volume(self.context, test_volume['id'])
test_volume['status'] = 'maintenance'
volume_api = cinder.volume.api.API()
self.assertRaises(exception.InvalidVolume,
volume_api.create_snapshot,
self.context,
test_volume,
'fake_name',
'fake_description')
@mock.patch.object(QUOTAS, 'commit', @mock.patch.object(QUOTAS, 'commit',
side_effect=exception.QuotaError( side_effect=exception.QuotaError(
'Snapshot quota commit failed!')) 'Snapshot quota commit failed!'))
@ -3037,11 +3154,19 @@ class VolumeTestCase(BaseVolumeTestCase):
'fake_description') 'fake_description')
def test_cannot_delete_volume_in_use(self): def test_cannot_delete_volume_in_use(self):
"""Test volume can't be deleted in in-use status."""
self._test_cannot_delete_volume('in-use')
def test_cannot_delete_volume_maintenance(self):
"""Test volume can't be deleted in maintenance status."""
self._test_cannot_delete_volume('maintenance')
def _test_cannot_delete_volume(self, status):
"""Test volume can't be deleted in invalid stats.""" """Test volume can't be deleted in invalid stats."""
# create a volume and assign to host # create a volume and assign to 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['id']) self.volume.create_volume(self.context, volume['id'])
volume['status'] = 'in-use' volume['status'] = status
volume['host'] = 'fakehost' volume['host'] = 'fakehost'
volume_api = cinder.volume.api.API() volume_api = cinder.volume.api.API()
@ -3673,6 +3798,12 @@ class VolumeTestCase(BaseVolumeTestCase):
volume_api.begin_detaching(self.context, volume) volume_api.begin_detaching(self.context, volume)
volume_get.assert_called_once_with(self.context, volume['id']) volume_get.assert_called_once_with(self.context, volume['id'])
volume_get.reset_mock()
volume['status'] = "maintenance"
self.assertRaises(exception.InvalidVolume, volume_api.begin_detaching,
self.context, volume)
volume_get.assert_called_once_with(self.context, volume['id'])
def test_begin_roll_detaching_volume(self): def test_begin_roll_detaching_volume(self):
"""Test begin_detaching and roll_detaching functions.""" """Test begin_detaching and roll_detaching functions."""
@ -3702,6 +3833,16 @@ class VolumeTestCase(BaseVolumeTestCase):
vol = db.volume_get(context.get_admin_context(), volume['id']) vol = db.volume_get(context.get_admin_context(), volume['id'])
self.assertEqual('test update name', vol['display_name']) self.assertEqual('test update name', vol['display_name'])
def test_volume_api_update_maintenance(self):
# create a raw vol
volume = tests_utils.create_volume(self.context, **self.volume_params)
volume['status'] = 'maintenance'
# use volume.api to update name
volume_api = cinder.volume.api.API()
update_dict = {'display_name': 'test update name'}
self.assertRaises(exception.InvalidVolume, volume_api.update,
self.context, volume, update_dict)
def test_volume_api_update_snapshot(self): def test_volume_api_update_snapshot(self):
# create raw snapshot # create raw snapshot
volume = tests_utils.create_volume(self.context, **self.volume_params) volume = tests_utils.create_volume(self.context, **self.volume_params)
@ -4073,14 +4214,14 @@ class VolumeTestCase(BaseVolumeTestCase):
# check volume properties # check volume properties
volume = db.volume_get(context.get_admin_context(), volume['id']) volume = db.volume_get(context.get_admin_context(), volume['id'])
self.assertEqual('newhost', volume['host']) self.assertEqual('newhost', volume['host'])
self.assertIsNone(volume['migration_status']) self.assertEqual('success', volume['migration_status'])
def _fake_create_volume(self, ctxt, volume, host, req_spec, filters,
allow_reschedule=True):
return db.volume_update(ctxt, volume['id'],
{'status': self.expected_status})
def test_migrate_volume_error(self): def test_migrate_volume_error(self):
def fake_create_volume(ctxt, volume, host, req_spec, filters,
allow_reschedule=True):
db.volume_update(ctxt, volume['id'],
{'status': 'available'})
with mock.patch.object(self.volume.driver, 'migrate_volume') as \ with mock.patch.object(self.volume.driver, 'migrate_volume') as \
mock_migrate,\ mock_migrate,\
mock.patch.object(self.volume.driver, 'create_export') as \ mock.patch.object(self.volume.driver, 'create_export') as \
@ -4099,7 +4240,7 @@ class VolumeTestCase(BaseVolumeTestCase):
host_obj, host_obj,
False) False)
volume = db.volume_get(context.get_admin_context(), volume['id']) volume = db.volume_get(context.get_admin_context(), volume['id'])
self.assertIsNone(volume['migration_status']) self.assertEqual('error', volume['migration_status'])
self.assertEqual('available', volume['status']) self.assertEqual('available', volume['status'])
@mock.patch.object(nova.API, 'update_server_volume') @mock.patch.object(nova.API, 'update_server_volume')
@ -4168,35 +4309,25 @@ class VolumeTestCase(BaseVolumeTestCase):
fake_volume = tests_utils.create_volume(self.context, size=1, fake_volume = tests_utils.create_volume(self.context, size=1,
host=CONF.host) host=CONF.host)
def fake_create_volume(ctxt, volume, host, req_spec, filters,
allow_reschedule=True):
db.volume_update(ctxt, volume['id'],
{'status': 'available'})
host_obj = {'host': 'newhost', 'capabilities': {}} host_obj = {'host': 'newhost', 'capabilities': {}}
with mock.patch.object(self.volume.driver, 'migrate_volume') as \ with mock.patch.object(self.volume.driver, 'migrate_volume') as \
mock_migrate_volume,\ mock_migrate_volume,\
mock.patch.object(self.volume.driver, 'copy_volume_data'), \ mock.patch.object(self.volume.driver, 'copy_volume_data'), \
mock.patch.object(self.volume.driver, 'delete_volume') as \ mock.patch.object(self.volume.driver, 'delete_volume') as \
delete_volume: delete_volume:
create_volume.side_effect = fake_create_volume create_volume.side_effect = self._fake_create_volume
self.volume.migrate_volume(self.context, fake_volume['id'], self.volume.migrate_volume(self.context, fake_volume['id'],
host_obj, True) host_obj, True)
volume = db.volume_get(context.get_admin_context(), volume = db.volume_get(context.get_admin_context(),
fake_volume['id']) fake_volume['id'])
self.assertEqual('newhost', volume['host']) self.assertEqual('newhost', volume['host'])
self.assertIsNone(volume['migration_status']) self.assertEqual('success', volume['migration_status'])
self.assertFalse(mock_migrate_volume.called) self.assertFalse(mock_migrate_volume.called)
self.assertFalse(delete_volume.called) self.assertFalse(delete_volume.called)
self.assertTrue(rpc_delete_volume.called) self.assertTrue(rpc_delete_volume.called)
self.assertTrue(update_migrated_volume.called) self.assertTrue(update_migrated_volume.called)
def test_migrate_volume_generic_copy_error(self): def test_migrate_volume_generic_copy_error(self):
def fake_create_volume(ctxt, volume, host, req_spec, filters,
allow_reschedule=True):
db.volume_update(ctxt, volume['id'],
{'status': 'available'})
with mock.patch.object(self.volume.driver, 'migrate_volume'),\ with mock.patch.object(self.volume.driver, 'migrate_volume'),\
mock.patch.object(volume_rpcapi.VolumeAPI, 'create_volume')\ mock.patch.object(volume_rpcapi.VolumeAPI, 'create_volume')\
as mock_create_volume,\ as mock_create_volume,\
@ -4208,7 +4339,7 @@ class VolumeTestCase(BaseVolumeTestCase):
# Exception case at migrate_volume_generic # Exception case at migrate_volume_generic
# source_volume['migration_status'] is 'migrating' # source_volume['migration_status'] is 'migrating'
mock_create_volume.side_effect = fake_create_volume mock_create_volume.side_effect = self._fake_create_volume
mock_copy_volume.side_effect = processutils.ProcessExecutionError mock_copy_volume.side_effect = processutils.ProcessExecutionError
volume = tests_utils.create_volume(self.context, size=0, volume = tests_utils.create_volume(self.context, size=0,
host=CONF.host) host=CONF.host)
@ -4220,7 +4351,7 @@ class VolumeTestCase(BaseVolumeTestCase):
host_obj, host_obj,
True) True)
volume = db.volume_get(context.get_admin_context(), volume['id']) volume = db.volume_get(context.get_admin_context(), volume['id'])
self.assertIsNone(volume['migration_status']) self.assertEqual('error', volume['migration_status'])
self.assertEqual('available', volume['status']) self.assertEqual('available', volume['status'])
def test_clean_temporary_volume(self): def test_clean_temporary_volume(self):
@ -4268,11 +4399,7 @@ class VolumeTestCase(BaseVolumeTestCase):
self.assertIsNone(volume['migration_status']) self.assertIsNone(volume['migration_status'])
def test_migrate_volume_generic_create_volume_error(self): def test_migrate_volume_generic_create_volume_error(self):
def fake_create_volume(ctxt, volume, host, req_spec, filters, self.expected_status = 'error'
allow_reschedule=True):
db.volume_update(ctxt, volume['id'],
{'status': 'error'})
with mock.patch.object(self.volume.driver, 'migrate_volume'), \ with mock.patch.object(self.volume.driver, 'migrate_volume'), \
mock.patch.object(volume_rpcapi.VolumeAPI, 'create_volume') as \ mock.patch.object(volume_rpcapi.VolumeAPI, 'create_volume') as \
mock_create_volume, \ mock_create_volume, \
@ -4280,7 +4407,7 @@ class VolumeTestCase(BaseVolumeTestCase):
clean_temporary_volume: clean_temporary_volume:
# Exception case at the creation of the new temporary volume # Exception case at the creation of the new temporary volume
mock_create_volume.side_effect = fake_create_volume mock_create_volume.side_effect = self._fake_create_volume
volume = tests_utils.create_volume(self.context, size=0, volume = tests_utils.create_volume(self.context, size=0,
host=CONF.host) host=CONF.host)
host_obj = {'host': 'newhost', 'capabilities': {}} host_obj = {'host': 'newhost', 'capabilities': {}}
@ -4291,18 +4418,14 @@ class VolumeTestCase(BaseVolumeTestCase):
host_obj, host_obj,
True) True)
volume = db.volume_get(context.get_admin_context(), volume['id']) volume = db.volume_get(context.get_admin_context(), volume['id'])
self.assertIsNone(volume['migration_status']) self.assertEqual('error', volume['migration_status'])
self.assertEqual('available', volume['status']) self.assertEqual('available', volume['status'])
self.assertTrue(clean_temporary_volume.called) self.assertTrue(clean_temporary_volume.called)
self.expected_status = 'available'
def test_migrate_volume_generic_timeout_error(self): def test_migrate_volume_generic_timeout_error(self):
CONF.set_override("migration_create_volume_timeout_secs", 2) CONF.set_override("migration_create_volume_timeout_secs", 2)
def fake_create_volume(ctxt, volume, host, req_spec, filters,
allow_reschedule=True):
db.volume_update(ctxt, volume['id'],
{'status': 'creating'})
with mock.patch.object(self.volume.driver, 'migrate_volume'), \ with mock.patch.object(self.volume.driver, 'migrate_volume'), \
mock.patch.object(volume_rpcapi.VolumeAPI, 'create_volume') as \ mock.patch.object(volume_rpcapi.VolumeAPI, 'create_volume') as \
mock_create_volume, \ mock_create_volume, \
@ -4311,7 +4434,8 @@ class VolumeTestCase(BaseVolumeTestCase):
mock.patch.object(time, 'sleep'): mock.patch.object(time, 'sleep'):
# Exception case at the timeout of the volume creation # Exception case at the timeout of the volume creation
mock_create_volume.side_effect = fake_create_volume self.expected_status = 'creating'
mock_create_volume.side_effect = self._fake_create_volume
volume = tests_utils.create_volume(self.context, size=0, volume = tests_utils.create_volume(self.context, size=0,
host=CONF.host) host=CONF.host)
host_obj = {'host': 'newhost', 'capabilities': {}} host_obj = {'host': 'newhost', 'capabilities': {}}
@ -4322,16 +4446,12 @@ class VolumeTestCase(BaseVolumeTestCase):
host_obj, host_obj,
True) True)
volume = db.volume_get(context.get_admin_context(), volume['id']) volume = db.volume_get(context.get_admin_context(), volume['id'])
self.assertIsNone(volume['migration_status']) self.assertEqual('error', volume['migration_status'])
self.assertEqual('available', volume['status']) self.assertEqual('available', volume['status'])
self.assertTrue(clean_temporary_volume.called) self.assertTrue(clean_temporary_volume.called)
self.expected_status = 'available'
def test_migrate_volume_generic_create_export_error(self): def test_migrate_volume_generic_create_export_error(self):
def fake_create_volume(ctxt, volume, host, req_spec, filters,
allow_reschedule=True):
db.volume_update(ctxt, volume['id'],
{'status': 'available'})
with mock.patch.object(self.volume.driver, 'migrate_volume'),\ with mock.patch.object(self.volume.driver, 'migrate_volume'),\
mock.patch.object(volume_rpcapi.VolumeAPI, 'create_volume')\ mock.patch.object(volume_rpcapi.VolumeAPI, 'create_volume')\
as mock_create_volume,\ as mock_create_volume,\
@ -4343,7 +4463,7 @@ class VolumeTestCase(BaseVolumeTestCase):
mock_create_export: mock_create_export:
# Exception case at create_export # Exception case at create_export
mock_create_volume.side_effect = fake_create_volume mock_create_volume.side_effect = self._fake_create_volume
mock_copy_volume.side_effect = processutils.ProcessExecutionError mock_copy_volume.side_effect = processutils.ProcessExecutionError
mock_create_export.side_effect = processutils.ProcessExecutionError mock_create_export.side_effect = processutils.ProcessExecutionError
volume = tests_utils.create_volume(self.context, size=0, volume = tests_utils.create_volume(self.context, size=0,
@ -4356,15 +4476,10 @@ class VolumeTestCase(BaseVolumeTestCase):
host_obj, host_obj,
True) True)
volume = db.volume_get(context.get_admin_context(), volume['id']) volume = db.volume_get(context.get_admin_context(), volume['id'])
self.assertIsNone(volume['migration_status']) self.assertEqual('error', volume['migration_status'])
self.assertEqual('available', volume['status']) self.assertEqual('available', volume['status'])
def test_migrate_volume_generic_migrate_volume_completion_error(self): def test_migrate_volume_generic_migrate_volume_completion_error(self):
def fake_create_volume(ctxt, volume, host, req_spec, filters,
allow_reschedule=True):
db.volume_update(ctxt, volume['id'],
{'status': 'available'})
def fake_migrate_volume_completion(ctxt, volume_id, new_volume_id, def fake_migrate_volume_completion(ctxt, volume_id, new_volume_id,
error=False): error=False):
db.volume_update(ctxt, volume['id'], db.volume_update(ctxt, volume['id'],
@ -4382,7 +4497,7 @@ class VolumeTestCase(BaseVolumeTestCase):
# Exception case at delete_volume # Exception case at delete_volume
# source_volume['migration_status'] is 'completing' # source_volume['migration_status'] is 'completing'
mock_create_volume.side_effect = fake_create_volume mock_create_volume.side_effect = self._fake_create_volume
mock_migrate_compl.side_effect = fake_migrate_volume_completion mock_migrate_compl.side_effect = fake_migrate_volume_completion
volume = tests_utils.create_volume(self.context, size=0, volume = tests_utils.create_volume(self.context, size=0,
host=CONF.host) host=CONF.host)
@ -4394,12 +4509,13 @@ class VolumeTestCase(BaseVolumeTestCase):
host_obj, host_obj,
True) True)
volume = db.volume_get(context.get_admin_context(), volume['id']) volume = db.volume_get(context.get_admin_context(), volume['id'])
self.assertIsNone(volume['migration_status']) self.assertEqual('error', volume['migration_status'])
self.assertEqual('available', volume['status']) self.assertEqual('available', volume['status'])
def _test_migrate_volume_completion(self, status='available', def _test_migrate_volume_completion(self, status='available',
instance_uuid=None, attached_host=None, instance_uuid=None, attached_host=None,
retyping=False): retyping=False,
previous_status='available'):
def fake_attach_volume(ctxt, volume, instance_uuid, host_name, def fake_attach_volume(ctxt, volume, instance_uuid, host_name,
mountpoint, mode): mountpoint, mode):
tests_utils.attach_volume(ctxt, volume['id'], tests_utils.attach_volume(ctxt, volume['id'],
@ -4410,7 +4526,8 @@ class VolumeTestCase(BaseVolumeTestCase):
old_volume = tests_utils.create_volume(self.context, size=0, old_volume = tests_utils.create_volume(self.context, size=0,
host=CONF.host, host=CONF.host,
status=initial_status, status=initial_status,
migration_status='migrating') migration_status='migrating',
previous_status=previous_status)
attachment_id = None attachment_id = None
if status == 'in-use': if status == 'in-use':
vol = tests_utils.attach_volume(self.context, old_volume['id'], vol = tests_utils.attach_volume(self.context, old_volume['id'],
@ -4460,7 +4577,8 @@ class VolumeTestCase(BaseVolumeTestCase):
'in-use', 'in-use',
'83c969d5-065e-4c9c-907d-5394bc2e98e2', '83c969d5-065e-4c9c-907d-5394bc2e98e2',
'some-host', 'some-host',
retyping=True) retyping=True,
previous_status='in-use')
def test_migrate_volume_completion_migrate_available(self): def test_migrate_volume_completion_migrate_available(self):
self._test_migrate_volume_completion() self._test_migrate_volume_completion()
@ -4469,7 +4587,9 @@ class VolumeTestCase(BaseVolumeTestCase):
self._test_migrate_volume_completion( self._test_migrate_volume_completion(
'in-use', 'in-use',
'83c969d5-065e-4c9c-907d-5394bc2e98e2', '83c969d5-065e-4c9c-907d-5394bc2e98e2',
'some-host') 'some-host',
retyping=False,
previous_status='in-use')
def test_retype_setup_fail_volume_is_available(self): def test_retype_setup_fail_volume_is_available(self):
"""Verify volume is still available if retype prepare failed.""" """Verify volume is still available if retype prepare failed."""
@ -4513,6 +4633,7 @@ class VolumeTestCase(BaseVolumeTestCase):
host=CONF.host, status='retyping', host=CONF.host, status='retyping',
volume_type_id=old_vol_type['id'], volume_type_id=old_vol_type['id'],
replication_status=rep_status) replication_status=rep_status)
volume['previous_status'] = 'available'
if snap: if snap:
self._create_snapshot(volume['id'], size=volume['size']) self._create_snapshot(volume['id'], size=volume['size'])
if driver or diff_equal: if driver or diff_equal:
@ -4528,27 +4649,30 @@ class VolumeTestCase(BaseVolumeTestCase):
project_id=project_id, project_id=project_id,
**reserve_opts) **reserve_opts)
with mock.patch.object(self.volume.driver, 'retype') as _retype: with mock.patch.object(self.volume.driver, 'retype') as _retype,\
with mock.patch.object(volume_types, 'volume_types_diff') as _diff: mock.patch.object(volume_types, 'volume_types_diff') as _diff,\
with mock.patch.object(self.volume, 'migrate_volume') as _mig: mock.patch.object(self.volume, 'migrate_volume') as _mig,\
_retype.return_value = driver mock.patch.object(db, 'volume_get') as get_volume:
_diff.return_value = ({}, diff_equal) get_volume.return_value = volume
if migrate_exc: _retype.return_value = driver
_mig.side_effect = KeyError _diff.return_value = ({}, diff_equal)
else: if migrate_exc:
_mig.return_value = True _mig.side_effect = KeyError
else:
_mig.return_value = True
if not exc: if not exc:
self.volume.retype(self.context, volume['id'], self.volume.retype(self.context, volume['id'],
vol_type['id'], host_obj, vol_type['id'], host_obj,
migration_policy=policy, migration_policy=policy,
reservations=reservations) reservations=reservations)
else: else:
self.assertRaises(exc, self.volume.retype, self.assertRaises(exc, self.volume.retype,
self.context, volume['id'], self.context, volume['id'],
vol_type['id'], host_obj, vol_type['id'], host_obj,
migration_policy=policy, migration_policy=policy,
reservations=reservations) reservations=reservations)
get_volume.assert_called_once_with(self.context, volume['id'])
# get volume/quota properties # get volume/quota properties
volume = db.volume_get(elevated, volume['id']) volume = db.volume_get(elevated, volume['id'])

View File

@ -49,6 +49,7 @@ from oslo_config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from oslo_utils import encodeutils from oslo_utils import encodeutils
from oslo_utils import importutils from oslo_utils import importutils
from oslo_utils import strutils
from oslo_utils import timeutils from oslo_utils import timeutils
import retrying import retrying
import six import six
@ -662,6 +663,16 @@ def _get_disk_of_partition(devpath, st=None):
return (devpath, st) return (devpath, st)
def get_bool_param(param_string, params):
param = params.get(param_string, False)
if not is_valid_boolstr(param):
msg = _('Value %(param)s for %(param_string)s is not a '
'boolean.') % {'param': param, 'param_string': param_string}
raise exception.InvalidParameterValue(err=msg)
return strutils.bool_from_string(param, strict=True)
def get_blkdev_major_minor(path, lookup_for_file=True): def get_blkdev_major_minor(path, lookup_for_file=True):
"""Get 'major:minor' number of block device. """Get 'major:minor' number of block device.

View File

@ -185,6 +185,17 @@ class API(base.Base):
safe = True safe = True
return safe return safe
def _is_volume_migrating(self, volume):
# The migration status 'none' means no migration has ever been done
# before. The migration status 'error' means the previous migration
# failed. The migration status 'success' means the previous migration
# succeeded. The migration status 'deleting' means the source volume
# fails to delete after a migration.
# All of the statuses above means the volume is not in the process
# of a migration.
return volume['migration_status'] not in (None, 'deleting',
'error', 'success')
def create(self, context, size, name, description, snapshot=None, def create(self, context, size, name, description, snapshot=None,
image_id=None, volume_type=None, metadata=None, image_id=None, volume_type=None, metadata=None,
availability_zone=None, source_volume=None, availability_zone=None, source_volume=None,
@ -355,7 +366,7 @@ class API(base.Base):
'vol_status': volume['status']}) 'vol_status': volume['status']})
raise exception.InvalidVolume(reason=msg) raise exception.InvalidVolume(reason=msg)
if volume['migration_status'] not in (None, 'deleting'): if self._is_volume_migrating(volume):
# Volume is migrating, wait until done # Volume is migrating, wait until done
LOG.info(_LI('Unable to delete volume: %s, ' LOG.info(_LI('Unable to delete volume: %s, '
'volume is currently migrating.'), volume['id']) 'volume is currently migrating.'), volume['id'])
@ -397,6 +408,12 @@ class API(base.Base):
@wrap_check_policy @wrap_check_policy
def update(self, context, volume, fields): def update(self, context, volume, fields):
if volume['status'] == 'maintenance':
LOG.info(_LI("Unable to update volume, "
"because it is in maintenance."), resource=volume)
msg = _("The volume cannot be updated during maintenance.")
raise exception.InvalidVolume(reason=msg)
vref = self.db.volume_update(context, volume['id'], fields) vref = self.db.volume_update(context, volume['id'], fields)
LOG.info(_LI("Volume updated successfully."), resource=vref) LOG.info(_LI("Volume updated successfully."), resource=vref)
@ -572,7 +589,7 @@ class API(base.Base):
# If we are in the middle of a volume migration, we don't want the user # If we are in the middle of a volume migration, we don't want the user
# to see that the volume is 'detaching'. Having 'migration_status' set # to see that the volume is 'detaching'. Having 'migration_status' set
# will have the same effect internally. # will have the same effect internally.
if volume['migration_status']: if self._is_volume_migrating(volume):
return return
if (volume['status'] != 'in-use' or if (volume['status'] != 'in-use' or
@ -599,6 +616,11 @@ class API(base.Base):
@wrap_check_policy @wrap_check_policy
def attach(self, context, volume, instance_uuid, host_name, def attach(self, context, volume, instance_uuid, host_name,
mountpoint, mode): mountpoint, mode):
if volume['status'] == 'maintenance':
LOG.info(_LI('Unable to attach volume, '
'because it is in maintenance.'), resource=volume)
msg = _("The volume cannot be attached in maintenance mode.")
raise exception.InvalidVolume(reason=msg)
volume_metadata = self.get_volume_admin_metadata(context.elevated(), volume_metadata = self.get_volume_admin_metadata(context.elevated(),
volume) volume)
if 'readonly' not in volume_metadata: if 'readonly' not in volume_metadata:
@ -623,6 +645,11 @@ class API(base.Base):
@wrap_check_policy @wrap_check_policy
def detach(self, context, volume, attachment_id): def detach(self, context, volume, attachment_id):
if volume['status'] == 'maintenance':
LOG.info(_LI('Unable to detach volume, '
'because it is in maintenance.'), resource=volume)
msg = _("The volume cannot be detached in maintenance mode.")
raise exception.InvalidVolume(reason=msg)
detach_results = self.volume_rpcapi.detach_volume(context, volume, detach_results = self.volume_rpcapi.detach_volume(context, volume,
attachment_id) attachment_id)
LOG.info(_LI("Detach volume completed successfully."), LOG.info(_LI("Detach volume completed successfully."),
@ -631,6 +658,13 @@ class API(base.Base):
@wrap_check_policy @wrap_check_policy
def initialize_connection(self, context, volume, connector): def initialize_connection(self, context, volume, connector):
if volume['status'] == 'maintenance':
LOG.info(_LI('Unable to initialize the connection for '
'volume, because it is in '
'maintenance.'), resource=volume)
msg = _("The volume connection cannot be initialized in "
"maintenance mode.")
raise exception.InvalidVolume(reason=msg)
init_results = self.volume_rpcapi.initialize_connection(context, init_results = self.volume_rpcapi.initialize_connection(context,
volume, volume,
connector) connector)
@ -651,6 +685,11 @@ class API(base.Base):
@wrap_check_policy @wrap_check_policy
def accept_transfer(self, context, volume, new_user, new_project): def accept_transfer(self, context, volume, new_user, new_project):
if volume['status'] == 'maintenance':
LOG.info(_LI('Unable to accept transfer for volume, '
'because it is in maintenance.'), resource=volume)
msg = _("The volume cannot accept transfer in maintenance mode.")
raise exception.InvalidVolume(reason=msg)
results = self.volume_rpcapi.accept_transfer(context, results = self.volume_rpcapi.accept_transfer(context,
volume, volume,
new_user, new_user,
@ -676,7 +715,13 @@ class API(base.Base):
cgsnapshot_id): cgsnapshot_id):
check_policy(context, 'create_snapshot', volume) check_policy(context, 'create_snapshot', volume)
if volume['migration_status'] is not None: if volume['status'] == 'maintenance':
LOG.info(_LI('Unable to create the snapshot for volume, '
'because it is in maintenance.'), resource=volume)
msg = _("The snapshot cannot be created when the volume is in "
"maintenance mode.")
raise exception.InvalidVolume(reason=msg)
if self._is_volume_migrating(volume):
# Volume is migrating, wait until done # Volume is migrating, wait until done
msg = _("Snapshot cannot be created while volume is migrating.") msg = _("Snapshot cannot be created while volume is migrating.")
raise exception.InvalidVolume(reason=msg) raise exception.InvalidVolume(reason=msg)
@ -802,7 +847,13 @@ class API(base.Base):
def _create_snapshot_in_db_validate(self, context, volume, force): def _create_snapshot_in_db_validate(self, context, volume, force):
check_policy(context, 'create_snapshot', volume) check_policy(context, 'create_snapshot', volume)
if volume['migration_status'] is not None: if volume['status'] == 'maintenance':
LOG.info(_LI('Unable to create the snapshot for volume, '
'because it is in maintenance.'), resource=volume)
msg = _("The snapshot cannot be created when the volume is in "
"maintenance mode.")
raise exception.InvalidVolume(reason=msg)
if self._is_volume_migrating(volume):
# Volume is migrating, wait until done # Volume is migrating, wait until done
msg = _("Snapshot cannot be created while volume is migrating.") msg = _("Snapshot cannot be created while volume is migrating.")
raise exception.InvalidVolume(reason=msg) raise exception.InvalidVolume(reason=msg)
@ -927,6 +978,12 @@ class API(base.Base):
def delete_volume_metadata(self, context, volume, def delete_volume_metadata(self, context, volume,
key, meta_type=common.METADATA_TYPES.user): key, meta_type=common.METADATA_TYPES.user):
"""Delete the given metadata item from a volume.""" """Delete the given metadata item from a volume."""
if volume['status'] == 'maintenance':
LOG.info(_LI('Unable to delete the volume metadata, '
'because it is in maintenance.'), resource=volume)
msg = _("The volume metadata cannot be deleted when the volume "
"is in maintenance mode.")
raise exception.InvalidVolume(reason=msg)
self.db.volume_metadata_delete(context, volume['id'], key, meta_type) self.db.volume_metadata_delete(context, volume['id'], key, meta_type)
LOG.info(_LI("Delete volume metadata completed successfully."), LOG.info(_LI("Delete volume metadata completed successfully."),
resource=volume) resource=volume)
@ -959,6 +1016,12 @@ class API(base.Base):
`metadata` argument will be deleted. `metadata` argument will be deleted.
""" """
if volume['status'] == 'maintenance':
LOG.info(_LI('Unable to update the metadata for volume, '
'because it is in maintenance.'), resource=volume)
msg = _("The volume metadata cannot be updated when the volume "
"is in maintenance mode.")
raise exception.InvalidVolume(reason=msg)
if delete: if delete:
_metadata = metadata _metadata = metadata
else: else:
@ -1216,10 +1279,10 @@ class API(base.Base):
resource=volume) resource=volume)
@wrap_check_policy @wrap_check_policy
def migrate_volume(self, context, volume, host, force_host_copy): def migrate_volume(self, context, volume, host, force_host_copy,
lock_volume):
"""Migrate the volume to the specified host.""" """Migrate the volume to the specified host."""
# We only handle "available" volumes for now
if volume['status'] not in ['available', 'in-use']: if volume['status'] not in ['available', 'in-use']:
msg = _('Volume %(vol_id)s status must be available or in-use, ' msg = _('Volume %(vol_id)s status must be available or in-use, '
'but current status is: ' 'but current status is: '
@ -1228,8 +1291,8 @@ class API(base.Base):
LOG.error(msg) LOG.error(msg)
raise exception.InvalidVolume(reason=msg) raise exception.InvalidVolume(reason=msg)
# Make sure volume is not part of a migration # Make sure volume is not part of a migration.
if volume['migration_status'] is not None: if self._is_volume_migrating(volume):
msg = _("Volume %s is already part of an active " msg = _("Volume %s is already part of an active "
"migration.") % volume['id'] "migration.") % volume['id']
LOG.error(msg) LOG.error(msg)
@ -1279,7 +1342,17 @@ class API(base.Base):
LOG.error(msg) LOG.error(msg)
raise exception.InvalidHost(reason=msg) raise exception.InvalidHost(reason=msg)
self.update(context, volume, {'migration_status': 'starting'}) # When the migration of an available volume starts, both the status
# and the migration status of the volume will be changed.
# If the admin sets lock_volume flag to True, the volume
# status is changed to 'maintenance', telling users
# that this volume is in maintenance mode, and no action is allowed
# on this volume, e.g. attach, detach, retype, migrate, etc.
updates = {'migration_status': 'starting',
'previous_status': volume['status']}
if lock_volume and volume['status'] == 'available':
updates['status'] = 'maintenance'
self.update(context, volume, updates)
# Call the scheduler to ensure that the host exists and that it can # Call the scheduler to ensure that the host exists and that it can
# accept the volume # accept the volume
@ -1352,7 +1425,7 @@ class API(base.Base):
LOG.error(msg) LOG.error(msg)
raise exception.InvalidVolume(reason=msg) raise exception.InvalidVolume(reason=msg)
if volume['migration_status'] is not None: if self._is_volume_migrating(volume):
msg = (_("Volume %s is already part of an active migration.") msg = (_("Volume %s is already part of an active migration.")
% volume['id']) % volume['id'])
LOG.error(msg) LOG.error(msg)
@ -1428,7 +1501,8 @@ class API(base.Base):
reservations = quota_utils.get_volume_type_reservation(context, volume, reservations = quota_utils.get_volume_type_reservation(context, volume,
vol_type_id) vol_type_id)
self.update(context, volume, {'status': 'retyping'}) self.update(context, volume, {'status': 'retyping',
'previous_status': volume['status']})
request_spec = {'volume_properties': volume, request_spec = {'volume_properties': volume,
'volume_id': volume['id'], 'volume_id': volume['id'],

View File

@ -566,7 +566,11 @@ class VolumeManager(manager.SchedulerDependentManager):
raise exception.InvalidVolume( raise exception.InvalidVolume(
reason=_("volume is not local to this node")) reason=_("volume is not local to this node"))
is_migrating = volume_ref['migration_status'] is not None # The status 'deleting' is not included, because it only applies to
# the source volume to be deleted after a migration. No quota
# needs to be handled for it.
is_migrating = volume_ref['migration_status'] not in (None, 'error',
'success')
is_migrating_dest = (is_migrating and is_migrating_dest = (is_migrating and
volume_ref['migration_status'].startswith( volume_ref['migration_status'].startswith(
'target:')) 'target:'))
@ -873,9 +877,6 @@ class VolumeManager(manager.SchedulerDependentManager):
host_name_sanitized, host_name_sanitized,
mountpoint, mountpoint,
mode) mode)
if volume['migration_status']:
self.db.volume_update(context, volume_id,
{'migration_status': None})
self._notify_about_volume_usage(context, volume, "attach.end") self._notify_about_volume_usage(context, volume, "attach.end")
LOG.info(_LI("Attach volume completed successfully."), LOG.info(_LI("Attach volume completed successfully."),
resource=volume) resource=volume)
@ -1439,13 +1440,6 @@ class VolumeManager(manager.SchedulerDependentManager):
self._clean_temporary_volume(ctxt, volume['id'], self._clean_temporary_volume(ctxt, volume['id'],
new_volume['id']) new_volume['id'])
def _get_original_status(self, volume):
attachments = volume['volume_attachment']
if not attachments:
return 'available'
else:
return 'in-use'
def _clean_temporary_volume(self, ctxt, volume_id, new_volume_id, def _clean_temporary_volume(self, ctxt, volume_id, new_volume_id,
clean_db_only=False): clean_db_only=False):
volume = self.db.volume_get(ctxt, volume_id) volume = self.db.volume_get(ctxt, volume_id)
@ -1506,14 +1500,15 @@ class VolumeManager(manager.SchedulerDependentManager):
new_volume = self.db.volume_get(ctxt, new_volume_id) new_volume = self.db.volume_get(ctxt, new_volume_id)
rpcapi = volume_rpcapi.VolumeAPI() rpcapi = volume_rpcapi.VolumeAPI()
orig_volume_status = self._get_original_status(volume) orig_volume_status = volume['previous_status']
if error: if error:
LOG.info(_LI("migrate_volume_completion is cleaning up an error " LOG.info(_LI("migrate_volume_completion is cleaning up an error "
"for volume %(vol1)s (temporary volume %(vol2)s"), "for volume %(vol1)s (temporary volume %(vol2)s"),
{'vol1': volume['id'], 'vol2': new_volume['id']}) {'vol1': volume['id'], 'vol2': new_volume['id']})
rpcapi.delete_volume(ctxt, new_volume) rpcapi.delete_volume(ctxt, new_volume)
updates = {'migration_status': None, 'status': orig_volume_status} updates = {'migration_status': 'error',
'status': orig_volume_status}
self.db.volume_update(ctxt, volume_id, updates) self.db.volume_update(ctxt, volume_id, updates)
return volume_id return volume_id
@ -1541,12 +1536,9 @@ class VolumeManager(manager.SchedulerDependentManager):
# asynchronously delete the destination id # asynchronously delete the destination id
__, updated_new = self.db.finish_volume_migration( __, updated_new = self.db.finish_volume_migration(
ctxt, volume_id, new_volume_id) ctxt, volume_id, new_volume_id)
if orig_volume_status == 'in-use': updates = {'status': orig_volume_status,
updates = {'migration_status': 'completing', 'previous_status': volume['status'],
'status': orig_volume_status} 'migration_status': 'success'}
else:
updates = {'migration_status': None}
self.db.volume_update(ctxt, volume_id, updates)
if orig_volume_status == 'in-use': if orig_volume_status == 'in-use':
attachments = volume['volume_attachment'] attachments = volume['volume_attachment']
@ -1556,6 +1548,7 @@ class VolumeManager(manager.SchedulerDependentManager):
attachment['attached_host'], attachment['attached_host'],
attachment['mountpoint'], attachment['mountpoint'],
'rw') 'rw')
self.db.volume_update(ctxt, volume_id, updates)
# Asynchronous deletion of the source volume in the back-end (now # Asynchronous deletion of the source volume in the back-end (now
# pointed by the target volume id) # pointed by the target volume id)
@ -1565,6 +1558,9 @@ class VolumeManager(manager.SchedulerDependentManager):
LOG.error(_LE('Failed to request async delete of migration source ' LOG.error(_LE('Failed to request async delete of migration source '
'vol %(vol)s: %(err)s'), 'vol %(vol)s: %(err)s'),
{'vol': volume_id, 'err': ex}) {'vol': volume_id, 'err': ex})
updates = {'migration_status': 'success',
'status': orig_volume_status,
'previous_status': volume['status']}
LOG.info(_LI("Complete-Migrate volume completed successfully."), LOG.info(_LI("Complete-Migrate volume completed successfully."),
resource=volume) resource=volume)
@ -1588,8 +1584,8 @@ class VolumeManager(manager.SchedulerDependentManager):
moved = False moved = False
status_update = None status_update = None
if volume_ref['status'] == 'retyping': if volume_ref['status'] in ('retyping', 'maintenance'):
status_update = {'status': self._get_original_status(volume_ref)} status_update = {'status': volume_ref['previous_status']}
self.db.volume_update(ctxt, volume_ref['id'], self.db.volume_update(ctxt, volume_ref['id'],
{'migration_status': 'migrating'}) {'migration_status': 'migrating'})
@ -1601,7 +1597,8 @@ class VolumeManager(manager.SchedulerDependentManager):
host) host)
if moved: if moved:
updates = {'host': host['host'], updates = {'host': host['host'],
'migration_status': None} 'migration_status': 'success',
'previous_status': volume_ref['status']}
if status_update: if status_update:
updates.update(status_update) updates.update(status_update)
if model_update: if model_update:
@ -1611,7 +1608,7 @@ class VolumeManager(manager.SchedulerDependentManager):
updates) updates)
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
updates = {'migration_status': None} updates = {'migration_status': 'error'}
if status_update: if status_update:
updates.update(status_update) updates.update(status_update)
self.db.volume_update(ctxt, volume_ref['id'], updates) self.db.volume_update(ctxt, volume_ref['id'], updates)
@ -1621,7 +1618,7 @@ class VolumeManager(manager.SchedulerDependentManager):
new_type_id) new_type_id)
except Exception: except Exception:
with excutils.save_and_reraise_exception(): with excutils.save_and_reraise_exception():
updates = {'migration_status': None} updates = {'migration_status': 'error'}
if status_update: if status_update:
updates.update(status_update) updates.update(status_update)
self.db.volume_update(ctxt, volume_ref['id'], updates) self.db.volume_update(ctxt, volume_ref['id'], updates)
@ -1831,7 +1828,7 @@ class VolumeManager(manager.SchedulerDependentManager):
context = ctxt.elevated() context = ctxt.elevated()
volume_ref = self.db.volume_get(ctxt, volume_id) volume_ref = self.db.volume_get(ctxt, volume_id)
status_update = {'status': self._get_original_status(volume_ref)} status_update = {'status': volume_ref['previous_status']}
if context.project_id != volume_ref['project_id']: if context.project_id != volume_ref['project_id']:
project_id = volume_ref['project_id'] project_id = volume_ref['project_id']
else: else: