Graduate share replication feature

From this patch, share replication feature is no longer considered
experimental. The experimental headers were removed from the share
replication APIs.

DocImpact
Partially-implements: bp graduate-share-replication-feature
Change-Id: Idf8af1c96df373fbcbb4024db490cb4dab42faf7
This commit is contained in:
silvacarloss 2020-08-19 22:59:27 -03:00 committed by Carlos Eduardo
parent f78e2fdee7
commit ee5ebc8462
8 changed files with 270 additions and 67 deletions

View File

@ -148,13 +148,14 @@ REST_API_VERSION_HISTORY = """
"progress" which indicates the completion of a share creation "progress" which indicates the completion of a share creation
operation as a percentage. operation as a percentage.
* 2.55 - Share groups feature is no longer considered experimental. * 2.55 - Share groups feature is no longer considered experimental.
* 2.56 - Share replication feature is no longer considered experimental.
""" """
# The minimum and maximum versions of the API supported # The minimum and maximum versions of the API supported
# The default api version request is defined to be the # The default api version request is defined to be the
# minimum version of the API supported. # minimum version of the API supported.
_MIN_API_VERSION = "2.0" _MIN_API_VERSION = "2.0"
_MAX_API_VERSION = "2.55" _MAX_API_VERSION = "2.56"
DEFAULT_API_VERSION = _MIN_API_VERSION DEFAULT_API_VERSION = _MIN_API_VERSION

View File

@ -305,3 +305,7 @@ user documentation.
2.55 (Maximum in Ussuri) 2.55 (Maximum in Ussuri)
------------------------ ------------------------
Share groups feature is no longer considered experimental. Share groups feature is no longer considered experimental.
2.56
----
Share replication feature is no longer considered experimental.

View File

@ -21,6 +21,9 @@ from manila.db import api as db_api
from manila import exception from manila import exception
from manila.i18n import _ from manila.i18n import _
PRE_GRADUATION_VERSION = '2.55'
GRADUATION_VERSION = '2.56'
class ShareReplicaExportLocationController(wsgi.Controller): class ShareReplicaExportLocationController(wsgi.Controller):
"""The Share Instance Export Locations API controller.""" """The Share Instance Export Locations API controller."""
@ -37,9 +40,19 @@ class ShareReplicaExportLocationController(wsgi.Controller):
msg = _("Share replica '%s' not found.") % share_replica_id msg = _("Share replica '%s' not found.") % share_replica_id
raise exc.HTTPNotFound(explanation=msg) raise exc.HTTPNotFound(explanation=msg)
@wsgi.Controller.api_version('2.47', experimental=True) @wsgi.Controller.api_version(
@wsgi.Controller.authorize '2.47', PRE_GRADUATION_VERSION, experimental=True)
def index(self, req, share_replica_id): def index(self, req, share_replica_id):
return self._index(req, share_replica_id)
# pylint: disable=function-redefined
@wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
def index(self, req, share_replica_id):
return self._index(req, share_replica_id)
# pylint: enable=function-redefined
@wsgi.Controller.authorize('index')
def _index(self, req, share_replica_id):
"""Return a list of export locations for the share instance.""" """Return a list of export locations for the share instance."""
context = req.environ['manila.context'] context = req.environ['manila.context']
self._verify_share_replica(context, share_replica_id) self._verify_share_replica(context, share_replica_id)
@ -51,9 +64,19 @@ class ShareReplicaExportLocationController(wsgi.Controller):
return self._view_builder.summary_list(req, export_locations, return self._view_builder.summary_list(req, export_locations,
replica=True) replica=True)
@wsgi.Controller.api_version('2.47', experimental=True) @wsgi.Controller.api_version(
@wsgi.Controller.authorize '2.47', PRE_GRADUATION_VERSION, experimental=True)
def show(self, req, share_replica_id, export_location_uuid): def show(self, req, share_replica_id, export_location_uuid):
return self._show(req, share_replica_id, export_location_uuid)
# pylint: disable=function-redefined
@wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
def show(self, req, share_replica_id, export_location_uuid):
return self._show(req, share_replica_id, export_location_uuid)
# pylint: enable=function-redefined
@wsgi.Controller.authorize('show')
def _show(self, req, share_replica_id, export_location_uuid):
"""Return data about the requested export location.""" """Return data about the requested export location."""
context = req.environ['manila.context'] context = req.environ['manila.context']
self._verify_share_replica(context, share_replica_id) self._verify_share_replica(context, share_replica_id)

View File

@ -31,6 +31,8 @@ from manila import share
MIN_SUPPORTED_API_VERSION = '2.11' MIN_SUPPORTED_API_VERSION = '2.11'
PRE_GRADUATION_VERSION = '2.55'
GRADUATION_VERSION = '2.56'
class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin): class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin):
@ -55,16 +57,28 @@ class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin):
except exception.ReplicationException as e: except exception.ReplicationException as e:
raise exc.HTTPBadRequest(explanation=six.text_type(e)) raise exc.HTTPBadRequest(explanation=six.text_type(e))
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True) @wsgi.Controller.api_version(
MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True)
def index(self, req): def index(self, req):
"""Return a summary list of replicas.""" """Return a summary list of replicas."""
return self._get_replicas(req) return self._get_replicas(req)
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True) @wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
def index(self, req): # pylint: disable=function-redefined
"""Return a summary list of replicas."""
return self._get_replicas(req)
@wsgi.Controller.api_version(
MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True)
def detail(self, req): def detail(self, req):
"""Returns a detailed list of replicas.""" """Returns a detailed list of replicas."""
return self._get_replicas(req, is_detail=True) return self._get_replicas(req, is_detail=True)
@wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
def detail(self, req): # pylint: disable=function-redefined
"""Returns a detailed list of replicas."""
return self._get_replicas(req, is_detail=True)
@wsgi.Controller.authorize('get_all') @wsgi.Controller.authorize('get_all')
def _get_replicas(self, req, is_detail=False): def _get_replicas(self, req, is_detail=False):
"""Returns list of replicas.""" """Returns list of replicas."""
@ -89,9 +103,19 @@ class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin):
return replicas return replicas
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True) @wsgi.Controller.api_version(
@wsgi.Controller.authorize MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True)
def show(self, req, id): def show(self, req, id):
"""Returns a detailed list of replicas."""
return self._show(req, id)
@wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
def show(self, req, id): # pylint: disable=function-redefined
"""Returns a detailed list of replicas."""
return self._show(req, id)
@wsgi.Controller.authorize('show')
def _show(self, req, id):
"""Return data about the given replica.""" """Return data about the given replica."""
context = req.environ['manila.context'] context = req.environ['manila.context']
@ -103,10 +127,19 @@ class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin):
return self._view_builder.detail(req, replica) return self._view_builder.detail(req, replica)
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True) @wsgi.Controller.api_version(
MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True)
@wsgi.response(202) @wsgi.response(202)
@wsgi.Controller.authorize
def create(self, req, body): def create(self, req, body):
return self._create(req, body)
@wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
@wsgi.response(202)
def create(self, req, body): # pylint: disable=function-redefined
return self._create(req, body)
@wsgi.Controller.authorize('create')
def _create(self, req, body):
"""Add a replica to an existing share.""" """Add a replica to an existing share."""
context = req.environ['manila.context'] context = req.environ['manila.context']
@ -142,9 +175,17 @@ class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin):
return self._view_builder.detail(req, new_replica) return self._view_builder.detail(req, new_replica)
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True) @wsgi.Controller.api_version(
@wsgi.Controller.authorize MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True)
def delete(self, req, id): def delete(self, req, id):
return self._delete_share_replica(req, id)
@wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
def delete(self, req, id): # pylint: disable=function-redefined
return self._delete_share_replica(req, id)
@wsgi.Controller.authorize('delete')
def _delete_share_replica(self, req, id):
"""Delete a replica.""" """Delete a replica."""
context = req.environ['manila.context'] context = req.environ['manila.context']
@ -161,11 +202,21 @@ class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin):
return webob.Response(status_int=http_client.ACCEPTED) return webob.Response(status_int=http_client.ACCEPTED)
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True) @wsgi.Controller.api_version(
@wsgi.action('promote') MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True)
@wsgi.response(202) @wsgi.response(202)
@wsgi.Controller.authorize @wsgi.action('promote')
def promote(self, req, id, body): def promote(self, req, id, body):
return self._promote(req, id, body)
@wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
@wsgi.response(202)
@wsgi.action('promote')
def promote(self, req, id, body): # pylint: disable=function-redefined
return self._promote(req, id, body)
@wsgi.Controller.authorize('promote')
def _promote(self, req, id, body):
"""Promote a replica to active state.""" """Promote a replica to active state."""
context = req.environ['manila.context'] context = req.environ['manila.context']
@ -189,30 +240,68 @@ class ShareReplicationController(wsgi.Controller, wsgi.AdminActionsMixin):
return self._view_builder.detail(req, replica) return self._view_builder.detail(req, replica)
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True) @wsgi.Controller.api_version(
MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True)
@wsgi.action('reset_status') @wsgi.action('reset_status')
def reset_status(self, req, id, body): def reset_status(self, req, id, body):
"""Reset the 'status' attribute in the database.""" """Reset the 'status' attribute in the database."""
return self._reset_status(req, id, body) return self._reset_status(req, id, body)
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True) # pylint: disable=function-redefined
@wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
@wsgi.action('reset_status')
def reset_status(self, req, id, body):
"""Reset the 'status' attribute in the database."""
return self._reset_status(req, id, body)
# pylint: enable=function-redefined
@wsgi.Controller.api_version(
MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True)
@wsgi.action('force_delete') @wsgi.action('force_delete')
def force_delete(self, req, id, body): def force_delete(self, req, id, body):
"""Force deletion on the database, attempt on the backend.""" """Force deletion on the database, attempt on the backend."""
return self._force_delete(req, id, body) return self._force_delete(req, id, body)
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True) # pylint: disable=function-redefined
@wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
@wsgi.action('force_delete')
def force_delete(self, req, id, body):
"""Force deletion on the database, attempt on the backend."""
return self._force_delete(req, id, body)
# pylint: enable=function-redefined
@wsgi.Controller.api_version(
MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True)
@wsgi.action('reset_replica_state') @wsgi.action('reset_replica_state')
@wsgi.Controller.authorize @wsgi.Controller.authorize
def reset_replica_state(self, req, id, body): def reset_replica_state(self, req, id, body):
"""Reset the 'replica_state' attribute in the database.""" """Reset the 'replica_state' attribute in the database."""
return self._reset_status(req, id, body, status_attr='replica_state') return self._reset_status(req, id, body, status_attr='replica_state')
@wsgi.Controller.api_version(MIN_SUPPORTED_API_VERSION, experimental=True) # pylint: disable=function-redefined
@wsgi.action('resync') @wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
@wsgi.response(202) @wsgi.action('reset_replica_state')
@wsgi.Controller.authorize @wsgi.Controller.authorize
def reset_replica_state(self, req, id, body):
"""Reset the 'replica_state' attribute in the database."""
return self._reset_status(req, id, body, status_attr='replica_state')
# pylint: enable=function-redefined
@wsgi.Controller.api_version(
MIN_SUPPORTED_API_VERSION, PRE_GRADUATION_VERSION, experimental=True)
@wsgi.response(202)
@wsgi.action('resync')
def resync(self, req, id, body): def resync(self, req, id, body):
return self._resync(req, id, body)
@wsgi.Controller.api_version(GRADUATION_VERSION) # noqa
@wsgi.response(202)
@wsgi.action('resync')
def resync(self, req, id, body): # pylint: disable=function-redefined
return self._resync(req, id, body)
@wsgi.Controller.authorize('resync')
def _resync(self, req, id, body):
"""Attempt to update/sync the replica with its source.""" """Attempt to update/sync the replica with its source."""
context = req.environ['manila.context'] context = req.environ['manila.context']
try: try:

View File

@ -202,11 +202,19 @@ fixture_reset_replica_status_with_different_roles = (
'role': 'admin', 'role': 'admin',
'valid_code': 202, 'valid_code': 202,
'valid_status': constants.STATUS_ERROR, 'valid_status': constants.STATUS_ERROR,
'microversion': '2.55'
},
{
'role': 'admin',
'valid_code': 202,
'valid_status': constants.STATUS_ERROR,
'microversion': '2.56'
}, },
{ {
'role': 'member', 'role': 'member',
'valid_code': 403, 'valid_code': 403,
'valid_status': constants.STATUS_AVAILABLE, 'valid_status': constants.STATUS_AVAILABLE,
'microversion': '2.55'
}, },
) )
@ -216,26 +224,31 @@ fixture_reset_replica_state_with_different_roles = (
'role': 'admin', 'role': 'admin',
'valid_code': 202, 'valid_code': 202,
'valid_status': constants.REPLICA_STATE_ACTIVE, 'valid_status': constants.REPLICA_STATE_ACTIVE,
'microversion': '2.55'
}, },
{ {
'role': 'admin', 'role': 'admin',
'valid_code': 202, 'valid_code': 202,
'valid_status': constants.REPLICA_STATE_OUT_OF_SYNC, 'valid_status': constants.REPLICA_STATE_OUT_OF_SYNC,
'microversion': '2.56'
}, },
{ {
'role': 'admin', 'role': 'admin',
'valid_code': 202, 'valid_code': 202,
'valid_status': constants.REPLICA_STATE_IN_SYNC, 'valid_status': constants.REPLICA_STATE_IN_SYNC,
'microversion': '2.55'
}, },
{ {
'role': 'admin', 'role': 'admin',
'valid_code': 202, 'valid_code': 202,
'valid_status': constants.STATUS_ERROR, 'valid_status': constants.STATUS_ERROR,
'microversion': '2.56'
}, },
{ {
'role': 'member', 'role': 'member',
'valid_code': 403, 'valid_code': 403,
'valid_status': constants.REPLICA_STATE_IN_SYNC, 'valid_status': constants.REPLICA_STATE_IN_SYNC,
'microversion': '2.55'
}, },
) )

View File

@ -28,6 +28,9 @@ from manila.tests.api import fakes
from manila.tests import db_utils from manila.tests import db_utils
GRADUATION_VERSION = '2.56'
@ddt.ddt @ddt.ddt
class ShareReplicaExportLocationsAPITest(test.TestCase): class ShareReplicaExportLocationsAPITest(test.TestCase):
@ -84,8 +87,9 @@ class ShareReplicaExportLocationsAPITest(test.TestCase):
db.share_export_locations_update( db.share_export_locations_update(
self.ctxt, self.share_replica3.id, replica3_exports) self.ctxt, self.share_replica3.id, replica3_exports)
@ddt.data('user', 'admin') @ddt.data(('user', '2.47'), ('admin', GRADUATION_VERSION))
def test_list_and_show(self, role): @ddt.unpack
def test_list_and_show(self, role, microversion):
summary_keys = [ summary_keys = [
'id', 'path', 'replica_state', 'availability_zone', 'preferred' 'id', 'path', 'replica_state', 'availability_zone', 'preferred'
] ]
@ -96,12 +100,15 @@ class ShareReplicaExportLocationsAPITest(test.TestCase):
admin_detail_keys = admin_summary_keys + ['created_at', 'updated_at'] admin_detail_keys = admin_summary_keys + ['created_at', 'updated_at']
self._test_list_and_show(role, summary_keys, detail_keys, self._test_list_and_show(role, summary_keys, detail_keys,
admin_summary_keys, admin_detail_keys) admin_summary_keys, admin_detail_keys,
microversion=microversion)
def _test_list_and_show(self, role, summary_keys, detail_keys, def _test_list_and_show(self, role, summary_keys, detail_keys,
admin_summary_keys, admin_detail_keys): admin_summary_keys, admin_detail_keys,
microversion='2.47'):
req = self._get_request(use_admin_context=(role == 'admin')) req = self._get_request(version=microversion,
use_admin_context=(role == 'admin'))
for replica_id in (self.active_replica_id, self.share_replica2.id, for replica_id in (self.active_replica_id, self.share_replica2.id,
self.share_replica3.id): self.share_replica3.id):
index_result = self.controller.index(req, replica_id) index_result = self.controller.index(req, replica_id)

View File

@ -21,6 +21,7 @@ from oslo_serialization import jsonutils
import six import six
from webob import exc from webob import exc
from manila.api.openstack import api_version_request as api_version
from manila.api.v2 import share_replicas from manila.api.v2 import share_replicas
from manila.common import constants from manila.common import constants
from manila import context from manila import context
@ -33,6 +34,9 @@ from manila.tests import db_utils
from manila.tests import fake_share from manila.tests import fake_share
CONF = cfg.CONF CONF = cfg.CONF
CAST_RULES_READONLY_VERSION = '2.30'
PRE_GRADUATION_VERSION = '2.55'
GRADUATION_VERSION = '2.56'
@ddt.ddt @ddt.ddt
@ -64,16 +68,24 @@ class ShareReplicasApiTest(test.TestCase):
kwargs['replica_state'] = constants.REPLICA_STATE_IN_SYNC kwargs['replica_state'] = constants.REPLICA_STATE_IN_SYNC
replica = db_utils.create_share_replica(**kwargs) replica = db_utils.create_share_replica(**kwargs)
path = '/v2/fake/share-replicas/%s/action' % replica['id'] path = '/v2/fake/share-replicas/%s/action' % replica['id']
microversion = kwargs.get('microversion', self.api_version)
experimental = True
if (api_version.APIVersionRequest(microversion) >=
api_version.APIVersionRequest(GRADUATION_VERSION)):
experimental = False
req = fakes.HTTPRequest.blank(path, script_name=path, req = fakes.HTTPRequest.blank(path, script_name=path,
version=self.api_version) version=microversion,
experimental=experimental)
req.method = 'POST' req.method = 'POST'
req.headers['content-type'] = 'application/json' req.headers['content-type'] = 'application/json'
req.headers['X-Openstack-Manila-Api-Version'] = self.api_version req.headers['X-Openstack-Manila-Api-Version'] = microversion
req.headers['X-Openstack-Manila-Api-Experimental'] = True req.headers['X-Openstack-Manila-Api-Experimental'] = True
return replica, req return replica, req
def _get_fake_replica(self, summary=False, admin=False, **values): def _get_fake_replica(
self, summary=False, admin=False,
microversion=share_replicas.MIN_SUPPORTED_API_VERSION, **values):
replica = fake_share.fake_replica(**values) replica = fake_share.fake_replica(**values)
replica['updated_at'] = '2016-02-11T19:57:56.506805' replica['updated_at'] = '2016-02-11T19:57:56.506805'
expected_keys = {'id', 'share_id', 'status', 'replica_state'} expected_keys = {'id', 'share_id', 'status', 'replica_state'}
@ -92,18 +104,37 @@ class ShareReplicasApiTest(test.TestCase):
expected_replica['share_server_id'] = replica['share_server_id'] expected_replica['share_server_id'] = replica['share_server_id']
expected_replica['host'] = replica['host'] expected_replica['host'] = replica['host']
if (api_version.APIVersionRequest(microversion) >=
api_version.APIVersionRequest(CAST_RULES_READONLY_VERSION)
and admin):
expected_replica['cast_rules_to_readonly'] = False
return replica, expected_replica return replica, expected_replica
def test_list_replicas_summary(self): def _get_request(self, microversion, is_admin=False):
fake_replica, expected_replica = self._get_fake_replica(summary=True) experimental = (api_version.APIVersionRequest(microversion) <=
api_version.APIVersionRequest(GRADUATION_VERSION))
req = fakes.HTTPRequest.blank(
'/share-replicas', version=microversion,
experimental=experimental, use_admin_context=is_admin)
return req
@ddt.data((False, PRE_GRADUATION_VERSION), (False, GRADUATION_VERSION))
@ddt.unpack
def test_list_replicas_summary(self, is_admin, microversion):
fake_replica, expected_replica = self._get_fake_replica(
summary=True, admin=is_admin, microversion=microversion)
self.mock_object(share_replicas.db, 'share_replicas_get_all', self.mock_object(share_replicas.db, 'share_replicas_get_all',
mock.Mock(return_value=[fake_replica])) mock.Mock(return_value=[fake_replica]))
res_dict = self.controller.index(self.replicas_req) req = self._get_request(is_admin=is_admin, microversion=microversion)
context = req.environ['manila.context']
res_dict = self.controller.index(req)
self.assertEqual([expected_replica], res_dict['share_replicas']) self.assertEqual([expected_replica], res_dict['share_replicas'])
self.mock_policy_check.assert_called_once_with( self.mock_policy_check.assert_called_once_with(
self.member_context, self.resource_name, 'get_all') context, self.resource_name, 'get_all')
def test_list_share_replicas_summary(self): def test_list_share_replicas_summary(self):
fake_replica, expected_replica = self._get_fake_replica(summary=True) fake_replica, expected_replica = self._get_fake_replica(summary=True)
@ -120,13 +151,15 @@ class ShareReplicasApiTest(test.TestCase):
self.mock_policy_check.assert_called_once_with( self.mock_policy_check.assert_called_once_with(
req_context, self.resource_name, 'get_all') req_context, self.resource_name, 'get_all')
@ddt.data(True, False) @ddt.data((True, PRE_GRADUATION_VERSION), (False, GRADUATION_VERSION))
def test_list_replicas_detail(self, is_admin): @ddt.unpack
fake_replica, expected_replica = self._get_fake_replica(admin=is_admin) def test_list_replicas_detail(self, is_admin, microversion):
fake_replica, expected_replica = self._get_fake_replica(
admin=is_admin, microversion=microversion)
self.mock_object(share_replicas.db, 'share_replicas_get_all', self.mock_object(share_replicas.db, 'share_replicas_get_all',
mock.Mock(return_value=[fake_replica])) mock.Mock(return_value=[fake_replica]))
req = self.replicas_req if not is_admin else self.replicas_req_admin req = self._get_request(is_admin=is_admin, microversion=microversion)
req_context = req.environ['manila.context'] req_context = req.environ['manila.context']
res_dict = self.controller.detail(req) res_dict = self.controller.detail(req)
@ -244,14 +277,17 @@ class ShareReplicasApiTest(test.TestCase):
self.mock_policy_check.assert_called_once_with( self.mock_policy_check.assert_called_once_with(
req_context, self.resource_name, 'get_all') req_context, self.resource_name, 'get_all')
@ddt.data(True, False) @ddt.data((True, PRE_GRADUATION_VERSION),
def test_show(self, is_admin): (False, GRADUATION_VERSION))
fake_replica, expected_replica = self._get_fake_replica(admin=is_admin) @ddt.unpack
def test_show(self, is_admin, microversion):
fake_replica, expected_replica = self._get_fake_replica(
admin=is_admin, microversion=microversion)
self.mock_object( self.mock_object(
share_replicas.db, 'share_replica_get', share_replicas.db, 'share_replica_get',
mock.Mock(return_value=fake_replica)) mock.Mock(return_value=fake_replica))
req = self.replicas_req if not is_admin else self.replicas_req_admin req = self._get_request(microversion, is_admin)
req_context = req.environ['manila.context'] req_context = req.environ['manila.context']
res_dict = self.controller.show(req, fake_replica.get('id')) res_dict = self.controller.show(req, fake_replica.get('id'))
@ -354,10 +390,12 @@ class ShareReplicasApiTest(test.TestCase):
self.mock_policy_check.assert_called_once_with( self.mock_policy_check.assert_called_once_with(
self.member_context, self.resource_name, 'create') self.member_context, self.resource_name, 'create')
@ddt.data(True, False) @ddt.data((True, PRE_GRADUATION_VERSION), (False, GRADUATION_VERSION))
def test_create(self, is_admin): @ddt.unpack
def test_create(self, is_admin, microversion):
fake_replica, expected_replica = self._get_fake_replica( fake_replica, expected_replica = self._get_fake_replica(
replication_type='writable', admin=is_admin) replication_type='writable', admin=is_admin,
microversion=microversion)
body = { body = {
'share_replica': { 'share_replica': {
'share_id': 'FAKE_SHAREID', 'share_id': 'FAKE_SHAREID',
@ -372,7 +410,7 @@ class ShareReplicasApiTest(test.TestCase):
'share_replicas_get_available_active_replica', 'share_replicas_get_available_active_replica',
mock.Mock(return_value=[{'id': 'active1'}])) mock.Mock(return_value=[{'id': 'active1'}]))
req = self.replicas_req if not is_admin else self.replicas_req_admin req = self._get_request(microversion, is_admin)
req_context = req.environ['manila.context'] req_context = req.environ['manila.context']
res_dict = self.controller.create(req, body) res_dict = self.controller.create(req, body)
@ -417,20 +455,23 @@ class ShareReplicasApiTest(test.TestCase):
self.mock_policy_check.assert_called_once_with( self.mock_policy_check.assert_called_once_with(
self.member_context, self.resource_name, 'delete') self.member_context, self.resource_name, 'delete')
def test_delete(self): @ddt.data(PRE_GRADUATION_VERSION, GRADUATION_VERSION)
def test_delete(self, microversion):
fake_replica = self._get_fake_replica( fake_replica = self._get_fake_replica(
share_id='FAKE_SHARE_ID', share_id='FAKE_SHARE_ID',
replica_state=constants.REPLICA_STATE_ACTIVE)[0] replica_state=constants.REPLICA_STATE_ACTIVE)[0]
req = self._get_request(microversion=microversion)
context = req.environ['manila.context']
self.mock_object(share_replicas.db, 'share_replica_get', self.mock_object(share_replicas.db, 'share_replica_get',
mock.Mock(return_value=fake_replica)) mock.Mock(return_value=fake_replica))
self.mock_object(share.API, 'delete_share_replica') self.mock_object(share.API, 'delete_share_replica')
resp = self.controller.delete( resp = self.controller.delete(
self.replicas_req, 'FAKE_REPLICA_ID') req, 'FAKE_REPLICA_ID')
self.assertEqual(202, resp.status_code) self.assertEqual(202, resp.status_code)
self.mock_policy_check.assert_called_once_with( self.mock_policy_check.assert_called_once_with(
self.member_context, self.resource_name, 'delete') context, self.resource_name, 'delete')
def test_promote_invalid_replica_id(self): def test_promote_invalid_replica_id(self):
body = {'promote': None} body = {'promote': None}
@ -501,25 +542,28 @@ class ShareReplicasApiTest(test.TestCase):
self.mock_policy_check.assert_called_once_with( self.mock_policy_check.assert_called_once_with(
self.member_context, self.resource_name, 'promote') self.member_context, self.resource_name, 'promote')
def test_promote(self): @ddt.data(PRE_GRADUATION_VERSION, GRADUATION_VERSION)
def test_promote(self, microversion):
body = {'promote': None} body = {'promote': None}
replica, expected_replica = self._get_fake_replica( replica, expected_replica = self._get_fake_replica(
replica_state=constants.REPLICA_STATE_IN_SYNC) replica_state=constants.REPLICA_STATE_IN_SYNC,
microversion=microversion)
self.mock_object(share_replicas.db, 'share_replica_get', self.mock_object(share_replicas.db, 'share_replica_get',
mock.Mock(return_value=replica)) mock.Mock(return_value=replica))
mock_api_promote_replica_call = self.mock_object( mock_api_promote_replica_call = self.mock_object(
share.API, 'promote_share_replica', share.API, 'promote_share_replica',
mock.Mock(return_value=replica)) mock.Mock(return_value=replica))
req = self._get_request(microversion=microversion)
resp = self.controller.promote(self.replicas_req, replica['id'], body) context = req.environ['manila.context']
resp = self.controller.promote(req, replica['id'], body)
self.assertEqual(expected_replica, resp['share_replica']) self.assertEqual(expected_replica, resp['share_replica'])
self.assertTrue(mock_api_promote_replica_call.called) self.assertTrue(mock_api_promote_replica_call.called)
self.mock_policy_check.assert_called_once_with( self.mock_policy_check.assert_called_once_with(
self.member_context, self.resource_name, 'promote') context, self.resource_name, 'promote')
@ddt.data('index', 'detail', 'show', 'create', 'delete', 'promote', @ddt.data('index', 'detail', '_show', '_create', '_delete_share_replica',
'reset_replica_state', 'reset_status', 'resync') '_promote', 'reset_replica_state', 'reset_status', '_resync')
def test_policy_not_authorized(self, method_name): def test_policy_not_authorized(self, method_name):
method = getattr(self.controller, method_name) method = getattr(self.controller, method_name)
@ -588,9 +632,10 @@ class ShareReplicasApiTest(test.TestCase):
@ddt.data(*fakes.fixture_reset_replica_status_with_different_roles) @ddt.data(*fakes.fixture_reset_replica_status_with_different_roles)
@ddt.unpack @ddt.unpack
def test_reset_status_with_different_roles(self, role, valid_code, def test_reset_status_with_different_roles(self, role, valid_code,
valid_status): valid_status, microversion):
context = self._get_context(role) context = self._get_context(role)
replica, action_req = self._create_replica_get_req() replica, action_req = self._create_replica_get_req(
microversion=microversion)
self._reset_status(context, replica, action_req, self._reset_status(context, replica, action_req,
valid_code=valid_code, status_attr='status', valid_code=valid_code, status_attr='status',
@ -611,10 +656,11 @@ class ShareReplicasApiTest(test.TestCase):
@ddt.data(*fakes.fixture_reset_replica_state_with_different_roles) @ddt.data(*fakes.fixture_reset_replica_state_with_different_roles)
@ddt.unpack @ddt.unpack
def test_reset_replica_state_with_different_roles(self, role, valid_code, def test_reset_replica_state_with_different_roles(
valid_status): self, role, valid_code, valid_status, microversion):
context = self._get_context(role) context = self._get_context(role)
replica, action_req = self._create_replica_get_req() replica, action_req = self._create_replica_get_req(
microversion=microversion)
body = {'reset_replica_state': {'replica_state': valid_status}} body = {'reset_replica_state': {'replica_state': valid_status}}
self._reset_status(context, replica, action_req, self._reset_status(context, replica, action_req,
@ -655,6 +701,15 @@ class ShareReplicasApiTest(test.TestCase):
self._force_delete(context, req, valid_code=resp_code) self._force_delete(context, req, valid_code=resp_code)
@ddt.data((PRE_GRADUATION_VERSION, 202),
(GRADUATION_VERSION, 202))
@ddt.unpack
def test_force_delete_replica(self, microversion, resp_code):
replica, req = self._create_replica_get_req(microversion=microversion)
context = self.admin_context
self._force_delete(context, req, valid_code=resp_code)
def test_force_delete_missing_replica(self): def test_force_delete_missing_replica(self):
replica, req = self._create_replica_get_req() replica, req = self._create_replica_get_req()
share_replicas.db.share_replica_delete( share_replicas.db.share_replica_delete(
@ -701,14 +756,16 @@ class ShareReplicasApiTest(test.TestCase):
self.assertEqual(400, resp.status_int) self.assertEqual(400, resp.status_int)
share_api_call.assert_called_once_with(self.admin_context, replica) share_api_call.assert_called_once_with(self.admin_context, replica)
@ddt.data(constants.REPLICA_STATE_ACTIVE, @ddt.data((constants.REPLICA_STATE_ACTIVE, PRE_GRADUATION_VERSION),
constants.REPLICA_STATE_IN_SYNC, (constants.REPLICA_STATE_IN_SYNC, PRE_GRADUATION_VERSION),
constants.REPLICA_STATE_OUT_OF_SYNC, (constants.REPLICA_STATE_OUT_OF_SYNC, GRADUATION_VERSION),
constants.STATUS_ERROR) (constants.STATUS_ERROR, GRADUATION_VERSION))
def test_resync(self, replica_state): @ddt.unpack
def test_resync(self, replica_state, microversion):
replica, req = self._create_replica_get_req( replica, req = self._create_replica_get_req(
replica_state=replica_state, host='skywalker@jedi#temple') replica_state=replica_state, host='skywalker@jedi#temple',
microversion=microversion)
share_api_call = self.mock_object( share_api_call = self.mock_object(
share.API, 'update_share_replica', mock.Mock(return_value=None)) share.API, 'update_share_replica', mock.Mock(return_value=None))
body = {'resync': {}} body = {'resync': {}}

View File

@ -0,0 +1,9 @@
---
prelude: >
- |
Share replication APIs have graduated from their `experimental feature state
<https://docs.openstack.org/manila/latest/contributor/experimental_apis.html>`_
from API version ``2.56``. One or more share replicas can be created from a
given share. They can also be promoted to be considered the active share,
resynchronized and deleted. These actions no longer require the inclusion of
``X-OpenStack-Manila-API-Experimental`` header in the API requests.